最も簡単な方法:「偽」の名前のないパイプ

最も簡単な方法:「偽」の名前のないパイプ

適切な同期を使用して可能な競合を制御することに加えて、bashはデータソースを複数のパイプラインに同時に供給し、後ですべての出力を共通のデータシンクに収集できます。

たとえば、電子メールを送信する前に別のプロセスを介して電子メールのヘッダーと本文を別々に前処理したい場合は、次のようにします。

cat email.txt \
  | { tee >(sed -ne '1,/^$/p' | process_header >&3) \
  | sed -e '1,/^$/d' | process_body; } 3>&1 \
| sendmail -oi -- [email protected]

これを考慮して、これらのパイプの1つの出力を使用して、他のパイプの1つのコマンドラインまたは最終データシンクに表示する方法を探しています。これまで私が達成できたのは、名前付きパイプと2つの入力ソースを許可するxargsに-aオプションを使用することです。

たとえば、電子メール本文の行数を電子メールの件名行に自動的に追加するには、次のようにします。

cat email.txt \
  | { tee >(sed -ne '1,/^$/p' >&3; : branch off the header) \
  | sed -e '1,/^$/d' \
    | tee >(wc -l >~/.fifo); : number of body lines into pipe; } 3>&1 \
| xargs -I% -a ~/.fifo sed -e '1,/^$/{/^Subject:/Is/$/ (%)/}' \
| sendmail ...

xargs -I% -a /dev/fd/4 4<~/.fifo ...以下も参照してください。)この例では、ファイルは~/.fifomkfifo ~/.fifo

ただし、最初の例と同様に、ファイル記述子とリダイレクトのみを使用して名前付きパイプなしでこれを実行しようとすると

cat email.txt \
  | { tee >(sed -ne '1,/^$/p' >&3) \
  | sed -e '1,/^$/d' | tee >(wc -l >&4); } 3>&1 \
| xargs -I% -a /dev/fd/4 sed -e '1,/^$/{/^Subject:/Is/$/ (%)/}' \
| sendmail ...

これによりエラーが発生します。

xargs: Cannot open input file ‘/dev/fd/4’: No such file or directory
bash: 4: Bad file descriptor

[修正する:呼び出し内で置き換えることも-a /dev/fd/4機能しません。存在しないという苦情は他のエラーに置き換えられます。出力()のfd 4が入力()のfd 4に接続されていないようです。 ]-a <(cat <&4)xargs/dev/fd/4Bad file descriptor>&4<&4/dev/fd/4

リダイレクトとプロセス拡張を賢く組み合わせて名前付きパイプを削除する方法はありますか?もちろん、次のようにデータソースを複数回指定する必要はありません。

nol="$(sed -e '1,/^$/d' email.txt | wc -l)"
sed -e "1,/^$/{/^Subject:/Is/$/ ($nol)/}" email.txt | sendmail ...

答え1

コマンドのエラーは、fd 4がまったく開いていないためです。

実際、2つの「無効なファイル記述子」メッセージが表示されます。 1つは(または)wc -lから1つです。cat <&4xargs -a /dev/fd/4

fd 4を開くには名前のないパイプが必要ですが、Bashで名前のないパイプを持つ唯一の公式の方法は、実際にコマンドを使用することですcoproc

しかし、特定のユースケースには素晴らしいショートカットがあります。

最も簡単な方法:「偽」の名前のないパイプ

このトリックはBash v5に文書化されていませんが、少なくともv4.3では動作します(まだv5をテストできません)。

一緒に使用すると、それをサポートするシステムで任意の「名前なし」パイプを取得できるいくつかの標準イディオムを利用します。合格」無名パイプ「私の言葉は」まず、または同等のコマンドを介してpファイルシステムにファイルタイプのFIFOを生成する必要はありません。mkfifo". (名前が指定されていないパイプのこの定義は正確ではありませんが、コマンドシェルを使用すると実際に意味があると言うことができます。)

これらの「名前なし」パイプのユースケースの例は次のとおりです。

cat email.txt | ( : {pipe}<> <(:) ; tee >(sed -e '1,/^$/d' | wc -l >&${pipe}) | xargs -I% -a <({ read count ; echo $count; } <&${pipe}) sed -e '1,/^$/{/^Subject:/Is/$/ (%)/}' )

上記のコマンドラインは、状況に応じて予想される結果を生成する必要があります。

故障説明: (明確に説明すると、コピーして貼り付けると動作しません)

cat email.txt | \  # pipe data to ...
    ( \  # a compound statement, which ...
     : {pipe}<> <(:) ; \ # ... first opens the unnamed pipe in RW mode and put its fd into the (arbitrary) variable ${pipe}
    tee \ # then mirrors the data from main stdin to ...
        >( \ # the side processing of main input ...
            sed -e '1,/^$/d' | wc -l \ # ... which counts the body lines sending the result ...
            >&${pipe} \ # ... to the unnamed pipe
         ) \
    | \ # the tee also pipes all main input to ...
    xargs -I% -a \ # an xargs that reads iterative lines from ...
        <({ read count ; echo $count ; } <&${pipe}) \ # a compound command that reads the one-single line (being the count provided by wc) from ${pipe} fd, and echoes it back to xargs -a
        sed -e \ # that finally executes the sed command which looks for Subject: line in header part
        '/1,^$/{/^Subject:/Is/$/ (%)/}' ; \ # to append it with the count number
    )

いくつかの追加の注意:

  • 1つは読み取り側用で、もう1つは書き込み側用の一般的なパイプペアを開く方法が見つからなかったため、名前のないパイプRWを開く必要があります。
  • つまり、読み取り部分にデータが入っていないという一般的なEOFイベント通知はあり得ず、他の方法で直接実行する必要がありますが、ここでは興味のある行が1つしかないという事実を活用できます。だから一つだけあってもread十分です。逆に、サイドチャネルから複数行を読み取る必要がある場合は、出力の末尾に追加された単純なEOF文字列など、ある種の帯域内EOF通知が必要ですxargs -a。読み取りを削除します。これは完全に可能ですが、コマンドライン入力時間はかなり長いです。帯域内のEOF文字列を削除することも可能ですが、より複雑です。
  • 名前のないパイプの管理は完全にあなた次第であるため、次のように明示的にexec {pipe}<&-閉じる必要があります。この例では、fdが子プロセスで作成されたため、これを行う必要はありません。

完全性を期すために、これには、coproc相互接続されたファイル記述子の一般的なペアを介して真の名前のないパイプを提供する同等のバージョンが使用されます。

名前のないパイプの正式な方法:coproc

coprocを使用する方法はいくつかありますが、あなたの場合の最良の方法は次のとおりです。

cat email.txt | (coproc cat ; : {input}<&${COPROC[0]} {output}>&${COPROC[1]} ; tee >(sed -e '1,/^$/d' | wc -l >&${output}) | xargs -I% -a <(exec cat <&${input}) sed -e '1,/^$/{/FOO/Is/$/ (%)/}' & )

故障説明: (明確に説明すると、コピーして貼り付けると動作しません)

cat email.txt | \ # pipe data to ...
    ( \ # a subcommand statement, which ...
    coproc cat ; \  # ... first spawns the coprocess, a simple cat command acting as a simple line-oriented bridge
    : {cp_output}<&${COPROC[0]} {cp_input}>&${COPROC[1]} ; \ # then copies coproc’s own fds into new ones whose number are put into (arbitrary) variables ${cp_output} and ${cp_input} 
    tee \ # and then mirrors the data from main stdin to ...
        >( \ # the side processing of main input ...
            sed -e '1,/^$/d' | wc -l \ # ... which counts the body lines sending the result ...
            >&${output} \ # ... to the (bridging) coproc
          ) \
    | \ # the tee also pipes all main input to ...
    xargs -I% -a \ # an xargs that reads iterative lines from ...
        <(exec cat <&${pipe}) \ # another cat that reads from the coproc bridging the count provided by wc, and echoes it back to xargs -a
        sed -e \ # that finally executes the sed command which looks for Subject: line in header part
        '/1,^$/{/^Subject:/Is/$/ (%)/}' ; \ # to append it with the count number
    )

もう一度メモを追加してください。

  • coprocのデータ(プロセスやfdsなど)がインタラクティブbashに漏洩しないように、サブステートメントを使用することをお勧めします(この獣をインタラクティブに実行するとします)。
  • そうでなければ、対応する coproc データ管理は完全にあなた次第であるため、または経由でexec {cp_input}<&-fds を明示的に閉じる必要があります。exec {COPROC[1]}<&-
  • coprocではすべてのコマンドを使用できますが、2つのfdで構成される単純なブリッジを使用することが便利な一般的な解決策である可能性があることを常に知っていますcat。ただし、両方のワーカープロセスのいずれかをcoproc自体に含めることで、パフォーマンスを最適化できます。たとえば、コマンドライン全体を多数並べ替える必要があります。
  • Bash v4のドキュメントによると、Bashは一度に1つのcoprocのみをサポートしています。
  • しかし、少なくともv4.3では、明示的な警告にもかかわらず、より多くのcoprocを許可し、Bash v5ドキュメントには制限はありません。
  • coprocがある場合は、各coprocに明示的な名前を使用する必要があります(詳細についてはマニュアルを参照)。
  • coprocのfdは、この例で使用されているパイプとプロセスの置き換えで生き残ることができるように、任意のfdに移動/コピーする必要があります。配列は${COPROC[*]}子プロセスとしてエクスポートされず、独自のfdは常にexecで閉じられるためです。
  • xargs -a ここでは、両方の標準入力で積極的に読み取る利点を享受できます。そしてしたがって、パイプのバッファが満たされないように-aしてくださいtee。それ以外の場合は、デッドロックが発生し、これを防ぐために、より洗練された方法が必要です。

関連情報