64

After reading 24.2. Local Variables, I thought that declaring a variable var with the keyword local meant that var's value was only accessible within the block of code delimited by the curly braces of a function.

However, after running the following example, I found out that var can also be accessed, read and written from the functions invoked by that block of code -- i.e. even though var is declared local to outerFunc, innerFunc is still able to read it and alter its value.

Run It Online

#!/usr/bin/env bash

function innerFunc() {
    var='new value'
    echo "innerFunc:                   [var:${var}]"
}

function outerFunc() {
    local var='initial value'

    echo "outerFunc: before innerFunc: [var:${var}]"
    innerFunc
    echo "outerFunc: after  innerFunc: [var:${var}]"
}

echo "global:    before outerFunc: [var:${var}]"
outerFunc
echo "global:    after  outerFunc: [var:${var}]"

Output:

global:    before outerFunc: [var:]               # as expected, `var` is not accessible outside of `outerFunc`
outerFunc: before innerFunc: [var:initial value]
innerFunc:                   [var:new value]      # `innerFunc` has access to `var` ??
outerFunc: after  innerFunc: [var:new value]      # the modification of `var` by `innerFunc` is visible to `outerFunc` ??
global:    after  outerFunc: [var:]

Q: Is that a bug in my shell (bash 4.3.42, Ubuntu 16.04, 64bit) or is it the expected behavior ?

EDIT: Solved. As noted by @MarkPlotnick, this is indeed the expected behavior.

Lesmana
  • 26,889
  • 20
  • 81
  • 86
maddouri
  • 741
  • 1
  • 6
  • 7
  • It is the expected behavior – fpmurphy May 11 '16 at 17:09
  • 4
    Am I the only one who thinks it's weird that on the last line of output the value of `var` is empty? `var` is set globally in `innerFunc`, so why doesn't it stick until the end of the script? – Harold Fischer Feb 05 '19 at 03:42
  • What about if `innerFunc` is called at the end (after the final `echo`)? It prints `innerFunc: [var:new value]`. It seems that this creates a new global `var` (as shown by one final additional `echo "global: after outerFunc: [var:${var}]"`). – user7543 Apr 27 '23 at 14:06

5 Answers5

51

Shell variables have a dynamic scope. If a variable is declared as local to a function, that scope remains in effect until the function returns, including during calls to other functions!

This is in contrast to most programming languages which have lexical scope. Perl has both: my for lexical scope, local or no declaration for dynamical scope.

There are two exceptions:

  1. in ksh93, if a function is defined with the standard function_name () { … } syntax, then its local variables obey dynamic scoping. But if a function is defined with the ksh syntax function function_name { … } then its local variable obey lexical/static scoping, so they are not visible in other functions called by this.

  2. the zsh/private autoloadable plugin in zsh provides with a private keyword/builtin which can be used to declare a variable with static scope.

ash, bash, pdksh and derivatives, bosh only have dynamic scoping.

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • Do all variables in the shell have a dynamic scope or does this only apply to variables declared with `local`? – Harold Fischer Jan 26 '19 at 00:56
  • @HaroldFischer All variables have dynamic scope. With a `typeset` or `declare` or `local` declaration, the scope is until the function returns. Without such a declaration, the scope is global. – Gilles 'SO- stop being evil' Jan 26 '19 at 14:46
  • 1
    IMHO, `If a variable is declared as local to a function, that scope remains until the function returns.` is not enough to explain what is dynamic scope verus lexical scope. The description alone is also applied to lexical scope. – rosshjb Jun 14 '20 at 18:18
  • 2
    @jinbeomhong No, with lexical scope, a variable from a function is not visible while this function calls another function. I've added a sentence to state this explicitly. – Gilles 'SO- stop being evil' Jun 15 '20 at 07:29
  • Does this also affect function calling builtins? Or does builtins have their own scope? – Franklin Yu Nov 30 '20 at 08:23
  • Also note that the module is (now?) called [`zsh/param/private`](http://zsh.sourceforge.net/Doc/Release/Zsh-Modules.html#The-zsh_002fparam_002fprivate-Module). – Franklin Yu Nov 30 '20 at 08:48
13

In function innerFunc() the var='new value' wasn't declared as local, therefore it's available in visible scope (once the function has been called).

Conversely, in function outerFunc() the local var='initial value' was declared as local, therefore it's not available in the global scope (even if the function has been called).

Because innerFunc() was called as a child of outerFunc(), var is within the the local scope of outerFunc().

man 1 bash may help clarify

local [option] [name[=value] ...]

For each argument, a local variable named name is created, and assigned value. The option can be any of the options accepted by declare. When local is used within a function, it causes the variable name to have a visible scope restricted to that function and its children. ...

The implied behavior that's expected in the description could be achieved by declaring local var='new value in function innerFunc().

As others have stated, this is not a bug in the bash shell. Everything's functioning as it should.

RalfFriedl
  • 8,816
  • 6
  • 23
  • 34
Joseph Tingiris
  • 1,706
  • 12
  • 20
  • 2
    Your first statement contradicts what the user is seeing. Printing the value of `var` in the global scope, after calling `innerFunc` through `outFunc`, does not print `new value`. – Kusalananda Oct 17 '18 at 18:25
11

It isn't a bug, the call inside the context of the outerFunc uses that local copy of $var. The "local" in outerFunc means the global isn't changed. If you call innerFunc outside of outerFunc, then there will be a change to the global $var, but not the outerFunc's local $var. If you added "local" to innerFunc, then outerFunc's $var wouldn't be changed - in essence, there'd be 3 of them:

  • $global::var
  • $outerFunc::var
  • $innerFunc::var

to use Perl's namespace format, sort of.

afbach
  • 136
  • 3
4

You can use a function to force local scope:

sh_local() {
  eval "$(set)" command eval '\"\$@\"'
}

Example:

x() {
  z='new value'
  printf 'function x, z = [%s]\n' "$z"
}
y() {
  z='initial value'
  printf 'function y before x, z = [%s]\n' "$z"
  sh_local x
  printf 'function y after x, z = [%s]\n' "$z"
}
printf 'global before y, z = [%s]\n' "$z"
y
printf 'global after y, z = [%s]\n' "$z"

Result:

global before y, z = []
function y before x, z = [initial value]
function x, z = [new value]
function y after x, z = [initial value]
global after y, z = [initial value]

Source

Zombo
  • 1
  • 5
  • 43
  • 62
  • 1
    iiuc, in this code, `x` is running in a separate process (because of `command`), so its logging message is coming from that process, not from the process in which `y` is running. – Greg Minshall Apr 28 '21 at 02:26
  • 1
    @GregMinshall I dont really know, shell code is awful, so I dont use it anymore really – Zombo Apr 28 '21 at 02:35
0

This is the expected behaviour. A local variable has dynamic scope: the variable is in scope until the function it has been declared in returns. Consequently, such variables are in scope for all functions called from this function.

If a function declares a new local variable, with the same name as another (local or global) variable, the new local variable will reside in a separate area of memory, and can hold values distinct from any others with the same name.

It doesn't matter where in the function a local declaration appears.

I modified the provided code a little as an example:

#!/usr/bin/env bash

function innerFunc() {
    echo "innerFunc:                   [var:${var}]"
}

function outerFunc() {
    local var='initial value'

    echo "outerFunc: before innerFunc: [var:${var}]"
    innerFunc
    echo "outerFunc: after  innerFunc: [var:${var}]"
}

echo "global:    before outerFunc: [var:${var}]"
outerFunc
echo "global:    after  outerFunc: [var:${var}]"
innerFunc
echo "global:    after  outerFunc: [var:${var}]"
user7543
  • 224
  • 2
  • 9