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
有时当你相信你的输入时就足够了。
我写了一个简短的脚本来检查环境变量是否存在,如果不存在则失败。此脚本的目的是在 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
有时当你相信你的输入时就足够了。