Suppose we have an if statement as follows:
if [ $(false) ]; then
echo "?"
fi
Then "?" is not printed (the condition is false). However, in the following case, "?!" is printed, why?
if [ $(false) -o $(false) ]; then
echo "?!"
fi
Suppose we have an if statement as follows:
if [ $(false) ]; then
echo "?"
fi
Then "?" is not printed (the condition is false). However, in the following case, "?!" is printed, why?
if [ $(false) -o $(false) ]; then
echo "?!"
fi
$(false)
doesn’t evaluate to false, it produces an empty string. Because it isn’t quoted,
if [ $(false) ]; then
evaluates to
if [ ]; then
which is false, because [
with an empty expression is false.
if [ $(false) -o $(false) ]; then
evaluates to
if [ -o ]; then
This doesn’t use the -o
operator, it evaluates -o
as an expression with a single string; [
with such an expression is true, so the then
part of the if
statement runs.
See the POSIX specification for test
, in particular:
The algorithm for determining the precedence of the operators and the return value that shall be generated is based on the number of arguments presented to test. (However, when using the "[...]" form, the <right-square-bracket> final argument shall not be counted in this algorithm.)
In the following list, $1, $2, $3, and $4 represent the arguments presented to test:
0 arguments:
Exit false (1).
1 argument:
Exit true (0) if $1 is not null; otherwise, exit false.
test
only considers operators if it is given at least two arguments.
If you want to use a command’s exit status as a condition, don’t put it either in a command substitution or in [ ]
:
if false; then
and
if false || false; then
Note too that test
’s -a
and -o
operators are deprecated and unreliable; you should use the shell’s &&
and ||
operators instead, e.g.
if [ "$a" = b ] || [ "$a" = c ]; then
-o
and -a
options for test
and [
are deprecated and their use is heavily discouraged by sane developers. There are technically other cases as well where they cause problems, but the specific case of one or both sides evaluating to an empty string unexpectedly and changing how the argument list is parsed is probably the biggest issue with their usage.
– Austin Hemmelgarn
Nov 26 '22 at 16:00
""
(i.e. an empty string, which is passed as a separate argument), but when unquoted it gets split+globbed, causing it to evaluate to nothing at all (i.e. no separate argument). As a result, [ "$(false)" -o "$(false)" ]
evaluates to false (empty string or empty string).
– Kevin
Nov 26 '22 at 23:07
I won't repeat the points made in the fine answers by @StephenKitt and @ilkkachu here, just focus on what it means to write if [ $(cmd) ]; then...
.
First note that [
is not a POSIX sh
operator, it's a separate utility also known as test
, here used in the if
part of a if
/then
/elif
/else
/fi
statement in the syntax of the sh
language.
Its job is to evaluate its arguments as a conditional expression. For instance, if it receives [
, a
, =
, b
and ]
as arguments or test
, a
, =
and b
, it will return a false/failure exit status as it has interpreted it as the "is 'a' the same string as 'b'?" conditional expression.
[
is often used as the one and only command in the if
part of a if
/then
/elif
/else
/fi
statement or in while
/do
/done
or until
/do
/done
statement, but it doesn't have to be nor do those statement have to invoke [
nor a single command.
Doing [ $(cmd) ]
means calling the [
command with [
, the result of the $(cmd)
expansion and ]
as separate arguments. Then [
interprets those arguments as a conditional expression and returns true or false depending on the result (also false if it can't understand the expression).
In POSIX sh, $(cmd)
expands to the standard output of cmd
stripped of all trailing newline characters, and as it's not quoted and in list context, subject to IFS-splitting, and then globbing (with the behaviour unspecified if there are NUL characters in that output).
So if [ $(cmd) ]
or test $(cmd)
doesn't really make any sense.
It's something like asking the question: "does the output of cmd
, once split+globbed make up a valid conditional expression that results to true?"
For instance, if cmd
outputs a,*
and $IFS
happens to contain ,
and the current working directory contains two files, one called =
, one called b
, [ $(cmd) ]
will call [
with [
, a
, =
, b
, ]
as arguments. Quite luckily, that happens to be a valid conditional expression, but one that returns false as a
is not b
.
$ cd "$(mktemp -d)"
$ touch = b
$ cmd() { echo 'a,*'; }
$ IFS=,
$ set -o xtrace
$ if [ $(cmd) ]; then echo yes; else echo no; fi
++ cmd
++ echo 'a,*'
+ '[' a = b ']'
+ echo no
no
Now, [ "$(cmd)" ]
makes quite a bit more sense. Quoting a command substitution doesn't prevent the stripping of trailing newline characters, still is unspecified when cmd
outputs NULs, but it prevents split+glob and empty removal. So [
will always be passed 3 arguments: [
, the output of cmd
stripped of trailing newline characters and ]
. With one argument beside [
and ]
, [
returns true if and only if that argument is not an empty string, it's the same as [ -n "$(cmd)" ]
.
So it's like saying: "does cmd
output at least one character that is not a newline character (or NUL)". For commands that output text (text being guaranteed to be made of lines and to not contain NULs), that's "does cmd
output at least one non-empty line?"
Except for short output, it does that in an inefficient way, as we end up reading the whole output and storing it in memory, and then pass is to [
.
if cmd | grep -q .; then...
Would have been more efficient as grep
would exit with true as soon as it finds a line that contains at least one character.
If cmd
was yes
, if [ "$(cmd)" ]
would eventually crash the shell with a not enough memory error, while if cmd | grep -q .
would output yes straight away (and yes
would be terminated via a SIGPIPE signal).
To test whether cmd
succeeds regardless of what it may or may not output, you just do:
if cmd; then
echo cmd succeeded
else
echo cmd failed
fi
if $(cmd); then
is another example of code that makes little sense. But there's a bit of a twist in the example of cmd
being false
.
Again, $(cmd)
is stripped of the trailing newline characters, subject to split+glob. But this time, the resulting words are not passed to a [
/ test
utility, but the first of those words if any is treated as the command to execute, and all the words passed as arguments to it.
So if cmd
outputs echo:hello:world
and $IFS
contains :
, that ends up running echo
with echo
, hello
and world
as arguments and assuming echo
manages to successfully write "hello world\n"
on its stdout, it will return true/success, so the then
part will be run.
Now, if cmd
produces no output or only newline characters and IFS-whitespace characters, then $(cmd)
will result in no word at all, so another command won't be run, and then at that point, it's the exit status of cmd
itself that will matter to the if
statement.
In:
if $(false); then
echo yes
else
echo no
fi
no
is output because since false
produces no output, no command was run and it's the exit status of false
that decides that the else
part be run.
In:
if $(echo true; false); then
echo yes
else
echo no
fi
and assuming $IFS
contains no t
, r
, u
nor e
character; true
ends up being run and it's its exit status that determines the branching in the if
statement.
[
is not a POSIXsh
operator, it's a separate utility akatest
, here used in theif
part of aif/then/elif/else/fi
statement in thesh
language syntax. – Stéphane Chazelas Nov 25 '22 at 13:08$(false)
was going to give you its exit code, because why would word splitting happen on an exit code? – Patrick Stevens Nov 28 '22 at 08:51