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

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

使用して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

答え2

概念と読みやすさの観点から、シェルは通常ファイルに興味があります。 「アドレス指定可能単位」はファイル、「アドレス」はファイル名です。シェルには、ファイルの有無、ファイルの種類、ファイル名の形式(ワイルドカードで始まる)をテストするさまざまな方法があります。シェルにはファイルの内容を操作するための基本的な要素はほとんどありません。シェルプログラマーは、ファイルの内容を処理するために別のプログラムを呼び出す必要があります。

指摘したように、シェルでのテキスト操作は、ファイルとファイル名の方向のために非常に遅く、不明瞭で歪んだプログラミングスタイルも必要です。

答え3

私たちの間には、オタクのための興味深い詳細がたくさん含まれているいくつかの複雑な答えがありますが、実際には非常に簡単です。シェルループで大容量ファイルを処理するのは遅すぎます。

質問者は一般的なシェルスクリプトに興味があるようです。このスクリプトは、コマンドラインの解析、環境設定、ファイルとディレクトリの確認、主要なタスクを開始する前に追加の初期化などで始まり、大規模なプロセスに進みます。行指向のテキストファイルです。

最初の部分(initialization)の場合、通常シェルコマンドが遅いことは重要ではありません。数十のコマンドといくつかの短いループだけを実行します。この部分を非効率的に作成しても、通常はすべての初期化を完了するのに1秒もかかりません。これは良いことです。これは一度だけ発生します。

しかし、数千または数百万行の大容量ファイルを扱い始めると、悪いシェルスクリプトは1行に数分の1秒かかります(数十ミリ秒に過ぎません)。これには数時間かかることがあります。

この時点で私たちは他のツールを使用する必要があり、Unixシェルスクリプトの利点はそれを簡単に実行できることです。

各行を表示するためにループを使用する代わりに、ファイル全体を渡す必要があります。コマンドパイプライン。これは、シェルがコマンドを何千回も何百万回も呼び出さずに一度だけ呼び出すことを意味します。実際、これらのコマンドにはファイルを1行ずつ処理するためのループがありますが、シェルスクリプトではなく、迅速かつ効率的に設計されています。

Unixには、パイプラインを構築するために使用できる単純なものから複雑なものまで、優れた組み込みツールがたくさんあります。私は通常単純なものから始めて、必要なときだけ複雑なものを使います。

私はまた、ほとんどのシステムで利用可能な標準ツールに固執し、移植性を維持しようとしますが、これは必ずしも可能ではありません。お好みの言語がPythonやRubyの場合は、ソフトウェアを実行するために必要なすべてのプラットフォームにその言語がインストールされていることを確認するためにさらに努力することもできます。 :-)

簡単なツールにはhead、、、、、、、、、 (2つのファイルをマージするとき)および1行ステートメントが含まれます。いくつかの人々は、パターンマッチングとコマンドを使用して驚くべきことを行うことができます。tailgrepsortcuttrsedjoinawksed

これはより複雑になり、実際に各行にいくつかのロジックを適用する必要がある場合にawk良いオプションです。つまり、1行(一部の人々はawkスクリプト全体を「1行」に入れます。読みやすくはありませんが)の1つです。短い外部スクリプトで。

解釈された言語(例:シェル)として、ラインごとのawk処理をどれだけ効率的に実行できるかは驚くべきことですが、このために特別に制作されており、本当に高速です。

Perlテキストファイルの操作に非常に上手で便利なライブラリがたくさんある他のスクリプト言語がたくさんあります。

最後に、必要に応じて良い古いCがあります。最高速度高い柔軟性(テキスト処理は少し面倒ですが)しかし、直面するすべてのファイル処理タスクに対して新しいCプログラムを書くことは非常に時間の無駄になる可能性があります。私はCSVファイルをたくさん使うので、さまざまなプロジェクトで再利用できるいくつかの汎用ユーティリティをCで書いています。効果的には、シェルスクリプトから呼び出すことができる「簡単で高速なUnixツール」の範囲を拡張するため、スクリプトのみを作成してほとんどのプロジェクトで作業できます。これは、毎回カスタムCコードを書いてデバッグするよりもはるかに高速です!

いくつかの最終的なヒント:

  • デフォルトのシェルスクリプトを起動することを忘れないでくださいexport LANG=C。さもなければ、多くのツールは通常の古いASCIIファイルをUnicodeとして扱い、それがずっと遅くなります。
  • export LC_ALL=Csort環境に関係なく一貫したソートを作成したい場合は、設定も検討してください。
  • データが必要な場合は、sort他のすべてよりも多くの時間(およびリソース:CPU、メモリ、ディスク)がかかる可能性があるため、ソートするコマンドの数sortとファイルサイズを最小限に抑えてください。
  • 可能であれば、通常、単一のパイプラインが最も効率的です。中間ファイルを使用して複数のパイプラインを順番に実行すると、読みやすくデバッグが簡単になりますが、プログラムにかかる時間が長くなります。

答え4

受け入れられた答えは、シェルからテキストファイルを解析することの欠点を明確に説明するので、良いです。しかし、人々はシェルループを使用するすべてを批判するために、主なアイデア(主にシェルスクリプトがテキスト処理操作をうまく処理できないこと)を崇拝してきました。

シェルループ自体には何の問題もありません。シェルスクリプト内のループやループ外のコマンド置換には問題がないという意味です。実際、ほとんどの場合、より慣用的な構文に置き換えることができます。たとえば、書かないでください。

for i in $(find . -iname "*.txt"); do
...
done

以下を書いてください:

for i in *.txt; do
...
done

awk他のシナリオでは、優れたテキスト処理機能を備えた一般的なプログラミング言語(Perl、Python、Rubyなど)や特定のファイルタイプ(XML sed、HTML、JSON)などのより専門的なツールに頼ることをお勧めします。cutjoinpastedatamashmiller

しかし、シェルループを使用するのが正しい選択です。知る:

  1. パフォーマンスが優先順位ではありません。スクリプトの速度は重要ですか?何時間ごとにクローンジョブでジョブを実行していますか?その場合、パフォーマンスは問題にならない可能性があります。そうであれば、ベンチマークを実行してシェルループがボトルネックではないことを確認してください。どのツールが「高速」または「遅い」の直観や先入観は、正確なベンチマークを置き換えることはできません。
  2. 読みやすさを維持します。シェルループに余りにも多くのロジックを追加して追いつくのが難しい場合は、このアプローチをもう一度検討してください。
  3. 複雑さは大幅に増加しません。
  4. セキュリティが維持されます。
  5. テストの可能性は問題ではありません。シェルスクリプトを適切にテストするのは十分に困難です。外部コマンドを使用すると、コードにバグがあるかどうかを知るのが難しくなったり、戻り値の誤った仮定の下で作業している場合に問題が発生します。
  6. シェルループは代替ループと同じ意味を持ちます。またはこれらの違いは、現在実行中の作業には重要ではありません。たとえば、上記のfindコマンドはサブディレクトリで繰り返されます.

前の声明を満たすことが不可能な作業ではないことを証明する例として、以下はよく知られている商用ソフトウェアインストーラによって使用されるパターンです。

i=1
MD5=... # embedded checksum
for s in $sizes
do
    checksum=`echo $VAR | cut -d" " -f $i`
    if <checksum condition>; then
       md5=`echo $MD5 | cut -d" " -f $i
       ...
done

非常にまれに実行され、明確な目的があり、簡潔で、不要な複雑さを追加せず、ユーザー制御入力を使用しないため、セキュリティは問題になりません。ループから別のプロセスを呼び出すことは重要ですか?別言します。

関連情報