我們如何執行儲存在變數中的命令?
$ 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
,-l
andfoo bar
(刪除引號),並將它們傳遞給ls
. (即使是命令名也被傳遞了,雖然不是所有的程序都使用它。)問題中提出的案例:
這裡的賦值將單個字元串分配
ls -l "/tmp/test/my dir"
給abc
:$ abc='ls -l "/tmp/test/my dir"'
下面,
$abc
在空白處拆分,並ls
獲取三個參數-l
,"/tmp/test/my
anddir"
(在第二個前面有一個引號,在第三個後面有另一個引號)。該選項有效,但路徑處理不正確:$ $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/Ubuntu
dash
中的預設值/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
!參考
- BashGuide中的分詞
- BashFAQ/050 或“我正在嘗試將命令放入變數中,但複雜的情況總是失敗!”
- 問題為什麼我的 shell 腳本會因空格或其他特殊字元而窒息?,其中討論了與引用和空格相關的許多問題,包括儲存命令。
- 轉義變數以用作另一個腳本的內容