The Grumpy Troll

Ramblings of a grumpy troll.

Shell Locality

When a shell function declares a variable to be local and then unsets it, does the name return to being global in scope or is it still “known” to be local, even though unset (via some kind of tombstone mechanism, perhaps)?

Let's test it. Spoiler: the results vary.

Note that POSIX does not provide local, this is a shell extension.

The Test

Here is our test as a pasteable one-liner:

i=0; s() { echo "$i: x: $x"; i=$((i+1));}; x=outside; s; foo() { s; local x; s;x=inside;s;unset x;s;unset x; s; }; foo; s

Breaking that down:

i=0                # loop-counter for our "show" function so we can see the steps
s() {              # a simple show function, using POSIX $((...)) for arithmetic
  echo "$i: x: $x"
  i=$((i+1))
}
x=outside    # our test variable is x and we give it a maskable value
s            # 0 -- prior state
foo() {
  s          # 1 -- inside function, before local
  local x    # from this point on, if `local` exists, x is non-global
  s          # 2 -- does the act of making the variable local clear its value?
  x=inside   # our "inside" value
  s          # 3 -- confirming value is set and variables work
  unset x    # reset x ... but what is left behind?
  s          # 4 -- should see an empty string
  unset x    # unknown: does this unset the global?
  s          # 5 -- final display inside the function
}
foo      # we need to call the function, since only zsh has anonymous functions
s            # 6 -- outside the function, show the global after double unset

The Results

Zsh

0: x: outside
1: x: outside
2: x:
3: x: inside
4: x:
5: x:
6: x: outside
  1. The local keyword resets the value of the variable (counter 2).
  2. The second unset did not unset the global (counter 6).

Bash (5)

0: x: outside
1: x: outside
2: x:
3: x: inside
4: x:
5: x:
6: x: outside

[edited to add Bash options:]

Bash behaves the same as zsh, by default. There is an option localvar_inherit to change this:

shopt -s localvar_inherit
foo
0: x: outside
1: x: outside
2: x: outside
3: x: inside
4: x:
5: x:
6: x: outside

There is also an option localvar_unset which impacts a more complicated form than we're addressing here.

Dash

0: x: outside
1: x: outside
2: x: outside
3: x: inside
4: x:
5: x:
6: x: outside
  1. The local keyword in dash does NOT reset the visible value, but does make future modifications only visible within local scope; this matches Bash with localvar_inherit.
  2. The second unset did not unset the global.

Ksh

This test was of the shell from apk add -U loksh inside an Alpine 3.12 Docker container, which installed version 6.7.1-r0 of loksh, described as “A Linux port of OpenBSD's ksh”.

0: x: outside
1: x: outside
2: x:
3: x: inside
4: x: outside
5: x:
6: x:
  1. The local keyword in ksh is immediately resetting the visible value.
  2. The second unset inside the function did unset the global variable!

FreeBSD sh

0: x: outside
1: x: outside
2: x:
3: x: inside
4: x:
5: x:
6: x: outside

This is the same as zsh and bash's default.

Conclusion

Writing portable shell is hard. Duh.

Categories: shell posix bourne bash zsh portability programming scripting