Bash

為什麼我的 shell 腳本會因空格或其他特殊字元而窒息?

  • March 19, 2020

或者,關於強大的文件名處理和其他在 shell 腳本中傳遞的字元串的介紹性指南。

我寫了一個在大多數情況下執行良好的 shell 腳本。但它會阻塞某些輸入(例如某些文件名)。

我遇到瞭如下問題:

  • 我有一個包含空格的文件名hello world,它被視為兩個單獨的文件helloworld.
  • 我有一個帶有兩個連續空格的輸入行,它們在輸入中縮小為一個。
  • 前導和尾隨空格從輸入行中消失。
  • 有時,當輸入包含其中一個字元\[*?時,它們會被一些文本替換,這些文本實際上是文件的名稱。
  • 輸入中有一個撇號'(或雙引號"),在那之後事情變得很奇怪。
  • 輸入中有一個反斜杠(或者:我使用的是 Cygwin,我的一些文件名有 Windows 樣式的\分隔符)。

發生了什麼事,我該如何解決?

始終在變數替換和命令替換周圍使用雙引號:"$foo","$(foo)"

如果您使用不$foo帶引號的,您的腳本將阻塞$(foo)包含空格或\[*?.

在那裡,你可以停止閱讀。好吧,這裡還有一些:

  • read—要使用內置函式****逐行讀取輸入read``while IFS= read -r line; do …
    ,請使用 Plainread專門處理反斜杠和空格。
  • xargs避免xargs。如果你必須使用xargs,那就做吧xargs -0。而不是find … | xargs更喜歡find … -exec …

xargs特別對待空白和字元\"'

這個答案適用於 Bourne/POSIX 風格的 shell(sh, ash, dash, bash, ksh, mksh, yash…)。Zsh 使用者應該跳過它並閱讀何時需要雙引號?反而。如果您想了解全部細節,請閱讀標准或您的 shell 手冊。


請注意,下面的解釋包含一些近似值(在大多數情況下都是正確的陳述,但會受到周圍環境或配置的影響)。

為什麼我需要寫作"$foo"?沒有引號會發生什麼?

$foo並不意味著“取變數的值foo”。這意味著更複雜的事情:

  • 首先,取變數的值。
  • 欄位拆分:將該值視為以空格分隔的欄位列表,並建構結果列表。例如,如果變數包含,foo * bar ​則此步驟的結果是 3 元素列表foo, *, bar
  • 文件名生成:將每個欄位視為一個 glob,即作為萬用字元模式,並將其替換為與該模式匹配的文件名列表。如果模式與任何文件都不匹配,則保持不變。在我們的範例中,這導致列表包含foo,然後是目前目錄中的文件列表,最後是bar。如果目前目錄為空,則結果為foo, *, bar

請注意,結果是一個字元串列表。shell 語法中有兩種上下文:列表上下文和字元串上下文。欄位拆分和文件名生成僅發生在列表上下文中,但大多數情況下都是這樣。雙引號分隔字元串上下文:整個雙引號字元串是單個字元串,不能拆分。(例外:"$@"擴展到位置參數列表,例如,如果有三個位置參數,"$@"則等效於。請參閱兩者之間的區別是什麼"$1" "$2" "$3" $ * and $ @? )

$(foo)使用或 的命令替換也是如此foo。附帶說明,不要使用foo: 它的引用規則很奇怪且不可移植,並且所有現代 shell 都支持$(foo),除了具有直覺的引用規則之外,它是絕對等價的。

算術替換的輸出也經歷了相同的擴展,但這通常不是問題,因為它只包含不可擴展的字元(假設IFS不包含數字或-)。

請參閱何時需要雙引號?有關可以省略引號的情況的更多詳細資訊。

除非您的意思是讓所有這些繁瑣的事情發生,否則請記住始終在變數和命令替換周圍使用雙引號。請注意:省略引號不僅會導致錯誤,還會導致安全漏洞

如何處理文件名列表?

如果你寫myfiles="file1 file2", 用空格分隔文件,這不適用於包含空格的文件名。Unix 文件名可以包含除/(始終是目錄分隔符)和空字節(在大多數 shell 的 shell 腳本中不能使用)以外的任何字元。

同樣的問題myfiles=*.txt; … process $myfiles。執行此操作時,變數myfiles包含 5 個字元的 string *.txt,並且在您寫入時$myfiles,萬用字元被擴展。這個範例實際上會起作用,直到您將腳本更改為myfiles="$someprefix*.txt"; … process $myfiles. 如果someprefix設置為final report,這將不起作用。

要處理任何類型的列表(例如文件名),請將其放入數組中。這需要 mksh、ksh93、yash 或 bash(或 zsh,沒有所有這些引用問題);普通的 POSIX shell(例如 ash 或 dash)沒有數組變數。

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88 具有不同賦值語法的數組變數set -A myfiles "someprefix"*.txt(如果需要 ksh88/bash 可移植性,請參閱不同 ksh 環境下的賦值變數)。Bourne/POSIX 樣式的 shell 有一個 one 數組,"$@"即您設置的位置參數數組,set它是函式的本地參數:

set -- "$someprefix"*.txt
process -- "$@"

以 開頭的文件名-呢?

在相關說明中,請記住文件名可以以-(破折號/減號)開頭,大多數命令將其解釋為表示選項。一些命令(如sh,setsort)也接受以 . 開頭的選項+。如果您有一個以可變部分開頭的文件名,請務必--在它之前傳遞,如上面的程式碼片段所示。這向命令表明它已到達選項的末尾,因此之後的任何內容都是文件名,即使它以-or開頭+

或者,您可以確保您的文件名以 . 以外的字元開頭-。絕對文件名以 開頭/,您可以./在相對名稱的開頭添加。下面的程式碼片段將變數的內容f轉換為一種“安全”的方式來引用同一個文件,該文件保證不以-nor開頭+

case "$f" in -* | +*) "f=./$f";; esac

關於這個主題的最後一點,請注意某些命令解釋-為標準輸入或標準輸出,即使在--. 如果您需要引用一個名為 的實際文件-,或者如果您正在呼叫這樣的程序並且您不希望它從標準輸入讀取或寫入標準輸出,請確保-按上述方式重寫。請參閱“du -sh ”和“du -sh ./”有什麼區別?進一步討論。

如何將命令儲存在變數中?

“命令”可以表示三件事:命令名稱(作為執行檔的名稱,帶或不帶完整路徑,或函式名稱,內置或別名),帶參數的命令名稱,或一段 shell 程式碼。因此有不同的方式將它們儲存在變數中。

如果您有命令名稱,只需儲存它並像往常一樣使用帶雙引號的變數。

command_path="$1"
…
"$command_path" --option --message="hello world"

如果您有一個帶參數的命令,問題與上面的文件名列表相同:這是一個字元串列表,而不是字元串。您不能只是將參數填充到一個中間有空格的字元串中,因為如果這樣做,您將無法區分作為參數一部分的空格和分隔參數的空格。如果你的 shell 有數組,你可以使用它們。

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

如果您使用沒有數組的外殼怎麼辦?如果您不介意修改它們,您仍然可以使用位置參數。

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

如果您需要儲存複雜的 shell 命令,例如重定向、管道等,該怎麼辦?或者如果您不想修改位置參數?然後你可以建構一個包含命令的字元串,並使用eval內置的。

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

注意定義中的嵌套引號code:單引號'…'分隔字元串文字,因此變數的值code是字元串/path/to/executable --option --message="hello world" -- /path/to/file1eval內置命令告訴 shell 解析作為參數傳遞的字元串,就好像它出現在腳本中一樣,所以此時引號和管道被解析,等等。

使用eval很棘手。仔細考慮什麼時候被解析。特別是,您不能只將文件名填充到程式碼中:您需要引用它,就像它在原始碼文件中一樣。沒有直接的方法可以做到這一點。code="$code $filename"如果文件名包含任何 shell 特殊字元(空格、、、、、、、$等);,則類似中斷。仍然休息。如果文件名包含. 有兩種解決方案。|``<``>``code="$code \"$filename\""``"$\```code="$code '$filename'"``'

  • 在文件名周圍添加一層引號。最簡單的方法是在它周圍添加單引號,並將單引號替換為'\''.
quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
code="$code '${quoted_filename%.}'"
  • 將變數擴展保留在程式碼中,以便在評估程式碼時查找它,而不是在建構程式碼片段時查找。這更簡單,但僅當變數在程式碼執行時仍然具有相同的值時才有效,而不是例如程式碼建構在循環中。
code="$code \"\$filename\""

最後,你真的需要一個包含程式碼的變數嗎?為程式碼塊命名最自然的方法是定義一個函式。

怎麼了read

沒有-r,read允許續行——這是一個單一的邏輯輸入行:

hello \
world

read將輸入行拆分為由字元分隔的欄位$IFS(沒有-r,反斜杠也會轉義這些)。例如,如果輸入是包含三個單詞的行,則read first second third設置first為輸入的第一個單詞、second第二個單詞和third第三個單詞。如果有更多單詞,最後一個變數包含設置前面的單詞後剩下的所有內容。前導和尾隨空格被修剪。

設置IFS為空字元串可避免任何修剪。請參閱為什麼經常使用 while IFS= read,而不是 IFS=; 閱讀時..更長的解釋。

有什麼問題xargs

的輸入格式xargs是空格分隔的字元串,可以選擇單引號或雙引號。沒有標準工具輸出這種格式。

xargs -L1or的輸入xargs -l幾乎是一個行列表,但不完全是 - 如果行尾有空格,則下一行是續行。

您可以xargs -0在適用的情況下使用(並且在可用的情況下:GNU(Linux、Cygwin)、BusyBox、BSD、OSX,但它不在 POSIX 中)。這是安全的,因為空字節不能出現在大多數數據中,尤其是在文件名中。要生成一個以空值分隔的文件名列表,請使用find … -print0(或者您可以find … -exec …按照下面的說明使用)。

如何處理找到的文件find

find … -exec some_command a_parameter another_parameter {} +

some_command必須是外部命令,不能是 shell 函式或別名。如果您需要呼叫 shell 來處理文件,請sh顯式呼叫。

find … -exec sh -c '
 for x do
   … # process the file "$x"
 done
' find-sh {} +

我還有其他問題

瀏覽此站點上的引用標籤,或者shellshell-script。(點擊“了解更多…”以查看一些一般提示和手動選擇的常見問題列表。)如果您已經搜尋但找不到答案,請詢問

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