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


196

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

以下のようステファンChazelasが指摘し、シェルのループを使用していない理由のいくつかはある概念信頼性読みやすさパフォーマンスセキュリティ

この回答では、信頼性読みやすさの側面について説明しています

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

パフォーマンスのために、ファイルまたはパイプから読み取る場合、whileループと読み取りは非常に遅くなります。これは、読み取りシェルに組み込まれたコマンドが一度に1文字ずつ読み取るためです。

どの程度概念セキュリティ面?



1
組み込みの読み取りシェルは、一度に1文字を読み取るのではなく、一度に1行を読み取ります。wiki.bash-hackers.org/commands/builtin/read
A.Danischewski

@ A.Danischewski:それはあなたのシェルに依存します。ではbash、一度に1つのバッファサイズを読み取りますdash。たとえば、試してください。unix.stackexchange.com/q/209123/38906
cuonglm

回答:


256

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

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つは、パイプと、すべてのコマンドがデフォルトで処理するデフォルトのstdin / stdout / stderrストリームでした。

45年の間に、コマンドの力を活用し、タスクに協力させるAPIほど優れたものは見つかりませんでした。それがおそらく今日でも人々がシェルを使用している主な理由です。

切断ツールと音訳ツールがあり、簡単に実行できます。

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

シェルは単に配管を行って(ファイルを開き、パイプをセットアップし、コマンドを呼び出します)、すべて準備ができたら、シェルは何もせずに流れます。これらのツールは、一方が他方をブロックしないように十分なバッファリングを使用して、効率的に独自のペースで同時に作業を行います。

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

呼び出しcutは、キッチンの引き出しを開け、ナイフを取り、使用し、洗浄し、乾燥させ、引き出しに戻すようなものです。あなたがするとき:

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

ファイルの各行ごとにread、キッチンの引き出しからツールを取得するように設計されています(そのために設計されていないため非常に不格好なものです)。次にechocutツールとツールの会議をスケジュールし、引き出しから取り出し、呼び出し、洗浄し、乾燥させ、引き出しに戻します。

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

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

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

IOW、特にテキストを処理するシェルでは、できるだけ少ないユーティリティを呼び出してタスクに協力させます。数千のツールを順番に実行して、各ツールが開始、実行、クリーンアップされてから次のツールを実行しないようにします。

ブルースのすばらしい答えのさらなる読書。シェルの低レベルのテキスト処理内部ツール(を除くzsh)は制限されており、扱いにくく、一般的なテキスト処理には一般的に適合しません。

性能

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

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

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

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

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

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

そのwhile readループと(おそらく)同等のcut -c3 < fileテストとの間に、私のクイックテストでは、テストのCPU時間の比率が約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

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

同じですechoechoシーケンスを展開します。ランダムファイルのコンテンツのような任意のコンテンツには使用できません。printf代わりにここが必要です。

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

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

ここで、さらにいくつかの注意事項があります。

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

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

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

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

コマンドのシェル変数引数の操作を開始すると、地雷原に入ります。

あなたがいる場合、あなたの変数を引用することを忘れ、忘れオプションマーカーの終わりを、マルチバイト文字(標準これらの日)とのロケールで動作し、あなたは遅かれ早かれ脆弱性になりますバグを導入する特定のです。

ループを使用する場合。

未定


24
明確で(鮮明に)読みやすく、非常に役立つ。改めてありがとうございます。これは実際、シェルスクリプトとプログラミングの根本的な違いについて、インターネット上で見た中で最も良い説明です。
ワイルドカード

2
このような投稿は、初心者がシェルスクリプトについて学習し、微妙な違いを確認するのに役立ちます。nullを取得しないように、参照変数を$ {VAR:-default_value}として追加する必要があります。-o nounsetを設定して、未定義の値を参照するときに叫びます。
unsignedzero

6
@ A.Danischewski、私はあなたがポイントを逃していると思う。はいcut、たとえば効率的です。cut -f1 < a-very-big-fileCで記述した場合と同じくらい効率的です。ひどく非効率的でエラーが発生しやすいのは、この答えで指摘されているシェルループ内のcutすべての行を呼び出すことですa-very-big-file。それは不必要なコードを書くことについてのあなたの最後の声明と一致し、あなたのコメントを理解していないのではないかと思わせます。
ステファンシャゼラス16

5
「45年の間に、コマンドの力を活用し、タスクに協力させるAPIほど優れたものは見つかりませんでした。」-実際、PowerShellは、バイトストリームではなく構造化データを渡すことで、恐ろしい解析の問題を解決しました。シェルがまだ使用していない唯一の理由(アイデアはかなり前からあり、現在標準のリストと辞書コンテナタイプが主流になったときにJavaの周りで基本的に結晶化しました)は、メンテナーがまだ同意できなかったことです一般的な使用するために構造化データ形式(。
ivan_pozdeev

6
@OlivierDulacそれはちょっとしたユーモアだと思う。そのセクションは永久に未定です。
ムル

43

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

ファイルとファイル名の向きのため、シェルでテキスト操作を行うのは非常に遅いですが、既に述べたように、不明瞭でゆがんだプログラミングスタイルも必要です。


25

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

質問者は、主な仕事に進む前に、コマンドラインの解析、環境設定、ファイルとディレクトリのチェック、およびもう少しの初期化で始まる典型的な種類のシェルスクリプトで興味深いと思います:行指向のテキストファイル。

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

しかし、数千から数百万行に及ぶ可能性のある大きなファイルの処理に進むと、シェルスクリプトが各行に相当な時間(ほんの数十ミリ秒であっても)を要することは問題ありません。合計すると数時間かかる可能性があるためです。

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

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

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

また、ほとんどのシステムで利用可能な標準ツールに固執し、常に使用できるとは限りませんが、使用方法をポータブルに保つようにします。また、お気に入りの言語がPythonまたはRubyである場合、ソフトウェアを実行する必要があるすべてのプラットフォームにインストールされるようにするための余分な労力を気にしないかもしれません:

シンプルなツールが含まれheadtailgrepsortcuttrsedjoin(2つのファイルをマージする場合)、およびawk他の多くの間でワンライナー、。一部の人々がパターンマッチングとsedコマンドを使用してできることは驚くべきことです。

それがより複雑になり、実際に各行に何らかのロジックを適用する必要がある場合awkは、良い選択肢です-ワンライナー(一部の人はawkスクリプト全体を「1行」に入れますが、それは非常に読みにくい短い外部スクリプト。

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

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

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

最後のヒント:

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

6
多くの単純なツールのパイプライン(具体的には、head、tail、grep、sort、cut、tr、sedなど)が不必要に使用されることがよくあります。これらのシンプルなツールのタスクも同様です。考慮すべきもう1つの問題は、パイプラインでは、パイプラインのフロントサイドのプロセスからリアサイドに表示されるプロセスに状態情報を簡単かつ確実に渡すことができないことです。このような単純なプログラムのパイプラインにawkプログラムを使用すると、単一の状態空間ができます。
ジャニス

14

はい、でも...

ステファンChazelasの正しい答えが基づいている固有のバイナリ、同様にすべてのテキスト操作を委譲の概念grepawksedなど。

落下、自分でたくさんのことを行うことが可能であるフォークが(でも、すべての仕事をしているため、別のインタプリタを実行しているよりも)速くなることがあります。

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

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

そして

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

テストと比較...

もちろん

ユーザー入力セキュリティに関する考慮事項はありません!

下でWebアプリケーションを作成しないでください!!

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

私の意味:

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

同じ人じゃない!

システム管理者が知る必要がある場合、彼が好む(そして最もよく知られている)ツールを使用してプロトタイプshell書くことができます。

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


1
良い例え。あなたのアプローチは、lololuxのアプローチよりも確かに効率的ですが、テンシバイの答え(シェルループを使用しないでこのIMOを行う正しい方法)が、あなたのものよりも桁違いに速いことに注意してください。そして、あなたが使用しない場合、あなたははるかに高速ですbash。(私のシステムでのテストでは、ksh93で3倍以上高速です)。bash一般的に最も遅いシェルです。zshそのスクリプトでも2倍高速です。また、引用符で囲まれていない変数との使用法に関するいくつかの問題もありreadます。あなたは実際にここで私のポイントの多くを説明しています。
ステファンシャゼラス16

@StéphaneChazelas私は同意します、bashはおそらく今日使用できる最も遅いシェルですが、とにかく最も広く使用されています。
F.ハウリ

私が投稿した@StéphaneChazelas のperlのバージョン私の答えを
F. HAURI

1
@Tensibaiは、あなたは見つけるでしょうPOSIXshAwkのセッドgrepedexcutsortjoinバッシュよりも信頼性の高い...すべてをのPerl。
ワイルドカード

1
@ Tensibai、U&Lが関係するすべてのシステムのうち、それらのほとんど(Solaris、FreeBSD、HP / UX、AIX、ほとんどの組み込みLinuxシステムなど)はbashデフォルトでインストールされていません。bash主に(私はそれはあなたが呼んでいるものだと仮定のみ、アップルのMacOSとGNUシステム上で発見された主要なディストリビューションの多くのシステムはまた、オプションパッケージ(のようなとしてそれを持っているけれども、) 、、zsh ...)tclpython
ステファンChazelas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.