为什么要在Bash中避免使用eval,我应该使用什么呢?

Zen*_*xer 94 unix linux bash eval

我一次又一次地看到Bash在Stack Overflow上使用eval的答案,并且答案得到了抨击,双关语是为了使用这种"邪恶"结构.为什么eval这么邪恶?

如果eval不能安全使用,我应该使用什么呢?

Zen*_*xer 138

除了眼睛之外,这个问题还有更多.我们将从明显的开始:eval有可能执行"脏"数据.脏数据是指未被重写为安全使用情况-XYZ的任何数据; 在我们的例子中,它是任何未经过格式化的字符串,以便安全评估.

乍一看,清理数据似乎很容易.假设我们抛出一系列选项,bash已经提供了一种清理单个元素的好方法,另一种方法是将整个数组清理为单个字符串:

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).
Run Code Online (Sandbox Code Playgroud)

现在说我们要添加一个选项来将输出重定向为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.
Run Code Online (Sandbox Code Playgroud)

看起来不错吧?问题是,eval解析两次命令行(在任何shell中).在解析第一遍引用时,删除了一层引用.删除引号后,会执行一些变量内容.

我们可以通过让变量扩展在内部进行来解决这个问题eval.我们所要做的就是单引一切,将双引号留在原处.一个例外:我们必须在之前扩展重定向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.
Run Code Online (Sandbox Code Playgroud)

这应该工作.只要这也是安全$1println是永远不会脏.

现在暂停一下:我使用的是与我们原来一直使用的相同的不带引号的语法sudo!它为什么在那里工作,而不是在这里?为什么我们必须单引一切? sudo有点现代:它知道在引号中包含它收到的每个参数,尽管这是一种过度简化. eval简单地连接一切.

不幸的是,eval像内置的shell 一样,sudo没有像对待那样的对待参数的替代品eval; 这很重要,因为它在执行时会占用周围代码的环境和范围,而不是像函数一样创建新的堆栈和范围.

eval替代品

特定用例通常具有可行的替代方案eval.这是一个方便的清单. command代表您通常发送的内容eval; 取而代之的是你喜欢的.

无操作

bash中的无操作中的简单冒号:

创建一个子shell

( command )   # Standard notation
Run Code Online (Sandbox Code Playgroud)

执行命令的输出

永远不要依赖外部命令.你应该始终控制返回值.把它们放在自己的路上:

$(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.
Run Code Online (Sandbox Code Playgroud)

基于变量的重定向

在调用代码时,映射&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!
Run Code Online (Sandbox Code Playgroud)

如果是一次性调用,则不必重定向整个shell:

func arg1 arg2 3>&2
Run Code Online (Sandbox Code Playgroud)

在被调用的函数内,重定向到&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!
Run Code Online (Sandbox Code Playgroud)

可变间接

场景:

VAR='1 2 3'
REF=VAR
Run Code Online (Sandbox Code Playgroud)

坏:

eval "echo \"\$$REF\""
Run Code Online (Sandbox Code Playgroud)

为什么?如果REF包含双引号,则会破坏并打开代码以利用漏洞.有可能对REF进行消毒,但是当你有这个时,这是浪费时间:

echo "${!REF}"
Run Code Online (Sandbox Code Playgroud)

没错,bash在版本2中内置了变量间接.它比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\""
Run Code Online (Sandbox Code Playgroud)

无论如何,新方法更直观,尽管对于经验丰富的编程人员来说,这似乎并不像以前那样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
Run Code Online (Sandbox Code Playgroud)

在旧版本的bash中,您可以使用变量间接:

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"`"
# ...
Run Code Online (Sandbox Code Playgroud)

  • 我错过了提到`eval'export $ var ='$ val'"``(?) (2认同)
  • `ref="${REF}_2" echo "${!ref}"` 示例是错误的,它不会按预期工作,因为 bash 在执行命令之前替换了变量。如果 ref 变量之前确实未定义,则替换的结果将是 ref="VAR_2" echo ""`,这就是将被执行的内容。 (2认同)

Tom*_*ale 14

如何eval安全

eval 可以安全使用 - 但需要首先引用它的所有参数.这是如何做:

这个功能可以为你做到:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

鉴于一些不受信任的用户输入:

% input="Trying to hack you; date"
Run Code Online (Sandbox Code Playgroud)

构造一个命令到eval:

% cmd=(echo "User gave:" "$input")
Run Code Online (Sandbox Code Playgroud)

评估它,看似正确的引用:

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Run Code Online (Sandbox Code Playgroud)

请注意,你被黑了.date是执行而不是按字面打印.

相反token_quote():

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
Run Code Online (Sandbox Code Playgroud)

eval 不是邪恶的 - 它只是被误解了:)

  • 我会比简单地“误解”更进一步,它也经常被“误用”并且确实不需要。Zenexer 的答案涵盖了很多此类情况,但任何“eval”的使用都应该是一个危险信号,并仔细检查以确认该语言确实没有提供更好的选择。 (3认同)
  • @Akito [“for”循环的“in Words”部分是可选的。](https://www.gnu.org/software/bash/manual/bash.html#Looping-Constructs) (2认同)
  • @Akito `for foo` 相当于 `for foo in "$@"`,因此 `for foo in "$1" … "$n"`。 (2认同)

Ali*_* M. 11

I\xe2\x80\x99ll 将这个答案分为两部分,我认为这涵盖了人们容易受到诱惑的大部分情况eval

\n
    \n
  1. 运行奇怪的命令
  2. \n
  3. 摆弄动态命名变量
  4. \n
\n

运行奇怪的命令

\n

很多时候,简单的索引数组就足够了,只要您在定义数组时养成有关双引号的良好习惯以保护扩展。

\n
# One nasty argument which must remain a single argument and not be split:\nf=\'foo bar\'\n# The command in an indexed array (use `declare -a` if you really want to be explicit):\ncmd=(\n    touch\n    "$f"\n    # Yet another nasty argument, this time hardcoded:\n    \'plop yo\'\n)\n# Let Bash expand the array and run it as a command:\n"${cmd[@]}"\n
Run Code Online (Sandbox Code Playgroud)\n

这将创建foo barplop yo(两个文件,而不是四个)。

\n

请注意,有时它可以生成更易读的脚本,仅将参数(或一堆选项)放入数组中(至少您乍一看就知道您\xe2\x80\x99正在运行什么):

\n
touch "${args[@]}"\ntouch "${opts[@]}" file1 file2\n
Run Code Online (Sandbox Code Playgroud)\n

作为奖励,数组可以让您轻松地:

\n
    \n
  1. 添加有关特定参数的评论:
  2. \n
\n
cmd=(\n    # Important because blah blah:\n    -v\n)\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 通过在数组定义中保留空行来对参数进行分组以提高可读性。
  2. \n
  3. 出于调试目的注释掉特定参数。
  4. \n
  5. 将参数附加到命令中,有时根据特定条件或在循环中动态附加:
  6. \n
\n
cmd=(myprog)\nfor f in foo bar\ndo\n    cmd+=(-i "$f")\ndone\nif [[ $1 = yo ]]\nthen\n    cmd+=(plop)\nfi\nto_be_added=(one two \'t h r e e\')\ncmd+=("${to_be_added[@]}")\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 在配置文件中定义命令,同时允许配置定义的包含空格的参数:
  2. \n
\n
readonly ENCODER=(ffmpeg -blah --blah \'yo plop\')\n# Deprecated:\n#readonly ENCODER=(avconv -bloh --bloh \'ya plap\')\n# [\xe2\x80\xa6]\n"${ENCODER[@]}" foo bar\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 使用 printf\xe2\x80\x99s 记录一个可靠的可运行命令,它完美地代表了正在运行的命令%q
  2. \n
\n
function please_log_that {\n    printf \'Running:\'\n    # From `help printf`:\n    # \xe2\x80\x9cThe format is re-used as necessary to consume all of the arguments.\xe2\x80\x9d\n    # From `man printf` for %q:\n    # \xe2\x80\x9cprinted in a format that can be reused as shell input,\n    # escaping  non-printable  characters with the proposed POSIX $\'\' syntax.\xe2\x80\x9d\n    printf \' %q\' "$@"\n    echo\n}\n\narg=\'foo bar\'\ncmd=(prog "$arg" \'plop yo\' $\'arg\\nnewline\\tand tab\')\nplease_log_that "${cmd[@]}"\n# \xe2\x87\x92 \xe2\x80\x9cRunning: prog foo\\ bar plop\\ yo $\'arg\\nnewline\\tand tab\'\xe2\x80\x9d\n# You can literally copy and paste that \xe2\x86\x91 to a terminal and get the same execution.\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 享受比eval字符串更好的语法突出显示,因为您不需要嵌套引号或使用$-s 来保证 \xe2\x80\x9c 不会立即被评估,但会在某个时刻\xe2\x80\x9d 被评估。
  2. \n
\n

对我来说,这种方法的主要优点(以及相反的缺点eval)是,您可以遵循与平常相同的关于引用、扩展等的逻辑。无需绞尽脑汁尝试将引号放在引号 \xe2\x80 中的引号中\x9cin advance\xe2\x80\x9d 同时尝试找出哪个命令将在哪个时刻解释哪对引号。当然,上面提到的许多事情都更难或根本不可能用 来实现eval

\n

eval有了这些,我在过去六年左右的时间里再也不需要依赖它们,并且可读性和稳健性(特别是关于包含空格的参数)可以说得到了提高。你甚至不需要知道是否IFS经过锤炼!当然,仍然存在一些边缘情况eval实际需要的边缘情况(我想,例如,如果用户必须能够通过交互式提示或其他方式提供完整的脚本片段),但希望 \xe2\x80 \x99s 不是你每天都会遇到的东西。

\n

摆弄动态命名变量

\n

declare -n(或其函数内local -n对应项)以及 ,${!foo}在大多数情况下都可以解决问题。

\n
$ help declare | grep -- -n\n      -n    make NAME a reference to the variable named by its value\n
Run Code Online (Sandbox Code Playgroud)\n

好吧,如果没有示例,它\xe2\x80\x99 就不是特别清楚:

\n
declare -A global_associative_array=(\n    [foo]=bar\n    [plop]=yo\n)\n\n# $1    Name of global array to fiddle with.\nfiddle_with_array() {\n    # Check this if you want to make sure you\xe2\x80\x99ll avoid\n    # circular references, but it\xe2\x80\x99s only if you really\n    # want this to be robust.\n    # You can also give an ugly name like \xe2\x80\x9c__ref\xe2\x80\x9d to your\n    # local variable as a cheaper way to make collisions less likely.\n    if [[ $1 != ref ]]\n    then\n        local -n ref=$1\n    fi\n    \n    printf \'foo \xe2\x86\x92 %s\\nplop \xe2\x86\x92 %s\\n\' "${ref[foo]}" "${ref[plop]}"\n}\n\n# Call the function with the array NAME as argument,\n# not trying to get its content right away here or anything.\nfiddle_with_array global_associative_array\n\n# This will print:\n# foo \xe2\x86\x92 bar\n# plop \xe2\x86\x92 yo\n
Run Code Online (Sandbox Code Playgroud)\n

(我喜欢这个技巧 \xe2\x86\x91,因为它让我感觉 \xe2\x80\x99m 将对象传递给我的函数,就像在面向对象的语言中一样。可能性令人难以置信。)

\n

至于${!\xe2\x80\xa6}(获取另一个变量命名的变量的值):

\n
foo=bar\nplop=yo\n\nfor var_name in foo plop\ndo\n    printf \'%s = %q\\n\' "$var_name" "${!var_name}"\ndone\n\n# This will print:\n# foo = bar\n# plop = yo\n
Run Code Online (Sandbox Code Playgroud)\n