Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Is there a POSIX Compliant way to limit the scope of a variable to the function it is declared in? i.e.:

Testing() {     TEST="testing" }  Testing echo "Test is: $TEST" 

should print "Test is:". I've read about the declare, local, and typeset keywords, but it doesn't look like they are required POSIX built-ins.

like image 545
John Avatar asked Sep 03 '13 17:09

John


People also ask

What is a POSIX compliant shell?

POSIX is an acronym for “Portable Operating System Interface”. POSIX shell is based on the standard defined in Portable Operating System Interface (POSIX) – IEEE P1003. 2. It is a set of standards codified by the IEEE and issued by ANSI and ISO. POSIX makes task of cross-platform software development easy.

What is POSIX shell script?

POSIX Shell is a command line shell for computer operating system which was introduced by IEEE Computer Society. POSIX stands for Portable Operating System Interface. POSIX Shell is based on the standard defined in Portable Operating System Interface (POSIX) – IEEE P1003.

Do Bash variables have scope?

Variable Scope of Bash FunctionsBy default, every variable has a global scope that means it is visible everywhere in the script. You can also create a variable as a local variable. When you declare a local variable within the function body, it is only visible within that function.


2 Answers

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_name do     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 $foobar test_value_1 $ let foobar test_value_2 $ echo $foobar test_value_2 $ let foobar test_value_3 $ echo $foobar test_value_3 $ unlet foobar $ echo $foobar test_value_2 $ unlet foobar $ echo $foobar test_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 $ f test1 test2 $ echo $x $y a 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.

like image 52
TaylanKammer Avatar answered Sep 28 '22 20:09

TaylanKammer


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=42 echo $x foo echo $x 
like image 29
Lars Brinkhoff Avatar answered Sep 28 '22 19:09

Lars Brinkhoff