しばらく前にスクリプトに関する質問に対する回答を投稿しました。誰かが次のコマンドを使用してはいけないと指摘しました。
for x in $(cat file); do something; done
しかし、代わりに:
while read f; do something; done < file
猫の無駄な使用記事では問題全体を説明したかったのですが、唯一の説明は次のとおりです。
バックティックの結果がシェルが許容できるコマンドラインの長さ以下であることを知らない限り、バックティックは非常に危険です。 (実際にはこれはカーネル制限です。limits.hの定数ARG_MAXはシステムがどのくらいかかるかを示します。POSIXでは、ARG_MAXが少なくとも4,096バイトになるように要求します。
これを正しく理解したら、コマンドで非常に大きなファイルの出力を使用すると、bash(?)がクラッシュする必要があります(limits.hファイルで定義されているARG_MAXを超える必要があります)。そこで、次のコマンドでARG_MAXを確認しました。
> grep ARG_MAX /usr/src/kernels/$(uname -r)/include/uapi/linux/limits.h
#define ARG_MAX 131072 /* # bytes of args + environ for exec() */
その後、スペースなしでテキストを含むファイルを作成しました。
> ls -l
-rw-r--r--. 1 root root 100000000 Aug 21 15:37 in_file
次に、次を実行します。
for i in $(cat in_file); do echo $i; done
ああ、ひどいことはありませんでした。
それでは、「ループのある猫を使用しないでください」全体が危険であるかどうかを確認するにはどうすればよいですか?
答え1
file
含める項目によって異なります。たとえば、IFSで区切られたシェルグローバルリストを含める場合(デフォルトはと仮定$IFS
):
/var/log/*.log /var/adm/*~
/some/dir/*.txt
さて、これがfor i in $(cat file)
行く方法です。引用解除が行うことは次のとおりです。$(cat file)
末尾の改行文字を削除した状態で、出力に分割+グローブ演算子を適用します。cat file
したがって、これらのglobの拡張によって引き起こされる各ファイル名を繰り返します(globがどのファイルとも一致しない場合、この場合、globはそのまま残りますが拡張されません)。
分離された各行を繰り返すには、file
次のようにします。
while IFS= read -r line <&3; do
{
something with "$line"
} 3<&-
done 3< file
ループを使用すると、for
空白でない各行を繰り返すことができます。
IFS = ' '#改行文字のみに分割します(実際には一連の改行文字と #改行は次のようになるため、先行および末尾の文字を無視します。 #IFS スペース文字) set -o noglob# 無効全体的な状況分割+グローブ演算子の一部: $(catファイル)の行に対して、次のようにします。 「$line」があるもの 完璧
しかし:
while read line; do
something with "$line"
done < file
それは言葉ではありません。それはfile
非常に複雑な方法でコンテンツを読むここ$IFS
で、バックスラッシュ文字は特別に扱われます。
とにかく、引用符付きテキストが参照するARG_MAX制限は、execve()
システムコール(引数と環境変数の累積サイズに関連)にあるため、適用できるファイルシステムを使用してコマンド実行が実行された場合にのみ適用されます。コマンド置換分割+グローブ演算子の非常に長い拡張です(テキストが複数のアカウントで誤解を招く可能性があり、間違っています)。
たとえば、次のように動作します。
cat -- $(cat file) # with shell implementations where cat is not builtin
しかし、以下ではそうではありません。
for i in $(cat file)
execve()
システムコールは含まれません。
比較する:
bash-4.4$ echo '/*/*/*/*' > file
bash-4.4$ true $(cat file)
bash-4.4$ n=0; for f in $(cat file); do ((n++)); done; echo "$n"
523696
bash-4.4$ /bin/true $(cat file)
bash: /bin/true: Argument list too long
bash
使用されているtrue
組み込みコマンドまたはループは機能しますfor
が、実行は機能しません/bin/true
。サイズはわずか9バイトですが、シェルはグローブを拡張するため、数メガバイトfile
だけ拡張されます$(cat file)
。/*/*/*/*
追加資料:
答え2
@chepnaコメントの違いを説明しました。
for i in $(cat in_file)
ファイルの行を繰り返すことなく、ファイルの内容のトークン化とパス名の拡張に起因する単語を繰り返します。
パフォーマンスとリソース使用量への影響を確認するために、1M行(〜19M)入力を使用して両方のケースで小さなベンチマークを実行し、以下を使用して時間とメモリ使用量を測定しました/usr/bin/time -v
。
test1.sh:
#!/bin/bash
while read x
do
echo $x > /dev/null
done < input
結果:
Command being timed: "./test1.sh"
User time (seconds): 12.41
System time (seconds): 2.03
Percent of CPU this job got: 110%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.07
Maximum resident set size (kbytes): 3088
test2.sh:
#!/bin/bash
for x in $(cat input)
do
echo $x > /dev/null
done
結果:
Command being timed: "./test2.sh"
User time (seconds): 17.19
System time (seconds): 3.13
Percent of CPU this job got: 109%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.51
Maximum resident set size (kbytes): 336356
両方のテストの全体的な結果を次にアップロードしました。ペーストビン。 Bashを使用すると、for i in $(cat ...)
より多くのメモリを占有し、実行速度が遅くなります。ただし、同じテストを別のシェルで実行しているかどうかによって結果が異なる場合があります。
答え3
while
ループに問題がある可能性があります。最も明白なのは、基本的にstdinを使用することです(so ssh -n
)。したがって、他にstdinが必要な場合、while
ループは失敗します。
$ find . -name "*.pm" | while read f; do aspell check $f; done
$
何もしませんが、aspell
Perlモジュール名のリストが占める端末が必要です。ループがfor
より適しています(ファイル名がPOSIXトークン化規則によって分割されていないと仮定)。
$ for f in $(find . -name \*.pm); do aspell check $f; done
...
while
デフォルトでは標準入力を使用しないためです。
また、while
自動データ損失が発生しやすくなります(for
同じ入力に対して異なる動作をします)。
$ echo -n mmm silent data loss | while read line; do echo $line; done
$ for i in $(echo -n mmm silent data loss); do echo $i; done
mmm
silent
data
loss
$
while
したがって、状況によっては危険で使用しないでください。