Why Bash is like that: Pseudo-syntax
Bash can seem pretty random and weird at times, but most of what people see as quirks have very logical (if not very good) explanations behind them. This series of posts looks at some of them.
# Why doesn't || work in [ ] ? if [ -f /etc/inetd.conf || -d /etc/xinetd.d ]; then .. # And why does it work in [[ ]] ? if [[ -f /etc/inetd.conf || -d /etc/xinetd.d ]]; then ..
Short answer: [ is a regular command, and can’t override ||. [[ is shell syntax.
[ is a pseudo-syntactical command. That is, it’s a regular command much like cp or grep, the name just happens to be a single opening square bracket (try
ls -l /usr/bin/[).
Seeing as how it’s a regular command, it can’t affect shell grammar. In
grep -q kittens file || echo kittens >> file, grep can’t know or do anything about the fact that it’s being used with “||”. The same goes for [ in the example.
In Bash (but not necessarily other shells), [ is now a builtin command emulating /usr/bin/[ for efficiency. There’s no reason why Bash couldn’t make
[ a || b ] work, but this would break compatibility.
Instead, we have [[ which is not bound by legacy, and does in fact alter shell syntax. [[ interprets ||, &&, globs and unquoted variable expansions in ways that an external [-command couldn’t, and an internal [ therefore can’t either.
To get around it without using [[, we’d do
[ a ] || [ b ] or
[ a -o b ].
# Why is this always true (should be false when foo is empty)? if [ -n $foo ]
Short answer: $foo disappears and [ -n ] is shorthand for [ -n “-n” ], which is true
This comes back to the fact that [ is a regular command. If $foo is empty, the shell runs
[ -n ], and there’s no way for [ to know that there was a variable that was expanded out.
[ -n x ] checks that x is not empty.
[ x ] is shorthand for the same.
[ -n ] therefore checks if dash-n is empty, which it isn’t, and thus the expression is always true.
Why doesn’t [ complain if you have -n without a parameter? Same thing again. It wouldn’t be able to tell that you said
[ $1 ] rather than
[ -n ], and then scripts would break whenever you work with your parameter list or any other strings starting with a dash.
Instead, you’d use
[ -n "$foo" ], which will prevent bash from removing foo when it’s empty, and instead send in a zero-length argument. Or you could use
[[ -n $foo ]], since [[ is built into the shell grammar and can tell that there is a variable there.
# Why doesn't this work? if [ grep -q text myfile ]
Short answer: [ is a command, grep is a command. Choose one.
if [ -n "$foo" ] is the same as
if test -n "$foo", which makes it more obvious how to use other commands (
if grep -q text myfile).
Pseudo-syntactical elements makes it harder to tell language from implementation for better and for worse. It makes code look neat, but can be confusing. Especially when basically all if-statements include [.
A similar effect is seen in here-documents (
cmd << EOF), where many people only ever see the delimiter “EOF” and think it’s some kind of keyword. Next time, use
cmd << KITTENS to tip off anyone reading your script.