67

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.

3
  • It is the expected behavior
    – fpmurphy
    Commented May 11, 2016 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? Commented Feb 5, 2019 at 3: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
    Commented Apr 27, 2023 at 14:06

6 Answers 6

52

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.

6
  • Do all variables in the shell have a dynamic scope or does this only apply to variables declared with local? Commented Jan 26, 2019 at 0: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. Commented Jan 26, 2019 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
    Commented Jun 14, 2020 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. Commented Jun 15, 2020 at 7:29
  • Does this also affect function calling builtins? Or does builtins have their own scope? Commented Nov 30, 2020 at 8:23
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.

1
  • 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
    Commented Oct 17, 2018 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.

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

2
  • 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. Commented Apr 28, 2021 at 2:26
  • 1
    @GregMinshall I dont really know, shell code is awful, so I dont use it anymore really
    – Zombo
    Commented Apr 28, 2021 at 2: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}]"
0

I got curious to know how will the state of var change if we run innerFunc in background (i.e. innerFunc &) and intentionally introduce a delay inside it, so it finishes after outerFunc has already returned. Also, what if the value of var got changed during the delay.

#!/bin/bash

runBg="$1"

outfi='./out.txt'
[ "$runBg" -eq 1 ] && outfi='./out_bg.txt'

{
    innerFunc() {
        if [ "$runBg" -eq 1 ]; then
            echo "innerFunc: BEFORE SLEEP:     [var:${var}]"
            sleep .5
            echo "innerFunc: AFTER SLEEP:      [var:${var}]"
        fi
        var='INNER'
        echo "innerFunc:                   [var:${var}]"
    }

    outerFunc() {
        local var='OUTER'

        echo "outerFunc: before innerFunc: [var:${var}]"
        if [ "$runBg" -eq 1 ]; then
            innerFunc &
            var='OUTER_B_innerFunc_is_in_bg'
        else
            innerFunc
        fi
        echo "outerFunc: after  innerFunc: [var:${var}]"
    }

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

} > "$outfi" 2>&1

And the output is:

➜  tmp.LX8xU7Gk71 ./scopes.sh 0                                                                                                                          /0.0s
➜  tmp.LX8xU7Gk71 ./scopes.sh 1                                                                                                                          /0.0s
➜  tmp.LX8xU7Gk71 cat out.txt
global:    before outerFunc: [var:]
outerFunc: before innerFunc: [var:OUTER]
innerFunc:                   [var:INNER]
outerFunc: after  innerFunc: [var:INNER]
global:    after  outerFunc: [var:]                                                                                                                      /0.0s
➜  tmp.LX8xU7Gk71 cat out_bg.txt
global:    before outerFunc: [var:]
outerFunc: before innerFunc: [var:OUTER]
outerFunc: after  innerFunc: [var:OUTER_B_innerFunc_is_in_bg]
global:    after  outerFunc: [var:]
innerFunc: BEFORE SLEEP:     [var:OUTER]
innerFunc: AFTER SLEEP:      [var:OUTER]
innerFunc:                   [var:INNER]  

The change is not seen by innerFunc, not even after placing export var before innerFunc &.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .