I'm trying to create a simple Bash script to check if the website is down and for some reason the "and" operator doesn't work:
#!/usr/bin/env bash
WEBSITE=domain.example
SUBJECT="$WEBSITE DOWN!"
EMAILID="an@email.example"
STATUS=$(curl -sI $WEBSITE | awk '/HTTP\/1.1/ { print $2 }')
STRING=$(curl -s $WEBSITE | grep -o "string_to_search")
VALUE="string_to_search"
if [ $STATUS -ne 200 ] && [[ "$STRING" != "$VALUE" ]]; then
echo "Website: $WEBSITE is down, status code: '$STATUS' - $(date)" | mail -s "$SUBJECT" $EMAILID
fi
The "-a" operator also doesn't work:
if [ $STATUS -ne 200 ] -a [[ "$STRING" != "$VALUE" ]]
Could you also please advise when to use:
single and double square brackets
parenthesis
?
-a
has duplicity. When used with the Bourne shell style test
command, a.k.a. [
, the it means and
. When used as a conditional expression then it is testing to see if a file exists. Yes it is confusing, best avoided.
What you have should work, unless ${STATUS}
is empty. It would probably be better to do:
if ! [ "${STATUS}" -eq 200 ] 2> /dev/null && [ "${STRING}" != "${VALUE}" ]; then
or
if [ "${STATUS}" != 200 ] && [ "${STRING}" != "${VALUE}" ]; then
It's hard to say, since you haven't shown us exactly what is going wrong with your script.
Personal opinion: never use [[
. It suppresses important error messages and is not portable to different shells.
Try this:
if [ "${STATUS}" -ne 100 -a "${STRING}" = "${VALUE}" ]
or
if [ "${STATUS}" -ne 100 ] && [ "${STRING}" = "${VALUE}" ]
Try this:
if [ $STATUS -ne 200 -a "$STRING" != "$VALUE" ]; then
Quote:
The "-a" operator also doesn't work: if [ $STATUS -ne 200 ] -a [[ "$STRING" != "$VALUE" ]]
For a more elaborate explanation: [
and ]
are not Bash reserved words. The if
keyword introduces a conditional to be evaluated by a job (the conditional is true if the job's return value is 0
or false otherwise).
For trivial tests, there is the test
program (man test
).
As some find lines like if test -f filename; then foo bar; fi
, etc. annoying, on most systems you find a program called [
which is in fact only a symlink to the test
program. When test
is called as [
, you have to add ]
as the last positional argument.
So if test -f filename
is basically the same (in terms of processes spawned) as if [ -f filename ]
. In both cases the test
program will be started, and both processes should behave identically.
Here's your mistake: if [ $STATUS -ne 200 ] -a [[ "$STRING" != "$VALUE" ]]
will parse to if
+ some job, the job being everything except the if
itself. The job is only a simple command (Bash speak for something which results in a single process), which means the first word ([
) is the command and the rest its positional arguments. There are remaining arguments after the first ]
.
Also not, [[
is indeed a Bash keyword, but in this case it's only parsed as a normal command argument, because it's not at the front of the command.
First, you don't need to capitalize your variable names. In general, all-caps variable names should be reserved for ones that are being export
ed into the environment for the benefit of other executables.
Second, what you have with &&
should work, but is not very resilient in the face of a status variable that might be empty or non-numeric.
Finally, the -a
attempt didn't work because -a
is part of the syntax of test
/ [
...]
; it goes inside the brackets.
Fundamentally, the most important thing to know about conditionals in the shell is that they are just commands. This if expression:
if foo; then
echo true
else
echo false
fi
Does exactly the same thing as this one:
foo
if [ $? -eq 0 ]; then
echo true
else
echo false
fi
So the thing after the if
(or while
or until
) can be any command whatsoever.
That goes for the &&
and ||
operators, too: the things on either side are just commands. The sequence foo && bar
runs the command foo
and then, only if that command exited with status 0, it runs bar
. (So it's effectively short for if foo; then bar; fi
.) Similarly, foo || bar
runs foo
and then, if that command exited with nonzero status, runs bar
. (So it's effectively short for if ! foo; then bar; fi
.)
That's the main thing that a lot of beginning shell programmers miss. The shell doesn't have expressions per se, just commands to run.
However, because you so often want to perform comparisons and other operations that would be Boolean expressions in regular non-shell programming languages, there is a special command that you can run to do those. It used to be a separate binary, but these days it's built into the shell. Its real name is test
, but it can also be invoked as [
, in which case it will complain if its last argument is not a matching ]
. The arguments inside the brackets or after test
constitute an expression to evaluate, and the test command exits with status 0 if the expression is true, and nonzero if it's false. So it turns the sort of thing that is normally what an if
statement expects in other programming languages into a command that you can run, effectively translating it for the shell.
The appearance of [
...]
can be misleading; it really is parsed just as if it were spelled test
, with the same command-line processing as any regular shell command. That means that if you try to compare things using <
or >
they'll be interpreted as redirects. Try to use (
and )
for grouping and you'll be creating a subshell (and probably generating a syntax error by doing so in the middle of a command's arguments). Try to combine expressions with &&
and ||
inside the brackets and you'll be terminating the command early and again triggering a syntax error.
You can still use &&
and ||
outside of the brackets; at that point you're just running multiple instances of the test command instead of just one. But you can also use -a
and -o
as long as they're inside the brackets. So these two pipelines produce the same result:
[ "$status" -ne 200 -a "$string" != "$value" ]
[ "$status" -ne 200 ] && [ "$string" != "value" ]
The difference is that the first one is all checked by one instance of the test
command, while the second runs two of them.
The Korn Shell introduced a new version of the test command spelled with two brackets instead of one, between which lies a special syntactic environment that is not parsed the same as a regular command. Between the double brackets you can safely use unquoted parameter expansions, unquoted parentheses for grouping, <
and >
for string comparison (stick to -lt
and -gt
to compare numerically), and &&
and ||
as Boolean operators. [[
...]]
also adds the ability to match against glob patterns ([[
foo == f* ]]
) and regular expressions ([[ foo =~ ^f ]]
).
Modern shells such as Bash and Zsh have inherited this construct from Ksh, but it is not part of the POSIX specification. If you're in an environment where you have to be strictly POSIX compliant, stay away from it; otherwise, it's basically down to personal preference.
Note that the arithmetic substitution expression $((
...))
is part of POSIX, and it has very similar rules to [[
...]]
. The main difference is that equality and inequality tests (which here produce a numeric value, 0 for false and 1 for true) are done numerically instead of stringwise: [[ 10 < 2 ]]
is true, but $(( 10 < 2 ))
produces 0.
Here again, modern shells have expanded upon the POSIX spec to add a standalone $
-less version of ((
...))
that functions as basically an autoquoted let
command, which you can use in if
expressions if you're dealing with numbers. So your first condition could also be written (( status != 200 ))
. Again, outside of having to stick to POSIX, that's down to personal preference. I like using ((
...))
whenever I'm dealing with numbers, but others prefer to just stick to [
or [[
and use the numeric operators like -lt
.
So for what it's worth, here's how I'd rewrite your code:
website=domain.example
subject="$website DOWN!"
email=an@email.example
value="string_to_search"
status=$(curl -sI "$website" | awk '/HTTP\/1.1/ { print $2 }')
string=$(curl -s "$website" | grep -o "$value")
if (( status != 200 )) && [[ $string != $value ]]; then
mail -s "$subject" "$email" <<<"Website: $website is down, status code: '$status' - $(date)"
fi
Success story sharing
STATUS
is empty, the code from @HTF would have failed on-ne: unary operator expected
. In your case, it will fail oninteger expression expected
, won't it ?$STATUS
might be empty and suggest a solution (quoting it). Your solution still fails with an emptySTATUS
, that is all I meant.${STATUS:-0"
. Will edit.[[
a="this is not an integer"; test "$a" -ge 0; [[ $a -ge 0 ]];
IMO, the error message is useful, and [[ incorrectly suppresses it. Also, [[ returns 0, while test returns 1. IMO, this makes[[
unusable.