5

Declaring a local variable in a bash function makes that variable only visible inside the function itself and its children, so if I run:

#!/bin/bash
set -e

func_one() {
  echo "${var}"
}

func_two() {
  local -r var="var from func_two"
  func_one
}

func_two

The output is:

var from func_two

Even if the var variable is declared as local and readonly inside func_two can be accessed from the function func_one. It is possible, in the latter, to declare a variable with the same name also local and readonly:

#!/bin/bash
set -e

func_one() {
  local -r var="var from func_one"
  echo "${var}"
}

func_two() {
  local -r var="var from func_two"
  func_one
}

func_two

The output is:

var from func_one

The same happens if func_one is called from an EXIT trap:

#!/bin/bash
set -e

func_one() {                                                                    
  local -r var="var from func_one"                                              
  echo "${var}"                                                                 
}                                                                               

func_two() {                                                                   
  local -r var="var from func_two"                                             
  trap 'func_one' EXIT
  echo "${var}"                                             
}                                                                               

func_two                                                                       

Running the code I receive:

var from func_two
var from func_one

However, if the EXIT trap is executed after an error (set -e option makes the script exit immediately if a command exits with a non zero status). It looks like it's not possible to reassign the var variable inside func_one:

#!/bin/bash
set -e

func_one() {                                                                    
  local -r var="var from func_one"                                              
  echo "${var}"                                                                 
}                                                                               

func_two() {                                                                   
  local -r var="var from func_two"                                             
  trap 'func_one' EXIT          
  echo "${var}"                                                
  false                                                                         
}                                                                               

func_two                                                                       

Running the code I receive:

var from func_two
local: var: readonly variable

Can anyone clarify to me why this happens? Thank you in advance.

1
  • 2
    I'm tempted to say this is another reason not to use set -e, but since set -e is defined by POSIX and local is a bash extension, it's possible that this is a bug in the implementation of local. I'll note that if you drop the -r option and call readonly var after the call to local, then the same code works as expected in dash (which has its own non-standard implementation of local) but produces the same error in bash. Commented Nov 24, 2019 at 14:49

1 Answer 1

3

It's a bug in Bash.

When you initially install func_one as an exit handler, Bash invokes it at the end of the script, after func_two has returned. All is well.

When you use a combination of set -e and invoke false from func_one, Bash is exiting the script, and invoking the exit handler, after the call to false, in other words, within func_one.

Bash implements "exit on error" by invoking longjmp to return control to the top level parser, passing the code ERREXIT. In the code that handles this case, there is a comment to the effect that the script should forget about any function that was executing, which it does by setting a variable, variable_context, to 0. It looks like variable_context is an index into a stack of naming scopes, and setting it back to 0 points it to the top-level global scope.

Next, Bash invokes the trap handler, which invokes func_one. Now variable_context is 1, that is, the same value it had within func_two. When the script tries to set var, Bash looks at the names defined in this context and discovers that var is already there, left over from func_two.

I confirmed this in the debugger and also with a workaround: if you add an intermediate function call, the script works, because now within func_one, variable_context is 2 and Bash doesn't see the leftover var from func_two anymore:

#!/bin/bash
set -e

func_one() {
  local -r var="var from func_one"
  echo "${var}"
}

func_intermediate() {
  func_one
}

func_two() {
  local -r var="var from func_two"
  echo "${var}"
  trap 'func_intermediate' EXIT
  false
}

func_two

Apparently within the Bash code unwinding the function call stack involves actually removing the variables (there is a function called kill_all_local_variables); just decrementing variable_context (or setting it to 0) isn't good enough. That's why the script works in the case where func_two returns first and is able to clean up its variables before Bash invokes func_one.

Update: It looks like variable_context is not an index into a stack (it's just a function nesting counter), and the code mallocs new space for variables when entering a function? So not 100% sure what's actually going on here, but Bash does find the func_two version of var inside func_one, and adding the intermediate call makes the problem go away, so it's some kind of issue with Bash not cleaning up after func_two when leaving it due to the "exit on error" setting and causing func_one to inherit its variables.

Sign up to request clarification or add additional context in comments.

3 Comments

Was planning to but it's only been 30 minutes. The bug has existed for at least 12 years so a few more hours probably won't make much of a difference.
@WillisBlackburn Thank you very much for your answer. If I understand correctly, at the moment the choices are 1) using the workaround or 2) just avoid using set -e.
I think you could also set a trap handler for ERR and then exit from that handler.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.