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

前端 未结 7 690
被撕碎了的回忆
被撕碎了的回忆 2020-12-08 00:27

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 \"T         


        
7条回答
  •  既然无缘
    2020-12-08 01:20

    It's possible to simulate local variables in a Posix Shell using a small set of general functions.
    The sample code below demonstrates two functions, called Local and EndLocal, which do the trick.

    • Use Local once to declare all local variables at the beginning of a routine.
      Local creates a new scope, and saves the previous definition of each local variable into a new global variable.
    • Use EndLocal before returning from the routine.
      EndLocal restores all previous definitions saved at the current scope, and deletes the last scope.
      Also note that EndLocal preserves the previous exit code.

    All functions are short, and use descriptive names, so they should be relatively easy to understand.
    They support variables with tricky characters like spaces, single and double quotes.
    Caution: They use global variables beginning with LOCAL_, so there's a small risk of collision with existing homonym variables.

    The Test routine recursively calls itself 3 times, and modifies a few local and global variables.
    The output shows that the A and B local variables are preserved, contrary to the global N variable.

    Code:

    #!/bin/sh
    
    #-----------------------------------------------------------------------------#
    # Manage pseudo-local variables in a Posix Shell
    
    # Check if a variable exists.
    VarExists() { # $1=Variable name
      eval "test \"\${${1}+true}\" = \"true\""
    }
    
    # Get the value of a variable.
    VarValue() { # $1=Variable name
      eval "echo \"\${$1}\""
    }
    
    # Escape a string within single quotes, for reparsing by eval
    SingleQuote() { # $1=Value
      echo "$1" | sed -e "s/'/'\"'\"'/g" -e "s/.*/'&'/"
    }
    
    # Set the value of a variable.
    SetVar() { # $1=Variable name; $2=New value
      eval "$1=$(SingleQuote "$2")"
    }
    
    # Emulate local variables
    LOCAL_SCOPE=0
    Local() { # $*=Local variables names
      LOCAL_SCOPE=$(expr $LOCAL_SCOPE + 1)
      SetVar "LOCAL_${LOCAL_SCOPE}_VARS" "$*"
      for LOCAL_VAR in $* ; do
        if VarExists $LOCAL_VAR ; then
          SetVar "LOCAL_${LOCAL_SCOPE}_RESTORE_$LOCAL_VAR" "SetVar $LOCAL_VAR $(SingleQuote "$(VarValue $LOCAL_VAR)")"
        else
          SetVar "LOCAL_${LOCAL_SCOPE}_RESTORE_$LOCAL_VAR" "unset $LOCAL_VAR"
        fi
      done
    }
    
    # Restore the initial variables
    EndLocal() {
      LOCAL_RETCODE=$?
      for LOCAL_VAR in $(VarValue "LOCAL_${LOCAL_SCOPE}_VARS") ; do
        eval $(VarValue "LOCAL_${LOCAL_SCOPE}_RESTORE_$LOCAL_VAR")
        unset "LOCAL_${LOCAL_SCOPE}_RESTORE_$LOCAL_VAR"
      done
      unset "LOCAL_${LOCAL_SCOPE}_VARS"
      LOCAL_SCOPE=$(expr $LOCAL_SCOPE - 1)
      return $LOCAL_RETCODE
    }
    
    #-----------------------------------------------------------------------------#
    # Test routine
    
    N=3
    Test() {
      Local A B
      A=Before
      B=$N
      echo "#1 N=$N A='$A' B=$B"
      if [ $N -gt 0 ] ; then
        N=$(expr $N - 1)
        Test
      fi
      echo "#2 N=$N A='$A' B=$B"
      A="After "
      echo "#3 N=$N A='$A' B=$B"
      EndLocal
    }
    
    A="Initial value"
    Test
    echo "#0 N=$N A='$A' B=$B"
    

    Output:

    larvoire@JFLZB:/tmp$ ./LocalVars.sh
    #1 N=3 A='Before' B=3
    #1 N=2 A='Before' B=2
    #1 N=1 A='Before' B=1
    #1 N=0 A='Before' B=0
    #2 N=0 A='Before' B=0
    #3 N=0 A='After ' B=0
    #2 N=0 A='Before' B=1
    #3 N=0 A='After ' B=1
    #2 N=0 A='Before' B=2
    #3 N=0 A='After ' B=2
    #2 N=0 A='Before' B=3
    #3 N=0 A='After ' B=3
    #0 N=0 A='Initial value' B=
    larvoire@JFLZB:/tmp$
    

    Using the same technique, I think it should be possible to dynamically detect if the local keyword is supported, and if it's not, define a new function called local that emulates it.
    This way, the performance would be much better in the normal case of a modern shell having built-in locals. And things would still work on an old Posix shell without it.
    Actually we'd need three dynamically generated functions:

    • BeginLocal, creating an empty pseudo-local scope, as is done in the beginning of my Local above, or doing nothing if the shell has built-in locals.
    • local, similar to my Local, defined only for shells not having built-in locals.
    • EndLocal, identical to mine, or just preserving the last exit code for shells having built-in locals.

提交回复
热议问题