Linux 为什么在 Bash 中应该避免使用 eval,我应该使用什么来代替?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17529220/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-07 00:11:25  来源:igfitidea点击:

Why should eval be avoided in Bash, and what should I use instead?

linuxbashunixeval

提问by Zenexer

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

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

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

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

采纳答案by Zenexer

There's more to this problem than meets the eye. We'll start with the obvious: evalhas 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.

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

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:

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

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

    printf "\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 -> ""
    #   3 -> "${*:2}"

    println '\e[31mError (%d): %s\e[m' "" "${*:2}"
    exit ""
}

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

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

function println
{
    eval printf "\n" "${@:3}" 
}

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

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.

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

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:

我们可以通过让变量扩展发生在eval. 我们所要做的就是单引号所有内容,保留双引号。一个例外:我们必须在 之前扩展重定向eval,因此必须保留在引号之外:

function println
{
    eval 'printf "\n" "${@:3}"' 
}

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

error 1234 Something went wrong.

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

这应该有效。只要这也是安全$1println是永远不会脏。

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

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

Unfortunately, there is no drop-in replacement for evalthat treats arguments like sudodoes, as evalis 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是治疗参数,像sudo呢,因为eval是内置的外壳; 这很重要,因为它在执行时会占用周围代码的环境和作用域,而不是像函数那样创建新的堆栈和作用域。

eval Alternatives

评估替代品

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

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

No-op

无操作

A simple colon is a no-op in bash:

一个简单的冒号在 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:

在调用代码时,映射&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!

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

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

func arg1 arg2 3>&2

Within the function being called, redirect to &3:

在被调用的函数中,重定向到&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:

为什么?如果 REF 包含双引号,这将破坏并打开代码以供利用。可以对 REF 进行消毒,但是当您拥有以下内容时,这是在浪费时间:

echo "${!REF}"

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

没错,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\""

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

无论如何,新方法更直观,尽管对于习惯于eval.

Associative arrays

关联数组

Associative arrays are implemented intrinsically in bash 4. One caveat: they must be created using declare.

关联数组本质上是在 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

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

在较旧版本的 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 "" 00000000`"
    echo -n "${key##*$}"
}

local var_key="VAR_`mkkey "$key"`"
# ...

回答by Tom Hale

How to make evalsafe

如何让eval安全

evalcanbe safely used - but all of its arguments need to be quoted first. Here's how:

eval可以安全地使用 - 但需要首先引用它的所有参数。就是这样:

This function which will do it for you:

此功能将为您完成:

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

Example usage:

用法示例:

Given some untrusted user input:

给定一些不受信任的用户输入:

% input="Trying to hack you; date"

Construct a command to eval:

构造一个命令来评估:

% cmd=(echo "User gave:" "$input")

Eval it, with seeminglycorrect quoting:

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

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

Note you were hacked. datewas executed rather than being printed literally.

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

Instead with token_quote():

取而代之的是token_quote()

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

evalisn't evil - it's just misunderstood :)

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