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" ]
[[ "$x" = '1' && "$y" = '1' ]]
-a
and-o
as obsolescent for this reason (which is what the[OB]
superscript next to their specification means). If you wrote[ "$x" = 1 ] && [ "$y" = 1 ]
instead, you'd be fine, and would be well within the realm of well-defined/standardized behavior.[ "x$x" = "x1" ]
to prevent arguments being misinterpreted as operators.dash
rather than Bash, it's still a useful practice.