2

I have not used this particular construct for the bash builtin test / [ commands, but I ran into it today and I'm confused. It looks like this:

[ -n "${FOO}" -a -r ${FOO}/bar ] && echo OK

I know what each switch does individually, but I am not sure about the behavior when they are grouped like this. Specifically, the -a -r [operand] part. Here is what the man page has to say:

test and [ evaluate conditional expressions using a set of rules based on the number of arguments.

0 arguments
The expression is false.

1 argument
The expression is true if and only if the argument is not null.

2 arguments
If the first argument is !, the expression is true if and only if the second argument is null. If the first argument is one of the unary conditional operators listed above under CONDITIONAL EXPRESSIONS, the expression is true if the unary test is true. If the first argument is not a valid unary conditional operator, the expression is false.

3 arguments
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.

4 arguments
If the first argument is !, the result is the negation of the three-argument expression composed of the remaining arguments. Otherwise, the expression is parsed and evaluated according to precedence using the rules listed above.

5 or more arguments
The expression is parsed and evaluated according to precedence using the rules listed above.

Ok, great. Since I provide 5 arguments, the expression will be parsed and the rules applied. I assume it will be split and evaluated in 2 parts, like this:

[ -n "${FOO}" ] && [ -a -r ${FOO}/bar ] && echo OK

But that isn't the case, as this yields [: -r: binary operator expected, it doesn't like [ -a -r ... ]. Of course, this works:

[ -n "${FOO}" ] && [ -a ${FOO}/bar ] && [ -r ${FOO}/bar ] && echo OK

And so does this:

[ -n "${FOO}" -a ${FOO}/bar ] && echo OK

but this fails:

[ -n "${FOO}" -r ${FOO}/bar ] && echo OK

adding to my confusion is the part that says:

The -a and -o operators are considered binary operators when there are three arguments

How exactly is the -a handled as a binary operator? I believe it tests the existence of a file, but does it have some other behavior here? What does it do with the second operand? How does the Bash builtin test parse the arguments in the original example? What am I missing?

Z4-tier
  • 7,287
  • 3
  • 26
  • 42
  • 1
    Note the `OB` markers in the standard, specifying that behavior with enough flags to make `-a` or `-o` relevant is considered obsolescent. – Charles Duffy Oct 24 '20 at 02:04

3 Answers3

1

You don't need -a after you parse it, since && takes its place. The equivalent parsed expression is

[ -n "${FOO}" ] && [ -r ${FOO}/bar ] && echo OK
Barmar
  • 741,623
  • 53
  • 500
  • 612
1

In your command

test -n "${FOO}" -r ${FOO}/bar 

you have two conditions, but you don't tell test whether both need to be true, or if it is sufficient that one is true, which is why you get an error. Hence you have to write one of

test -n "${FOO}" -a -r ${FOO}/bar 
test -n "${FOO}" -o -r ${FOO}/bar 

In your command

-n "${FOO}" -a ${FOO}/bar 

you also have two conditions, but here you clearly say (using -a) that both need to be true. Hence this is OK.

user1934428
  • 19,864
  • 7
  • 42
  • 87
  • is `-a` in this context interpreted as an "and" applied to the two operands `-n "${FOO}"` and `-r ${FOO}/bar` ?? That's almost too easy, I was fixated on it's unary usage to test if a file exists... – Z4-tier Mar 03 '20 at 13:04
  • `-a` is _and_, `-o` is _or_ and `!` is _not_ – user1934428 Mar 03 '20 at 14:38
  • I was having some extreme tunnel vision. Very frustrating for `-a` to mean 2 different things in a context dependent way. Assuming that **if** `${FOO}/bar` exists, **then** it is also readable: `[ -n "${FOO}" -a -r ${FOO}/bar ]` would be exactly the same as `[ -n "${FOO}" -a -a ${FOO}/bar ]`. That's pretty ugly. – Z4-tier Mar 03 '20 at 17:12
  • I was surprised that the latter one does work. I could verify that both `/usr/bin/test` and the bash-builtin `test` allow it, while the zsh-builtin `test` does not allow this command. I don't think that `/usr/bin/test` interprets `-a` as unary operator, and indeed `test -n "${FOO}" -a -a -a ${FOO}/bar` is accepted as well. Similarily, `test -n "${FOO}" -o -o -o ${FOO}/bar` is also accepted as syntactically correct. – user1934428 Mar 04 '20 at 08:05
  • @Z4-tier : Interestingly, the _test_ man page explicitly recommends **not** using `-a` and `-o`: _ Binary -a and -o are inherently ambiguous. Use 'test EXPR1 && test EXPR2' or 'test EXPR1 || test EXPR2' instead._ My guess is that the weird /usr/bin/test and bash's _test_ interpretation of -a and -o are for compatibility with very old Bourne shell scripts, while zsh, which does not really care about compatibility to other shells, did this right and rejects such strange constructs. – user1934428 Mar 04 '20 at 08:09
  • 2
    That is funny. This is definitely going on my "don't ever do this" list. I absolutely don't understand why anyone would want to use such arcane constructs when exactly the same thing can be expressed in a way that is pretty much universally understood. At best it saves, what, 3 key strokes? A few bytes on the file size? You must be right, this has to be a throwback to original Bourne shell (which added a `test` builtin in 1981). The `-a -a -a` reminds me of this: https://en.wikipedia.org/wiki/Buffalo_buffalo_Buffalo_buffalo_buffalo_buffalo_Buffalo_buffalo – Z4-tier Mar 04 '20 at 15:58
1

The other answers to this are both correct, but I will add my own so as to directly address exactly what lead to my confusion in the first place.

This applies to the Bash builtins and also (with some slight changes to the examples) the BSD /bin/test.

The test / [ (I'll use test to refer to both) command implements a grammar that is inherently ambiguous: it will accept the switches -a and -o, but their meaning depends on the context. When applied as a connective between two boolean operands, -a acts as a logical AND. But when used as a unary operator, it checks for the existence of the filename given in the next argument. Similarly, -o is treated as a logical OR and also as a test of whether a shell option is enabled.

This opens the door to some strange looking expressions. As discussed in the comments under the answer by @user1934428, There is this:

# Assuming that if ${FOO}/bar exists, then it is readable: 

[ -n "${FOO}" -a -r "${FOO}"/bar ] 

# is exactly the same as either of the following:

[ -n "${FOO}" -a -a "${FOO}"/bar ]
[ -n "${FOO}" ] && [ -a "${FOO}"/bar ]

Maybe that seems trivial? Perhaps you crave confusion? I'm not even going to try deciphering these:

# Assuming that ${FOO}/bar exists and is readable, This returns 1:

[ -n "${FOO}" -a -a -a -a "${FOO}"/bar ]

# and this returns 0:

[ -n "${FOO}" -a -a -a -a -o -o -o "${FOO}"/bar ]

My takeaway from this is simple enough: stick with basic and unambiguous constructs when using test. Don't poke a sleeping bear.

Z4-tier
  • 7,287
  • 3
  • 26
  • 42