bash で標準出力と標準エラーを 2つのコマンドに出し分ける

Pocket

本記事は シェルスクリプトのカレンダー | Advent Calendar 2021 - Qiita 17日目の記事だ。

殆どカレンダーが埋まってなかったので、思いついたネタで埋めちゃえ埋めちゃえ。

今回は、 bash 系列 (bash, zsh 等) の プロセス置換 (process substitution) 機能の話だ。

このプロセス置換は POSIX 互換の機能では無いため、以降の例は ash 系列 (busybox hush (ash), dash 等) では利用できない。

bash のプロセス置換

bash 系の プロセス置換 という機能と、 tee コマンドを組み合わせて、 同時に複数のコマンドにパイプできるらしい。

詳しい説明を読んでも、正直頭がさっぱり追いつかないが…

要は、 引数部分でファイルを指定するべき場所で、コマンドの標準入出力を代用できる機能ということか。

例えば、 "cat <(ls ./a) <(ls ./b)" とすれば、 "ls ./a" の内容と "ls ./b" の内容が連結されて出力されるし、
"command0 | tee >(command1) | command2" とすれば、 command0 の標準出力が、 command1 の標準入力と、 command2 の標準入力両方に渡される。
また、「ファイルの読み書きの代替」となるため、 "command0 2> >(command1)" のようにリダイレクト先のファイル名の替わりにプロセス置換を使えば、標準エラーだけを command1 の標準入力に渡すことができる。

ちょっとその尖った使い所を考えてみる。


例えばこんな、標準出力と標準エラーを吐き出すスクリプトがあったとしよう。

$ cat <<'EOF' > ./testecho.sh
#!/bin/bash
echo "stdout1" >&1
echo "stderr1" >&2
echo "stdout2" >&1
echo "stderr2" >&2
EOF
$ chmod u+x ./testecho.sh

このスクリプトを実行し、 標準エラーだけ command2 に渡して、 標準出力は command1 に渡したい場合、 プロセス置換を使うと以下のようにできる。

$ ./testecho.sh 2> >(command2) | command1
# -> 各コマンドの入力
#   command1:
#     stdout1
#     stdout2
#   command2:
#     stderr1
#     stderr2

ここで更に、 標準出力と標準エラーの両方を command2 に渡して、 標準出力だけを command1 に渡したい場合、 ちょっと複雑になるが以下のようにして実現できる。

$ ./testecho.sh 2>&1 > >(tee >(command1)) | command2
# -> 各コマンドの入力
#   command1:
#     stdout1
#     stdout2
#   command2:
#     stdout1
#     stderr1
#     stderr2
#     stdout2

少し複雑なので分解して考えてみよう。

  1. まずは testecho.sh のリダイレクト部分 (赤枠) を考える。
    1. リダイレクトを複数並べる場合は左から評価されるが、後ろに書いたリダイレクトで上書きされるような動きをするので、右から順番にリダイレクトされると考えるとわかりやすい。 ^1
      このため、まずは (黄色下線部) の部分を見てみよう。
      この部分は、標準出力をファイルにリダイレクトしているだけ (つまり、只の "> ファイルパス") の表記だ。
      ただ、ファイルパスの代わりにプロセス置換 (">(cmd_list)") が使われており、 標準出力が tee の標準入力へ書き込まれている。

      1. さらに、 tee のファイル出力もまた、プロセス置換を使って command1 の標準入力に渡される。
        結果的に、 testecho.sh の標準出力だけが、 command1 の標準入力に渡されることになる。
      2. 次に、 tee の標準出力のほう、 これは ./testecho.sh を実行した標準出力に戻ってくる。
    2. さて、 testecho.sh のリダイレクトに話を戻すと、 その次のリダイレクト (水色下線部) の "2>&1" によって、 tee の標準出力と ./testecho.sh の標準エラーが、標準出力側に統合される。
  2. 最後に、 その統合された標準出力が、 パイプで command2 に渡される。
    結果的に、 testecho.sh の標準出力と標準エラーの両方が、 command2 の標準入力に渡されることになる。

ちなみに、 ./testecho.sh の標準出力と標準エラーの出力速度が早いと、上記の出力例のように command2 に渡される標準出力と標準エラーが順不同になってしまう。

なお、以下のようにやっても同じ結果になるはずだ。

$ ./testecho.sh > >(command2) 2>&1 > >(tee >(command1))

実用例

例えば、以下のようにすると、 コマンドの標準出力だけメールを出しつつ、メールの内容と 標準エラーの両方を journal に書き込む事ができる。

./testecho.sh 2>&1 > >(tee >(/usr/sbin/sendmail notify@example.com)) | /usr/bin/systemd-cat

ただ、どうせ journal に記録するなら、 -t オプションを使って、以下の 3 つを識別子で分けて記録したほうが良いかもしれない。

  • ./testecho.sh の標準エラー
  • ./testecho.sh の標準出力
  • sendmail の標準出力&エラー
./testecho.sh 2> >(/usr/bin/systemd-cat -t cmderr) > >(tee >(/usr/bin/systemd-cat -t mailout /usr/sbin/sendmail -v notify@example.com) | /usr/bin/systemd-cat -t cmdout)

うーん、 ここまで来ると初見で動作を理解できる気がしない。

参考:

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください