Shell 腳本輸出在作為參數傳遞給腳本時錯誤地分割

Shell 腳本輸出在作為參數傳遞給腳本時錯誤地分割

假設我有以下兩個 shell 腳本:

#!/bin/sh
#This script is named: args.sh

echo 1 "\"Two words\"" 3

, 和:

#!/bin/sh
#This script is named: test.sh

echo "Argument 1: "$1
echo "Argument 2: "$2
echo "Argument 3: "$3

當我將腳本稱為:

sh test.sh $(sh args.sh)

,我收到:

Argument 1: 1
Argument 2: "Two
Argument 3: words"

當我期望得到:

Argument 1: 1
Argument 2: Two words
Argument 3: 3

複製 的輸出sh args.sh並將其貼上為 的輸入sh test.sh就可以了;所以我認為這實際上並不是 shell 正在做的事情。我可以透過呼叫來實現所需/預期的輸出sh args.sh | xargs sh test.sh

但是,我想知道是否有一種等效的方法可以在不將第一個腳本(args.sh)的輸出通過管道傳輸到 xargs 的情況下執行此操作?我希望腳本按原始順序調用;參數腳本將參數輸出到第二個。我也在尋找為什麼此調用不能按預期工作的解釋?

答案1

部分問題是 args.sh 傳回的字串沒有像直接命令一樣進行解析,而是僅透過 $IFS ( $' \t\n') 的值進行解析。嘗試使用以下命令開啟命令追蹤set -x

$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh 1 '"Two' 'words"' 3
Argument 1: 1
Argument 2: "Two
Argument 3: words"
$

請注意以單一 開頭的行+:有四個參數,其中'"Two''words"'兩個解析為單獨的參數。您想要做的是更改 $IFS。

$ set -x
$ IFS='"'
+ IFS='"'
$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh '1 ' 'Two words' ' 3'
Argument 1: 1
Argument 2: Two words
Argument 3:  3
$

這不適用於每個輸出。最好的方法是更改​​ args.sh 的輸出,以使用不同於空格的內容分隔輸出,例如逗號或冒號:

$ cat /tmp/args.sh
#!/bin/sh
#This script is named: args.sh

echo "1,Two words,3"
$ IFS=,
$ sh /tmp/test.sh $(sh /tmp/args.sh)
+ sh /tmp/args.sh
+ sh /tmp/test.sh 1 Two words 3
Argument 1: 1
Argument 2: Two words
Argument 3: 3
$

答案2

當變數替換$var或命令替換$(cmd)不加引號時,結果將發生以下轉換:

  1. 將產生的字串拆分為單字。分割發生在空白處(空格、製表符和換行符序列);這可以透過設定來配置IFS(參見1,2)。
  2. 每個產生的單字都被視為全域模式,如果它與某些檔案匹配,則該單字將被檔案名稱清單取代。

請注意,結果不是字串,而是字串列表。此外,請注意,"此處不涉及引號字元;它們是 shell 來源語法的一部分,而不是字串擴充的一部分。

shell 程式設計的一般規則是始終在變數和命令替換兩邊加上雙引號,除非您知道為什麼必須將它們去掉。所以在 中test.sh,寫echo "Argument 1: $1".要將參數傳遞給test.sh,您遇到了麻煩:您需要將單字清單從 傳遞args.shtest.sh,但您選擇的方法涉及命令替換,並且僅提供了簡單字串的方式。

如果你能保證傳遞的參數不包含任何換行符,而呼叫過程稍作改變也是可以接受的,那麼你可以設定IFS為只包含換行符。確保args.sh每行輸出一個檔案名,並且沒有雜亂的引號。

IFS='
'
test.sh $(args.sh)
unset IFS

如果參數可能包含任意字元(大概除了空字節,不能將其作為參數傳遞),則需要執行一些編碼。任何編碼都可以;當然,這與直接傳遞參數不同:那是不可能的。例如,在(如果您的 shell 不支持,則args.sh替換為實際的製表符):\t

for x; do
  printf '%s_\n' "$x" |
  sed -e 's/q/qq/g' -e 's/ /qs/g' -e 's/\t/qt/g' -e 's/$/qn/'
done

並在test.sh

for arg; do
  decoded=$(printf '%s\n' "$arg" |
            sed -e 's/qs/ /g' -e 's/qt/\t/g' -e 's/qn/\n/g' -e 's/qq/q/g')
  decoded=${decoded%_qn}
  # process "$decoded"
done

您可能更願意更改test.sh為接受字串列表作為輸入。如果字串不包含換行符,args.sh | test.sh則在args.sh(解釋):

while IFS= read -r line; do
  # process "$line"
done

另一種完全不需要引用的方法是從第一個腳本呼叫第二個腳本。


args.sh "$@"

相關內容