シェルループを使用してテキストを処理するのはなぜ悪い習慣と見なされますか?

シェルループを使用してテキストを処理するのはなぜ悪い習慣と見なされますか?

使用してwhileループPOSIXシェルでテキストを処理することは一般的に悪い習慣と見なされますか?

〜のようにステファン・チャゼラスは次のように指摘しています。、シェルループを使用しないいくつかの理由は次のとおりです。概念的信頼できる読みやすさパフォーマンスそして安全

これ回答説明した信頼できるそして読みやすさ側面:

while IFS= read -r line <&3; do
  printf '%s\n' "$line"
done 3< "$InputFile"

~のためパフォーマンスwhileループ、読むファイルやパイプから読み込むのは非常に遅いです。シェル組み込みを読む一度に1文字ずつ読みます。

どうですか?概念的そして安全側面?

ベストアンサー1

はい、次のようなものがたくさんあります。

while read line; do
  echo $line | cut -c3
done

またはより悪い:

for line in $(cat file); do
  foo=$(echo $line | awk '{print $2}')
  bar=$(echo $line | awk '{print $3}')
  doo=$(echo $line | awk '{print $5}')
  echo $foo whatever $doo $bar
done

(笑わないでください。私はこんなことをたくさん見ました。)

通常、シェルスクリプトの初心者から始まります。これは、CやPythonなどの命令型言語で何をするのかを簡単に文字通り翻訳したものです。しかし、シェルで作業を行う方法ではなく、例は非常に非効率的です。各入力ラインのサブプロセスであり、完全に信頼できず(セキュリティ上の問題を引き起こす可能性があります)、ほとんどのバグを修正するとコードが読み取れなくなります。

概念的に

Cまたは他のほとんどの言語では、ビルディングブロックはコンピュータコマンドより1レベル上にあります。プロセッサに何をすべきか、次に何をすべきかを伝えます。手でプロセッサを拾い、細かく管理します。ファイルを開き、その分のバイトを読み、こういうことをして、その仕事をします。

シェルは高級言語です。言語ではないと言うこともできます。すべてのコマンドラインソルバーよりも優先されます。操作はユーザーが実行するコマンドによって実行され、シェルはコマンドを調整します。

Unixから出てきた最も偉大なものの1つは管路デフォルトでは、すべてのコマンドが処理するデフォルトのstdin/stdout/stderrストリームです。

過去50年間、私たちはこのAPIよりもコマンドの力を活用し、一緒に作業して作業を行うためのより良い方法を見つけることができませんでした。これが今日の人々がまだ貝殻を使用する主な理由です。

トリミングツールと音訳ツールがある場合は、単に次のことができます。

cut -c4-5 < in | tr a b > out

シェルはパイプ操作(ファイルを開く、パイプ設定、コマンドを呼び出す)のみを行い、すべてが準備されると、シェルで何もせずに正常に実行されます。ツールは、1つのツールが他のツールをブロックしないように、十分なバッファリングを使用して同時に作業を効率的に独自の速度で実行します。美しいがシンプルです。

ただし、ツールを呼び出すには費用がかかります(パフォーマンスの面で開発する予定です)。これらのツールは、C言語で書かれた何千ものコマンドです。プロセスを作成し、ツールをロードして初期化し、クリーンアップしてプロセスを削除して待つ必要があります。

呼ぶcutことは台所の引き出しを開け、ナイフを拾って使用し、きれいにし、乾燥し、引き出しに戻すのと同じです。これを行うとき:

while read line; do
  echo $line | cut -c3
done < file

readこれは、ファイルの各行に対してキッチンドロワーからツールをインポートするのと同じです(これは非常に不器用なアプローチです)。これはこのような用途に設計されていません。)、1行を読み、読書ツールを清掃して、引き出しに戻します。その後、ツールのechoセッションをスケジュールしcut、引き出しから取り出し、回収し、洗浄し、乾燥させ、引き出しに戻すなどの作業を行います。

これらのツール(readおよびツール)の一部はほとんどのシェルに組み込まれていますが、まだ別のプロセスで実行する必要があるため、echoここではあまり違いはありません。echocut

玉ねぎを固めるのと似ていますが、ナイフを洗って台所の引き出しに入れてください。

ここで最も明確な方法は、cut引き出しからツールを取り出し、玉ねぎ全体を切り、作業全体が終わったら引き出しに戻すことです。

IOW、シェルでは、特にテキストを処理するときに何千ものツールを順番に実行し、各ツールが起動、実行、クリーンアップされるのを待つのではなく、できるだけ少ないユーティリティを呼び出して作業に適しています。次のツールをもう一度実行してください。

追加読書ブルースの答えは素晴らしいです.shellの低レベルテキスト処理内部ツールは(おそらくは除くzsh)制限的で面倒で、通常はプレーンテキスト処理には適していません。

パフォーマンス

前述したように、コマンドを実行するにはコストがかかります。命令が組み込まれていないとコストは膨大ですが、組み込まれていてもコストは途方もなくなります。

シェルはこのように動作するようには設計されておらず、高性能プログラミング言語であるとも主張していません。彼らはそうではありません。彼らは単にコマンドラインソルバーです。したがって、これに関して最適化はほとんど行われなかった。

また、シェルは別のプロセスでコマンドを実行します。これらのビルディングブロックは共通のメモリや状態を共有しません。fgets()or Cでは、fputs()これはstdioの関数です。 stdioは、高価なシステムコールを頻繁に防ぐために、すべてのstdio関数の入出力用の内部バッファを保持します。

対応する組み込みシェルユーティリティ(read、、、echoprintfでもこれを行うことはできません。read1行を読むことができるように設計されています。改行文字を過ぎて読むと、実行する次のコマンドがこれを逃すという意味です。したがってread、一度に1バイトずつ読み取る必要があります(一部の実装では、チャンクで読み取って逆にすると入力が通常のファイルである場合は最適化されますが、これは通常のファイルでのみ機能bashします。ユーティリティより一般的です)。

出力側でも同様です。echo出力をバッファリングすることはできず、実行する次のコマンドはそのバッファを共有しないため、すぐに出力する必要があります。

明らかに命令を順番に実行することは、命令を待たなければならないことを意味します。これは、シェルからツールに制御を渡す小さなスケジューラダンスです。これはまた、パイプラインで長期実行ツールインスタンスを使用するのとは異なり、複数のプロセッサを同時に(利用可能な場合)利用できないことを意味します。

クイックテストでは、while readこのループと(おそらく)同等のループ間のCPU時間の割合はcut -c3 < file約40000(1秒対半日)でした。ただし、シェル組み込み機能のみを使用しても:

while read line; do
  echo ${line:2:1}
done

(ここで使用されていますbash)はまだ1:600(1秒対10分)です。

信頼性/読みやすさ

コードを正しく合わせるのは難しいです。私が提示した例は、現場でよく見られるものですが、バグが多いです。

readさまざまなタスクを実行できる便利なツールです。ユーザーの入力を読み、それを単語に分割して別の変数に保存できます。 read lineするいいえ入力行を読むことも、非常に特定の方法で行を読み取ることもできます。実際に読む内容は次のとおりです。性格入力時に$IFS区切り文字または改行文字をエスケープするには、バックスラッシュで区切られた単語を使用できます。

デフォルト値は$IFS次のように入力します。

   foo\/bar \
baz
biz

read line期待どおりに"foo/bar baz"保存されませ$lineん。" foo\/bar \"

実際に必要な行を読むには、次のものが必要です。

IFS= read -r line

これは非常に直感的ではありませんが、そのままであり、シェルをこのように使用しないでください。

echo.extendedシーケンスと同じですecho。任意のファイルの内容など、任意の内容と一緒に使用することはできません。ここに必要ですprintf

もちろん代表的な場合もあります変数を引用するのを忘れました。誰もがそれに陥る。これについての詳細は次のとおりです。

while IFS= read -r line; do
  printf '%s\n' "$line" | cut -c3
done < file

それでは、いくつかの注意事項を見てみましょう。

  • を除いて、zsh入力に少なくともGNUテキストユーティリティに問題が発生しないNUL文字が含まれている場合、この方法は機能しません。
  • 最後の改行文字の後にデータがある場合はスキップします。
  • ループ内ではstdinがリダイレクトされるため、内部コマンドがstdinから読み込まれないように注意する必要があります。
  • ループ内部コマンドの場合、成功は気にしません。通常、エラー(ディスクがいっぱい、読み取りエラー...)の状況は正しく処理されず、通常は次のものを使用するよりも優れています。正しい同じ。多くのコマンド(複数の実装を含む)printfも、終了状態で標準出力への書き込み失敗を反映しません。

上記の問題のいくつかを解決するには、次のようになります。

while IFS= read -r line <&3; do
  {
    printf '%s\n' "$line" | cut -c3 || exit
  } 3<&-
done 3< file
if [ -n "$line" ]; then
    printf '%s' "$line" | cut -c3 || exit
fi

これを見分けることがますます難しくなってきています。

パラメータを介してコマンドにデータを渡したり、変数から出力を取得したりするには、他にも多くの問題があります。

  • パラメータサイズの制限
  • NUL文字(テキストユーティリティにも問題があります)
  • -引数が(または時々)で始まる場合、+オプションと見なされます。
  • これらのループ内で一般的に使用される様々な命令の様々な特性、例えば、exprtest
  • さまざまなシェルの(制限された)テキスト演算子は、マルチバイト文字を一貫して処理しません。
  • ...

セキュリティに関する考慮事項

シェルを使い始めると変わりやすいそしてコマンドパラメータ、あなたは地雷原に入っています。

もしあなたなら変数を引用するのを忘れました。、忘れるオプション閉じるタグ、マルチバイト文字(現在の標準)を使用するロケールで作業すると、近いうちに脆弱性になるバグが発生する可能性があります。

ループを使いたいとき

テキストを処理するためにシェルループを使用することは、シェルがうまくいくこと、つまり外部プログラムの実行を実行することが含まれるときに意味があるかもしれません。

たとえば、次のようなループが適している可能性があります。

while IFS= read -r line; do
    someprog -f "$line"
done < file-list.txt

上記の簡単な場合(入力が変更されていない状態で渡される)someprogは、次のようにすることもできますxargs

<file-list.txt tr '\n' '\0' | xargs -r0 -n1 someprog -f 

またはGNUを使用してくださいxargs

xargs -rd '\n' -n1 -a file-list.txt someprog -f

おすすめ記事