POSIX-Compliant Way to Scope Variables to a Function in a Shell Script POSIX-Compliant Way to Scope Variables to a Function in a Shell Script shell shell

POSIX-Compliant Way to Scope Variables to a Function in a Shell Script


It is normally done with the local keyword, which is, as you seem to know, not defined by POSIX. Here is an informative discussion about adding 'local' to POSIX.

However, even the most primitive POSIX-compliant shell I know of which is used by some GNU/Linux distributions as the /bin/sh default, dash (Debian Almquist Shell), supports it. FreeBSD and NetBSD use ash, the original Almquist Shell, which also supports it. OpenBSD uses a ksh implementation for /bin/sh which also supports it. So unless you're aiming to support non-GNU non-BSD systems like Solaris, or those using standard ksh, etc., you could get away with using local. (Might want to put some comment right at the start of the script, below the shebang line, noting that it is not strictly a POSIX sh script. Just to be not evil.) Having said all that, you might want to check the respective man-pages of all these sh implementations that support local, since they might have subtle differences in how exactly they work. Or just don't use local:

If you really want to conform fully to POSIX, or don't want to mess with possible issues, and thus not use local, then you have a couple options. The answer given by Lars Brinkhoff is sound, you can just wrap the function in a sub-shell. This might have other undesired effects though. By the way shell grammar (per POSIX) allows the following:

my_function()(  # Already in a sub-shell here,  # I'm using ( and ) for the function's body and not { and }.)

Although maybe avoid that to be super-portable, some old Bourne shells can be even non-POSIX-compliant. Just wanted to mention that POSIX allows it.

Another option would be to unset variables at the end of your function bodies, but that's not going to restore the old value of course so isn't really what you want I guess, it will merely prevent the variable's in-function value to leak outside. Not very useful I guess.

One last, and crazy, idea I can think of is to implement local yourself. The shell has eval, which, however evil, yields way to some insane possibilities. The following basically implements dynamic scoping a la old Lisps, I'll use the keyword let instead of local for further cool-points, although you have to use the so-called unlet at the end:

# If you want you can add some error-checking and what-not to this.  At present,# wrong usage (e.g. passing a string with whitespace in it to `let', not# balancing `let' and `unlet' calls for a variable, etc.) will probably yield# very very confusing error messages or breakage.  It's also very dirty code, I# just wrote it down pretty much at one go.  Could clean up.let(){    dynvar_name=$1;    dynvar_value=$2;    dynvar_count_var=${dynvar_name}_dynvar_count    if [ "$(eval echo $dynvar_count_var)" ]    then        eval $dynvar_count_var='$(( $'$dynvar_count_var' + 1 ))'    else        eval $dynvar_count_var=0    fi    eval dynvar_oldval_var=${dynvar_name}_oldval_'$'$dynvar_count_var    eval $dynvar_oldval_var='$'$dynvar_name    eval $dynvar_name='$'dynvar_value}unlet()for dynvar_namedo    dynvar_count_var=${dynvar_name}_dynvar_count    eval dynvar_oldval_var=${dynvar_name}_oldval_'$'$dynvar_count_var    eval $dynvar_name='$'$dynvar_oldval_var    eval unset $dynvar_oldval_var    eval $dynvar_count_var='$(( $'$dynvar_count_var' - 1 ))'done

Now you can:

$ let foobar test_value_1$ echo $foobartest_value_1$ let foobar test_value_2$ echo $foobartest_value_2$ let foobar test_value_3$ echo $foobartest_value_3$ unlet foobar$ echo $foobartest_value_2$ unlet foobar$ echo $foobartest_value_1

(By the way unlet can be given any number of variables at once (as different arguments), for convenience, not showcased above.)

Don't try this at home, don't show it to children, don't show it your co-workers, don't show it to #bash at Freenode, don't show it to members of the POSIX committee, don't show it to Mr. Bourne, maybe show it to father McCarthy's ghost to give him a laugh. You have been warned, and you didn't learn it from me.

EDIT:

Apparently I've been beaten, sending the IRC bot greybot on Freenode (belongs to #bash) the command "posixlocal" will make it give one some obscure code that demonstrates a way to achieve local variables in POSIX sh. Here is a somewhat cleaned up version, because the original was difficult to decipher:

f(){    if [ "$_called_f" ]    then        x=test1        y=test2        echo $x $y    else        _called_f=X x= y= command eval '{ typeset +x x y; } 2>/dev/null; f "$@"'    fi}

This transcript demonstrates usage:

$ x=a$ y=b$ ftest1 test2$ echo $x $ya b

So it lets one use the variables x and y as locals in the then branch of the if form. More variables can be added at the else branch; note that one must add them twice, once like variable= in the initial list, and once passed as an argument to typeset. Note that no unlet or so is needed (it's a "transparent" implementation), and no name-mangling and excessive eval is done. So it seems to be a much cleaner implementation overall.

EDIT 2:

Comes out typeset is not defined by POSIX, and implementations of the Almquist Shell (FreeBSD, NetBSD, Debian) don't support it. So the above hack will not work on those platforms.


I believe the closest thing would be to put the function body inside a subshell.

E.g. try this

foo(){  ( x=43 ; echo $x )}x=42echo $xfooecho $x


This is actually built into the design of POSIX function declarations.

If you would like a variable declared in the parent scope, to be accessible in a function, but leave its value in the parent scope unchanged, simply:

*Declare your function using an explicit subshell, i.e., use a

  • subshell_function() (with parentheses), not

  • inline_function() { with braces ;}


The behavior of inline grouping vs. subshell grouping is consistent throughout the entire language.

If you want to "mix and match", start with an inline function, then nest subshell functions as necessary. It's clunky, but works.