生成されたファイル名のリストを引数リストとして使用—スペースあり


16

によって収集されfindたファイル名のリストを使用してスクリプトを起動しようとしています。特別なものはなく、次のようになります。

$ myscript `find . -name something.txt`

問題は、パス名の一部にスペースが含まれているため、引数の展開時に2つの無効な名前に分割されることです。通常、名前を引用符で囲みますが、ここでは逆引用符の展開によって挿入されます。find各ファイル名の出力をフィルタリングして引用符で囲みましたが、bashがそれらを見るまでにそれらを取り除くには遅すぎて、ファイル名の一部として扱われます:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

はい、それがコマンドラインの処理方法のルールですが、どうすれば回避できますか?

これは恥ずかしいことですが、私は正しいアプローチを思い付くことに失敗しています。私は最終的にそれをどのように行うかを見つけましたxargs -0 -n 10000...しかし、それは私がまだ聞きたいと思うようなsuchいハックです:バッククォート展開の結果を引用する方法、または別の方法で同じ効果を達成するにはどうすればよいですか?

編集:私は、事実について混乱していたxargs それはそう言われていますか、システムの制限を超えることがありますしない限り、単一の引数リストにコレクトすべての引数を。まっすぐに設定してくれてありがとう!他の人は、受け入れられた答えを読んでいるときにこれを覚えておいてください。なぜなら、それは直接指摘されていないからです。

私は答えを受け入れましたが、私の質問は残ります:バックティック(または$(...))拡張でスペースを保護する方法はありませんか?(受け入れられた解決策は非bash回答であることに注意してください)。


シェルがファイル名の区切り文字として使用するものを変更する必要があると思います(たとえば、IFSの値で遊ぶことにより、1つの可能な方法はIFS="、改行です")。しかし、すべてのファイル名でスクリプトを実行する必要はありますか?そうでない場合は、findを使用して各ファイルのスクリプトを実行することを検討してください。
njsg 14年

IFSを変更することは素晴らしい考えであり、考えもしていませんでした!コマンドラインの使用には実用的ではありませんが、それでも使用できます。:-)そして、はい、目標はすべての引数をスクリプトの同じ呼び出しに渡すことです。
アレクシス14年

回答:


12

あなたは、いくつかの実装で使用して次のことを行うことができますfindし、xargsこのように。

$ find . -type f -print0 | xargs -r0 ./myscript

または、標準では、単にfind

$ find . -type f -exec ./myscript {} +

次のサンプルディレクトリがあるとします。

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

今、私はこれを持っているとしましょう./myscript

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

今、次のコマンドを実行すると。

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

または、2番目のフォームを次のように使用する場合:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

詳細

検索+ xargs

上記の2つの方法は、見た目は異なりますが、基本的に同じです。1つ目は、findから出力を取得し、find \0を介して-print0スイッチを介してNULL()を使用して出力を分割することです。xargs -0特にさんはNULLを使用して分割することを入力するように設計されています。その非標準の構文はGNUによって導入されたfindxargsも、最新のBSDのようないくつかの他に、最近発見されました。この-rオプションは、GNU では何も見つからないがBSDでは見つからないmyscript場合の呼び出しを回避するために必要です。findfind

注:このアプローチ全体は、非常に長い文字列を渡さないという事実にかかっています。そうである場合、の2回目の呼び出しが./myscript開始され、findからの残りの結果が残ります。

+で検索

これが標準的な方法です(ただし、比較的最近(2005年)にGNU実装に追加されただけですfind)。私たちがしていることをする能力xargsは、文字通り組み込まれていfindます。そのfindため、ファイルのリストを検索し、そのリストを後に指定されたコマンドに収まるだけの数の引数に渡します-exec(この場合は{}直前にのみ指定でき+ます)。必要に応じてコマンドを数回実行します。

なぜ引用しないのですか?

最初の例では、引数を分離するためにNULLを使用することで、引用の問題を完全に回避することでショートカットを取っています。ときにxargsこのリストを与えられて、それを効果的に私たちの個々のコマンド原子を保護し、NULL値に分割することを指示しています。

2番目の例では、結果を内部に保持しfindているため、各ファイルアトムが何であるかを把握し、それらを適切に処理することを保証します。

コマンドラインの最大サイズは?

この質問は時々出てくるので、おまけとしてこの答えに追加します。これは主に将来見つけられるようにするためです。を使用xargsして、環境の制限を確認できます。

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072

1
感謝しますが、すべての引数をスクリプトの同じ呼び出しに渡す必要があります。これは問題の説明にありますが、偶然ではないことを明確にしたわけではないと思います。
アレクシス14年

@alexis-回答をもう一度お読みください。スクリプトの1回の呼び出しにすべての引数を渡します。
slm

私はのろわれます!私はその+議論について知りませんでしたfind(そしてあなた+も散文で使用しているので、最初にあなたの説明を逃しました)。しかし、もっと重要なのxargsは、デフォルトで何が起こるかを誤解していたことです!!! UnixのIを使用しての三十年で、今までに使用しなかったが、私は私のツールボックスを知っていたと思ったんきた...
アレクシス

@alexis-私たちが言っていることを見逃してしまったと思いました。はいxargs、コマンドの悪魔です。findできることを理解するために、それとのマニュアルページを何度も読む必要があります。5月のスイッチは互いに矛盾しているため、混乱を招きます。
slm

@alexis-また、ツールボックスに追加するもう1つのことは、ネストされたコマンドの実行に逆引用符/逆ティックを使用せず、$(..)代わりに今すぐ使用します。引用符などのネストを自動的に処理します。バックティックは非推奨です。
slm

3
find . -name something.txt -exec myscript {} +

上記でfindは、一致するすべてのファイル名を検索し、それらを引数として提供しますmyscript。これは、スペースやその他の奇数文字に関係なくファイル名で機能します。

すべてのファイル名が1行に収まる場合、myscriptが1回実行されます。リストがシェルで処理するには長すぎる場合、findは必要に応じてmyscriptを複数回実行します。

詳細:コマンドラインにいくつのファイルが収まりますか? コマンドラインman findfindビルドすると「xargsがビルドするのとほぼ同じ方法」と言います。また、man xargs制限はシステムに依存しており、を実行して制限を決定できることを確認してください xargs --show-limits。(getconf ARG_MAX可能性もあります)。Linuxでは、制限は通常(常にではありませんが)コマンドラインあたり約200万文字です。


2

@slmのすばらしい答えへのいくつかの追加。

引数のサイズの制限は、execve(2)システムコールにあります(実際には、引数と環境文字列とポインターの累積サイズにあります)。myscriptシェルが解釈できる言語で記述されている場合は、実行する必要はないかもしれません。別のインタープリターを実行せずにシェルで解釈できるようにすることができます。

スクリプトを次のように実行する場合:

(. myscript x y)

次のようなものです。

myscript x y

それを実行するのではなく、現在のシェルの子によって解釈されることを除いて(最終的にはさらに引数を指定して実行 sh(または、もしあればshe-bang行が指定するもの)を伴います)。

明らかに、コマンドの使用find -exec {} +はできません。シェルの組み込みコマンドであるため、ではなくシェルによって実行される必要があります。..find

zsh、簡単です:

IFS=$'\0'
(. myscript $(find ... -print0))

または:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

とはいえ、その機能のほとんどはグロビングに組み込まれているため、そもそもzsh必要ありません。findzsh

bashただし、変数にNUL文字を含めることはできないため、別の方法を見つける必要があります。1つの方法は次のとおりです。

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

4.0以降のglobstarオプションでzshスタイルの再帰的グロビングを使用することもできますbash

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

4.3 **で修正されるまで、ディレクトリへのシンボリックリンクをたどっていることに注意してくださいbash。また、グロビング修飾子をbash実装していないzshため、findそこにあるすべての機能を取得できないことに注意してください。

別の選択肢は、GNUを使用することlsです。

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

あなたが確認する場合は、上記の方法も使用することができますmyscriptされ、実行(引数リストが大きすぎると失敗)一度だけ。Linuxの最近のバージョンでは、次の方法で引数リストの制限を引き上げたり解除したりできます。

ulimit -s 1048576

(1GiBスタックサイズ、その4分の1をarg + envリストに使用できます)。

ulimit -s unlimited

(制限なし)


1

ほとんどのシステムでは、xargsまたはを使用して、プログラムに渡されるコマンドラインの長さに制限があり-exec command {} +ます。からman find

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

呼び出しはずっと少なくなりますが、1回であるとは限りません。あなたがすべきことは、コマンドライン引数に基づいて、スクリプト内のNULで区切られたファイル名をstdinから読み取ることです-o -。私は次のようなことをします:

$ find . -name something.txt -print0 | myscript -0 -o -

それにmyscript応じてオプション引数を実装します。


はい、OSは渡すことができる引数の数/サイズに制限を課しています。最新のLinuxシステムでは、これは(巨大)(linux.die.net/man/2/execve)(スタックサイズの1 / 4、0x7FFFFFFF引数)です。AFAIK bash自体には制限がありません。私のリストはずっと小さく、私の問題はどのようにxargs機能するかを誤解したり、誤って覚えていることが原因でした。あなたのソリューションは確かに最も堅牢ですが、この場合はやり過ぎです。
アレクシス14年

0

バックティック(または$(...))展開のスペースを保護する方法はありませんか?

いいえ、ありません。何故ですか?

Bashには、保護すべきものとすべきでないものを知る方法がありません。

UNIXファイル/パイプには配列がありません。これは単なるバイトストリームです。``または内のコマンド$()はストリームを出力します。bashはそれを飲み込み、単一の文字列として扱います。そのポイントとして、2つの選択肢しかありません:引用符で囲む、1つの文字列として保持する、または裸にして、bashが構成された動作に従って分割するようにします。

ですから、あなたが配列の配列を持つバイトのフォーマットを定義することである、とすることを望む場合は何をすべきかのようなどのようなツールのxargsfindやる:あなたがそれらを実行する場合-0、引数、彼らは要素を終了バイナリ配列フォーマットに従って動作しますnullバイト。それ以外の場合は不透明なバイトストリームにセマンティクスを追加します。

残念ながら、bashnullバイトで文字列を分割するように構成することはできません。/unix//a/110108/17980に感謝しますzsh

xargs

コマンドを1回実行し、xargs -0 -n 10000それで問題が解決したと言いました。そうではなく、10000を超えるパラメーターがある場合に、コマンドが複数回実行されることが保証されます。

厳密に一度だけ実行するか失敗するようにしたい場合は、-x引数と-n引数より大きい引数を指定する必要があり-sます(実際:長さゼロの引数全体とコマンド名が収まらないほど十分に大きい-sサイズ)。(man xargs、以下の抜粋を参照)

現在使用しているシステムのスタックは約8Mに制限されているため、ここに制限があります。

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

バッシュ

外部コマンドを使用したくない場合、/unix//a/110108/17980に示すように、配列を供給するwhile-readループがbashが物事を分割する唯一の方法ですnullバイト。

( . ... "$@" )スタックサイズの制限を回避するためにスクリプトをソースするというアイデアはクールです(試しましたが、うまくいきました!)が、通常の状況ではおそらく重要ではありません。

プロセスパイプに特別なfdを使用することは、stdinから何かを読みたい場合に重要ですが、それ以外の場合は必要ありません。

それで、日常の家庭のニーズのための最も単純な「ネイティブ」な方法:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

プロセスツリーをきれいで見やすくする必要がある場合は、このメソッドを使用してexec mynonscript "${files[@]}"、bashプロセスをメモリから削除し、呼び出されたコマンドに置き換えます。xargsコマンドが一度だけ実行される場合でも、呼び出されたコマンドの実行中は常にメモリに残ります。


ネイティブのbashメソッドに反するのはこれです:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bashは配列処理用に最適化されていません。


man xargs

-n max-args

コマンドラインごとに最大max-args引数を使用します。サイズ(-sオプションを参照)を超えた場合、max-argsよりも少ない引数が使用されます。ただし、-xオプションが指定されていない場合、xargsは終了します。

-s max-chars

コマンドと初期引数、および引数文字列の末尾の終端ヌルを含む、コマンドラインごとに最大max-chars文字を使用します。許容される最大値はシステムに依存し、execの引数の長さの制限として計算され、環境のサイズから2048バイトのヘッドルームを差し引いたものです。この値が128KiBより大きい場合、128Kibがデフォルト値として使用されます。それ以外の場合、デフォルト値は最大値です。1KiBは1024バイトです。

-バツ

サイズ(-sオプションを参照)を超えた場合は終了します。


すべてのトラブルに感謝しますが、基本的な前提は、bashが通常は精巧な見積もり処理システムを使用するという事実を無視します。しかし、バッククォート展開ではありません。以下の比較(所与の両方のエラーを、その差を示して):ls "what is this"対をls `echo '"what is this"'` 。誰かが、バッククォートの結果に対してクォート処理を実装することを怠っていました。
アレクシス

バッククォートがクォート処理を行わないことを嬉しく思います。彼らが単語分割を行うという事実は、現代のコンピューティングの歴史において、十分に混乱した見た目、頭を悩ませる、セキュリティ上の欠陥を引き起こしました。
クラック

問題は「バックティック(または$(...))展開でスペースを保護する方法はないのか?」です。そのため、そのような状況では行われない処理を無視するのが適切だと思われます。
クラック

NULLで終わる要素配列形式は、配列を表現する最も簡単で安全な方法です。それbashは明らかに残念なことに、ネイティブにサポートしていないのは残念ですzsh
クラック

実際、今週だけを使用printf "%s\0"xargs -0て、中間ツールがシェルによって解析された文字列を介してパラメーターを渡すというクォートの状況を回避しました。引用は常にあなたに噛み付くように戻ってきます。
クラック
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.