Ed Morton
Ed Morton

Reputation: 203665

Why is a printed null treated differently from a literal null in a condition?

Both of these commands having the same result makes sense:

$ if 1; then printf "success\n"; else printf "failure\n"; fi
-bash: 1: command not found
failure

$ if $(printf 1); then printf "success\n"; else printf "failure\n"; fi
-bash: 1: command not found
failure

because $(printf "1") is printing 1 before 1 is then executed in the if.

Given that, though, I don't understand why these produce different results:

$ if ""; then printf "success\n"; else printf "failure\n"; fi
-bash: : command not found
failure

$ if ; then printf "success\n"; else printf "failure\n"; fi
-bash: syntax error near unexpected token `;'

$ if $(printf ""); then printf "success\n"; else printf "failure\n"; fi
success

Why would the null string output by $(printf "") be treated differently than the null string coded explicitly as "" in the first command or the missing argument from the 2nd command? What is it that's being executed in the final command and found to succeed and why?


Update - making sure I get it!

So applying @chepner's answer to the scripts above I've added explanations (please correct me if I got anything wrong):

$ if 1; then printf "success\n"; else printf "failure\n"; fi
-bash: 1: command not found
failure

The shell parses if 1;, sees something that it expects to be a command but is actually the number 1 and so fails "command not found"

$ if $(printf 1); then printf "success\n"; else printf "failure\n"; fi
-bash: 1: command not found
failure

The shell parses if $(printf 1);, sees something that it expects to be a command, $(printf 1), executes it successfully which outputs 1. The shell sees that something was output (1) and given any output in this context the shell expects that "something" to also be a command that it should able to execute but is actually the number 1 and so fails "command not found".

$ if ""; then printf "success\n"; else printf "failure\n"; fi
-bash: : command not found
failure

The shell parses if "";, sees something that it expects to be a command but is actually the string "" and so fails "command not found"

$ if ; then printf "success\n"; else printf "failure\n"; fi
-bash: syntax error near unexpected token `;'

The shell parses if ;, does not find the command it expected to find in that context and so fails "syntax error"

$ if $(printf ""); then printf "success\n"; else printf "failure\n"; fi
success

The shell parses if $(printf "");, sees something that it expects to be a command, $(printf ""), executes it successfully which outputs nothing which the shell accepts as such and so has no new command to execute and so applies the success exit status of the last command it did run ($(printf "")) to the condition as a whole and so succeeds.

Upvotes: 2

Views: 75

Answers (2)

chepner
chepner

Reputation: 531275

Short answer: the null string output by printf disappears during the word-splitting process applied to the command substitution before the shell tries to perform command lookup on it.


When the shell reads its input, it needs to read and parse an entire command before it evaluates the command.

After reading if, the parser is committed to parsing an entire if statement before it does any evaluation. The next thing it expects is a compound list to serve as the condition. A compound list cannot begin with a semicolon, though, so the parser immediately signals an error when it sees if ;.

Skipping a little bit of detail, suffice to say that the string $(print "") does parse as a compound list. No evaluation is required yet, so if $(print ""); then ... successfully parses.

After parsing is complete, now the shell actually has to evaluate it. The process of doing so is documented under "SIMPLE COMMAND EXPANSION" in the bash man page, quoted below in its entirety with relevant passages highlighted:

SIMPLE COMMAND EXPANSION

When a simple command is executed, the shell performs the following expansions, assignments, and redirections, from left to right.

  1. The words that the parser has marked as variable assignments (those preceding the command name) and redirections are saved for later process- ing.

  2. The words that are not variable assignments or redirections are expanded. If any words remain after expansion, the first word is taken to be the name of the command and the remaining words are the arguments.

  3. Redirections are performed as described above under REDIRECTION.

  4. The text after the = in each variable assignment undergoes tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal before being assigned to the variable.

    If no command name results, the variable assignments affect the current shell environment. Otherwise, the variables are added to the environment of the executed command and do not affect the current shell environment. If any of the assignments attempts to assign a value to a readonly variable, an error occurs, and the command exits with a non-zero status.

    If no command name results, redirections are performed, but do not affect the current shell environment. A redirection error causes the command to exit with a non-zero status.

    If there is a command name left after expansion, execution proceeds as described below. Otherwise, the command exits. If one of the expansions con- tained a command substitution, the exit status of the command is the exit status of the last command substitution performed. If there were no com- mand substitutions, the command exits with a status of zero.

So, $(print "") succeeds but expands to an empty list of words. As a result, the condition for the if statement "executes" by immediately returning with the exit status of the command substitution, resulting in the true branch being taken.

Upvotes: 2

lojza
lojza

Reputation: 1891

Look at exit codes:

$ 1; echo $?
1: command not found
127

$ $(printf 1); echo $?
1: command not found
127

$ ""; echo $?
Command '' not found    
127

$(printf ""); echo $?
0

You probably wanted:

$ if [ 1 ]; then printf "success\n"; else printf "failure\n"; fi
success

$ if [ $(printf 1) ]; then printf "success\n"; else printf "failure\n"; fi
success

$ if [ "" ]; then printf "success\n"; else printf "failure\n"; fi
failure

$ if [ $(printf "") ]; then printf "success\n"; else printf "failure\n"; fi
failure

Note the space after [ and before ]

Check man [

Upvotes: 1

Related Questions