为什么在 Bash 中应该避免使用 eval,我应该使用什么来代替? [英] Why should eval be avoided in Bash, and what should I use instead?

查看:36
本文介绍了为什么在 Bash 中应该避免使用 eval,我应该使用什么来代替?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

解决方案

这个问题远不止眼前的问题.我们将从显而易见的开始:eval 有可能执行脏"数据.脏数据是任何没有被重写为安全使用情况-XYZ 的数据;在我们的例子中,它是任何尚未格式化的字符串,以便可以安全地进行评估.

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

函数 println{# 从第二个元素开始,将每个元素作为单独的参数发送.# printf 的参数:# 1 ->$1
"# 2 ->$2"# 3 ->3美元"# 4 ->4美元"#   等等.printf "$1
" "${@:2}"}功能错误{# 将第一个元素作为一个参数发送,其余元素作为组合参数发送.# 打印参数:# 1 ->'e[31mError (%d): %se[m'# 2 ->$1"# 3 ->${*:2}"println 'e[31mError (%d): %se[m' "$1" "${*:2}"退出$1"}# 这个...错误 1234 出了点问题.# 和这个...错误 1234 '出了点问题.'# 结果是相同的输出(只要 $IFS 没有被修改).

现在假设我们要添加一个选项以将输出重定向为 println 的参数.当然,我们可以在每次调用时重定向 println 的输出,但为了举例,我们不打算这样做.我们需要使用 eval,因为变量不能用于重定向输出.

函数 println{eval printf "$2
" "${@:3}" $1}功能错误{println '>&2' 'e[31mError (%d): %se[m' "$1" "${*:2}"退出 $1}错误 1234 出了点问题.

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

我们可以通过在 eval 中进行变量扩展来解决这个问题.我们所要做的就是将所有内容单引号,而将双引号留在原处.一个例外:我们必须在 eval 之前扩展重定向,因此必须保留在引号之外:

函数 println{eval 'printf "$2
" "${@:3}"' $1}功能错误{println '&2' 'e[31mError (%d): %se[m' "$1" "${*:2}"退出 $1}错误 1234 出了点问题.

这应该有效.只要 println 中的 $1 永不脏,它也是安全的.

现在稍等一下:我一直使用与 sudo 最初使用相同的 未加引号 语法!为什么它在那里工作,而不是在这里?为什么我们必须用单引号引用所有内容?sudo 更现代一些:它知道将它接收到的每个参数用引号括起来,尽管这过于简化了.eval 简单地连接所有内容.

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

eval 替代方案

特定用例通常有 eval 的可行替代方案.这是一个方便的列表.command 表示您通常会发送给 eval 的内容;随意替换.

无操作

一个简单的冒号在 bash 中是一个空操作:

<预><代码>:

创建子shell

( command ) # 标准符号

执行命令的输出

永远不要依赖外部命令.您应该始终控制返回值.把这些放在他们自己的行上:

$(command) # 首选`command` # 旧:应该避免,通常被认为已弃用# 嵌套:$(command1 "$(command2)")`command "`command`"` # 小心: 仅使用旧样式转义 $ 和 ,并且# 特殊情况 ` 导致嵌套.

基于变量的重定向

在调用代码时,将&3(或任何高于&2)映射到您的目标:

exec 3<&0 # 从标准输入重定向exec 3>&1 # 重定向到标准输出exec 3>&2 # 重定向到标准错误执行3>/dev/null # 不要在任何地方保存输出执行3>file.txt # 重定向到文件执行3>"$var" # 重定向到存储在 $var 中的文件——仅适用于文件!exec 3<&0 4>&1 #输入输出!

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

func arg1 arg2 3>&2

在被调用的函数内,重定向到&3:

command <&3 # 重定向标准输入command >&3 # 重定向标准输出命令 2>&3 # 重定向标准错误command &>&3 # 重定向标准输出和标准错误命令 2>&1 >&3 # 同上,但适用于较旧的 bash 版本command >&3 2>&1 # 将标准输出重定向到 &3,并将标准错误重定向到标准输出:顺序很重要command <&3 >&4 #输入输出!

变量间接

场景:

VAR='1 2 3'REF=VAR

不好:

eval "echo "$$REF""

为什么?如果 REF 包含双引号,这将破坏并打开代码以供利用.可以对 REF 进行消毒,但这样做是在浪费时间:

echo "${!REF}"

没错,bash 从版本 2 开始就内置了变量间接寻址.如果你想做一些更复杂的事情,它比 eval 有点棘手:

# 添加到场景:VAR_2='4 5 6'# 我们可以使用:本地 ref="${REF}_2"回声${!ref}"# 对比 bash <2 方法,对于那些习惯于 eval 的人来说可能更简单:eval "echo "$${REF}_2""

无论如何,新方法更直观,尽管对于习惯于eval的有经验的编程人员来说,这似乎不是这种方式.

关联数组

关联数组是在 bash 4 中实现的.一个警告:它们必须使用 declare 创建.

declare -A VAR # 本地声明 -gA VAR # 全局# 括号和内容之间使用空格;我听说过一些细微错误的报告# 在某些版本上,当它们被省略时与键中的空格有关.声明 -A VAR=( ['']='a' [0]='1' ['duck']='quack' )VAR+=( ['alpha']='beta' [2]=3 ) # 组合数组VAR['cow']='moo' # 设置单个元素unset VAR['cow'] # 取消设置单个元素unset VAR # 取消整个数组的设置unset VAR[@] # 取消整个数组的设置unset VAR[*] # 使用对应于文件中的一个键来取消设置每个元素# 当前目录;如果 * 不展开,则取消整个数组的设置local KEYS=( "${!VAR[@]}" ) # 获取 VAR 中的所有键

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

VAR=( ) # 这将存储我们的密钥.# 用一个简单的键存储一个值.# 在 bash 4 之前,您需要在全局范围内声明它以使其成为全局范围.# 在 bash 4 中,使用 -g 选项.声明VAR_$key"="$value"VAR+="$key"# 或者,如果您的版本缺少 +=VAR=( "$VAR[@]" "$key" )# 恢复一个简单的值.local var_key="VAR_$key" # 保存值的变量名local var_value="${!var_key}" # 实际值——需要bash 2# 对于 <bash 2,此方法需要 eval.只要 $key 不脏就安全.local var_value="`eval echo -n "$$var_value""# 如果你不需要快速枚举索引,并且你使用的是 bash 2+,这个# 可以将每个操作减少到一行:声明 "VAR_$key"="$value" # 存储echo "`var_key="VAR_$key" echo -n "${!var_key}"`" # 检索# 如果你使用更复杂的值,你需要散列你的键:功能键{本地密钥="`mkpasswd -5R0 "$1" 00000000`"echo -n "${key##*$}"}本地 var_key="VAR_`mkkey "$key"`"# ...

Time and time again, I see Bash answers on Stack Overflow using eval and the answers get bashed, pun intended, for the use of such an "evil" construct. Why is eval so evil?

If eval can't be used safely, what should I use instead?

解决方案

There's more to this problem than meets the eye. We'll start with the obvious: eval has the potential to execute "dirty" data. Dirty data is any data that has not been rewritten as safe-for-use-in-situation-XYZ; in our case, it's any string that has not been formatted so as to be safe for evaluation.

Sanitizing data appears easy at first glance. Assuming we're throwing around a list of options, bash already provides a great way to sanitize individual elements, and another way to sanitize the entire array as a single string:

function println
{
    # Send each element as a separate argument, starting with the second element.
    # Arguments to printf:
    #   1 -> "$1
"
    #   2 -> "$2"
    #   3 -> "$3"
    #   4 -> "$4"
    #   etc.

    printf "$1
" "${@: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): %se[m'
    #   2 -> "$1"
    #   3 -> "${*:2}"

    println 'e[31mError (%d): %se[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).

Now say we want to add an option to redirect output as an argument to println. We could, of course, just redirect the output of println on each call, but for the sake of example, we're not going to do that. We'll need to use eval, since variables can't be used to redirect output.

function println
{
    eval printf "$2
" "${@:3}" $1
}

function error
{
    println '>&2' 'e[31mError (%d): %se[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Looks good, right? Problem is, eval parses twice the command line (in any shell). On the first pass of parsing one layer of quoting is removed. With quotes removed, some variable content gets executed.

We can fix this by letting the variable expansion take place within the eval. All we have to do is single-quote everything, leaving the double-quotes where they are. One exception: we have to expand the redirection prior to eval, so that has to stay outside of the quotes:

function println
{
    eval 'printf "$2
" "${@:3}"' $1
}

function error
{
    println '&2' 'e[31mError (%d): %se[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

This should work. It's also safe as long as $1 in println is never dirty.

Now hold on just a moment: I use that same unquoted syntax that we used originally with sudo all of the time! Why does it work there, and not here? Why did we have to single-quote everything? sudo is a bit more modern: it knows to enclose in quotes each argument that it receives, though that is an over-simplification. eval simply concatenates everything.

Unfortunately, there is no drop-in replacement for eval that treats arguments like sudo does, as eval is a shell built-in; this is important, as it takes on the environment and scope of the surrounding code when it executes, rather than creating a new stack and scope like a function does.

eval Alternatives

Specific use cases often have viable alternatives to eval. Here's a handy list. command represents what you would normally send to eval; substitute in whatever you please.

No-op

A simple colon is a no-op in bash:

:

Create a sub-shell

( command )   # Standard notation

Execute output of a command

Never rely on an external command. You should always be in control of the return value. Put these on their own lines:

$(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.

Redirection based on variable

In calling code, map &3 (or anything higher than &2) to your target:

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!

If it were a one-time call, you wouldn't have to redirect the entire shell:

func arg1 arg2 3>&2

Within the function being called, redirect to &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!

Variable indirection

Scenario:

VAR='1 2 3'
REF=VAR

Bad:

eval "echo "$$REF""

Why? If REF contains a double quote, this will break and open the code to exploits. It's possible to sanitize REF, but it's a waste of time when you have this:

echo "${!REF}"

That's right, bash has variable indirection built-in as of version 2. It gets a bit trickier than eval if you want to do something more complex:

# 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""

Regardless, the new method is more intuitive, though it might not seem that way to experienced programmed who are used to eval.

Associative arrays

Associative arrays are implemented intrinsically in bash 4. One caveat: they must be created using 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

In older versions of bash, you can use variable indirection:

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"`"
# ...

这篇关于为什么在 Bash 中应该避免使用 eval,我应该使用什么来代替?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆