18

I have seen constructs in scripts such as this:

if somevar="$(somecommand 2>/dev/null)"; then
...
fi

Is this documented somewhere? How is the return status of a variable determined and how does it relate to command substitution? (For instance, would I get the same result with if echo "$(somecommand 2>/dev/null)"; then?)

2 Answers 2

27

It is documented (for POSIX) in Section 2.9.1 Simple Commands of The Open Group Base Specifications.  There's a wall of text there; I direct your attention to the last paragraph:

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.

So, for example,

   Command                                         Exit Status
$ FOO=BAR                                   0 (but see also the note from icarus, below)
$ FOO=$(bar)                                Exit status from "bar"
$ FOO=$(bar)$(quux)                         Exit status from "quux"
$ FOO=$(bar) baz                            Exit status from "baz"
$ foo $(bar)                                Exit status from "foo"

This is how bash works, too.  But see also the “not so simple” section at the end.

phk, in his question Assignments are like commands with an exit status except when there’s command substitution?, suggests

… it appears as if an assignment itself counts as a command … with a zero exit value, but which applies before the right side of the assignment (e.g., a command substitution call…)

That’s not a terrible way of looking at it.  A crude scheme for determining the return status of a simple command (one not containing ;, &, |, && or ||) is:

  • Scan the line from left to right until you reach the end or a command word (typically a program name).

  • If you see a variable assignment, the return status for the line just might be 0.

  • If you see a command substitution — i.e., $(…) — take the exit status from that command.

  • If you reach an actual command (not in a command substitution), take the exit status from that command.

  • The return status for the line is the last number you encountered.
    Command substitutions as arguments to the command, e.g., foo $(bar), don’t count; you get the exit status from foo.  To paraphrase phk’s notation, the behavior here is

      temporary_variable  = EXECUTE( "bar" )
      overall_exit_status = EXECUTE( "foo", temporary_variable )
    

But this is a slight oversimplification.  The overall return status from

A=$(cmd1)  B=$(cmd2)  C=$(cmd3)  D=$(cmd4)  E=mc2
is the exit status from cmd4.  The E= assignment that occurs after the D= assignment does not set the overall exit status to 0.

icarus, in his answer to phk’s question, raises an important point: variables can be set as readonly.  The third-to-last paragraph in Section 2.9.1 of the POSIX standard says,

If any of the variable assignments attempt to assign a value to a variable for which the readonly attribute is set in the current shell environment (regardless of whether the assignment is made in that environment), a variable assignment error shall occur.  See Consequences of Shell Errors for the consequences of these errors.

so if you say

readonly A
C=Garfield A=Felix T=Tigger

the return status is 1.  It doesn’t matter if the strings Garfield, Felix, and/or Tigger are replaced with command substitution(s) — but see notes below.

Section 2.8.1 Consequences of Shell Errors has another bunch of text, and a table, and ends with

In all of the cases shown in the table where an interactive shell is required not to exit, the shell shall not perform any further processing of the command in which the error occurred.

Some of the details make sense; some don’t:

  • The A= assignment sometimes aborts the command line, as that last sentence seems to specify.  In the above example, C is set to Garfield, but T is not set (and, of course, neither is A).
  • Similarly, C=$(cmd1) A=$(cmd2) T=$(cmd3) executes cmd1 but not cmd3.
    But, in my versions of bash (which include 4.1.X and 4.3.X), it does execute cmd2.  (Incidentally, this further impeaches phk’s interpretation that the exit value of the assignment applies before the right side of the assignment.)

But here’s a surprise:

In my versions of bash,

readonly A
C=something A=something T=something cmd0

does execute cmd0.  In particular,

C=$(cmd1)   A=$(cmd2)   T=$(cmd3)   cmd0
executes cmd1 and cmd3, but not cmd2.  (Note that this is the opposite of its behavior when there is no command.)  And it sets T (as well as C) in the environment of cmd0.  I wonder whether this is a bug in bash.


Not so simple:

The first paragraph of this answer refers to “simple commands”.  The specification says,

A “simple command” is a sequence of optional variable assignments and redirections, in any sequence, optionally followed by words and redirections, terminated by a control operator.

These are statements like the ones in my first example block:

$ FOO=BAR
$ FOO=$(bar)
$ FOO=$(bar) baz
$ foo $(bar)

the first three of which include variable assignments, and the last three of which include command substitutions.

But some variable assignments aren’t quite so simple.  bash(1) says,

Assignment statements may also appear as arguments to the alias, declare, typeset, export, readonly, and local builtin commands (declaration commands).

For export, the POSIX specification says,

EXIT STATUS

    0
      All name operands were successfully exported.
    >0
      At least one name could not be exported, or the -p option was specified and an error occurred.

And POSIX doesn’t support local, but bash(1) says,

It is an error to use local when not within a function.  The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.

Reading between the lines, we can see that declaration commands like

export FOO=$(bar)

and

local FOO=$(bar)

are more like

foo $(bar)

insofar as they ignore the exit status from bar and give you an exit status based on the main command (export, local, or foo).  So we have weirdness like

   Command                                           Exit Status
$ FOO=$(bar)                                    Exit status from "bar"
                                                  (unless FOO is readonly)
$ export FOO=$(bar)                             0 (unless FOO is readonly,
                                                  or other error from “export”)
$ local FOO=$(bar)                              0 (unless FOO is readonly,
                                                  statement is not in a function,
                                                  or other error from “local”)

which we can demonstrate with

$ export FRIDAY=$(date -d tomorrow)
$ echo "FRIDAY   = $FRIDAY, status = $?"
FRIDAY   = Fri, May 04, 2018  8:58:30 PM, status = 0
$ export SATURDAY=$(date -d "day after tomorrow")
date: invalid date ‘day after tomorrow’
$ echo "SATURDAY = $SATURDAY, status = $?"
SATURDAY = , status = 0

and

myfunc() {
    local x=$(echo "Foo"; true);  echo "x = $x -> $?"
    local y=$(echo "Bar"; false); echo "y = $y -> $?"
    echo -n "BUT! "
    local z; z=$(echo "Baz"; false); echo "z = $z -> $?"
}

$ myfunc
x = Foo -> 0
y = Bar -> 0
BUT! z = Baz -> 1

Luckily ShellCheck catches the error and raises SC2155, which advises that

export foo="$(mycmd)"

should be changed to

foo=$(mycmd)
export foo

and

local foo="$(mycmd)"

should be changed to

local foo
foo=$(mycmd)

Credit and Reference

I got the idea of concatenating command substitutions — $(bar)$(quux) — from Gilles’s answer to How can I get bash to exit on backtick failure in a similar way to pipefail?, which contains a lot of information relevant to this question.

13
  • 1
    Great, thank you! For bonus points, do you know how local ties into this? E.g. local foo=$(bar)?
    – Wildcard
    Mar 19, 2016 at 3:37
  • 1
    For the second example (just FOO=$(bar)) it's worth noting that theoretically both the exit status and the assignment might play a role, see unix.stackexchange.com/a/341013/117599
    – phk
    Jan 30, 2017 at 20:39
  • 1
    @Wildcard:  I’m glad you like it.  I’ve just updated it again; a big part of the version that you just read was wrong.  As long as you’re here, what do you think?  Is this a bug in bash, that A=foo cmd runs cmd even if A is read-only? Jan 31, 2017 at 6:11
  • 1
    @phk:  (1) Interesting theory, but I’m not sure how it makes sense.  Take another look at the example just before my “surprise” heading.  If A is readonly, the command C=value₁ A=value₂ T=value₃ sets C but not T (and, of course, A is not set) — the shell terminated processing the command line, ignoring T=value₃, because A=value₂ is an error.  (2) Thanks for the link to the Stack Overflow question — I’ve posted come comments on it. Feb 7, 2017 at 23:00
  • 1
    @Wildcard "For bonus points, do you know how local ties into this?". Yes... local=$(false) has exit value 0 because (from the man page): It is an error to use local when not within a function. The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.. This is not enough trollface on the world for the genius who designed it that way. May 3, 2018 at 17:36
2

It is documented in Bash (LESS=+/'^SIMPLE COMMAND EXPANSION' bash):

If there is a command name left after expansion ... . Otherwise, the command exits. ... If there were no command substitutions, the command exits with a status of zero.

In other words (my words):

If there is no command name left after expansion, and no command substitutions were executed, the command line exits with a status of zero.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .