How is the return status of a variable assignment determined?
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 fromfoo
. To paraphrase phk’s notation, the behavior here istemporary_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=mc2is 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 toGarfield
, butT
is not set (and, of course, neither isA
). - Similarly,
C=$(cmd1) A=$(cmd2) T=$(cmd3)
executescmd1
but notcmd3
.
But, in my versions of bash (which include 4.1.X and 4.3.X), it does executecmd2
. (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) cmd0executes
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
, andlocal
builtin commands (declaration commands).
For export
, the POSIX specification says,
EXIT STATUS
0
All name operands were successfully exported.
>0At 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 unlesslocal
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.
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.