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
0All 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 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.
local
ties into this? E.g.local foo=$(bar)
? – Wildcard Mar 19 '16 at 03:37FOO=$(bar)
) it's worth noting that theoretically both the exit status and the assignment might play a role, see http://unix.stackexchange.com/a/341013/117599 – phk Jan 30 '17 at 20:39FOO=$(bar)
)? The first three examples (all the ones that begin withFOO=
) have an exit status of 1 ifFOO
is read-only. – G-Man Says 'Reinstate Monica' Jan 31 '17 at 06:10A=foo cmd
runscmd
even ifA
is read-only? – G-Man Says 'Reinstate Monica' Jan 31 '17 at 06:11A
is readonly, the commandC=value₁ A=value₂ T=value₃
setsC
but notT
(and, of course,A
is not set) — the shell terminated processing the command line, ignoringT=value₃
, becauseA=value₂
is an error. (2) Thanks for the link to the [SO] question — I’ve posted come comments on it. – G-Man Says 'Reinstate Monica' Feb 07 '17 at 23:00local=$(false)
has exit value0
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. – David Tonhofer May 03 '18 at 17:36test
as an arbitrary identifier. As I’m sure you know, it’s a shell builtin command, and that just makes things confusing. (6) You reported only half of what you discovered! I concede that Wildcard’s question focused your search, but, once you found the shellcheck SC2155 error page, … (Cont’d) – G-Man Says 'Reinstate Monica' May 03 '18 at 21:01local=$(false)
— is a typo. … (Cont’d) – G-Man Says 'Reinstate Monica' May 03 '18 at 21:01