- sh(1)のパイプの中で左辺のコマンドの戻り値を
${PIPESTATUS[0]}
で参照することはできない。 - grep(1)でマッチの結果にかかわらず、すべての行を標準出力へ出力することはできない。
- tee(1)でパイプの入力を複数のファイルディスクリプタに複製して出力することはできない。
何をやろうとしたかというと、ウイルススキャンの結果を取得したかっただけです。
ユーザから渡されたファイルを、処理の前にウイルススキャンする(スキャン陽性だったらユーザにメッセージを返す)処理があったりします。ウイルススキャンソフトにプログラミング言語用のAPIがあればいいんですが、ない場合はコマンドラインのスキャナを呼び出すことになります。こんな感じ。
/path/to/scanner /path/to/tmp/tmpXXXXXX
ここでtmpXXXXXXは
ウイルススキャン毎に作られる一時ディレクトリと思ってください。
標準出力をsyslogに吐きたい
scanner
は見つかったマルウェアの名前を標準出力に吐きます。なので、その内容はログに取っておきたい。syslogを使うとして、こんな感じにします。
/path/to/scanner /path/to/tmp/tmpXXXXXX | logger -t scanner
陽性・陰性を戻り値で取得したい
ただ、scanner
は戻り値でスキャンの陽性・陰性を返します。上記のままだとscanner
コマンドでなくてパイプライン全体の戻り値が返ってしまうので、PIPESTATUS
変数を使ってscanner
コマンドの戻り値だけを参照します。
/path/to/scanner /path/to/tmp/tmpXXXXXX | logger -t scanner; exit ${PIPESTATUS[0]}
標準出力の内容を引っ掛けたい
さて、ここでひとつ困ったことがあって、このscanner
コマンドは指定されたディレクトリにアクセスできないと「マルウェアの含まれるファイルが1つもなかった=陰性」と判断する仕様になっています。tmpXXXXXX
ディレクトリのパーミッションは文書管理システムが内部で一時ディレクトリを作るときに指定しているので、アクセスできないというのはシステムにバグがある。なので、この状況を引っ掛けてエラーなり何なり返したい。
scanner
は標準出力にスキャンしたファイル数を出力します。指定されたディレクトリにアクセスできなかった場合は「files: 0
」という文字列が標準出力に出るので、これを引っかけましょう。
/path/to/scanner /path/to/tmp/tmpXXXXXX | grep -F 'files: 0' | logger -t scanner; exit ${PIPESTATUS[0]}
困ったこと
さて、すると困ったことに、grep -F 'files: 0'
の結果はどこにも返せません。それに、syslogには標準出力に'files: 0'
があるときしかログが残らなくなっちゃいます。
まずは「scanner
コマンドの標準出力に'files: 0'
という内容があった場合は専用の戻り値を返したい。ただしそのような内容がなかった場合はscanner
コマンドの戻り値をそのまま返したい」という状況を解決したいわけです。ただ、パイプラインの中で、パイプラインの最初に実行しているscanner
コマンドの戻り値は参照できません。${PIPESTATUS[0]}
を参照すると、パイプラインの左辺ではなく、前回実行したパイプラインの戻り値を参照していまいます。*1 こんな感じ。
$ echo -n $ sh -c "exit 1" | echo ${PIPESTATUS[0]} 0 $ sh -c "exit 2" | echo ${PIPESTATUS[0]} 1 $ sh -c "exit 3" | echo ${PIPESTATUS[0]} 2
それから「grep(1)の結果がどうあれ、grep(1)に入ってきた内容をそのままlogger(1)コマンドに渡したい」という状況も解決したい。PowerShellだと-PassThru
オプションを持たせたcmdletの実装事例がありましたが、grep(1)には同様のオプションはないようです。
パイプの入力をtee(1)で複数のファイルディスクリプタに複製して出力したりできないかな?というのも調べましたが、そんなオプションはないみたい。
*1:パイプの右辺のプログラムの実行中は左辺のプログラムもまだ動いているから当たり前といえば当たり前ですね