web-dev-qa-db-ja.com

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

while loop を使用してテキストを処理していますか?

StéphaneChazelasが指摘 のように、シェルループを使用しない理由のいくつかはconceptual信頼性読みやすさパフォーマンスおよびsecurity

これは answer信頼性の信頼性を説明しています側面:

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

performanceの場合、whileループと read は、ファイルまたはパイプからの読み取り時に非常に遅くなります。 read Shell built-in は一度に1文字を読み取るためです。

conceptualsecurityの側面はどうですか?

207
cuonglm

はい、次のような多くのことがわかります。

_while read line; do
  echo $line | cut -c3
done
_

またはもっと悪い:

_for line in `cat file`; do
  foo=`echo $line | awk '{print $2}'`
  echo whatever $foo
done
_

(笑わないでください、私はそれらの多くを見てきました)。

一般的には、シェルスクリプト初心者から。これらは、Cやpythonなどの命令型言語で行うことの単純なリテラル翻訳ですが、それはシェルで行う方法ではなく、これらの例は非常に非効率的で完全に信頼性が低く(潜在的にセキュリティの問題につながる)、管理するかどうかほとんどのバグを修正すると、コードが判読できなくなります。

概念的に

Cまたは他のほとんどの言語では、ビルディングブロックはコンピューターの命令より1レベル上です。プロセッサに何をすべきか、次に何をすべきかを伝えます。手でプロセッサーを取り、それをマイクロ管理します。そのファイルを開き、その数のバイトを読み取り、これを実行し、それでそれを行います。

シェルはより高いレベルの言語です。それは言語でさえないと言うかもしれません。彼らはすべてのコマンドラインインタープリターの前にいます。ジョブは実行するコマンドによって実行され、シェルはそれらを調整することのみを目的としています。

Unixが導入した素晴らしい点の1つは、pipeと、すべてのコマンドがデフォルトで処理するデフォルトのstdin/stdout/stderrストリームでした。

50年の間、コマンドの力を利用し、それらをタスクに協力させるには、そのAPIよりも優れているとは言えませんでした。これがおそらく、今日でもシェルが使用されている主な理由です。

あなたは切断ツールと音訳ツールを持っています、そしてあなたは簡単に行うことができます:

_cut -c4-5 < in | tr a b > out
_

シェルは配管(ファイルを開き、パイプを設定し、コマンドを呼び出す)を実行しているだけで、準備が完了すると、シェルは何もせずにフローします。ツールは同時に機能します。一方が他方をブロックしないように十分なバッファリングを使用して、自分のペースで効率的にツールを実行します。ツールは美しく、しかもシンプルです。

ただし、ツールの呼び出しにはコストがかかります(パフォーマンスポイントで開発します)。これらのツールは、Cで数千の命令を使用して記述できます。プロセスを作成し、ツールをロードして初期化してからクリーンアップし、プロセスを破棄して待機する必要があります。

cutの起動は、キッチンの引き出しを開けるようなもので、ナイフを持って使い、洗い、乾かして、引き出しに戻します。あなたがするとき:

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

それは、ファイルの各行のようなもので、キッチンの引き出しからreadツールを取得します( それはそのように設計されていないため なので、非常に扱いにくいものです)。ツールを引き出しに戻します。次に、echoおよびcutツールのミーティングをスケジュールし、引き出しから取り出して、呼び出し、洗浄し、乾燥させて、引き出しに戻します。

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

タマネギを切るようなものですが、ナイフを洗って、各スライスの間にあるキッチンの引き出しに戻します。

ここで明らかな方法は、cutツールを引き出しから取り出し、玉ねぎ全体をスライスして、すべての作業が完了した後、引き出しに戻すことです。

IOW、シェルでは、特にテキストを処理するために、できるだけ少ないユーティリティを呼び出して、それらをタスクに連携させ、各ツールが起動、実行、クリーンアップして次のツールを実行する前に数千のツールを順番に実行しないでください。

さらに詳しく ブルースの細かい答え を読んでください。シェルの低レベルのテキスト処理内部ツール(多分zshを除く)は制限があり、扱いにくく、一般に一般的なテキスト処理には適していません。

パフォーマンス

前述のように、1つのコマンドを実行するとコストがかかります。そのコマンドが組み込まれていない場合は莫大なコストになりますが、それらが組み込まれている場合でも、コストは大きくなります。

そして、シェルはそのように実行するように設計されておらず、パフォーマンスの高いプログラミング言語であるというふりをしていません。彼らはそうではなく、単なるコマンドラインインタープリターです。したがって、この面ではほとんど最適化が行われていません。

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

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

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

明らかに、コマンドを順番に実行することは、それらを待つ必要があることを意味します。それは、シェルからツールへ、そして戻って制御を与える小さなスケジューラダンスです。つまり、(パイプラインで長時間実行されるツールのインスタンスを使用するのではなく)複数のプロセッサを同時に利用できない場合もあります。

その_while read_ループと(おそらく)同等の_cut -c3 < file_の間に、私のクイックテストでは、CPU時間の比率が約40000です(1秒と半日)。ただし、Shellビルトインのみを使用する場合でも、次のようになります。

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

(ここではbashを使用)、それでも約1:600(1秒vs 10分)です。

信頼性/読みやすさ

そのコードを正しくするのは非常に困難です。私が挙げた例は実際に頻繁に見られますが、多くのバグがあります。

readは、さまざまなことができる便利なツールです。ユーザーからの入力を読み取り、単語に分割してさまざまな変数に格納できます。 _read line_は、入力の行をしないか、または非常に特殊な方法で行を読み取ります。実際には、入力からwordsを読み取り、_$IFS_で区切られた単語と、バックスラッシュを使用して区切り文字または改行文字をエスケープできます。

_$IFS_のデフォルト値を使用して、次のような入力を行います。

_   foo\/bar \
baz
biz
_

_read line_は、期待どおり_"foo/bar baz"_ではなく_$line_に_" foo\/bar \"_を格納します。

行を読み取るには、実際に次のものが必要です。

_IFS= read -r line
_

あまり直感的ではありませんが、シェルはそのように使用することを意図していないことを覚えておいてください。

echoについても同様です。 echoはシーケンスを展開します。ランダムファイルのコンテンツなど、任意のコンテンツには使用できません。代わりにここにprintfが必要です。

そしてもちろん、誰もが陥る典型的な変数の引用を忘れるがあります。だからそれはもっとです:

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

さて、さらにいくつかの警告:

  • zshを除いて、少なくともGNUテキストユーティリティには問題がないのに、入力にNUL文字が含まれている場合は機能しません。
  • 最後の改行の後にデータがある場合は、スキップされます
  • ループ内ではstdinがリダイレクトされるため、その中のコマンドがstdinから読み取らないことに注意する必要があります。
  • ループ内のコマンドについては、コマンドが成功したかどうかに注意を払っていません。通常、エラー(ディスクがいっぱい、読み取りエラー...)の状態は適切に処理されません。通常、correctの場合よりも不十分です。

上記の問題のいくつかに対処したい場合、次のようになります。

_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...などのループで通常使用されるさまざまなコマンドのさまざまな癖.
  • 一貫性のない方法でマルチバイト文字を処理するさまざまなシェルの(制限された)テキスト操作演算子。
  • ...

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

Shellvariablesarguments to commandsで作業を開始すると、地雷フィールドに入る。

変数を引用するのを忘れるオプションマーカーの終わり を忘れる、マルチバイト文字を使用するロケール(最近の標準)で作業する場合は、遅かれ早かれ脆弱性となるバグ。

ループを使用したい場合。

未定

271

概念と読みやすさに関する限り、シェルは通常ファイルに関心があります。それらの「アドレス可能な単位」はファイルであり、「アドレス」はファイル名です。シェルには、ファイルの存在、ファイルタイプ、ファイル名のフォーマット(グロビングで始まる)をテストするためのあらゆる種類のメソッドがあります。シェルには、ファイルの内容を処理するためのプリミティブがほとんどありません。シェルプログラマは、ファイルの内容を処理するために別のプログラムを呼び出す必要があります。

ファイルとファイル名の向きが原因で、シェルでテキスト操作を実行するのは非常に遅くなります(ご指摘のとおり)が、不明確で歪んだプログラミングスタイルも必要です。

43
Bruce Ediger

いくつかの複雑な答えがあり、私たちの間でオタクに多くの興味深い詳細を与えていますが、それは本当に非常に単純です-シェルループで大きなファイルを処理するのは遅すぎるだけです。

質問者は、典型的な種類のシェルスクリプトで興味深いと思います。このスクリプトは、コマンドラインの解析、環境設定、ファイルとディレクトリのチェック、およびメインジョブに進む前に少し初期化することから始まる場合があります。行指向のテキストファイル。

最初の部分(initialization)については、通常、シェルコマンドが遅いことは問題ではありません。実行されるのは数十のコマンドだけで、おそらく2、3の短いループが含まれます。その部分を非効率的に書いたとしても、通常、すべての初期化を実行するのに1秒もかかりません。それは問題ありません。それは一度だけ起こります。

しかし、何千、何百万行もある大きなファイルの処理に入ると、 良くない シェルスクリプトでは、各行に数分の1秒(たとえ数十ミリ秒であっても)がかかるため、合計で数時間かかる可能性があります。

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

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

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

私はまた、ほとんどのシステムで利用できる標準的なツールにこだわり、常に使用できるわけではありませんが、私の使用をポータブルに保つように努めます。そして、あなたの好きな言語がPythonまたはRubyである場合、ソフトウェアを実行する必要があるすべてのプラットフォームにインストールされることを確認する余分な労力を気にしないでしょう:-)

シンプルなツールには、headtailgrepsortcuttrsedjoin(2つのファイルをマージする場合)、awkワンライナー、その他多数があります。一部の人々がパターンマッチングとsedコマンドで何ができるかは驚くべきことです。

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

awkは(シェルのような)インタプリタ言語であるため、行ごとの処理を非常に効率的に実行できるのは驚くべきことですが、これは専用に設計されており、非常に高速です。

そして、Perlと、テキストファイルの処理に非常に優れた他の多数のスクリプト言語があり、多くの便利なライブラリが付属しています。

最後に、必要に応じて、古き良きCがあります。 最大速度 と高い柔軟性(ただし、テキスト処理は少し面倒です)。しかし、遭遇するさまざまなファイル処理タスクごとに新しいCプログラムを書くのは、おそらくあなたの時間の非常に悪い使い方です。私はCSVファイルを頻繁に使用するため、Cでいくつかの汎用ユーティリティを作成して、さまざまなプロジェクトで再利用できます。実際、これにより、シェルスクリプトから呼び出すことができる「シンプルで高速なUnixツール」の範囲が拡大されるため、スクリプトを作成するだけでほとんどのプロジェクトを処理できます。これは、カスタムCコードを毎回作成してデバッグするよりもはるかに高速です。

最後のヒント:

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

はい、でも...

StéphaneChazelasの正しい答え は、すべてのテキスト操作をgrepawksedなどの特定のバイナリに委任するという Shell コンセプトに基づいています。

bash は自分で多くのことを実行できるため、forksをドロップすると、(すべてのジョブを実行するために別のインタープリターを実行するよりも)速くなる場合があります。

サンプルについては、この投稿をご覧ください。

https://stackoverflow.com/a/38790442/1765658

そして

https://stackoverflow.com/a/7180078/1765658

テストして比較...

もちろん

ユーザー入力セキュリティは考慮されていません!

bash !!の下でWebアプリケーションを記述しないでください。

しかし、 Shell の代わりに bash を使用できる多くのサーバー管理タスクでは、組み込みのbashを使用すると非常に効率的です。

私の意味:

bin utilsのような書き込みツールは、システム管理と同じ種類の作業ではありません。

だから同じ人ではない!

sysadminsShellを知っている必要がある場合、彼の好みの(そして最もよく知られている)を使用してprototypesを書き込むことができますツール。

この新しいユーティリティ(プロトタイプ)が本当に便利な場合は、より適切な言語を使用して専用ツールを開発できる人もいます。

15
F. Hauri