72

I am confused about what error code the command will return when executing a variable assignment plainly and with command substitution:

a=$(false); echo $?

It outputs 1, which let me think that variable assignment doesn't sweep or produce new error code upon the last one. But when I tried this:

false; a=""; echo $?

It outputs 0, obviously this is what a="" returns and it override 1 returned by false.

I want to know why this happens, is there any particularity in variable assignment that differs from other normal commands? Or just be cause a=$(false) is considered to be a single command and only command substitution part make sense?

-- UPDATE --

Thanks everyone, from the answers and comments I got the point "When you assign a variable using command substitution, the exit status is the status of the command." (by @Barmar), this explanation is excellently clear and easy to understand, but speak doesn't precise enough for programmers, I want to see the reference of this point from authorities such as TLDP or GNU man page, please help me find it out, thanks again!

jww
  • 97,681
  • 90
  • 411
  • 885
Reorx
  • 2,801
  • 2
  • 24
  • 29

5 Answers5

77

Upon executing a command as $(command) allows the output of the command to replace itself.

When you say:

a=$(false)             # false fails; the output of false is stored in the variable a

the output produced by the command false is stored in the variable a. Moreover, the exit code is the same as produced by the command. help false would tell:

false: false
    Return an unsuccessful result.
    
    Exit Status:
    Always fails.

On the other hand, saying:

$ false                # Exit code: 1
$ a=""                 # Exit code: 0
$ echo $?              # Prints 0

causes the exit code for the assignment to a to be returned which is 0.


EDIT:

Quoting from the manual:

If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed.

Quoting from BASHFAQ/002:

How can I store the return value and/or output of a command in a variable?

...

output=$(command)

status=$?

The assignment to output has no effect on command's exit status, which is still in $?.

This is not bash-specific. Quoting the end of section 2.9.1 "Simple Commands" in the "Shell & Utilities" volume of the The Open Group Base Specifications Issue 7, POSIX.1-2017 :

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

MarcH
  • 18,738
  • 1
  • 30
  • 25
devnull
  • 118,548
  • 33
  • 236
  • 227
  • 90
    Keep in mind that this doesn't apply to `local` variables (eg, `local output=$(command)`), because `local` is a command itself and its exit-code will overwrite that of the assigned function. – sevko Jul 19 '14 at 02:45
  • 1
    @sevko this distinction makes all the difference, thanks for pointing that out! – qodeninja Apr 02 '15 at 17:20
  • 31
    Regarding @sevko & @qodeninja 's comments about `local`s, this can easily be circumvented by initialization after declaration unless the variable is also `readonly`: `local my_var; my_var=$(command); local status=$?` – Adrian Günter Jul 26 '15 at 01:10
  • 8
    My god, this wasted so much of my time. Thank you @sevko, I may have never figured this out on my own. – justin.m.chase Dec 06 '18 at 20:53
  • 3
    The pitfall of combining `local` declaration with command substitution is described in [Bash Pitfall #27](https://mywiki.wooledge.org/BashPitfalls#local_var.3D.24.28cmd.29). It also affects `declare` and `export`. – Robin A. Meade Oct 31 '20 at 23:18
  • shellcheck catches and explains the `local` and `export` pitfalls. shellcheck saves lives, use it. – MarcH Jun 01 '23 at 22:18
22

Note that this isn't the case when combined with local, as in local variable="$(command)". That form will exit successfully even if command failed.

Take this Bash script for example:

#!/bin/bash

function funWithLocalAndAssignmentTogether() {
    local output="$(echo "Doing some stuff.";exit 1)"
    local exitCode=$?
    echo "output: $output"
    echo "exitCode: $exitCode"
}

function funWithLocalAndAssignmentSeparate() {
    local output
    output="$(echo "Doing some stuff.";exit 1)"
    local exitCode=$?
    echo "output: $output"
    echo "exitCode: $exitCode"
}

funWithLocalAndAssignmentTogether
funWithLocalAndAssignmentSeparate

Here is the output of this:

nick.parry@nparry-laptop1:~$ ./tmp.sh 
output: Doing some stuff.
exitCode: 0
output: Doing some stuff.
exitCode: 1

This is because local is actually a builtin command, and a command like local variable="$(command)" calls local after substituting the output of command. So you get the exit status from local.

ruakh
  • 175,680
  • 26
  • 273
  • 307
Nick P.
  • 323
  • 2
  • 8
  • 3
    The reason is that `local` (likewise for `export`) is a bash builtin function, and the way it's invoked with the first example result in a success, thus a `0` exit code. – metatoaster Jan 21 '19 at 03:48
  • I hope you don't mind, I've edited this answer to clarify the situation; the issue only arises when you combine `local` with the assignment in a single statement. (Fun fact: because `local` is a builtin command, rather than special syntax, you can even write something like `local "$(echo foo=bar)"` and it will behave the same as `local foo=bar`, creating a local variable named `foo` and initializing it to `bar`. By contrast, the command `"$(echo foo=bar)"` will give you `bash: foo=bar: command not found`, because Bash isn't expecting an assignment at that point.) – ruakh Sep 20 '19 at 18:39
  • Hah! This is exactly the detail that was tripping me up! – Geoff Aug 18 '23 at 21:12
5

I came across the same problem yesterday (Aug 29 2018).

In addition to local mentioned in Nick P.'s answer and @sevko's comment in the accepted answer, declare in global scope also has the same behavior.

Here's my Bash code:

#!/bin/bash

func1()
{
    ls file_not_existed
    local local_ret1=$?
    echo "local_ret1=$local_ret1"

    local local_var2=$(ls file_not_existed)
    local local_ret2=$?
    echo "local_ret2=$local_ret2"

    local local_var3
    local_var3=$(ls file_not_existed)
    local local_ret3=$?
    echo "local_ret3=$local_ret3"
}

func1

ls file_not_existed
global_ret1=$?
echo "global_ret1=$global_ret1"

declare global_var2=$(ls file_not_existed)
global_ret2=$?
echo "global_ret2=$global_ret2"

declare global_var3
global_var3=$(ls file_not_existed)
global_ret3=$?
echo "global_ret3=$global_ret3"

The output:

$ ./declare_local_command_substitution.sh 2>/dev/null 
local_ret1=2
local_ret2=0
local_ret3=2
global_ret1=2
global_ret2=0
global_ret3=2

Note the values of local_ret2 and global_ret2 in the output above. The exit codes are overwritten by local and declare.

My Bash version:

$ echo $BASH_VERSION 
4.4.19(1)-release
Zhi Zhu
  • 49
  • 1
  • 6
3

(not an answer to original question but too long for comment)

Note that export A=$(false); echo $? outputs 0! Apparently the rules quoted in devnull's answer no longer apply. To add a bit of context to that quote (emphasis mine):

3.7.1 Simple Command Expansion

...

If there is a command name left after expansion, execution proceeds as described below. Otherwise, the command exits. If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed. If there were no command substitutions, the command exits with a status of zero.

3.7.2 Command Search and Execution [ — this is the "below" case]

IIUC the manual describes var=foo as special case of var=foo command... syntax (pretty confusing!). The "exit status of the last command substitution" rule only applies to the no-command case.

While it's tempting to think of export var=foo as a "modified assignment syntax", it isn't — export is a builtin command (that just happens to take assignment-like args).

=> If you want to export a var AND capture command substitution status, do it in 2 stages:

A=$(false)
# ... check $?
export A

This way also works in set -e mode — exits immediately if the command substitution return non-0.

Beni Cherniavsky-Paskin
  • 9,483
  • 2
  • 50
  • 58
0

As others have said, the exit code of the command substitution is the exit code of the substituted command, so

FOO=$(false)
echo $?
---
1

However, unexpectedly, adding export to the beginning of that produces a different result:

export FOO=$(false)
echo $?
---
0

This is because, while the substituted command false fails, the export command succeeds, and that is the exit code returned by the statement.

Spongman
  • 9,665
  • 8
  • 39
  • 58