一次又一次,我在Stack Overflow上看到使用eval的Bash答案,而这些答案会因为使用这样一个"邪恶"构造而遭到双关语的抨击.为什么eval这么邪恶?

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

推荐答案

这个问题有很多不切实际的地方.我们将从一个显而易见的问题开始: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).

现在假设我们想添加一个选项,将输出重定向为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.

看起来不错,对吧?问题是,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.

这应该行得通.它也很安全,只要println中的$1从不脏.

现在请稍等片刻:我使用的unquoted语法与我们最初一直使用的sudo语法相同!为什么它在那里有效,而不是在这里?为什么我们要单报价?sudo有点现代:它知道将收到的每一个论点都用引号括起来,尽管这过于简化了.eval只是将所有内容连接起来.

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

eval Alternatives

特定用例通常有eval个可行的替代方案.这是一个方便的 list .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包含双引号,这将 destruct 并打开代码进行攻击.对REF进行消毒是可能的,但如果你有以下情况,那就是浪费时间:

echo "${!REF}"

没错,从版本2开始,bash就内置了变量间接寻址.如果你想做更复杂的事情,它比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

在旧版本的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"`"
# ...

Linux相关问答推荐

如何注释掉SLURM中的延迟调度命令?

通过ssh传输参数时避免字符串拆分

eBPF:仅使用系统调用加载 eBPF 程序并将其附加到 sys_enter_execve

cu可以从串口读取,但我自己的C程序不能

从 ALSA USB 硬件设备获取 USB 设备文件路径

/proc/mounts 没有像 /proc/self/mountinfo 这样的源信息

匹配模式和提取

`__meminit` 在 Linux 内核中意味着什么?

查找在特定日期从特定机器登录的所有用户

未定义的引用 'shm_open',已在此处添加 -lrt 标志

为什么导入 SQL 这么慢?

如何在没有 IDE 的情况下构建和部署三星 SmartTV 应用程序(例如:在 Linux 上)

自动化 Amazon EBS 快照 任何人在 linux 上都有一个好的脚本或解决方案

如何使用 Ansible 等待服务器重启?

在 Python 中删除 Root 权限

ldconfig 错误:使用 Linux 加载程序时不是符号链接

卸载从源代码构建的python?

更改 /etc/profile 后,我需要做什么来重置我的 shell?

如何限制我网站的 API 用户?

Linux 目录列表中只有问号