sh:通过字符串名称访问变量值

sh: variable value access by string name

我写了一个简短的脚本来检查环境变量是否存在,如果不存在则失败。此脚本的目的是在 CI 服务器上使用,该服务器具有通过控制台加载的配置变量。我想确保已设置这些变量,如果未设置,则提前使作业失败。

这些作业的执行环境是一个基于 Alpine Linux 的 Docker 容器。 我只能访问 sh 我想避免安装另一个 shell,例如 bash,以尽可能保持图像大小。

脚本大致如下所示:

#!/bin/sh

AWS_ACCESS_KEY_ID=123  # provided by CI

 _fail_without() {
  VAR_NAME=
  VAR_VAL=$(eval echo "$$VAR_NAME")

   if [[ -z "${VAR_VAL}" ]]; then
    echo "${VAR_NAME} not set; aborting"
    exit 1
  else
    echo "${VAR_NAME} exists"
  fi
}

_fail_without AWS_ACCESS_KEY_ID
_fail_without AWS_SECRET_ACCESS_KEY

...具有预期的标准输出:

AWS_ACCESS_KEY_ID exists
AWS_SECRET_ACCESS_KEY not set; aborting

如您所见,我传递了变量名称的 字符串值 ,而不是变量本身,因此失败将被正确记录。所有这一切都很好。但是,我担心依赖 eval 访问行 VAR_VAL=$(eval echo "$$VAR_NAME") 中的变量值的潜在安全隐患。

问题是:这是一种可行的方法吗?是否有任何需要注意的安全隐患?如果有,是否有更安全或更好的替代方案?我不能使用 declare,并且 printf 的行为方式似乎与 bash 中的行为方式不同。

经过一番深思熟虑,阅读 posix shell manual and finding any good in posix utilities 我终于决定我会使用变量扩展 ${var:?}${var?} 来检查变量是否已设置或未设置,是否为 null 以及我将使用 expr 实用程序和 BRE posix 正则表达式来检查变量是否是有效的变量名。

以下是我最终得到的函数。最后是一个小的测试函数和一些测试用例。我觉得 expr BRE 匹配是最不可移植的部分,但是我在 var_is_name.

中找不到任何误报
#!/bin/sh

# var ####################################################################################################

#
# Check if arguments are a valid "name" identifier in the POSIX shell contects
# @args identifiers
# @returns
# 0 - all identifiers are valid names
# 1 - any one of identifiers is not a valid name
# 2 - internal error
# 3 - even worse internal error
var_is_name() {
    # 3.230 Name
    # In the shell command language, a word consisting solely of underscores, digits, and alphabetics from the portable character set. The first character of a name is not a digit.
    local _var_is_name_i
    for _var_is_name_i; do
        expr "$_var_is_name_i" : '[_a-zA-Z][_a-zA-Z0-9]*$' >/dev/null || return $?
    done
}

# @args identifiers
# @returns Same as var_is_name but returns `2` in case if any of the arguments is not a valid name
var_is_name_error_on_fail() {
    local _var_is_name_error_on_fail_ret
    var_is_name "$@" && _var_is_name_error_on_fail_ret=$? || _var_is_name_error_on_fail_ret=$?
    if [ "$_var_is_name_error_on_fail_ret" -eq 1 ]; then return 2
    elif [ "$_var_is_name_error_on_fail_ret" -ne 0 ]; then return "$_var_is_name_error_on_fail_ret"
    fi
}

# @args identifiers
# @returns 
# 0 - if all identifiers are set
# 1 - if any of the identifiers is not set
# other - in case of error (ex. any of the identifiers is not a valid name)
var_is_set() {
    var_is_name_error_on_fail "$@" || return $?
    local _var_is_set_i
    for _var_is_set_i; do
        if ! ( eval printf %.0s "\"${$_var_is_set_i?}\"" ) 2>/dev/null; then
            return 1
        fi
    done
    return 0
}

# @args identifiers
# @returns 
# 0 - if all identifiers are null
# 1 - if any of the identifiers is not null
# other - in case of error (ex. any of the identifiers is not a valid name)
var_is_null() {
    var_is_name_error_on_fail "$@" || return $?
    var_is_set "$@" || return $?
    local _var_is_null_i
    for _var_is_null_i; do
        ( eval printf %.0s "\"${$_var_is_null_i:?}\"" ) 2>/dev/null || return 0
    done
    return 1
}

# @args identifiers
# @returns 
# 0 - if all identifiers are not null
# 1 - if any of the identifiers is null
# other - in case of error (ex. any of the identifiers is not a valid name)
var_is_not_null() {
    var_is_name_error_on_fail "$@" || return $?
    var_is_set "$@" || return $?
    local _var_is_not_null_ret
    var_is_null "$@" && _var_is_not_null_ret=$? || _var_is_not_null_ret=$?
    if [ "$_var_is_not_null_ret" -eq 0 ]; then
        return 1
    elif [ "$_var_is_not_null_ret" -eq 1 ]; then
        return 0;
    fi
    return "$_var_is_not_null_ret"
}

#################################################################################################################

var_test() {
    local ret

    var_is_name "$@" && ret=$? || ret=$?
    if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "" "name"
    elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "" "name"
    else printf "err var_is_name %s %s\n" "" "$ret"; fi

    var_is_set "$@" && ret=$? || ret=$?
    if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "" "set"
    elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "" "set"
    elif [ "$ret" -eq 2 ]; then printf "var_is_set %s errored\n" ""
    else printf "err var_is_set %s %s\n" "" "$ret"; fi

    var_is_null "$@" && ret=$? || ret=$?
    if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "" "null"
    elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "" "null"
    elif [ "$ret" -eq 2 ]; then printf "var_is_null %s errored\n" ""
    else printf "err var_is_null %s %s\n" "" "$ret"; fi

    var_is_not_null "$@" && ret=$? || ret=$?
    if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "" "not_null"
    elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "" "not_null"
    elif [ "$ret" -eq 2 ]; then printf "var_is_not_null %s errored\n" ""
    else printf "err var_is_not_null %s %s\n" "" "$ret"; fi

    echo
}

var_test '$()'
var_test '$()def'
var_test 'abc$()'
var_test 'abc$()def'
echo "unset a"; var_test a
a=; echo "a=$a"; var_test a
a=""; echo "a=\"\""; var_test a
a='$(echo I will format your harddrive >&2)'; echo "a='$a'"; var_test a
a='!@$%^&*(){}:"|<>>?~'\'; echo "a='$a'"; var_test a

当 运行 在 alpine 中时,脚本将输出:

# the script saved in /tmp/script.sh
$ chmod +x /tmp/script.sh
$ docker run --rm -ti -v /tmp:/mnt alpine /mnt/script.sh
$() is not name
var_is_set $() errored
var_is_null $() errored
var_is_not_null $() errored

$()def is not name
var_is_set $()def errored
var_is_null $()def errored
var_is_not_null $()def errored

abc$() is not name
var_is_set abc$() errored
var_is_null abc$() errored
var_is_not_null abc$() errored

abc$()def is not name
var_is_set abc$()def errored
var_is_null abc$()def errored
var_is_not_null abc$()def errored

unset a
a is name
a is not set
a is not null
a is not not_null

a=
a is name
a is set
a is null
a is not not_null

a=""
a is name
a is set
a is null
a is not not_null

a='$(echo I will format your harddrive >&2)'
a is name
a is set
a is not null
a is not_null

a='!@$%^&*(){}:"|<>>?~''
a is name
a is set
a is not null
a is not_null

也就是说,我认为这对于简单的 "is a variable set or not" 检查来说太麻烦了。有时我只是相信其他人不会做奇怪的事情,如果他们这样做,那会破坏他们的计算机而不是我的。所以有时我会建议只接受像你这样的简单解决方案 - [ -n "$(eval echo "\"${$var}\"")" ] && echo "$var is set" || echo "$var is not set 有时当你相信你的输入时就足够了。