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

标签 linux bash unix eval

一次又一次,我使用 eval 在 Stack Overflow 上看到 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 中来解决这个问题。 .我们所要做的就是单引号所有内容,保留双引号。一个异常(exception):我们必须在 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.

这应该有效。只要$1也是安全的在 println从不脏。

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

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

评估替代品

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

无操作

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

创建子 shell
( 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 进行 sanitizer ,但是当您拥有以下内容时,这是在浪费时间:
echo "${!REF}"

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

无论如何,新方法更直观,尽管对于习惯于 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 - 为什么在 Bash 中应该避免使用 eval,我应该使用什么来代替?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17529220/

相关文章:

mysql - Bash mysql数组没有得到空值

php - 使用sed从php文件中删除注释

go - 如何在 Go 中捕获任意 UNIX 信号

c++ - c++ 中是否有任何函数来检查特定路径中给定的文件是否为脚本(.sh)文件

python - 我如何从 Linux 上的 python 中的进程 ID 获取用户名?

python - Raspberry Pi 3 通过 720p USB 摄像头提高 FPS

php - 使用 PHP 从基于 Linux 的文件服务器上传/检索文件

c - Linux系统调用: getaddrinfo return -2

bash - 如何检查字符串是否只包含数字/数字字符

unix - awk 错误 : "cannot open - too many open files"