Bash

我們如何執行儲存在變數中的命令?

  • January 30, 2022
$ ls -l /tmp/test/my\ dir/
total 0

我想知道為什麼以下執行上述命令的方法失敗​​或成功?

$ abc='ls -l "/tmp/test/my dir"'

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0

這已在 unix.SE 上的許多問題中討論過,我將嘗試收集我能在這裡提出的所有問題。下面是對各種嘗試失敗的原因和方式的描述,以及使用函式(對於固定命令)或 shell 數組(Bash/ksh/zsh)或$@偽數組(POSIX sh)正確執行此操作的方法,以及關於使用eval來執行此操作的說明。一些參考在最後。

出於此處的目的,它是否只是命令參數或要儲存在變數中的命令名稱並不重要。在啟動命令之前,它們的處理方式相似,此時 shell 僅將第一個單詞作為要執行的命令的名稱。


為什麼會失敗

您面臨這些問題的原因是分詞以及從變數擴展的引號不充當引號,而只是普通字元的事實。

(請注意,這與所有其他程式語言相似:例如char *s = "foo()"; printf("%s\n", s),不呼叫foo()C 中的函式,而只是列印字元串foo()。shell 是一種程式語言,而不是宏處理器。)

請記住,它是在命令行上處理引號和變數擴展的 shell,將其從單個字元串轉換為最終傳遞給啟動命令的字元串列表。該程序本身看不到任何引號。例如,如果給定 command ls -l "foo bar",shell 會將其轉換為三個字元串ls, -land foo bar(刪除引號),並將它們傳遞給ls. (即使是命令名也被傳遞了,雖然不是所有的程序都使用它。)

問題中提出的案例:

這裡的賦值將單個字元串分配ls -l "/tmp/test/my dir"abc

$ abc='ls -l "/tmp/test/my dir"'

下面,$abc在空白處拆分,並ls獲取三個參數-l, "/tmp/test/myand dir"(在第二個前面有一個引號,在第三個後面有另一個引號)。該選項有效,但路徑處理不正確:

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

在這裡,擴展被引用,所以它被保留為一個單詞。shell 試圖找到一個字面上稱為 的程序ls -l "/tmp/test/my dir",包括空格和引號。

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

在這裡,$abc被分割了,只有第一個結果詞被作為參數-c,所以 Bash 只是ls在目前目錄中執行。其他詞是 bash 的參數,用於填充$0,$1等。

$ bash -c $abc
'my dir'

使用bash -c "$abc", 和eval "$abc",還有一個額外的 shell 處理步驟,它確實使引號起作用,但也會導致所有 shell 擴展被再次處理,因此存在意外執行的風險,例如從使用者提供的數據進行命令替換,除非你是引用非常小心。


更好的方法來做到這一點

儲存命令的兩種更好的方法是 a) 使用函式,b) 使用數組變數(或位置參數)。

使用函式:

只需在內部聲明一個帶有命令的函式,然後像執行命令一樣執行該函式。函式內命令的擴展僅在命令執行時處理,而不是在定義時處理,並且您不需要引用各個命令。

# define it
myls() {
   ls -l "/tmp/test/my dir"
}

# run it
myls

使用數組:

數組允許創建多個單詞變數,其中單個單詞包含空格。在這裡,各個單詞儲存為不同的數組元素,"${array[@]}"擴展將每個元素擴展為單獨的 shell 單詞:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

語法有點可怕,但數組還允許您逐段建構命令行。例如:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
   mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

或保持部分命令行不變並使用數組填充其中的一部分,如選項或文件名:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

數組的缺點是它們不是標準功能,因此普通的 POSIX shell(如Debian/Ubuntudash中的預設值/bin/sh)不支持它們(但見下文)。但是,Bash、ksh 和 zsh 可以,因此您的系統很可能有一些支持數組的 shell。

使用"$@"

在不支持命名數組的 shell 中,仍然可以使用位置參數(偽數組"$@")來保存命令的參數。

以下應該是與上一節中的程式碼位等效的可移植腳本位。該數組被替換"$@"為位置參數列表。設置"$@"是用 完成的set,並且雙引號"$@"很重要(這些導致列表的元素被單獨引用)。

首先,簡單地儲存一個帶有參數的命令"$@"並執行它:

set -- ls -l "/tmp/test/my dir"
"$@"

有條件地為命令設置部分命令行選項:

set -- ls
if [ "$want_detail" = 1 ]; then
   set -- "$@" -l
fi
set -- "$@" "$targetdir"

"$@"

"$@"用於選項和操作數:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(當然,"$@"通常會填充腳本本身的參數,因此您必須在重新調整用途之前將它們保存在某個地方"$@"。)


使用eval(這裡要小心!)

eval接受一個字元串並將其作為命令執行,就像在 shell 命令行中輸入它一樣。這包括所有的報價和擴展處理,這既有用又危險。

在簡單的情況下,它允許做我們想做的事:

cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"

隨著eval,引號被處理,所以ls最終只看到兩個參數-l/tmp/test/my dir,就像我們想要的那樣。eval也很聰明,可以連接它得到的任何參數,所以eval $cmd在某些情況下也可以工作,但是例如所有的空白執行將被更改為單個空格。最好在此處引用變數,因為這將確保它未被修改為eval.

但是,在命令字元串中包含使用者輸入是很危險的eval。例如,這似乎有效:

read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";

**但是如果使用者給出的輸入包含單引號,他們可以跳出引號並執行任意命令!**例如,使用 input '$(whatever)'.txt,您的腳本會愉快地執行命令替換。相反,它可能是rm -rf(或更糟)。

問題在於 的值$filename嵌入在eval執行的命令行中。它之前被擴展過eval,它看到了例如命令ls -l ''$(whatever)'.txt'。您需要對輸入進行預處理以確保安全。

如果我們以另一種方式來做,將文件名保留在變數中,並讓eval命令擴展它,它又會更安全:

read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";

請注意,外部引號現在是單引號,因此不會發生內部擴展。因此,eval看到命令ls -l "$filename"並自己安全地擴展文件名。

但這與僅將命令儲存在函式或數組中沒有太大區別。對於函式或數組,就沒有這樣的問題,因為單詞一直是分開的,並且沒有對filename.

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

幾乎唯一使用的原因eval是可變部分涉及無法通過變數(管道、重定向等)引入的 shell 語法元素。但是,您隨後需要在命令行上引用/轉義需要保護免受額外解析步驟影響的所有其他內容(請參見下面的連結)。無論如何,最好避免在命令中嵌入來自使用者的輸入eval


參考

引用自:https://unix.stackexchange.com/questions/444946