簡體   English   中英

為什么要在Bash中避免使用eval,我應該用什么來代替?

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

一次又一次,我在 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永遠不會臟,它也是安全的。

現在等一下:我一直使用我們最初在sudo使用的相同的未加引號的語法! 為什么它在那里工作,而不是在這里? 為什么我們必須用單引號引用所有內容? sudo更現代一些:它知道將收到的每個參數用引號括起來,盡管這過於簡單化了。 eval只是連接所有內容。

不幸的是,沒有像sudo那樣處理參數的eval替代品,因為eval是內置的 shell; 這很重要,因為它在執行時會占用周圍代碼的環境和作用域,而不是像函數那樣創建新的堆棧和作用域。

評估替代品

特定用例通常有eval可行替代方案。 這是一個方便的列表。 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 包含雙引號,這將破壞並打開代碼以供利用。 可以對 REF 進行消毒,但是當您擁有以下內容時,這是在浪費時間:

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

如何使eval安全

可以安全地使用eval - 但需要先引用它的所有參數。 就是這樣:

此功能將為您完成:

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

用法示例:

給定一些不受信任的用戶輸入:

% input="Trying to hack you; date"

構造一個命令來評估:

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

評估它,看似正確的引用:

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

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

取而代之的是token_quote()

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

eval不是邪惡的 - 它只是被誤解了 :)

我將把這個答案分成兩部分,我認為這涵蓋了人們容易被eval誘惑的大部分情況:

  1. 運行奇怪的命令
  2. 擺弄動態命名的變量

運行奇怪的命令

很多很多時候,簡單的索引 arrays就足夠了,前提是您在定義數組時養成使用雙引號保護擴展的良好習慣。

# One nasty argument which must remain a single argument and not be split:
f='foo bar'
# The command in an indexed array (use `declare -a` if you really want to be explicit):
cmd=(
    touch
    "$f"
    # Yet another nasty argument, this time hardcoded:
    'plop yo'
)
# Let Bash expand the array and run it as a command:
"${cmd[@]}"

這將創建foo barplop yo (兩個文件,而不是四個)。

請注意,有時它可以生成更具可讀性的腳本,將 arguments(或一堆選項)放入數組中(至少乍一看你知道你在運行什么):

touch "${args[@]}"
touch "${opts[@]}" file1 file2

作為獎勵,arrays 讓您輕松地:

  1. 添加關於特定參數的評論:
cmd=(
    # Important because blah blah:
    -v
)
  1. 通過在數組定義中保留空行來將 arguments 分組以提高可讀性。
  2. 出於調試目的注釋掉特定的 arguments。
  3. Append arguments 到您的命令,有時根據特定條件動態或循環:
cmd=(myprog)
for f in foo bar
do
    cmd+=(-i "$f")
done
if [[ $1 = yo ]]
then
    cmd+=(plop)
fi
to_be_added=(one two 't h r e e')
cmd+=("${to_be_added[@]}")
  1. 在配置文件中定義命令,同時允許包含配置定義的空格 arguments:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
  1. 使用 printf 的%q記錄一個健壯的可運行命令,它完美地代表了正在運行的內容:
function please_log_that {
    printf 'Running:'
    # From `help printf`:
    # “The format is re-used as necessary to consume all of the arguments.”
    # From `man printf` for %q:
    # “printed in a format that can be reused as shell input,
    # escaping  non-printable  characters with the proposed POSIX $'' syntax.”
    printf ' %q' "$@"
    echo
}

arg='foo bar'
cmd=(prog "$arg" 'plop yo' $'arg\nnewline\tand tab')
please_log_that "${cmd[@]}"
# ⇒ “Running: prog foo\ bar plop\ yo $'arg\nnewline\tand tab'”
# You can literally copy and paste that ↑ to a terminal and get the same execution.
  1. 享受比eval字符串更好的語法高亮顯示,因為您不需要嵌套引號或使用“不會立即評估但會在某個時候評估”的$ -s。

對我來說,這種方法的主要優點(相反是eval的缺點)是您可以按照與通常相同的邏輯進行引用、擴展等。無需絞盡腦汁嘗試“提前”將引號放在引號中” 同時試圖弄清楚哪個命令將在哪個時刻解釋哪對引號。 當然,上面提到的許多事情用eval更難或根本不可能實現。

有了這些,在過去六年左右的時間里,我再也不必依賴eval了,而且可讀性和健壯性(特別是關於包含空格的 arguments)可以說得到了提高。 您甚至不需要知道IFS是否已被調和,當然,仍然存在實際可能需要eval的邊緣情況(我想,例如,如果用戶必須能夠提供完整的通過交互式提示或其他方式編寫腳本),但希望這不是您每天都會遇到的事情。

擺弄動態命名的變量

declare -n (或其函數內的local -n對應物)以及${!foo} ,大部分時間都在做這個把戲。

$ help declare | grep -- -n
      -n    make NAME a reference to the variable named by its value

好吧,沒有例子就不是特別清楚:

declare -A global_associative_array=(
    [foo]=bar
    [plop]=yo
)

# $1    Name of global array to fiddle with.
fiddle_with_array() {
    # Check this if you want to make sure you’ll avoid
    # circular references, but it’s only if you really
    # want this to be robust.
    # You can also give an ugly name like “__ref” to your
    # local variable as a cheaper way to make collisions less likely.
    if [[ $1 != ref ]]
    then
        local -n ref=$1
    fi
    
    printf 'foo → %s\nplop → %s\n' "${ref[foo]}" "${ref[plop]}"
}

# Call the function with the array NAME as argument,
# not trying to get its content right away here or anything.
fiddle_with_array global_associative_array

# This will print:
# foo → bar
# plop → yo

(我喜歡這個技巧 ↑ 因為它讓我覺得我正在將對象傳遞給我的函數,就像在面向對象的語言中一樣。可能性令人難以置信。)

至於${!…} (獲取另一個變量命名的變量的值):

foo=bar
plop=yo

for var_name in foo plop
do
    printf '%s = %q\n' "$var_name" "${!var_name}"
done

# This will print:
# foo = bar
# plop = yo

關於什么

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

要么

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM