Z4-tier
Z4-tier

Reputation: 7988

Bash: test (builtin) with several arguments: how are they parsed?

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?

Upvotes: 2

Views: 1064

Answers (3)

Z4-tier
Z4-tier

Reputation: 7988

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.

Upvotes: 1

user1934428
user1934428

Reputation: 22291

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.

Upvotes: 1

Barmar
Barmar

Reputation: 781779

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

Upvotes: 1

Related Questions