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

一次又一次,我看到Bash使用eval回答堆栈溢出问题,并且为了使用这样的“邪恶”构造,答案会被打破,双关语意图。 为什么eval如此邪恶?

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

这个问题比眼前还要多。 我们将从明显的开始: eval有可能执行“脏”的数据。 脏数据是指未被重写为安全使用情况下的任何数据。 在我们的例子中,它是任何没有格式化的string,以便评估安全。

乍一看,清理数据看起来很简单。 假设我们正在抛出一个选项列表,bash已经提供了一种清理单个元素的好方法,另一种方法是将整个数组作为单个string进行清理:

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

现在说我们要添加一个选项来将输出redirect为println的参数。 当然,我们可以在每个调用中redirectprintln的输出,但是为了举例,我们不打算这样做。 我们需要使用eval ,因为variables不能用来redirect输出。

 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. 

看起来不错,对吧? 问题是,evalparsing命令行的两倍(在任何shell中)。 在parsing的第一个过程中删除了一层引用。 除去引号,一些variables内容被执行。

我们可以通过在eval进行variables扩展来解决这个问题。 我们所要做的就是单引号,留下双引号。 一个例外:我们必须在eval之前扩展redirect,这样就不得不在引号之外:

 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从不肮脏,它也是安全的。

现在请稍等片刻:我使用的是原来与sudo一起使用的相同的不带引号的语法! 为什么它在那里工作,而不是在这里? 为什么我们要单引号呢? sudo是一个更现代一点:它知道引用它收到的每个论点,尽pipe这是一个过于简化。 eval简单地连接一切。

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

eval替代品

具体的用例往往有可行的替代eval 。 这是一个方便的列表。 command代表你通常会发送给eval ; 替代你的任何请求。

无操作

一个简单的冒号在一个没有操作的bash中:

创build一个子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. 

基于variables的redirect

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

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

 func arg1 arg2 3>&2 

在被调用的函数中,redirect到&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进行消毒,但是这样做会浪费时间:

 echo "${!REF}" 

没错,bash在版本2中内置了可变间接。如果你想做更复杂的事情,它会比eval更复杂:

 # Add to scenario: VAR_2='4 5 6' # We could use: local ref="${REF}_2" echo "${!ref}" # Or: 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中实现的。一个警告:它们必须使用declare创build。

 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中,可以使用variables间接寻址:

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

关于什么

 ls -la /path/to/foo | grep bar | bash 

要么

 (ls -la /path/to/foo | grep bar) | bash