56

是否有一种符合 POSIX 标准的方法来将变量的范围限制为声明它的函数?IE:

Testing()
{
    TEST="testing"
}

Testing
echo "Test is: $TEST"

应该打印“测试是:”。我已经阅读了有关 declare、local 和 typeset 关键字的信息,但看起来它们不是 POSIX 内置必需的。

4

7 回答 7

78

它通常使用local关键字完成,如您所知,POSIX 未定义该关键字。这是关于将 'local' 添加到 POSIX的内容丰富的讨论。

然而,即使是我所知道的最原始的兼容 POSIX 的 shell,它也被某些 GNU/Linux 发行版用作/bin/sh默认值,dash(Debian Almquist Shell)也支持它。FreeBSD 和 NetBSD 使用ash原始的 Almquist Shell,它也支持它。OpenBSD 使用一个也支持它的ksh实现。/bin/sh因此,除非您的目标是支持非 GNU 非 BSD 系统,如 Solaris,或使用标准 ksh 等的系统,否则您可以使用local. (可能想在脚本开头的 shebang 行下方添加一些注释,注意它不是严格意义上的 POSIX sh 脚本。只是为了不作恶。)说了这么多,您可能想检查相应的所有这些sh支持的实现的手册页local,因为它们的工作方式可能存在细微差别。或者只是不使用local

如果您真的想完全符合 POSIX,或者不想弄乱可能的问题,因此不使用local,那么您有几个选择。Lars Brinkhoff 给出的答案是合理的,您可以将函数包装在子外壳中。不过,这可能会产生其他不良影响。顺便说一句,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允许它。

另一种选择是unset在函数体末尾使用变量,但这当然不会恢复旧值,所以我猜这并不是你想要的,它只会防止变量的函数内值泄漏到外面。我猜不是很有用。

我能想到的最后一个疯狂的想法是实现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

(顺便说一下unlet,为方便起见,可以一次给定任意数量的变量(作为不同的参数),上面没有展示。)

不要在家里尝试,不要给孩子看,不要给你的同事看,不要#bash在 Freenode 看,不要给 POSIX 委员会的成员看,不要给伯恩先生看,也许给麦卡锡父亲的鬼魂看,让他笑一笑。你被警告了,你没有从我这里学到。

编辑:

显然我被打败了,greybot在 Freenode(属于#bash)上发送 IRC 机器人命令“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

因此,它允许在 if 表单的分支中使用变量xy本地变量。then可以在else分支处添加更多变量;请注意,必须将它们添加两次,一次像variable=在初始列表中一样,一次作为参数传递给typeset. 请注意,不需要unlet左右(它是一个“透明”的实现),并且没有进行名称修改和过度eval操作。因此,总体而言,它似乎是一个更清洁的实现。

编辑2:

POSIX 没有定义出来typeset,并且 Almquist Shell(FreeBSD、NetBSD、Debian)的实现不支持它。所以上面的hack在这些平台上不起作用。

于 2013-09-03T20:28:39.893 回答
9

我相信最接近的是将函数体放在子shell中。

例如试试这个

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

x=42
echo $x
foo
echo $x
于 2013-09-03T18:38:46.687 回答
4

这实际上内置于 POSIX 函数声明的设计中。

如果您希望在父作用域中声明 的变量可以在函数中访问,但父作用域中保留其值不变,只需:

*使用显式子shell声明您的函数,即使用

  • subshell_function() (with parentheses),不是

  • inline_function() { with braces ;}


内联分组与子shell 分组的行为在整个语言中是一致的。

如果您想“混合搭配”,请从内联函数开始,然后根据需要嵌套子外壳函数。它很笨重,但很有效。

于 2020-11-21T18:55:39.813 回答
2

这是一个启用范围的函数:

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
于 2017-02-25T06:12:41.487 回答
2

如果你想和我一起下地狱,我已经对这个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 ))
}
于 2018-05-30T07:23:07.910 回答
0

可以使用一小组通用函数在 Posix Shell 中模拟局部变量。
下面的示例代码演示了两个函数,称为 Local 和 EndLocal,它们可以解决问题。

  • 使用 Local 一次在例程开始时声明所有局部变量。
    Local 创建一个新的作用域,并将每个局部变量的先前定义保存到一个新的全局变量中。
  • 在从例程返回之前使用 EndLocal。
    EndLocal 恢复保存在当前范围内的所有先前定义,并删除最后一个范围。
    另请注意,EndLocal 保留了先前的退出代码。

所有函数都很短,并且使用描述性名称,因此它们应该相对容易理解。
它们支持带有诸如空格、单引号和双引号等复杂字符的变量。
注意:它们使用以 LOCAL_ 开头的全局变量,因此与现有同名变量发生冲突的风险很小。

Test 例程递归调用自身 3 次,并修改了一些局部和全局变量。
输出显示 A 和 B 局部变量被保留,与全局 N 变量相反。

代码:

#!/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 具有内置的 locals,则什么也不做。
  • local,类似于我的 Local,只为没有内置 locals 的 shell 定义。
  • EndLocal,与我的相同,或者仅保留具有内置本地变量的 shell 的最后退出代码。
于 2020-12-02T10:27:39.080 回答
-5

function myfunc {使用语法定义函数,并用于typeset myvar定义变量。如果您以这种方式定义函数而不是使用myfunc(){语法,那么所有常见的 Bourne shell(bash、zsh、ksh '88 和 '93)都将本地化定义为的变量typeset(以及排版的别名,如integer)。

或者重新发明轮子。无论哪个漂浮你的船。;)

编辑:虽然问题要求使用 POSIX,并且这不是符合 POSIX 的函数定义语法,但询问的人在稍后的评论中表示他正在使用 bash。将“typset”与替代函数定义语法结合使用是最好的解决方案,因为真正的 POSIX 机制需要分叉新子外壳的额外开销。

于 2013-09-04T03:40:56.043 回答