339

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

?

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
HTF
  • 6,632
  • 6
  • 30
  • 49
  • 7
    Could you please be more precise as to what "doesn't work" ? Do you have a specific error message, or does is simply not provide the expected output ? – Julien Vivenot Nov 16 '12 at 00:33
  • I was actually receiving "unary operator expected" so it looks like quoting helps – HTF Nov 16 '12 at 00:40
  • 2
    `-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. – cdarke Nov 16 '12 at 13:43
  • Check this out:http://theunixshell.blogspot.com/2013/05/list-of-bash-conditional-statements.html – Vijay Apr 20 '14 at 18:28

5 Answers5

474

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.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • 3
    If `STATUS` is empty, the code from @HTF would have failed on `-ne: unary operator expected`. In your case, it will fail on `integer expression expected`, won't it ? – Julien Vivenot Nov 16 '12 at 00:28
  • 2
    I understand that. But you highlight the issue that `$STATUS` might be empty and suggest a solution (quoting it). Your solution still fails with an empty `STATUS`, that is all I meant. – Julien Vivenot Nov 16 '12 at 00:32
  • 2
    @jvivenot You have a point. (My response to your comment was made before you edited your comment, when your comment merely read "the code ... would have failed". A simple solution is to use `${STATUS:-0"`. Will edit. – William Pursell Nov 16 '12 at 00:33
  • Sorry, your edit still does not work. For example : `STATUS=; [ $STATUS -ne 13 ] 2>/dev/null && echo foo` does not output `foo`, even though it should (empty is different from 13). What you first suggested, `${STATUS:-0}` looks far better. – Julien Vivenot Nov 16 '12 at 00:42
  • @jvivenot Using `${STATUS:-0}` will fail if `STATUS=foo`, but `! [ "$STATUS" -eq 200 ] 2> /dev/null && echo foo` works. IMO, it is better to avoid `-eq` and use `!=`, though. – William Pursell Nov 16 '12 at 00:53
  • Wow, [they](https://en.wikibooks.org/wiki/Bash_Shell_Scripting#if_statements) are using `[[` extensively. – BairDev Jul 01 '15 at 06:18
  • Google's Shell Style Guide [recommends](https://google.github.io/styleguide/shell.xml?showone=Test,_%5B_and_%5B%5B#Test,_%5B_and_%5B%5B) using `[[` – cogell May 19 '16 at 19:43
  • 2
    Yes, but Google's Shell Style Guide is misnamed, because it is a style guide for bash. If you care about portability (and you should), you cannot use `[[` – William Pursell May 19 '16 at 19:48
  • 2
    Further, consider the (lack of) error message generated by [[ in `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. – William Pursell Apr 18 '18 at 17:16
  • Very harsh opinion of not using [[, the other way around, the double brackets are recommended, also look at https://stackoverflow.com/a/669486/3263659 – Mercury May 04 '21 at 14:15
  • 2
    @Mercury Yes, I'm strongly opinionated about this. The lack of an error message from `[[` is problematic. `[[` solves problems caused by misunderstandings about how `test` works. IMO, a better solution to those problems is to gain an understanding of how to use `test`. – William Pursell May 04 '21 at 14:58
  • Google sent me here for "bash and operator". This is not straight forward without reading the question. I recommend an edit to save years for humanity over all. What is lacking IMO: what is `;`? why is `[[` good or bad? difference between the options you present? – Gulzar Mar 07 '22 at 15:40
87

Try this:

if [ "${STATUS}" -ne 100 -a "${STRING}" = "${VALUE}" ]

or

if [ "${STATUS}" -ne 100 ] && [ "${STRING}" = "${VALUE}" ]
kelvin
  • 1,421
  • 13
  • 28
AnshBikram
  • 2,028
  • 13
  • 8
42

Try this:

if [ $STATUS -ne 200 -a "$STRING" != "$VALUE" ]; then
BrenanK
  • 667
  • 5
  • 4
  • worked for me. I think there might be more than one ways but I am satisfied with this for now. – Kushal Ashok Aug 26 '16 at 12:32
  • Looks like it works better in some cases. The && operator does not look to work with shell substitution and with two &&. I would suggest trying this option first. – R. W. Prado Oct 08 '21 at 23:31
23

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 condition to be evaluated by a process 1. The condition is true if the process's exit status is 0, or false otherwise).

For use as condition evaluation processes, there is the test program (man test), which is a program that lets you evaluate simple conditions, like file existance tests, string tests, and combinations thereof using e.g. -a and -o logical operator arguments.

As some find lines like if test -f filename; then foo bar; fi, etc. annoying, there is also a program called [ which is in fact a symlink to the test program. When test is called as [, it expects to find ] as an additional terminating argument - this syntax is made up by the test program and bash is not aware of it at all.

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 process invocation. In other words the [ program will be run with arguments $STATUS, -ne, 200, ], -a, [[, "$STRING", !=, "$VALUE", ]] 2. Now note that there is a ] somewhere in the middle of the argument list passed to [, and also there is a ] missing at the end of the list. This is why the program will complain.

Also note, in constrast to the [ program, [[ is indeed a special, "reserved" word in the shell's syntax (it's a bash extension, not standardized in POSIX sh). But it's only recognized as a special word if it's the first word in a command. In your example, the [[ occurs later in the command and so will be parsed as a normal argument, literally the string "[[" and be passed as part of the argument list to the invocation of the [ program.


1 Actually it need not be a single process but can be a job, a combination of processes connected by |, &&, || etc..

2 Not literally these arguments - the shell will do variable substitution for $STATUS etc.

Jo So
  • 25,005
  • 6
  • 42
  • 59
8

First, you don't need to capitalize your variable names. In general, all-caps variable names should be reserved for ones that are being exported 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
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Mark Reed
  • 91,912
  • 16
  • 138
  • 175