web-dev-qa-db-ja.com

シェルが「猫の無駄な使用」を自動的に修正しないのはなぜですか?

多くの人々は、コードに沿ったワンライナーとスクリプトを使用しています

cat "$MYFILE" | command1 | command2 > "$OUTPUT"

最初のcatは、技術的には新しいプロセスを開始する必要があるため、しばしば「猫の無駄な使用」と呼ばれます(多くの場合/usr/bin/cat)コマンドがされていれば、これを回避できます

< "$MYFILE" command1 | command2 > "$OUTPUT"

シェルはcommand1を指定し、そのstdinに指定されたファイルを指定するだけです。

シェルがこの変換を自動的に行わないのはなぜですか? "猫の無用な使用"構文は読みやすく、無用な猫を自動的に取り除くための十分な情報がシェルにあるはずです。 catはPOSIX標準で定義されているため、パスでバイナリを使用する代わりに、シェルが内部で実装できるようにする必要があります。シェルには、1つの引数バージョンのみの実装とパス内のバイナリへのフォールバックを含めることもできます。

28

2つのコマンドは同等ではありません。エラー処理を検討してください。

cat <file that doesn't exist> | lessは、パイプされたプログラムに渡される空のストリームを生成します。そのため、何も表示されない表示になります。

< <file that doesn't exist> lessは、バーを開くことに失敗し、その後、まったく開かなくなります。

前者を後者に変更しようとすると、潜在的に空白の入力でプログラムを実行することを期待しているスクリプトがいくつも壊れる可能性があります。

25
UKMonkey

"catの無用な使用"は、スクリプトを実行したときに実際に何が実行されるかよりも、コードを記述する方法の方が重要です。これは一種の設計 anti-pattern であり、おそらくより効率的な方法で実行できる何かを実行する方法です。新しいツールを作成するために与えられたツールを最適に組み合わせる方法の理解の失敗です。パイプラインでいくつかのsedおよび/またはawkコマンドをストリング化することも、この同じアンチパターンの症状であると言うことができると私は主張します。

スクリプト内の「catの無用な使用」のインスタンスを修正することは、主にスクリプトのソースコードを手動で修正することの問題です。 ShellCheck などのツールは、明らかなケースを指摘することでこれを支援できます。

$ cat script.sh
#!/bin/sh
cat file | cat
$ shellcheck script.sh

In script.sh line 2:
cat file | cat
    ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.

シェルスクリプトの性質上、シェルにこれを自動的に実行させることは困難です。スクリプトの実行方法は、親プロセスから継承された環境と、使用可能な外部コマンドの特定の実装によって異なります。

シェルは必ずしもcatが何であるかを知っているわけではありません。これは、$PATHの任意の場所からのanyコマンド、または関数の可能性があります。

組み込みコマンド(一部のシェルにある場合があります)の場合、[would組み込みのcatコマンド。それを行う前に、元のcatの後に、パイプライン内の次のコマンドに関する仮定を追加で行う必要があります。

標準入力からの読み取りは、パイプに接続されている場合とファイルに接続されている場合の動作が少し異なることに注意してください。パイプはシークできないため、パイプラインの次のコマンドの動作に応じて、パイプラインが再配置された場合、動作が異なる場合とそうでない場合があります(入力がシーク可能かどうかを検出し、それがそうであるか、またはそうではありません。いずれにしても、動作は異なります)。

この質問は(very一般的な意味で)「 独自に構文エラーを修正しようとするコンパイラはありますか? "(Software Engineering StackExchangeサイトで)に似ています。 、その質問は明らかに構文エラーに関するものですが、無駄なデザインパターンではありません。意図に基づいてコードを自動的に変更するという考え方は、ほとんど同じです。

51
Kusalananda

それは役に立たないので。

_cat file | cmd_の場合、cmdのfd _0_(stdin)はパイプになり、_cmd <file_の場合は通常のファイル、デバイスになる可能性があります、など.

パイプは通常のファイルとは異なるセマンティクスを持ち、そのセマンティクスはない通常のファイルのそれらのサブセットです:

  • 通常のファイルは、意味のある方法でselect(2) edまたはpoll(2) edをオンにすることはできません。上のselect(2)は、常に「準備完了」を返します。 Linuxのepoll(2)などの高度なインターフェイスは、通常のファイルでは機能しません。

  • linuxには、パイプのみで機能するシステムコール(splice(2)vmsplice(2)tee(2))があります[1]

catは頻繁に使用されるため、余分なプロセスを回避するシェル組み込みとして実装できますが、そのパスを開始すると、ほとんどのコマンドで同じことができます-変換より遅くて不格好なPerlまたはpythonにシェルします。 continuations の代わりに、パイプのような使いやすい構文を使用して別のスクリプト言語を記述する方がよいでしょう。代わりに;-)

[1]状況に応じて作成されていない簡単な例が必要な場合は、コメント「 ここ でいくつかの説明を付けて、私の「execdin from stdin」git Gist を見ることができます。 。 UUoCなしで機能させるために内部にcatを実装すると、2倍または3倍大きくなります。

36
mosvy

無駄な猫を見つけるのは本当に難しいからです。

私が書いたシェルスクリプトがあった

cat | (somecommand <<!
...
/proc/self/fd/3
...
!) 0<&3

catsu -c 'script.sh' someuserを介して呼び出されたために削除された場合、シェルスクリプトは本番環境で失敗しました。明らかに不要なcatにより、標準入力の所有者は、スクリプトを実行していたユーザーを/proc経由で再び開くように変更しました。

17
Joshua

tl; dr:コストが超過するため、シェルは自動的に行いませんおそらく利点があります。

他の回答では、stdinがパイプであるのとそれがファイルであるのとの技術的な違いが指摘されています。これを念頭に置いて、シェルは次のいずれかを実行できます。

  1. catを組み込みとして実装し、ファイルとパイプの区別を維持します。これにより、幹部のコストと、おそらくフォークのコストを節約できます。
  2. ファイル/パイプが重要かどうかを確認するために使用されるさまざまなコマンドの知識を備えたパイプラインの完全な分析を実行し、それに基づいて行動します。

次に、各アプローチのコストと利点を検討する必要があります。利点は十分に単純です:

  1. どちらの場合も、(catの)execは避けてください
  2. 2番目のケースでは、リダイレクト置換が可能な場合、フォークの回避。
  3. パイプを使用する必要がある場合は、mightでfork/vforkを回避できる場合がありますが、多くの場合そうではありません。これは、猫に相当するものが、残りのパイプラインと同時に実行される必要があるためです。

したがって、特にフォークを回避できる場合は、CPU時間とメモリを少し節約できます。もちろん、機能が実際に使用されたときにのみ、この時間とメモリを節約できます。そして、本当に節約できるのはfork/exec時間だけです。大きなファイルの場合、時間はほとんどI/O時間です(つまり、ディスクからファイルを読み取る猫)。つまり、パフォーマンスが実際に重要であるシェルスクリプトでcatが(無用に)どのくらいの頻度で使用されるのでしょうか。 testのような他の一般的なShellビルトインと比較します。catが重要な場所で使用される10分の1の頻度であっても、testが(無用に)使用されているとは想像できません。それは推測であり、私は測定していません。これは、実装を試みる前に行う必要があることです。 (または、同様に、他の誰かに、例えば機能リクエストで実装するように依頼します。)

次に尋ねるのは、費用はいくらですか。頭に浮かぶ2つのコストは、次のとおりです。(a)サイズを増やし(したがって、メモリの使用量を増やす)、追加のメンテナンス作業を必要とするシェル内の追加コードは、バグなどの別のスポットです。 (b)後方互換性の驚き、POSIX catは、GNU coreutils catなどの多くの機能を省略しているため、catビルトインが実装する内容に注意する必要があります。

  1. 追加の組み込みオプションはおそらくそれほど悪くはありません-すでにたくさん存在している場所にもう1つ組み込みを追加します。それが役立つことを示すプロファイリングデータがある場合、おそらくお気に入りのシェルの作者にそれを追加するように説得することができます。

  2. パイプラインの分析に関しては、シェルは現在このようなことは何もしていないと思います(パイプラインの終わりを認識してフォークを回避できるシェルもいます)。基本的には、(プリミティブ)オプティマイザーをシェルに追加します。オプティマイザは、多くの場合、複雑なコードであり、多くのバグの原因となっています。そして、それらのバグは驚くべきものになる可能性があります。シェルスクリプトのわずかな変更がバグを回避またはトリガーする可能性があります。

Postscript:同様の分析を猫の無用な用途に適用できます。利点:読みやすくなります(ただしcommand1がファイルを引数として取る場合、おそらくそうではありません)。コスト:追加のforkとexec(およびcommand1がファイルを引数として受け取ることができる場合、おそらくより混乱するエラーメッセージ)。分析で猫を無駄に使うように言われたら、先に進んでください。

13
derobert

catコマンドは、-stdinのマーカーとして受け入れることができます。 ( [〜#〜] posix [〜#〜] 、 "ファイルが「-」の場合、catユーティリティはその時点で標準入力から読み取ります ")これにより、ファイルまたはstdinを簡単に処理できるようになります。

シェルの引数$1-である次の2つの簡単な代替案を検討してください。

cat "$1" | nl    # Works completely transparently
nl < "$1"        # Fails with 'bash: -: No such file or directory'

catが役立つもう1つのタイミングは、単にシェル構文を維持するために何もしないこととして意図的に使用される場合です。

file="$1"
reader=cat
[[ $file =~ \.gz$ ]] && reader=zcat
[[ $file =~ \.bz2$ ]] && reader=bzcat
"$reader" "$file"

最後に、UUOCが実際に正しく呼び出されるのは、catが通常のファイル(デバイスや名前付きパイプではない)であることがわかっているファイル名で使用され、フラグがない場合だけです。コマンドに与えられます:

cat file.txt

その他の状況では、cat自体のoropertiesが必要になる場合があります。

10
roaima

Catコマンドは、シェルが必ずしも実行できない(または少なくとも簡単に実行できない)ことを実行できます。たとえば、タブ、キャリッジリターン、改行など、通常は表示されない可能性のある文字を印刷するとします。シェルの組み込みコマンドだけでこれを行う方法があるかもしれませんが、頭から離れていることは考えられません。 GNU catのバージョンは、_-A_引数または_-v -E -T_引数を使用して実行できます(ただし、他のバージョンのcatについては知りません)。また、_-n_を使用して各行の先頭に行番号を付加します(ここでも、GNU以外のバージョンでこれが可能な場合はIDK)。

Catのもう1つの利点は、複数のファイルを簡単に読み取れることです。これを行うには、単に_cat file1 file2 file3_と入力します。シェルで同じことを行うと、注意が必要なループが同じ結果を得る可能性が最も高いですが、物事はトリッキーになります。そうは言っても、そのような単純な代替手段が存在する場合、本当にそのようなループを書くために時間をかけたいですか?私はしません!

Catは事前にコンパイルされたプログラムであるため、catでファイルを読み取ると、シェルよりもCPUの使用量が少なくなります(明らかな例外は、組み込みのcatを持つシェルです)。大きなファイルのグループを読み取る場合、これが明らかになる可能性がありますが、自分のマシンではこれを行ったことがないため、確信が持てません。

Catコマンドは、コマンドが標準入力を受け入れないように強制する場合にも役立ちます。以下を検討してください。

_echo 8 | sleep_

数字「8」は、標準入力を受け入れることを意図したものではないため、「sleep」コマンドでは受け入れられません。したがって、スリープはその入力を無視し、引数の欠如について不平を言って終了します。ただし、次のように入力した場合:

echo 8 | sleep $(cat)

多くのシェルはこれを_sleep 8_に拡張し、スリープは終了する前に8秒間待機します。 sshでも同様のことができます。

_command | ssh 1.2.3.4 'cat >> example-file'_

このコマンドは、1.2.3.4のアドレスを持つマシンにexample-fileを追加し、「command」から出力されたものを含みます。

そして、それは(おそらく)表面を引っ掻いているだけです。猫がもっと便利な例を見つけたらきっと見つけることができると思いますが、この投稿は十分長いです。だから、私はこれを言って結論を出します:これらのシナリオすべて(および他のいくつか)を予想するようにシェルに要求することは、実際には実行可能ではありません。

6
TSJNachos117

ユーザーがcat$PATH POSIX catとは異なります(ただし、どこかで何かをログに記録する可能性があるバリアント)。その場合、シェルで削除する必要はありません。

PATHは動的に変化する可能性があり、その場合catはあなたが信じているものとは異なります。あなたが夢見る最適化を行うシェルを書くのはかなり難しいでしょう。

また、実際には、catは非常に高速なプログラムです。それを避けるための実用的な理由はほとんどありません(美学を除く)。

FOSDEM2018のYann Regis-Gianasによる優れた Parsing POSIX [s] hell トークも参照してください。それはあなたがシェルであなたが夢見ていることをしようとしないようにする他の正当な理由を与えます。

パフォーマンスが実際にシェルの問題である場合、誰かが高度なプログラム全体のコンパイラ最適化、静的ソースコード分析、ジャストインタイムコンパイル技術を使用するシェルを提案します(これら3つのドメインすべてに数十年の進歩と科学的出版物があり、専用です) [〜#〜] sigplan [〜#〜] )などの会議悲しいことに、興味深い研究トピックとしてさえ、それは現在研究機関やベンチャーキャピタリストから資金提供を受けていません。私はそれが単に努力する価値がないと推定しています。言い換えると、シェルを最適化するための重要な市場はおそらくありません。このような研究に費やす資金が50万ユーロある場合、簡単に誰かを見つけることができ、価値のある結果が得られると思います。

実用的な側面では、パフォーマンスを改善するために、より優れたスクリプト言語(Python、AWK、Guileなど)で小さな(100行以下の)シェルスクリプトを書き直すことが一般的に行われます。また、(多くのソフトウェアエンジニアリング上の理由から)大規模なシェルスクリプトを記述することは妥当ではありません。100行を超えるシェルスクリプトを記述する場合は、読みやすさとメンテナンス上の理由から、より適切な言語で書き直すことを検討する必要があります。 :プログラミング言語として、シェルは非常に貧弱です。ただし、大きな生成シェルスクリプトが多数ありますが、それには十分な理由があります(たとえば、GNU autoconf生成configureスクリプト)。

巨大なテキストファイルについては、それらをcatsingle引数として渡すことはお勧めできません。ほとんどのシステム管理者は、(シェルスクリプトが1分以上かかる場合)実行するには、最適化を検討し始めます)。大きなギガバイトファイルの場合、cat決してそれらを処理するための優れたツールではありません。

@Kusalanandaの回答(および@alephzeroコメント)に追加すると、猫は何でもかまいません。

alias cat='gcc -c'
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

または

echo 'echo 1' > /usr/bin/cat
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

システム上のcat(それ自体)または/ usr/bin/catが実際に連結ツールのcatである理由はありません。

2
Rob

猫の2つの「役に立たない」用途:

sort file.txt | cat header.txt - footer.txt | less

...ここでcatは、ファイルとパイプ入力を混在させるために使用されます。

find . -name '*.info' -type f | sh -c 'xargs cat' | sort

...ここでxargsは、事実上無限の数のファイル名を受け入れ、catを必要なだけ実行して、1つのストリームのように動作させることができます。したがって、これはxargs sortを直接使用しない場合に大きなファイルリストで機能します。

1
tasket