在Shell脚本中将变量范围限定为函数的符合POSIX的方法

zengzsys  于 2023-03-19  发布在  Shell
关注(0)|答案(8)|浏览(145)

是否有一种POSIX兼容的方式将变量的作用域限制在声明它的函数中?例如:

Testing()
{
    TEST="testing"
}

Testing
echo "Test is: $TEST"

应该打印“Test is:“。我读过declare、local和typeset关键字,但看起来它们不像是POSIX内置函数所必需的。

qvk1mo1f

qvk1mo1f1#

这通常是用local关键字来完成的,正如你所知道的,POSIX没有定义这个关键字。
然而,即使是我所知道的最原始的符合POSIX的shell,也被某些GNU/Linux发行版用作/bin/sh缺省值dash(Debian Almquist Shell),支持它。FreeBSD和NetBSD使用ash,最初的Almquist Shell,OpenBSD使用ksh实现/bin/sh/bin/sh也支持它,所以除非你打算支持非GNU的非BSD系统,比如Solaris,或者那些使用标准ksh的用户等等,您可以使用local。(可能需要在脚本的开头,shebang行下面添加一些注解,指出它严格来说并不是POSIX sh脚本。只是为了不那么邪恶。)您可能需要查看所有这些支持localsh实现各自的手册页,因为它们在工作方式上可能存在细微的差异。
如果你真的想完全符合POSIX,或者不想惹麻烦,因此不使用local,那么你有两个选择。Lars Brinkhoff给出的答案是合理的,你可以把函数 Package 在一个子shell中。尽管这可能有其他不希望的效果。顺便说一下shell语法(每个POSIX)允许以下操作:

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

尽管为了实现超级可移植性,可能要避免这种情况,但一些旧的Bourne shell甚至可能不兼容POSIX,我只想提一下POSIX允许这样做。
另一个选择是把变量放在函数体的末尾,但是这并不会恢复原来的值,所以我想这并不是你想要的,它只是防止变量的函数内值泄露出去,我想这不是很有用。
我能想到的最后一个疯狂的想法是自己实现local。shell有eval,不管它有多邪恶,都会给一些疯狂的可能性让路。下面基本上实现了一个老Lisps的动态作用域,我将使用关键字let代替local来获得更多的酷点。尽管你必须在最后使用unlet

# 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

现在您可以:

$ 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 unlet可以一次给定任意数量的变量(作为不同的参数),为了方便起见,上面没有展示。)
不要在家里尝试,不要给孩子看,不要给你的同事看,不要给Freenode的#bash看,不要给POSIX委员会的成员看,不要给伯恩先生看,也许给麦卡锡神父的鬼魂看,给予他笑一笑。你已经被警告过了,你不是从我这里学来的。

编辑:

显然我被打败了,给Freenode(属于#bash)上的IRC机器人greybot发送命令“posixlocal”会让它给予一些晦涩的代码,演示如何在POSIX sh中实现局部变量。下面是一个稍微清理过的版本,因为原始代码很难破译:

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
}

此脚本演示了用法:

$ x=a
$ y=b
$ f
test1 test2
$ echo $x $y
a b

因此,它允许使用变量xy作为if形式的then分支中的局部变量。注意,必须添加两次,一次像初始列表中的variable=,另一次作为参数传递给typeset。注意,不需要unlet之类的东西(这是一个“透明”实现),也没有名称修饰和过多的eval。因此,总体上看起来它是一个更干净的实现。

编辑2:

typeset不是由POSIX定义的,Almquist Shell的实现(FreeBSD,NetBSD,Debian)也不支持它,所以上面的攻击在这些平台上不起作用。

a8jjtwal

a8jjtwal2#

我相信最接近的方法是将函数体放在子shell中。
试试这个

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

x=42
echo $x
foo
echo $x
xriantvc

xriantvc3#

这实际上内置在POSIX函数声明的设计中。
如果你想让一个变量在父作用域 * 中声明*,在函数中可访问,但是保持它在父作用域中的值不变,只需:

  • 使用显式子shell声明函数,即使用
  • subshell_function()(with parentheses),* 不是 *

  • inline_function(){ with braces ;}

内联分组和子 shell 分组的行为在整个语言中是一致的。
如果你想“混搭”,从内联函数开始,然后根据需要嵌套子shell函数,这很笨拙,但是可以工作。

rur96b6h

rur96b6h4#

下面是一个启用作用域的函数:

scope() {
  eval "$(set)" command eval '\"\$@\"'
}

示例脚本:

x() {
  y='in x'
  echo "$y"
}
y='outside x'
echo "$y"
scope x
echo "$y"

结果:

outside x
in x
outside x
2cmtqfgy

2cmtqfgy5#

如果您想和我一起下地狱,我已经对eval概念做了一个更详细的实现。
这个函数会自动记录准作用域变量,可以用更熟悉的语法调用,并且在离开嵌套作用域时正确地取消设置(而不仅仅是空值化)变量。

用法

如你所见,你可以用push_scope进入一个作用域,用_local声明你的准局部变量,用pop_scope离开一个作用域,用_unset取消设置一个变量,当你再次回到那个作用域时,pop_scope会重新取消设置它。

your_func() {
    push_scope
    _local x="baby" y="you" z

    x="can"
    y="have"
    z="whatever"
    _unset z

    push_scope
    _local x="you"
    _local y="like"
    pop_scope

    pop_scope
}

代码

所有的乱码变量名后缀都是额外安全的名称冲突。

# Simulate entering of a nested variable scope
# To be used in conjunction with push_scope(), pop_scope(), and _local()
push_scope() {
    SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D=$(( $SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D + 1 ))
}

# Store the present value of the specified variable(s), allowing use in a new scope.
# To be used in conjunction with push_scope(), pop_scope(), and _local()
#
# Parameters:
# $@ : string; name of variable to store the value of
scope_var() {
    for varname_FB94CFD263CF11E89500036F7F345232 in "${@}"; do
        eval "active_varnames_FB94CFD263CF11E89500036F7F345232=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES}\""

        # echo "Active varnames: ${active_varnames_FB94CFD263CF11E89500036F7F345232}"

        case " ${active_varnames_FB94CFD263CF11E89500036F7F345232} " in
            *" ${varname_FB94CFD263CF11E89500036F7F345232} "* )
                # This variable was already stored in a previous call
                # in the same scope. Do not store again.
                # echo "Push \${varname_FB94CFD263CF11E89500036F7F345232}, but already stored."
                :
                ;;

            * )
                if eval "[ -n \"\${${varname_FB94CFD263CF11E89500036F7F345232}+x}\" ]"; then
                    # Store the existing value from the previous scope.
                    # Only variables that were set (including set-but-empty) are stored
                    # echo "Pushing value of \$${varname_FB94CFD263CF11E89500036F7F345232}"
                    eval "SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_FB94CFD263CF11E89500036F7F345232}=\"\${${varname_FB94CFD263CF11E89500036F7F345232}}\""
                else
                    # Variable is unset. Do not store the value; an unstored
                    # value will be used to indicate its unset state. The
                    # variable name will still be registered.
                    # echo "Not pushing value of \$${varname_FB94CFD263CF11E89500036F7F345232}; was previously unset."
                    :
                fi

                # Add to list of variables managed in this scope.
                # List of variable names is space-delimited.
                eval "SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES}${varname_FB94CFD263CF11E89500036F7F345232} \""
                ;;
        esac

        unset active_varnames_FB94CFD263CF11E89500036F7F345232
    done

    unset varname_FB94CFD263CF11E89500036F7F345232
}

# Simulate declaration of a local variable
# To be used in conjunction with push_scope(), pop_scope(), and _local()
#
# This function is a convenience wrapper over scope_var().
#
# Can be called just like the local keyword.
# Example usage: _local foo="foofoofoo" bar="barbarbar" qux qaz=""
_local() {
    for varcouple_44D4987063D111E8A46923403DDBE0C7 in "${@}"; do
        # Example string: foo="barbarbar"
        varname_44D4987063D111E8A46923403DDBE0C7="${varcouple_44D4987063D111E8A46923403DDBE0C7%%=*}"
        varvalue_44D4987063D111E8A46923403DDBE0C7="${varcouple_44D4987063D111E8A46923403DDBE0C7#*=}"
        varvalue_44D4987063D111E8A46923403DDBE0C7="${varvalue_44D4987063D111E8A46923403DDBE0C7#${varcouple_44D4987063D111E8A46923403DDBE0C7}}"

        # Store the value for the previous scope.
        scope_var "${varname_44D4987063D111E8A46923403DDBE0C7}"

        # Set the value for this scope.
        eval "${varname_44D4987063D111E8A46923403DDBE0C7}=\"\${varvalue_44D4987063D111E8A46923403DDBE0C7}\""

        unset varname_44D4987063D111E8A46923403DDBE0C7
        unset varvalue_44D4987063D111E8A46923403DDBE0C7
        unset active_varnames_44D4987063D111E8A46923403DDBE0C7
    done

    unset varcouple_44D4987063D111E8A46923403DDBE0C7
}

# Simulate unsetting a local variable.
#
# This function is a convenience wrapper over scope_var().
# 
# Can be called just like the unset keyword.
# Example usage: _unset foo bar qux
_unset() {
    for varname_6E40DA2E63D211E88CE68BFA58FE2BCA in "${@}"; do
        scope_var "${varname_6E40DA2E63D211E88CE68BFA58FE2BCA}"
        unset "${varname_6E40DA2E63D211E88CE68BFA58FE2BCA}"
    done
}

# Simulate exiting out of a nested variable scope
# To be used in conjunction with push_scope(), pop_scope(), and _local()
pop_scope() {
    eval "varnames_2581E94263D011E88919B3D175643B87=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES}\""

    # Cannot iterate over $varnames by setting $IFS; $IFS does not work
    # properly on zsh. Workaround using string manipulation.
    while [ -n "${varnames_2581E94263D011E88919B3D175643B87}" ]; do
        # Strip enclosing spaces from $varnames.
        while true; do
            varnames_old_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87}"
            varnames_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87# }"
            varnames_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87% }"

            if [ "${varnames_2581E94263D011E88919B3D175643B87}" = "${varnames_2581E94263D011E88919B3D175643B87}" ]; then
                break
            fi
        done

        # Extract the variable name for the current iteration and delete it from the queue.
        varname_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87%% *}"
        varnames_2581E94263D011E88919B3D175643B87="${varnames_2581E94263D011E88919B3D175643B87#${varname_2581E94263D011E88919B3D175643B87}}"

        # echo "pop_scope() iteration on \$SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}"
        # echo "varname: ${varname_2581E94263D011E88919B3D175643B87}"

        if eval "[ -n \""\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}+x}"\" ]"; then
            # echo "Value found. Restoring value from previous scope."
            # echo eval "${varname_2581E94263D011E88919B3D175643B87}=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}}\""
            eval "${varname_2581E94263D011E88919B3D175643B87}=\"\${SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}}\""
            unset "SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARVALUE_${varname_2581E94263D011E88919B3D175643B87}"
        else
            # echo "Unsetting \$${varname_2581E94263D011E88919B3D175643B87}"
            unset "${varname_2581E94263D011E88919B3D175643B87}"
        fi

        # Variable cleanup.
        unset varnames_old_2581E94263D011E88919B3D175643B87
    done

    unset SCOPE${SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D}_VARNAMES
    unset varname_2581E94263D011E88919B3D175643B87
    unset varnames_2581E94263D011E88919B3D175643B87

    SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D=$(( $SCOPENUM_CEDD88E463CF11E8A72A3F9E5F08767D - 1 ))
}
5t7ly7z5

5t7ly7z56#

可以使用一小组通用函数在Posix Shell中模拟局部变量。
下面的示例代码演示了两个函数,分别称为Local和EndLocal,这两个函数可以实现此目的。

  • 使用Local一次可在例程开始时声明所有局部变量。

Local创建一个新的作用域,并将每个局部变量以前的定义保存到新的全局变量中。

  • 在从例程返回之前使用EndLocal。

EndLocal还原保存在当前作用域中的所有以前的定义,并删除最后一个作用域。
还请注意,EndLocal保留以前的退出代码。
所有函数都很短,并且使用描述性名称,因此它们应该相对容易理解。
它们支持带有空格、单引号和双引号等复杂字符的变量。
注意:它们使用以LOCAL_开头的全局变量,因此与现有同名变量冲突的风险很小。
Test例程递归调用自身3次,并修改一些局部和全局变量。
输出显示,与全局变量N相反,局部变量A和B被保留。
代码:

#!/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"

输出:

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$

使用同样的技术,我认为应该可以动态检测是否支持local关键字,如果不支持,则定义一个名为local的新函数来模拟它。
这样一来,在现代shell具有内置局部变量的正常情况下,性能会更好,而在没有内置局部变量的旧Posix shell上,性能也会更好。
实际上,我们需要三个动态生成的函数:

  • BeginLocal,创建一个空的伪局部作用域,就像我在上面的Local一开始所做的那样,或者如果shell有内置的局部变量,则什么也不做。
  • local,类似于我的Local,只为 * 没有 * 内置局部变量的shell定义。
  • EndLocal,与我的相同,或者只保留具有内置局部变量的shell的最后一个退出代码。
mlnl4t2r

mlnl4t2r7#

变量命名约定

OpenBSD开发人员通常使用前导下划线来定义仅限函数的变量(示例可以在 /etc/netstart/etc/rc 中找到)。如果您坚持这种约定,那么您有两个变量作用域:但是,您仍然会遇到一个函数覆盖其他函数的变量的问题。
为了更进一步,你可以在一个函数的变量前面加上它的名字,或者名字的缩写,例如:

func1() { _f1_var=v1; echo $_f1_var; }
func2() { _f2_var=v2; echo $_f2_var; }

下面是一些在POSIX兼容的shell脚本中维护作用域变量时可以遵循的约定:
| 变量|说明|
| - ------|- ------|
| 变量名称|环境变数|
| 变量名称|脚本常量(可能时使用readonly定义)|
| 变量名称|全局变量|
| 函数变量名称|局部函数变量|
此外,《* 可移植 shell 编程:Bourne Shell示例的广泛集合 * 指出:
避免函数内部使用的变量和函数外部使用的变量之间冲突的最简单方法是采用一种防止冲突的命名约定。例如,在本书的shell函数中,任何只在函数内部使用的变量的名称都以下划线字符开头。
请注意,递归函数调用虽然是可能的,但并不是非常有用,因为并不是为函数的每个示例创建一组新的变量。

最佳实践

1.对于函数较少且名称冲突几率较低的简单脚本,不应考虑变量范围。
1.对于具有许多全局变量和函数变量的脚本,并且函数大多从脚本的顶层调用,建议使用下划线前缀命名约定命名函数变量。
1.如果函数调用其他函数,或者脚本包含使用dot命令的其他脚本,则建议在函数级别使用变量命名约定。
这是一个很好的问题,可惜没有一个好的答案。

93ze6v8z

93ze6v8z8#

使用function myfunc {语法定义函数,并使用typeset myvar定义变量。如果您使用myfunc(){语法定义函数,则所有常见的Bourne shell(bash、zsh、ksh '88和'93)都将本地化使用typeset定义的变量(以及像integer这样的别名)。
或者重新发明轮子。无论哪一个能使你的船浮起来。)
编辑:虽然这个问题要求使用POSIX,但这不是一个符合POSIX的函数定义语法,提问者在后面的评论中指出他使用的是bash。使用“typset”与替代的函数定义语法相结合是最好的解决方案,因为真正的POSIX机制需要派生一个新的子shell的额外开销。

相关问题