Assignments are like commands with an exit status except when there's command substitution?

The exit status for assignments is strange. The most obvious way for an assignment to fail is if the target variable is marked readonly.

$ err(){ echo error ; return ${1:-1} ; }
$ PS1='$? $ '
0 $ err 42
error
42 $ A=$(err 12)
12 $ if A=$(err 9) ; then echo wrong ; else E=$? ; echo "E=$E ?=$?" ; fi
E=9 ?=0
0 $ readonly A
0 $ if A=$(err 10) ; then echo wrong ; else E=$? ; echo "E=$E ?=$?" ; fi
A: is read only
1 $

Note that neither the true nor false paths of the if statement were taken, the assignment failing stopped execution of the entire statement. bash in POSIX mode and ksh93 and zsh will all abort a script if an assignment fails.

To quote the POSIX standard on this:

A command without a command name, but one that includes a command substitution, has an exit status of the last command substitution that the shell performed.

This is exactly the part of the shell grammar involved in

 foo=$(err 42)

which comes from a simple_command (simple_command → cmd_prefix → ASSIGNMENT_WORD). So if an assignment succeeds then the exit status is zero unless command substitution was involved, in which case the exit status is the status of the last one. If the assignment fails then the exit status is non-zero, but you may not be able to catch it.


You say,

… 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.  But it 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.

Also, as icarus points out, variables can be set as readonly.  Consider the following variation on icarus’s example:

$ err() { echo "stdout $*"; echo "stderr $*" >&2; return ${1:-1}; }
$ readonly A
$ Z=$(err 41 zebra) A=$(err 42 antelope) B=$(err 43 badger)
stderr 41 zebra
stderr 42 antelope
bash: A: readonly variable
$ echo $?
1
$ printf "%s = %s\n" Z "$Z" A "$A" B "$B"
Z = stdout 41 zebra
A =
B =
$

Even though A is readonly, bash executes the command substitution to the right of A= — and then aborts the command because A is readonly.  This further contradicts your interpretation that the exit value of the assignment applies before the right side of the assignment.