ChatGPT解决这个技术问题 Extra ChatGPT

An "and" operator for an "if" statement in Bash

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

?

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 ?
I was actually receiving "unary operator expected" so it looks like quoting helps
-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.

W
William Pursell

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.


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 ?
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.
@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.
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 [[
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.
k
kelvin

Try this:

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

or

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

B
BrenanK

Try this:

if [ $STATUS -ne 200 -a "$STRING" != "$VALUE" ]; then

worked for me. I think there might be more than one ways but I am satisfied with this for now.
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.
C
Community

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.


S
Stephen Ostermiller

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