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.

  • 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`. – chepner Nov 24 '19 at 14:49

1 Answers1

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.

Willis Blackburn
  • 8,068
  • 19
  • 36
  • 1
    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. – Willis Blackburn Nov 24 '19 at 16:52
  • @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. – Egidio Docile Nov 24 '19 at 17:42
  • I think you could also set a trap handler for ERR and then exit from that handler. – Willis Blackburn Nov 24 '19 at 18:24