1

I get the following output:

$ touch bar
$ if [ -a bar ]; then echo "Yes"; fi
Yes
$ if [ ! -a bar ]; then echo "Yes"; fi
Yes
$ rm bar
$ if [ -a bar ]; then echo "Yes"; fi
$ if [ ! -a bar ]; then echo "Yes"; fi
Yes

I expected that the second command should not print "Yes" because ! -a should be the opposite of -a. What's going wrong?

Using -f instead of -a does behave as I would expect.

Version is GNU bash, version 4.3.42(3)-release (i686-pc-cygwin).

M.M
  • 138,810
  • 21
  • 208
  • 365
  • In both `bash` and `sh` the option `-a` within `[ expression ]` can be evaluated as `logical and` `&` such as `if [ -f /tmp/test -a -f /tmp/other ]; echo ok; fi`. – alvits Nov 06 '15 at 03:59
  • @alvits I see. I got the idea of using `-a` from [this thread](http://stackoverflow.com/questions/5553352/how-do-i-check-if-file-exists-in-makefile) which was nice enough to not mention the issue :) – M.M Nov 06 '15 at 04:00

3 Answers3

3

From the bash man page for [ with three arguments (note my emphasis):

The following conditions are applied in the order listed.

  • If the second argument is one of the binary conditional operators listed above under CONDITIONAL EXPRESSIONS, the result of the expression is the result of the binary test using the first and third arguments as operands. The '-a' and '-o' operators are considered binary operators when there are three arguments.

  • If the first argument is '!', the value is the negation of the two-argument test using the second and third arguments.

  • If the first argument is exactly '(' and the third argument is exactly ')', the result is the one-argument test of the second argument.

  • Otherwise, the expression is false.

In other words, -a is not acting as you expect it to in this case. It's actually being treated as the logical-and operator and, since both ! and bar are considered true, the result of anding them is also true:

pax> if [ ! ] ; then echo yes ; fi
yes
pax> if [ bar ] ; then echo yes ; fi
yes

The foibles of [ are many and varied, which is why sensible bash coders will use [[ instead :-)

Community
  • 1
  • 1
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
2

If you want to stick with [ ... ], use (quoted) parentheses to disambiguate[1] .

if [ ! \( -a bar \) ]; then echo "Yes"; fi    

If you're writing a bash script (without needing to worry about portability), use [[ ... ]], which obviates the need for parentheses:

if [[ ! -a bar ]]; then echo "Yes"; fi       

[1] Barmar, in a since deleted answer, explained the ambiguity inherent in [ ! -a bar ]:
-a serves both as a unary file-existence operator and as the binary logical AND operator.
In the case at hand, -a, due to the presence of 3 arguments, is interpreted as the logical operator, treating ! and bar as its literal operands: both ! and bar are non-empty strings, which are considered truthy in a Boolean context, so the overall expression is always true.

By contrast, bash's [[ ... ]] construct exclusively uses && for logical AND (and || for OR) to avoid this ambiguity; in other words: -a is only recognized as the unary file-existence operator, so there's no ambiguity, and parentheses aren't needed.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
1

try this :

$ touch bar
$ if [[ -a bar ]]; then echo "Yes"; fi
Yes
$ if [[ ! -a bar ]]; then echo "Yes"; fi
<NOTHING>

Ref : http://mywiki.wooledge.org/BashFAQ/031

Special primitives that [[ is defined to have, but [ may be lacking (depending on the implementation):

Description
Primitive
Example
entry (file or directory) exists
-e
[[ -e $config ]] && echo "config file exists: $config"
file is newer/older than other file
-nt / -ot
[[ $file0 -nt $file1 ]] && echo "$file0 is newer than $file1"
two files are the same
-ef
[[ $input -ef $output ]] && { echo "will not overwrite input file: $input"; exit 1; } 
negation
!
[[ ! -u $file ]] && echo "$file is not a setuid file"
Akhil Thayyil
  • 9,263
  • 6
  • 34
  • 48