一次又一次地,在 Stack Overflow 上看到使用 eval
的 Bash 答案,结果因使用这种“邪恶”的构造而受到批评。为什么 eval
如此“邪恶”?
如果不能安全地使用 eval
,我应该使用什么代替?
eval
有可能执行“脏”数据。脏数据是指任何未被重写为安全使用于情境XYZ的数据,在我们的情况下,是任何未经格式化以便进行评估的字符串。function println
{
# Send each element as a separate argument, starting with the second element.
# Arguments to printf:
# 1 -> "$1\n"
# 2 -> "$2"
# 3 -> "$3"
# 4 -> "$4"
# etc.
printf "$1\n" "${@:2}"
}
function error
{
# Send the first element as one argument, and the rest of the elements as a combined argument.
# Arguments to println:
# 1 -> '\e[31mError (%d): %s\e[m'
# 2 -> "$1"
# 3 -> "${*:2}"
println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit "$1"
}
# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).
现在假设我们想要添加一个选项,将输出重定向作为println的参数。当然,我们可以在每次调用时仅重定向println的输出,但是为了举例说明,我们不会这样做。我们需要使用eval
,因为变量无法用于重定向输出。
function println
{
eval printf "$2\n" "${@:3}" $1
}
function error
{
println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit $1
}
error 1234 Something went wrong.
function println
{
eval 'printf "$2\n" "${@:3}"' $1
}
function error
{
println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit $1
}
error 1234 Something went wrong.
println
中的$1
从未被恶意篡改,就是安全的。sudo
中使用的相同的未引用语法!为什么它在那里起作用,而不是在这里?为什么我们必须将所有内容都用单引号括起来?sudo
更加现代化:它知道对其接收到的每个参数都加上引号,尽管这是一个过度简化的说法。eval
仅仅是将所有内容连接起来。eval
的替代品像sudo
一样处理参数,因为eval
是一个shell内置命令;这一点很重要,因为当它执行时,它会承担周围代码的环境和范围,而不是像函数一样创建一个新的堆栈和范围。
特定的用例通常有可行的替代方法来代替eval
。以下是一个便捷的列表。在command
中代替您想要发送给eval
的任何内容。
在bash中,一个简单的冒号是一个无操作符:
:
( command ) # Standard notation
永远不要依赖外部命令。您应该始终控制返回值。将它们放在自己的行中:
$(command) # Preferred
`command` # Old: should be avoided, and often considered deprecated
# Nesting:
$(command1 "$(command2)")
`command "\`command\`"` # Careful: \ only escapes $ and \ with old style, and
# special case \` results in nesting.
在调用代码中,将&3
(或任何大于&2
的值)映射到您的目标:
exec 3<&0 # Redirect from stdin
exec 3>&1 # Redirect to stdout
exec 3>&2 # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt # Redirect to file
exec 3> "$var" # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1 # Input and output!
如果这只是一次性的调用,你就不需要重定向整个 shell:
func arg1 arg2 3>&2
在被调用的函数中,重定向到&3
:
command <&3 # Redirect stdin
command >&3 # Redirect stdout
command 2>&3 # Redirect stderr
command &>&3 # Redirect stdout and stderr
command 2>&1 >&3 # idem, but for older bash versions
command >&3 2>&1 # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4 # Input and output!
场景:
VAR='1 2 3'
REF=VAR
不好:
eval "echo \"\$$REF\""
为什么?如果REF包含双引号,它将会被破坏并暴露给攻击者。虽然可以对REF进行清理,但有更好的方法:
echo "${!REF}"
eval
棘手一些。# Add to scenario:
VAR_2='4 5 6'
# We could use:
local ref="${REF}_2"
echo "${!ref}"
# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""
尽管有经验的程序员习惯于使用eval
,但新方法更加直观。
关联数组在bash 4中被内置实现。一个注意点是:必须使用declare
创建。
declare -A VAR # Local
declare -gA VAR # Global
# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )
VAR+=( ['alpha']='beta' [2]=3 ) # Combine arrays
VAR['cow']='moo' # Set a single element
unset VAR['cow'] # Unset a single element
unset VAR # Unset an entire array
unset VAR[@] # Unset an entire array
unset VAR[*] # Unset each element with a key corresponding to a file in the
# current directory; if * doesn't expand, unset the entire array
local KEYS=( "${!VAR[@]}" ) # Get all of the keys in VAR
VAR=( ) # This will store our keys.
# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )
# Recover a simple value.
local var_key="VAR_$key" # The name of the variable that holds the value
local var_value="${!var_key}" # The actual value--requires bash 2
# For < bash 2, eval is required for this method. Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""
# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value" # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`" # Retrieve
# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
local key="`mkpasswd -5R0 "$1" 00000000`"
echo -n "${key##*$}"
}
local var_key="VAR_`mkkey "$key"`"
# ...
eval "export $var='$val'"
的提及……(?) - Zrinexport "$var"="$val"
才是你想要的。你可能只有在 var='$var2'
的情况下才会使用你的形式,而且你想要双重引用它——但你不应该在 bash 中尝试做任何类似的事情。如果你真的必须这样做,你可以使用 export "${!var}"="$val"
。 - Zenexerx="echo hello world";
,那么要执行 x
中包含的任何内容,我们可以使用 eval $x
。然而,$($x)
是错误的,对吗?是的:$($x)
是错误的,因为它会运行 echo hello world
,然后尝试运行捕获的输出(至少在我认为你使用它的上下文中是这样),这将失败,除非你有一个叫做 hello
的程序。 - Jonathan Lefflerref="${REF}_2" echo "${!ref}"
这个示例是错误的,因为bash在执行命令之前会先替换变量。如果 ref
变量确实未定义,则替换结果将是 ref="VAR_2" echo ""
,这也就是实际执行的内容。 - Yoory N.eval
eval
可以被安全地使用,但是它的所有参数都需要先加上引号。以下是方法:
这个函数可以帮你实现:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
示例用法:
给定一些不受信任的用户输入:
% input="Trying to hack you; date"
构建一个用于执行的命令:% cmd=(echo "User gave:" "$input")
使用表面上看起来正确的引用进行评估:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
注意,你的账户被黑了。执行了date
而不是直接打印出来。
相反,使用token_quote()
:
相反,使用token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
不是邪恶的,它只是被误解了 :)
for
循环中的 in words
部分是可选的。】(https://www.gnu.org/software/bash/manual/bash.html#Looping-Constructs) - Tom Haleeval
的情况都应该引起警惕,并仔细检查是否已经有语言提供的更好的选项。 - dimo414eval "$(token_quote "${cmd[@]}")"
相对于直接写 "${cmd[@]}"
有什么优势呢?Bash 能够自己扩展数组并运行生成的命令;我在这里得到了正确的输出。 - Alice M.eval
所吸引的大部分情况:
很多时候,简单的索引数组就足够了,前提是你在定义数组时采取良好的习惯,使用双引号来保护扩展。
# One nasty argument which must remain a single argument and not be split:
f='foo bar'
# The command in an indexed array (use `declare -a` if you really want to be explicit):
cmd=(
touch
"$f"
# Yet another nasty argument, this time hardcoded:
'plop yo'
)
# Let Bash expand the array and run it as a command:
"${cmd[@]}"
foo bar
和plop yo
(两个文件,而不是四个)。touch "${args[@]}"
touch "${opts[@]}" file1 file2
cmd=(
# Important because blah blah:
-v
)
cmd=(myprog)
for f in foo bar
do
cmd+=(-i "$f")
done
if [[ $1 = yo ]]
then
cmd+=(plop)
fi
to_be_added=(one two 't h r e e')
cmd+=("${to_be_added[@]}")
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
function please_log_that {
printf 'Running:'
# From `help printf`:
# “The format is re-used as necessary to consume all of the arguments.”
# From `man printf` for %q:
# “printed in a format that can be reused as shell input,
# escaping non-printable characters with the proposed POSIX $'' syntax.”
printf ' %q' "$@"
echo
}
arg='foo bar'
cmd=(prog "$arg" 'plop yo' $'arg\nnewline\tand tab')
please_log_that "${cmd[@]}"
# ⇒ “Running: prog foo\ bar plop\ yo $'arg\nnewline\tand tab'”
# You can literally copy and paste that ↑ to a terminal and get the same execution.
eval
字符串更好的语法高亮,因为您不需要嵌套引号或使用“将在某个时刻评估但不会立即评估”的$
。对我来说,这种方法的主要优点(相反,eval
的缺点)是您可以遵循通常的引用、扩展等逻辑。无需费尽心思地试图“提前”在引号中放置引号中的引号,同时试图弄清楚哪个命令将在哪个时刻解释哪个引号对。当然,上面提到的许多事情使用eval
更难甚至根本无法实现。
使用这些方法,我过去六年从未依赖于eval
,可读性和鲁棒性(特别是涉及包含空格的参数)得到了提高。您甚至不需要知道IFS
是否被篡改!当然,仍然存在一些边缘情况,可能确实需要使用eval
(例如,如果用户必须能够通过交互提示或其他方式提供一个完整的脚本片段),但希望这不是您每天都会遇到的事情。
declare -n
(或其函数内部的local -n
替代方案),以及${!foo}
大多数时候都可以解决问题。
$ help declare | grep -- -n
-n make NAME a reference to the variable named by its value
嗯,没有例子的话就不是特别清楚:
declare -A global_associative_array=(
[foo]=bar
[plop]=yo
)
# $1 Name of global array to fiddle with.
fiddle_with_array() {
# Check this if you want to make sure you’ll avoid
# circular references, but it’s only if you really
# want this to be robust.
# You can also give an ugly name like “__ref” to your
# local variable as a cheaper way to make collisions less likely.
if [[ $1 != ref ]]
then
local -n ref=$1
fi
printf 'foo → %s\nplop → %s\n' "${ref[foo]}" "${ref[plop]}"
}
# Call the function with the array NAME as argument,
# not trying to get its content right away here or anything.
fiddle_with_array global_associative_array
# This will print:
# foo → bar
# plop → yo
我喜欢这个技巧 ↑,因为它让我感觉像在面向对象的语言中将对象传递给函数一样。它的可能性令人惊叹。
至于 ${!…}
(用于获取由另一个变量命名的变量的值):
foo=bar
plop=yo
for var_name in foo plop
do
printf '%s = %q\n' "$var_name" "${!var_name}"
done
# This will print:
# foo = bar
# plop = yo
eval
可能有问题;请参见 https://www.vidarholen.net/contents/blog/?p=716 了解其他一些例子。有许多 shell 构造最终会被评估、扩展等,而仅仅关注 eval 是错误的,因为它会产生虚假的安全感。重要的是要理解使用任何不受信任的数据的风险以及如何利用它。话虽如此,一个好的 SO 回答应该假定存在不受信任的外部数据,或者至少警告可能的陷阱,所以我倾向于支持 bashing,除了可能过度指责eval
。 - Thomas Guyot-Sionnesteval
往往很容易用另一种方法替代 - 在某些情况下,甚至只需直接运行命令而无需使用字符串 - 因此值得对其进行警告。绝对想要使用eval
通常会导致过度的事情,比如该页面上的答案将整个数组转换为另一个数组(然后转换为字符串),而初始数组本可以直接用于以相同的安全性和不使用eval
的方式运行命令,据我所知。 - Alice M.