3

When using bash test or [ builtin commands to evaluate conditional expressions with logical NOT !, should the exclamation point go inside or outside the brackets? Does it matter?

Here is a trivial MRE.

$ var=1
$ if [ $var -gt 0 ]; then echo "evaluated to true"; else echo "evaluated to false";  fi
evaluated to true
$ if ! [ $var -gt 0 ]; then echo "evaluated to true"; else echo "evaluated to false";  fi
evaluated to false
$ if [ ! $var -gt 0 ]; then echo "evaluated to true"; else echo "evaluated to false";  fi
Josh
  • 303
  • On your other question, there's an answer with 12 up-votes that has been accepted. That answer is correct... – Andy Dalton May 16 '20 at 03:19
  • The answer to the other question that I commented on discusses double brackets [[...]]. My question here is about single brackets [...]. – Josh May 16 '20 at 03:29
  • In the context in which you'd framed the question, there is no difference between [[...]] and [...]. – Andy Dalton May 16 '20 at 03:31
  • 2
    https://stackoverflow.com/q/36238287/2072269 Also, your MRE is missing output – muru May 16 '20 at 06:47
  • @AndyDalton, That linked question on SO doesn't seem to be from Josh, if that's what you're referring to? – ilkkachu May 16 '20 at 09:37
  • I’m voting to close this question because it was asked in a comment on SO but actually has an answer there. – AdminBee May 18 '20 at 16:37
  • @AdminBee, this comment? And what's the answer then? The post there discusses [[ .. ]] which is implemented quite differently from [ .. ] in Bash. I don't see an answer about [ .. ] there. That's also just a comment which are prone to disappearing without notice. Not that closing this would matter very much at this point either... – ilkkachu May 18 '20 at 18:15
  • @ilkkachu, I think AdminBee is referring to the question muru found here, which I missed in my initial search. However, your answer here discusses what happens if the conditional expression throws an error, which is not mentioned in the other answers and, IMO, is the most important difference between putting the exclamation point inside or outside the brackets. – Josh May 18 '20 at 18:28
  • @ilkkachu You are right; I should have linked the answer explicitly. And yes, I was referring to the Q&A muru found. All considering, I think it is reasonable to retract my close vote here. – AdminBee May 19 '20 at 07:07
  • 1
    @AdminBee, I mean yeah, you're right in that Is there a difference between negating before/after a test command? is the same question, just that the system doesn't allow for marking duplicates cross-site. – ilkkachu May 19 '20 at 09:34
  • Would the solution be for ikkachu to copy their answer to the question on SO and then to close my question with a note directing people to the SO question? If so, @Uncle Billy also had a succinct answer that has gotten upvotes. – Josh May 19 '20 at 12:28

3 Answers3

3

Yes, it does matter.

If you put the ! outside the [ ... ], then any error will be ignored:

var=nono

if ! [ "$var" -gt 0 ]; then echo YES; fi
bash: [: 0.1: integer expression expected
YES

if [ ! "$var" -gt 0 ]; then echo YES; fi
bash: [: 0.1: integer expression expected

That's because if ! command; then ... will match when the exit status of command is non-zero, and a [ expr ] command will have a non-zero exit status both in the case of an error and in the case where expr has evaluated to false.

Only in ksh (NOT in bash) are the two kind of similar, because in ksh a non-numeric variable will be treated as 0 in that context, instead of being an error.

3

! [ "$var" -gt 0 ] vs. [ ! "$var" -gt 0 ] shouldn't matter with current shells and utilities as long as there are no errors.


But note that you wrote if ! [ $var -gt 0 ] and if [ ! $var -gt 0 ] without the quotes around $var and that does matter!

That would break if $var was empty or contained whitespace. You'd get e.g. if ! [ -gt 0 ] where the [ test would print an error message and return a falsy exit code of 2. It would then be inverted by !, and the condition as whole would be true!

$ var=
$ if ! [ $var -gt 0 ]; then echo yes; fi
bash: [: -gt: unary operator expected
yes

In that particular context, [ ! $var -gt 0 ] could be seen as safer, as the falsy error return would stay falsy, and the branch not taken. Here, the immediate problem is in the lack of quotes, though, and that's easy to fix.

About quotes, see: When is double-quoting necessary?


However, even apart from the quoting issue, non-numeric values in $var would also give an error from [ and make the condition false. Because of that, you should test that the inputs are valid beforehand or otherwise deal with the possibility. In some cases, the proper action in case of an error could be to take the branch, e.g. if it contained an error exit. Sadly, the shell doesn't really have a good way of detecting errors, as there are no exceptions or such. To tell an error from a false result, the raw value in $? would need to be saved and consulted.

Or the test could be written so that the error case coincides with an appropriate result. For example, here the test is written with an extra negation instead of [ "$1" -le 0 ], but this has the advantage that an invalid input will trigger the error exit. This only works with the ! outside [.

$ cat foo.sh
#!/bin/bash

if ! [ "$1" -gt 0 ]; then
    echo "invalid input '$1'"
    exit 1
fi
echo "doing work on '$1'"

$ bash foo.sh
foo.sh: line 3: [: : integer expression expected
invalid input ''

The variations of values of the variable and the different tests in a matrix:

   test   \   $n    |   '0'       '1'      'x'     
 -------------------+------------------------------------            
  [   "$n" -le 0 ]  |  truthy    falsy    falsy  (w/error)
  [ ! "$n" -gt 0 ]  |  truthy    falsy    falsy  (w/error)
! [   "$n" -gt 0 ]  |  truthy    falsy    truthy (w/error)

Perhaps somewhat relatedly, in times of yore, versions of [ were known to get confused when the operands within a test looked like operators. E.g. with something like [ ! = x ] the ! could be taken as a negation and the expression as whole could not be parsed. One might fear that putting the ! inside [ might compound that issue, but I don't know if that ever would have happened in practice. In any case, POSIX-conforming versions of [ should be safe from that, at least as long as you don't use -a and -o.

See: What's the purpose of adding a prefix on both sides of a shell variable comparison to a string literal?, especially Stéphane's great answer there (from which I nicked the [ ! = x ] example).

ilkkachu
  • 138,973
  • Let's see if I understand. ! outside the brackets inverts the exit status of thetest/[ function: 0 becomes 1; non-zero becomes 0. An error inside the brackets may cause non-zero exit status, which the ! would convert to 0, which is undesirable. ! inside the brackets inverts the true/false status of the conditional expression (the core of what I'm trying to do with this code), and this inverted true/false status is passed to test/[. In this case, an error inside the brackets would be passed to test/[, which is the desired behavior. About right? Thanks. – Josh May 16 '20 at 18:31
  • @Josh, with ! inside the brackets, the test/[ utility deals with it, and inverts the return code, unless there's an error. – ilkkachu May 16 '20 at 21:30
  • @Josh, ! outside takes place after test/[ runs, so it also inverts the error case. ! inside doesn't. I added an ugly table of the cases. – ilkkachu May 16 '20 at 21:42
1

It only matters if you care about what's evaluating the !, which you may do if you take failures in the [ utility into account (e.g. $var not actually being an integer, in which case the [ utility would fail and its exit status would be non-zero, i.e. not because the test evaluated to false but because the utility failed to perform the test).

In if [ ! "$var" -gt 0 ]; then ...; fi, the ! is an argument to [ and will therefore be evaluated by the [ utility, making it internally negate the sense of the test before terminating with the appropriate exit status. (If $var is not an integer, the test would fail, and the branch would not be taken.)

In if ! [ "$var" -gt 0 ]; then ...; fi, the shell will negate (well, toggle between 0 and non-zero) the exit status of the [ utility before using it for the if statement. (If $var is not an integer, the test would fail, the shell would negate the non-zero exit status and interpret it as true, and the branch would be taken.)

For this simple test, I would probably rewrite the test as [ "$var" -le 0 ] for ease of reading and understanding the code, and, if need be, employ validation of $var as an integer.

Kusalananda
  • 333,661