シェル スクリプトの出力がスクリプトに引数として渡されるときに誤って分割される

シェル スクリプトの出力がスクリプトに引数として渡されるときに誤って分割される

次の 2 つのシェル スクリプトがあるとします。

#!/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問題なく動作します。したがって、これは実際にはシェルが行っていることではないと想定しています。代わりに を呼び出すことで、目的の出力/期待される出力を実現できますsh args.sh | xargs sh test.sh

しかし、最初のスクリプト (args.sh) の出力を xargs にパイプせずにこれを行う同等の方法があるかどうか疑問に思っています。スクリプトを元の順序で呼び出す必要があります。引数スクリプトは、2 番目のスクリプトにパラメータを出力します。また、この呼び出しが期待どおりに動作しない理由についても説明を求めています。

答え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"
$

1 つの で始まる行に注意してください+。引数は 4 つあり、 と'"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. 結果の文字列を単語に分割します。分割は空白(スペース、タブ、改行の連続)で行われます。これは設定で変更できますIFS12)。
  2. 結果の各単語は glob パターンとして扱われ、いくつかのファイルに一致する場合、その単語はファイル名のリストに置き換えられます。

結果は文字列ではなく、文字列のリストであることに注意してください。さらに、引用符のような文字は"ここでは関係ないことに注意してください。これらはシェル ソース構文の一部であり、文字列展開の一部ではありません。

シェル プログラミングの一般的なルールは、変数とコマンドの置換を常に二重引用符で囲むことです (ただし、二重引用符を省略する必要がある理由がわかっている場合は除きます)。したがってtest.sh、 では と記述しますecho "Argument 1: $1"。 に引数を渡すには、問題が発生します。からtest.shに単語のリストを渡す必要がありますが、選択した方法にはコマンド置換が含まれており、単純な文字列しか渡せません。args.shtest.sh

渡される引数に改行が含まれないことを保証でき、呼び出しプロセスを少し変更しても問題ない場合は、 を改行のみを含むように設定できます。が、余分な引用符なしで 1 行に 1 つのファイル名だけを出力するIFSことを確認してください。args.sh

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

引数に任意の文字 (引数として渡すことができない null バイトを除く) が含まれる可能性がある場合は、何らかのエンコードを実行する必要があります。エンコードはどれでもかまいません。もちろん、引数を直接渡すのと同じではありません。それは不可能です。たとえば、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

引用符をまったく使用しなくて済む別の方法は、最初のスクリプトから 2 番目のスクリプトを呼び出すことです。


args.sh "$@"

関連情報