Bash Functions – Why Does Local fn=$(…) Mask the $? Status Code?

bash

Two function definitions, the only difference is that the first combines the local storage keyword with the assignment, while the second separates them:

function foo {
    local fn=$(mktemp -p /path/does/not/exist 2>/dev/null)
    echo $?
}

function bar {
    local fn
    fn=$(mktemp -p /path/does/not/exist 2>/dev/null)
    echo $?
}

foo
bar

This echoes "0" then "1". I expect it to echo "1" then "1". It seems like the value of $? is the result of the assignment to local, rather than the result of the command substitution.

Why does bash 4.2.46(1)-release behave this way?

Best Answer

I thought this behavior was documented explicitly, because it's such a gotcha (especially when running bash scripts with -o errexit!), but it doesn't seem to. My copy of the manual says the following (about global, which behaves the same as local when within a function):

The return status is zero unless an invalid option is encountered, an attempt is made to define a function using ‘-f foo=bar’, an attempt is made to assign a value to a readonly variable, an attempt is made to assign a value to an array variable without using the compound assignment syntax [...], one of the names is not a valid shell variable name, an attempt is made to turn off readonly status for a readonly variable, an attempt is made to turn off array status for an array variable, or an attempt is made to display a non-existent function with -f.

So it would appear that local is not a keyword in the sense that one would expect in other programming languages: when an assignment-like parameter is provided to local, that does not qualify the initialization; rather, the local built-in command takes care of making the assignment happen, and the return code is that of local itself, not of the code possibly run in the initializer, and that return code will only be non-zero in the list of conditions listed above.

To perhaps answer the question in a more literal sense, as bishop mentioned in a comment, bash maintainer Chet Ramey was once asked if he would consider making local reflect failures happening during assignment, and responded, in essence, that assigning is not local's main mission:

Because that's not what local and its siblings [...] do. These builtins exist to assign and modify variable attributes. As an added feature, they support value assignment at the same time, but the important function is the attribute setting. They don't need to know how the value was computed. [...] Since the function is setting the attribute or value, the exit status should reflect whether or not that succeeded.

It may be worth noting that the same behavior can also be observed in the zsh shell.

The solution is to separate the two operations:

local variable
variable=$( somecommand )

exit_status=$?
Related Question