0

I made a small experiment to save in one line some context.

It works exactly as I wanted. So this post is: 1. to share it with the community. 2. to improve it, or have a complete other solution because it is ugly and cryptic.

The situation is the following : there is a global variable and I want locally in functions to be able to change it and restore it at the end:

x()
{
    typeset loc=$glob
    glob=<val>
    (...)
    glob=$loc
}

This is too verbose for me, and asking for typos.

So I tried this (code ready to use):

#!/bin/bash

glob=1
indent=0

# appliance
trace()
{
   printf "%$((${indent}*3))s %s\n" "" "$1"
}

# the context stuff (creation and destruction)
# in variable, because with a function, I'd need to create a $(sub shell) and it wouldn't work
new_glob='trap restore_context RETURN;typeset loc=$glob;glob'

restore_context()
{
   res=$?
   glob=$loc
   trap - RETURN
}

# common test stuff, to isolate the traces
test_call()
{
   typeset res
   trace "in ${FUNCNAME[1]}, before $1,glob=$glob"
   (( indent++ ))
   eval $1
   res=$?
   (( indent-- ))

   trace "in ${FUNCNAME[1]}, res of $1=$res"
   trace "in ${FUNCNAME[1]}, after $1,glob=$glob"

   return $res
}

# Russian dolls function
f()
{
   eval "$new_glob=6"
   test_call g
   return 16
}

g()
{
   eval "$new_glob=7"
   test_call h
   return 17
}

h()
{
   eval "$new_glob=8"
   trace "in h, glob=$glob"
   i
   return 18
}

i()
{
   trace "in i, glob=$glob"
}

# main
test_call f

Here the script calls f, which calls g, which call h. Each function changes the global variable and then restore it.

Output:

# ./test_rtrap
 in main, before f,glob=1
    in f, before g,glob=6
       in g, before h,glob=7
          in h, glob=8
          in i, glob=8
       in g, res of h=18
       in g, after h,glob=7
    in f, res of g=17
    in f, after g,glob=6
 in main, res of f=16
 in main, after f,glob=1

The important point here is that function i() prints 8, and not 1, as using local variables would do.

To reach this result, I use a trap function on RETURN.

Now the above function x simply becomes:

x()
{
   eval "$new_glob=6"
   (...)
}

That's a pity that I had to use an eval and a variable containing (part of) code. It is not natural, quite cryptic. But I needed it because using a function there would have required a subshell, with the related variables context issues.

So, not perfect, not very beautiful, but less verbose, and it works.

Is there a better way to perform this, than the ugly eval "$new_glob=6"

3
  • I'm voting to close this question as off-topic because this might be a better question for code review but even there it may be off topic.
    – jesse_b
    Nov 9, 2019 at 13:46
  • Question is: is there a better way to perform this, that the ugly eval "$new_glob=6"
    – Jacques
    Nov 9, 2019 at 13:49
  • 1
    Then edit it to ask that specific question, please.
    – muru
    Nov 9, 2019 at 13:56

2 Answers 2

1

Way way over the top! At least for the example shown.

Replace the eval "$new_glob=6" with local glob=6 and get rid of the new_glob variable.

In other words rather than writing

x()
{
   typeset loc=$glob
   glob=<val>
   (...)
   glob=$loc
}

just write

x()
{
   typeset glob
   glob=<val>
}

optionally replacing typeset with local or declare.

Bash is a dynamically scoped language. Currently in the linked article it describes one implementation strategy

An even simpler implementation is the representation of dynamic variables with simple global variables. The local binding is performed by saving the original value in an anonymous location on the stack that is invisible to the program. When that binding scope terminates, the original value is restored from this location. In fact, dynamic scope originated in this manner. Early implementations of Lisp used this obvious strategy for implementing local variables, and the practice survives in some dialects which are still in use, such as GNU Emacs Lisp.

which pretty much describes the bash code provided.

11
  • My answer on unix.stackexchange.com/questions/393928/… might also me helpful.
    – icarus
    Nov 9, 2019 at 14:30
  • You completely missed the point, or I completely missed yours ;-) ! Maybe my explanations are not clear enough. I don't want to cover the global variable by a local variable, I really want to change (and restore) the global variable itself during a function call. It might be an environment variable for example (not my case) .
    – Jacques
    Nov 9, 2019 at 21:28
  • I believe you missed mine. Did you try changing f(), g(), and h() as I suggested? When I do this I get identical output from the modified program. You say you "don't want to cover the global variable by a local variable", but what does that mean? If "f" calls "g" then inside "g" you see the value for "glob" that was established by "f". You can't see the "global" value of glob (whatever that means).
    – icarus
    Nov 9, 2019 at 22:14
  • You're creating a local variable glob. I don't want that. I want to change the global glob, not to hide it with a local variable. Of course, the output might be the same for this specific test case, but your local variable is only visible inside the function, while the global one affects the global scope.
    – Jacques
    Nov 10, 2019 at 13:44
  • "your local variable is only visible inside the function" is exactly the point. The difference between lexical scoping and dynamic scoping is the meaning of "inside". For lexical scoping "inside" means roughly "from the point of the declaration until the end of the text block" and is something that in a compiled language can be determined at compile time. For dynamic scope "inside" means roughly "whilst this function is executing" and so is something that can only be determined at runtime. Your tour de force bash code does almost exactly what the quoted text says.
    – icarus
    Nov 10, 2019 at 16:52
-1

OK (thanks Icarus), so IMHO bash is really weird here. I essentially re-implemented its behavior.

Local variable in bash are NOT local !! They just locally change/set the value of global/environment variables and restore their value upon exit of the functions where they are """locally defined""". This is called dynamic scoping.

Local in bash means "local value of a global variable", which is weird (for me). For example, ksh93 behave differently, a local variable being really a local variable (i.e. hiding the global one). This is called lexical/dynamic scoping and, by far, most languages use this.

To illustrate this, I wrote another test program:

if (( $# == 0 )); then
   # run this script with specified shells
   bash -c "$0 -c"
   ksh93 -c "$0 -c"
   exit 0
fi

# which shell am I
readlink /proc/$$/exe

# declarations
local="Dynamic scoping"          # i.e. func i view "local" value
global="Lexical/Static scoping"  # i.e. func i views "global" value

glob="$global"

# non POSIX declarations
function f { typeset glob="$local"; i; }
function i { echo "      $glob"; }

# call
echo "   non POSIX ( function f )"
f

# POSIX declations
pf() { typeset glob="$local"; i; }
pi() { echo "      $glob"; }

# call
echo "   POSIXi ( f() )"
pf

Syntactically, these instructions/script run on both bash and ksh93.

Function f defines a local variable, and calls i. In lexical scoping situations, function i has access to the global variable, since it is defines lexically out of function f scope. In dynamic scoping, no local variable is created, only the global variable is created, function f only setting its value during its own execution lifetime. When function i is called, function f still "lives", so the value global variable value still has value assigned function f. Only upon function f return, the shell will restore the value that the function changed.

Here is its output:

/u-blox/gallery/ubx/det/re6_64/8.0/bin/bash
   non POSIX ( function f )
      Dynamic scoping
   POSIXi ( f() )
      Dynamic scoping
/bin/ksh93
   non POSIX ( function f )
      Lexical/Static scoping
   POSIXi ( f() )
      Dynamic scoping

Now, the question is: how to implement lexical scoping in bash? ;-)

2
  • 2
    What you call "weird" is called dynamic scoping, and it is a very well known alternative to lexical (sometimes called "static") scoping. It's not weird, it's just a different fundamental option. With lexical scoping, you can tell the meaning of a name just by looking at the source code. With dynamic scoping, the meaning of a name cannot be known until the moment that meaning is needed at run time. Emacs Lisp and PowerShell are two other languages which use dynamic scoping.
    – AlexP
    Nov 15, 2019 at 14:53
  • The "function f { }" syntax is non-standard. POSIX only specifies the meaning of "f() { }".
    – icarus
    Nov 15, 2019 at 15:20

You must log in to answer this question.

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