Shell – Error in shell bracket test when string is a left-parenthesis

shelltest

I used to be confident about the fact that quoting strings is always a good practice in order to avoid having the shell parsing it.

Then I came across this:

$ x='('
$ [ "$x" = '1' -a "$y" = '1' ]
bash: [: `)' expected, found 1

Trying to isolate the problem, getting the same error:

$ [ '(' = '1' -a '1' = '1' ]
bash: [: `)' expected, found 1

I solved the problem like this:

[ "$x" = '1' ] && [ "$y" = '1' ]

Still I need to know what's going on here.

Best Answer

This is a very obscure corner case that one might consider a bug in how the test [ built-in is defined; however, it does match the behaviour of the actual [ binary available on many systems. As far as I can tell, it only affects certain cases and a variable having a value that matches a [ operator like (, !, =, -e, and so on.

Let me explain why, and how to work around it in Bash and POSIX shells.


Explanation:

Consider the following:

x="("
[ "$x" = "(" ] && echo yes || echo no

No problem; the above yields no error, and outputs yes. This is how we expect stuff to work. You can change the comparison string to '1' if you like, and the value of x, and it'll work as expected.

Note that the actual /usr/bin/[ binary behaves the same way. If you run e.g. '/usr/bin/[' '(' = '(' ']' there is no error, because the program can detect that the arguments consist of a single string comparison operation.

The bug occurs when we and with a second expression. It does not matter what the second expression is, as long as it is valid. For example,

[ '1' = '1' ] && echo yes || echo no

outputs yes, and is obviously a valid expression; but, if we combine the two,

[ "$x" = "(" -a '1' = '1' ] && echo yes || echo no

Bash rejects the expression if and only if x is ( or !.

If we were to run the above using the actual [ program, i.e.

'/usr/bin/[' "$x" = "(" -a '1' = '1' ] && echo yes || echo no

the error would be understandable: since the shell does the variable substitutions, the /usr/bin/[ binary only receives parameters ( = ( -a 1 = 1 and the terminating ], it understandably fails to parse whether the open parentheses start a sub-expression or not, there being an and operation involved. Sure, parsing it as two string comparisons is possible, but doing it greedily like that might cause issues when applied to proper expressions with parenthesized sub-expressions.

The problem, really, is that the shell [ built-in behaves the same way, as if it expanded the value of x before examining the expression.

(These ambiguities, and others related to variable expansion, were a large reason why Bash implemented and now recommends using the [[ ... ]] test expressions instead.)


The workaround is trivial, and often seen in scripts using older sh shells. You add a "safe" character, often x, in front of the strings (both values being compared), to ensure the expression is recognized as a string comparison:

[ "x$x" = "x(" -a "x$y" = "x1" ]
Related Question