web-dev-qa-db-ja.com

パイプを安全かつ順次に実行する方法は?

Linuxでは、パイプを実行することは可能ですか?

_cmd1 | cmd2
_

そのような方法で:

  1. _cmd2_は、_cmd1_が完全に終了するまで実行を開始せず、

  2. _cmd1_にエラーがある場合、_cmd2_はまったく実行されず、パイプの終了ステータスは_cmd1_の終了ステータスになります。

例を挙げると、このパイプの作り方は次のとおりです。

_false | echo ok
_

何も出力せず、ゼロ以外のステータスを返しますか?


失敗したソリューション1

_set -o pipefail
_

パイプにはゼロ以外の終了ステータスがありますが、_cmd2_が失敗しても、_cmd1_は引き続き実行されます。

失敗したソリューション2

_cmd1 && cmd2
_

これはパイプではありません。 I/Oリダイレクトはありません。

失敗したソリューション3

_mkfifo /tmp/fifo
cmd1 > /tmp/fifo && cmd2 < /tmp/fifo
_

ブロックします。

最適ではないソリューション

_touch /tmp/file
cmd1 > /tmp/file && cmd2 < /tmp/file
_

これは機能しているようです。しかし、いくつかの欠点があります。

  1. I/Oが遅いディスクにデータを書き込みます。 (確かにtmpfsを使用できますが、それは追加のシステム要件です)。

  2. 一時ファイル名は慎重に選択する必要があります。そうしないと、既存のファイルが誤って上書きされる可能性があります。 mktempが役立つ場合がありますが、名前のないパイプを使用すると、命名の手間を完全に節約できます。

  3. 一時ファイルが存在するファイルシステムは、データ全体を保持するのに十分な大きさではない可能性があります。

  4. 一時ファイルは自動クリーンアップされません。

3
Cyker

_cmd1_の出力のサイズはわかりませんが、パイプ 制限されたバッファサイズ です。その量のデータがパイプに書き込まれると、それ以降の書き込みは、誰かがパイプを読み取るまでブロックされます(失敗したソリューション3の種類)。

ブロックしないことを保証するメカニズムを使用する必要があります。非常に大きなデータの場合は、一時ファイルを使用します。それ以外の場合、データをメモリに保持する余裕がある場合(結局のところ、パイプを使用したアイデアでした)、次のように使用します。

_result=$(cmd1) && cmd2 < <(printf '%s' "$result")
unset result
_

ここで、_cmd1_の結果は変数resultに格納されます。 _cmd1_が成功すると、_cmd2_が実行され、resultのデータが提供されます。最後に、resultが設定解除され、関連するメモリが解放されます。

注:以前は、ヒア文字列(_<<< "$result"_)を使用して_cmd2_にデータを供給していましたが、StéphaneChazelasがbashが一時ファイルを作成することを確認しました。 。

コメントの質問への回答:

  • はい、コマンドは連鎖させることができますアドリブ

    _result=$(cmd1) \
    && result=$(cmd2 < <(printf '%s' "$result")) \
    && result=$(cmd3 < <(printf '%s' "$result")) \
    ...
    && cmdN < <(printf '%s' "$result")
    unset result
    _
  • いいえ、上記のソリューションは次の理由でバイナリデータには適していません。

    1. コマンド置換$(...)は、末尾の改行をすべて削除します。
    2. コマンド置換の結果のNUL文字(_\0_)の動作は指定されていません(たとえば、Bashはそれらを破棄します)。
  • はい、バイナリデータに関するこれらすべての問題を回避するには、_base64_(またはuuencode、またはNUL文字と末尾の改行のみを処理する自家製のエンコーダ)のようなエンコーダーを使用できます。

    _result=$(cmd1 > >(base64)) && cmd2 < <(printf '%s' "$result" | base64 -d)
    unset result
    _

    ここでは、_cmd1_の終了値をそのまま維持するために、プロセス置換(>(...))を使用する必要がありました。

とはいえ、データがディスクに書き込まれないようにするためだけに、これもかなり面倒なようです。中間の一時ファイルがより良い解決策です。 ステファンの答え を参照してください。これは、それに関するほとんどの懸念に対処します。

4
xhienne

パイプコマンドの要点は、コマンドを同時に実行して、一方が他方の出力を読み取ることです。それらを順番に実行する場合、および配管のメタファーを保持する場合は、最初のコマンドの出力をバケットにパイプして(保存して)、バケットを空にして他のコマンドに入れる必要があります。

ただし、パイプでそれを行うということは、最初のコマンド用に2つのプロセス(コマンドとパイプのもう一方の端から出力を読み取ってバケットに格納する別のプロセス)と、2番目のコマンド用に2つ(1つはバケットを一方の端に空にする)を持つことを意味しますコマンドがもう一方の端からそれを読み取るためのパイプの)。

バケットには、メモリまたはファイルシステムのいずれかが必要です。メモリは適切に拡張できないため、パイプが必要です。ファイルシステムはもっと理にかなっています。それが/tmpはです。一時ファイルが削除されてからかなり後になるまでデータがフラッシュされない可能性があるため、ディスクにはデータが表示されない可能性が高いことに注意してください。そうでなければ、そもそもデータが大きすぎてメモリに収まらなかったはずです。

一時ファイルは常にシェルで使用されることに注意してください。ほとんどのシェルでは、ヒアドキュメントとヒア文字列は一時ファイルで実装されています。

に:

cat << EOF
foo
EOF

ほとんどのシェルは一時ファイルを作成し、書き込みと読み取りのために開き、削除し、fooで埋めてから、読み取り用に開いたfdから複製されたstdinを使用してcatを実行します。ファイルは、いっぱいになる前に削除されます(これにより、書き込まれた内容が停電に耐える必要がないという手がかりをシステムに与えます)。

あなたはここで同じことをすることができます:

tmp=$(mktemp) && {
  rm -f -- "$tmp" &&
    cmd1 >&3 3>&- 4<&- &&
    cmd2 <&4 4<&- 3>&-
} 3> "$tmp" 4< "$tmp"

そうすれば、ファイルが最初から削除されるので、クリーンアップについて心配する必要はありません。バケットにデータを入れたり出したりするための追加のプロセスは必要ありません、cmd1およびcmd2自分でやる。

出力をメモリに保存したい場合は、そのためにシェルを使用することはお勧めできません。 zsh以外の最初のシェルは、変数に任意のデータを格納できません。何らかの形式のエンコーディングを使用する必要があります。そして、そのデータを渡すために、ヒアドキュメントまたはヒア文字列を使用するときにディスクに書き込まない場合、データをメモリに数回複製することになります。

たとえば、代わりにPerlを使用できます。

 Perl -MPOSIX -e '
   sub status() {return WIFEXITED($?) ? WEXITSTATUS($?) : WTERMSIG($?) | 128}
   $/ = undef;
   open A, "-|", "cmd1" or die "open A: $!\n";
   $out = <A>;
   close A;
   $status = status;
   exit $status if $status != 0;

   open B, "|-", "cmd2" or die "open B: $!\n";
   print B $out;
   close B;
   exit status'
2

これは率直に言ってひどいバージョンで、 moreutils のさまざまなツールをつなぎ合わせています。

chronic sh -c '! { echo 123 ; false ; }' | mispipe 'ifne -n false' 'ifne echo ok'

それでもまだ十分ではありません。失敗した場合は1を返し、そうでない場合は0を返します。ただし、最初のコマンドが成功しない限り2番目のコマンドを開始せず、最初のコマンドが機能したかどうかに応じて失敗または成功したコードを返し、ファイルを使用しません。

より一般的なバージョンは次のとおりです。

chronic sh -c '! '"$CMD1" | mispipe 'ifne -n false' "ifne $CMD2"

これにより、3つのmoreutilsツールがまとめられます。

  • chronicは、失敗しない限り、静かにコマンドを実行します。この場合、シェルを実行して最初のコマンドを実行し、成功/失敗の結果を反転できるようにします。コマンドは静かに実行されますif失敗し、成功した場合は最後に出力を出力します。
  • mispipeは2つのコマンドをパイプでつなぎ、最初のコマンドの終了ステータスを返します。これは、set -o pipefailの効果に似ています。コマンドは文字列として提供されるため、区別することができます。
  • ifneは、標準入力が空でない場合、または-nで空の場合にプログラムを実行します。 2回使用しています。

    • 1つ目はifne -n falseです。これはfalseを実行し、入力が空である場合に終了コードとして使用します(chronicがそれを食べたことを意味し、cmd1が失敗したことを意味します)。

      入力が空でない場合、falseは実行されず、catのように入力が渡され、0で終了します。出力は、mispipeによって次のコマンドにパイプされます。

    • 2番目はifne cmd2です。これは、入力が空でない場合にcmd2を実行します。その入力はifne -n falseの出力であり、コマンドが成功したときに発生するchronicの出力が空でない場合に正確に空になりません。

      入力が空の場合、cmd2は実行されず、ifneはゼロを終了します。 mispipeはとにかく終了値を破棄します。


このアプローチには(少なくとも)2つの欠陥が残っています。

  1. 前述のように、実際の終了コードcmd1が失われ、ブール値のtrue/falseになります。終了コードに意味がある場合、それは失われます。 shコマンドでコードをファイルに保存し、必要に応じて後で再ロード(ifne -n sh -c 'read code <FILENAME ; rm -f FILENAME; exit $code'など)することができます。
  2. cmd1が出力なしで成功する可能性がある場合、とにかくすべてが崩壊します。

さらに、もちろん、それはパイプでまとめられたかなりまれなコマンドであり、明白ではない意味で、慎重に引用されています。

1
Michael Homer

まず、falseは標準出力に何も出力せず、echoは標準入力から読み取らないため、例false | echo okは無意味です。これに対する「解決策」はfalse && echo okです。

cmd1 && cmd2

これはcmd1を実行し、cmd2が正常に実行を完了するまでcmd1を開始しません。

次のようなパイプラインで

cmd1 | cmd2

2つのコマンドは常に同時に開始されます(これは「失敗したソリューション1」で気付くものです)。それらを同期するのは、cmd2の出力からのcmd1の読み取りです。パイプラインは、あるプログラムからの出力を、同時に実行されている別のプログラムの入力に渡す方法です。

cmd1cmd2が読み取るものを出力していることをシミュレートするが、同時実行性を取り除くには、cmd1からの出力をcmd2の一時ファイルに保存する必要があります。読み取り:

cmd1 >outfile && cmd2 <outfile

一時ファイルは次のように処理できます。

trap 'rm -f "$tmpfile"' EXIT
tmpfile=$(mktemp)

cmd1 >"$tmpfile" && cmd2 <"$tmpfile"

これにより、シェルを終了するときにトリガーされるトラップが設定されます。トラップは一時ファイルを削除します。

メモリファイルシステムに$TMPDIRがある場合、ディスクへの書き込みに対してI/Oペナルティは発生しません。

ファイルのサイズが心配な場合は、何があってもディスクに保存する必要があります(パイプも内容を保持できません。これは、「失敗したソリューション3」で気づくものです)。 。


xhienneのソリューション Bashの場合:

result=$(cmd1) && cmd2 <<< "$result"
unset result

これは、結果が空行で終わらないテキストの場合は機能しますが、nullバイトが含まれている場合は失敗します(これらはbashによって破棄されます)。

これを軽減するために、結果をbase64エンコードすることができます。

set -o pipefail # ksh/zsh/bash
result=$( cmd1 | base64 ) && base64 -d <<<"$result" | cmd2
unset result

これは、特に結果が大きい場合($resultのbase64エンコーディングはバイナリより3分の1大きくなります)、メモリとCPUの両方の使用量の観点からひどいの考えです。バイナリ結果をディスクに書き込み、そこから読み取る方がはるかに優れています。

また、bash一時ファイルを使用して<<<を実装することに注意してください。

1
Kusalananda