バックティック出力で引用符が異なるように処理されます。

バックティック出力で引用符が異なるように処理されます。

背景

findスペースを含むファイル名のリスト(リストを介して)をカスタムPythonスクリプトに渡したいと思います。したがって、find各結果の周りに引用符を追加するように設定しました。

find ./testdata -type f -printf "\"%p\" "

結果:

"./testdata/export (1).csv" "./testdata/export (2).csv" "./testdata/export (3).csv"

test.pyこの質問に答えるために、私のカスタムスクリプト()が次のことをしているとしましょう。

#!/usr/bin/python3
import sys 


print(sys.argv)

観察結果

ケース1:

引用符付きパラメーターを手動でリストします。

入力する:./test.py "./testdata/export (1).csv" "./testdata/export (2).csv" "./testdata/export (3).csv"

出力:['./test.py', './testdata/export (1).csv', './testdata/export (2).csv', './testdata/export (3).csv']

ケース2:

使用xargs

入力する:find ./testdata -type f -printf "\"%p\" " | xargs ./test.py

出力:['./test.py', './testdata/export (1).csv', './testdata/export (2).csv', './testdata/export (3).csv']

(つまり、出力は次のようになります。ケース1)

ケース3:

バックティックを使用してください。

入力する:./test.py `find ./testdata -type f -printf "\"%p\" "`

出力:['./test.py', '"./testdata/export', '(1).csv"', '"./testdata/export', '(2).csv"', '"./testdata/export', '(3).csv"']

2つのことが変更されました。

  • "./testdata/exportこれで(1).csv"2つの別々のパラメータになりました。
  • 引用はまだ議論の一部です。

質問

  1. バックティックバージョンが異なる動作をするのはなぜですか?

  2. バックティックで引用符を含める方法はありますか?つまり、xargs?と同じように動作します。

コメント

ここで何が起こっているのかは本当に想像できません。論理的な説明は、バックティックで示されるコマンド出力が1つの大きな引数として扱われることです。しかし、なぜ空白に分割されるのですか?

したがって、次の最良の説明は、スペースで区切られた各文字列が、引用符であるかどうかにかかわらず、別々の引数として扱われることです。そうですか?それでは、バックティックがなぜこのような奇妙な動作をするのでしょうか?私たちがほとんどの場合、欲しいものはそうではありません...

答え1

したがって、次の最良の説明は、スペースで区切られた各文字列が、引用符であるかどうかにかかわらず、別々の引数として扱われることです。そうですか?

はい、例をご覧ください。https://mywiki.wooledge.org/WordSplittingそしてスペースやその他の特殊文字が原因でシェルスクリプトが停止するのはなぜですか?そしていつ二重引用符が必要ですか?

シェルは、引用符が拡張の結果ではなく、元のコマンドラインに表示された場合にのみ引用符を処理し、引用符自体は引用符で囲まれません。

それでは、バックティックがなぜこのような奇妙な動作をするのでしょうか?私たちがほとんどの場合、欲しいものはそうではありません...

まあ、変なのは相対的です。ある人が何らかの状況で望むものは、他の状況ではまったく望むものではないかもしれません。

しかし、次のことを考慮してください。

a="blah blah"
somecmd -f "$a"

仕組みは、somecmd変数に含まれる文字列をパラメータとして使用することですaその中に何が入っていても。これは、Pythonなどの「実際の」プログラミング言語で動作する方法と似ていますsubprocess.call(["somecmd", "-f", a])。シンプルで清潔で完全に安全です。変数に特殊文字がなくても混乱を招く可能性があります。

文字列がスクリプトの外部から出てくる、ファイルから読み取られた、ユーザーが入力した場合、またはファイル名拡張の結果である場合、これは重要です。

echo "Please enter a filename: "
read -r a
somecmd -f "$a"

拡張結果を引用符で処理する場合、対になっていない引用Don't stop me now.mp3符があるため、ファイル名を入力できません。

また、追加の拡張のためにすべての拡張結果を処理する必要がありますか?かなり不快なことをするようにa設定してください。$(rm -rf $HOME).txtこれは完全に有効なファイル名なので*.txt

拡張後は引用符とエスケープだけを処理し、追加の拡張は処理しないことを提案できるため、これはやや誇張されています。ペアリングされていない一重引用符はまだ問題であり、$(find -printf "\"%p\"")二重引用符を含むファイル名ではまだ機能しません。

このように機能することもできますが、マジックハン​​ドルが静かでないほど、事故の可能性が低くなります。(殻に関しては、とても健全で幸いだと思います。)


findしかし、あなたは正しいです。つまり、シェルから文字列リストを取得するための即時かつ直接的な方法がないことを意味します。これは実際にsys.argvPythonと同じように文字列リストとして欲しいものです。引用符ではありません。

次のことができます。

find -print0 | xargs -0 ./test.py

-print0findファイル名をNULバイトで区切り文字(改行ではなく)として印刷するように要求し、それが私たちに必要なものであることを-0伝えます。xargsこれは、ファイル名に含めることができない唯一のバイトがNULバイトであるために機能します。少なくともGNUとFreeBSDで見つけることができます-print0-0

またはBashから:

mapfile -d '' files < <(find -print0)
./test.py "${files[@]}"

これは、プロセス置換と配列に使用されるのと同じ NUL で区切られた文字列です。

または、Bash(shopt -s globstar)と同様の機能を持つ他のプログラムでファイル名以外の条件でフィルタリングする必要がない場合:

shopt -s globstar
./test.py ./testdata/**

**のように*ただ再帰的です。

または標準ツールを使用してください。

find -exec ./test.py {} +

findtest.pyこれは、ファイル名のリストを別の場所に渡さずに独自に実行するように要求することで、問題全体を解決します。ただし、リストをどこかに保存する必要がある場合は役に立ちません。+最後のことは、各ファイルに対して一度だけ実行されることに注意してください-exec ./test.py {} \;test.py

答え2

xargs入力を特別に処理します。

すべての改行シーケンスと空白シーケンス(一部の実装ではスペースとタブ以上)を区切り文字として処理し、先行シーケンスと末尾シーケンスを無視し、独自の特別な方法で引用を処理します。'...'引用に使用できます"..."が、\同じ構文で使用されます。 way sh( and"..."'...'二重引用符ですが、改行を含めることはできず、\newline行の連続ではなく文字通りの改行です)。

したがって、次のような入力のために:

   "foo \ bar" 'x'\
y

xargs2つのfoo \ bar合計x<newline>yパラメータを生成します。

Split + glob演算子は、`...`POSIXシェルのリストコンテキストで引用されていないコマンドの置き換え(古代と現代の両方の形式)を保持します。$(...)入力は複雑な規則を使用して文字に分割され、結果の$IFS単語は次のようになります。ファイル名の生成。見積もり処理はまったくありません。

このように入力すると

  "a* b"

デフォルト値(SPC、TAB、NL)を使用して、現在のディレクトリから始まるファイル名のリスト$IFSに追加の拡張単語を生成します。"a*"ab"

コマンドラインは次のとおりです。

cmd "a* b"
cmd2 "x\"y"

シェル構文のコードです。シェルの構文では、空白、改行、および引用符も特別な意味を持ち、異なる方法で解釈されますxargs。上記のコードは2つのコマンドで解析されます。改行でコマンドを区切り、cmd "a* b"2つの単語に解析します。スペースで単語を区切ってcmdシェル引用演算子なので、対応する内側とSPCは特に処理されません。 .etc.ちょっと待ってください。a* b"..."*

シェルと同じ方法でトークン化するためにglob修飾子がzshあります(zshはデフォルトではPOSIXではありません。これは、分割+globではなくリストコンテキストで引用されていないコマンド置換に対してのみ分割を実行するためです)。また、globzもあります。Q参照の 1 つのレイヤーを削除する修飾子です。このシェルでは、次のことができます。

output_of_cmd=$(find...) # no split+glob here as we're assigning to
                         # scalar variable. It's not a list context

words=("${(Q@)${(z)output_of_cmd}}") # array assignment
your-app "${words[@]}"

答え3

コマンド置換のシェル拡張により、引用符が失われました。再引用してください。$()バックティックの代わりにこの形式を使用することをお勧めします。コードを読みやすくします。

eval ./test.py "$(find ./testdata -type f  -printf "\"%p\" ")"

更新:他の例と同様に、evalを前にしました。これにより、正しい拡張/引用が発生し、Pythonの個別に引用されたパラメータを取得できます。

関連情報