13

Consider a simple debugging style where $debug would be set either to true or false according to some command-line flag:

$debug && echo "Something strange just happened" >&2

I know there are better ways to protect against value setting, such as ${debug:-false} or even a full-featured logMessage()-type function, but let's park that for now.

Assume the bad situation where $debug remains unset or empty. I expected that $debug && echo … for such an empty $debug would be parsed and evaluated into && echo …, which would trigger a consequent syntax error. But instead it seems to be evaluated into something like : && echo …, which ends up executing the echo.

I've tested with bash, ksh, and dash and the scenario remains consistent.

My understanding was that these two lines parsed out as equivalent, but clearly they do not:

unset debug; $debug && echo stuff
unset debug; && echo stuff

Why does the code act as if an unset variable is logically true instead of failing with an error such as syntax error near unexpected token `&&'?

roaima
  • 107,089
  • 14
  • 139
  • 261
  • 1
    It's not about variables, but about commands. try to set `$debug` to `ls` and you might get the idea. https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash might help. – stoney Oct 13 '22 at 12:56
  • @stoney I'm using `true` and `false` as commands. Whether you replace them with `ls` or something else, the question is about the case when `$debug` isn't set at all. – roaima Oct 13 '22 at 13:06
  • Do you find `$(echo "$debug") && echo "Something strange just happened" >&2` equally surprising? or less? or more? – Kamil Maciorowski Oct 13 '22 at 13:55
  • @KamilMaciorowski interesting question. I had to think about that for a while. On first view less surprising. However, on consideration I'm tempted to put it in the same bucket as my question, so equally surprising. (Particularly having tried `$(echo -n "")` && …`) – roaima Oct 13 '22 at 14:04
  • i usually use numbers 1 and 0 instead of words true and false, to test I say like `(( debug )) && echo Something` – Jasen Oct 13 '22 at 23:01
  • 1
    See also https://stackoverflow.com/questions/18338234/bash-booleans-default-to-true – Barmar Oct 14 '22 at 14:18
  • @Barmar I don't see the relevance of that here; can you explain please? – roaima Oct 14 '22 at 15:11
  • 2
    @roaima It's the exact same thing: an empty command after variable expansion is successful. – Barmar Oct 14 '22 at 15:13

4 Answers4

13

But syntax isn't checked after the expansion is done.

$foo is a syntactically valid simple command regardless of the value the variable has and the number of fields produced after the expansion. It's one shell "word" before expansions, and that's enough.

Having $foo && whatever be equal to && whatever would be similar to expecting the shell to parse the results of the expansion for other syntax too, keywords, quotes, whatever. That's not done.

Hence:

foo="then if"
$foo

is not a syntax error, but tries to run a regular command called then (which probably doesn't exist and you get an error, but it could exist, and the "command not found" error is a different one from the syntax error from trying to run then if all by itself.).

And:

foo=
$foo

is not a syntax error, but tries to run a... well, it doesn't because there's no "command name", and while that's a sort of a special case, it's not an error. See 2.9.1 Simple Commands:

When a given simple command is required to be executed, the following expansions, assignments, and redirections shall all be performed from the beginning of the command text to the end:

  1. [assignments, redirections]

  2. The words that are not variable assignments or redirections shall be expanded. If any fields remain following their expansion, the first field shall be considered the command name and remaining fields are the arguments for the command. [...]

Note the "if".

What results without a command name is that variable assignments affect the current execution environment, and if there is no command substitution either, then the exit status is zero.

So if $foo is empty or unset:

$foo

just does nothing and sets the exit status to zero, and

bar=asdf $foo

sets $bar to asdf and the exit status to zero. The same as just bar=asdf, but if $foo did expand to a command name, the assignment would only apply for the command.

ilkkachu
  • 133,243
  • 15
  • 236
  • 397
10

From the Shell Command Language specification, section 2.9.1 Simple Commands, after all the redirections, glob/parameter expansions, etc. are performed:

If there is a command name, execution shall continue as described in Command Search and Execution. If there is no command name, but the command contained a command substitution, the command shall complete with the exit status of the last command substitution performed. Otherwise, the command shall complete with a zero exit status.

You're in the "otherwise" case.

ilkkachu
  • 133,243
  • 15
  • 236
  • 397
William Pursell
  • 3,497
  • 1
  • 16
  • 19
  • 1
    Interesting. This seems to be a pretty direct explanation of: `$(true) ; echo $?` outputting `0` and `$(false) ; echo $?` outputting `1`. – Jim L. Oct 13 '22 at 18:29
  • 1
    @JimL., yes, and it might be more useful in practice with assignments, e.g. `if ! foo="$(some cmd)"; then echo "error"; fi` (since well, with just `$(foo) ; echo $?`, if `foo` printed any output, that would run as a command and define the exit status) – ilkkachu Oct 14 '22 at 15:06
7

A simple command consists of assignments, redirections and arguments (the first of which used to derive the command to run).

All are optional. If there's no arguments, the command succeeds as long as all the redirections can be performed and the last run command substitution in the target of redirections and assignments (or the ones used to generate an empty list of arguments) succeeds, though IIRC, it's unspecified which of the redirections and assignments are done first.

All those succeed:

  • a=whatever: no command, no redirection, only assignment
  • $(true): no command, no redirection, no assignment, successful command substitution that results in an empty list.
  • a=$(false) b=$(true). Only assignment, the last command substitution succeeds.
  • > /dev/null: no command, successful redirection
  • > "$(echo /dev/null)": no command, successful command substitution and redirection
  • empty=; a=foo $empty > /dev/null: assignments and redirections, and an expansion that results in an empty list, so no argument/command.
  • empty=; $empty: same as above without assignment nor redirection.

These fail:

  • false
  • a=$(false)
  • $(false)
  • > /etc/passwd/foo
  • < /dev/null"$(false)"
  • ...

Unspecified:

  • $(true) > /dev/null$(false)
  • a=$(false) > /dev/null$(true)
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • 2
    Thanks. But I'm not sure that's answering my question. `$qweqweqwe && echo yes` succeeds and writes `yes`. But `&& echo yes` fails quite reasonably with a syntax error – roaima Oct 13 '22 at 13:08
4

Consider that && is an operator that joins 2 lists of commands pipelines (3.2.4 Lists of Commands).

The first command is $debug. If the variable is unset, there is no command -- it's just like hitting Enter at a prompt: no command is executed but also no non-zero exit status.

Since there was no error on the left-hand side of &&, the right-hand side is free to run.

Quoting will give you the error you seek

  • bash

    $ unset debug; $debug && echo hmm
    hmm
    $ unset debug; "$debug" && echo hmm
    bash: : command not found
    
  • ksh

    $ unset debug; $debug && echo hmm
    hmm
    $ unset debug; "$debug" && echo hmm
    ksh: : cannot execute [Is a directory]
    
  • dash

    $ unset debug; $debug && echo hmm
    hmm
    $ unset debug; "$debug" && echo hmm
    dash: 9: : Permission denied
    
glenn jackman
  • 84,176
  • 15
  • 116
  • 168
  • Why is `unset debug; $debug && echo hmm` ok but `unset debug; && echo hmm` fails? Don't they parse out as the same? (I'm comfortable with `"" && echo hmm` being an error) – roaima Oct 13 '22 at 14:14
  • 1
    Don't know. I can only hand-wave "order of expansions", "quirks of the parser" etc. A careful reading of [Executing Commands](https://www.gnu.org/software/bash/manual/bash.html#Executing-Commands) will possible reveal an answer – glenn jackman Oct 13 '22 at 14:17
  • 1
    I'm favouring hand-waving myself, along with "if it hurts, don't do that". But I was curious – roaima Oct 13 '22 at 14:19
  • 2
    @roaima I believe the point is that the shell parses its input first (and `$debug && echo hmm` is fine to the parser), then it performs expansions (and does all the other things), but no result of these steps is parsed again. – fra-san Oct 13 '22 at 14:23
  • @fra-san I think you've nailed it, along with glennjackmann's reference to [Executing Commands](https://www.gnu.org/software/bash/manual/bash.html#Executing-Commands) - in particular "_If there is a command name left after expansion, execution proceeds as described below. Otherwise, the command exits._" – roaima Oct 13 '22 at 14:56
  • It's not like just hitting enter at the prompt, that would be syntactically very different from a simple command that expands to nothing. For one, that empty line doesn't affect `$?`, and you can put empty lines in far more places than simple commands (even ones that expand to nothing). – ilkkachu Oct 14 '22 at 15:15