シェルスクリプトで標準入力を加工する際の注意点
シェルスクリプトで標準入力を加工する際に若干ハマリかけたので、今回はそのお話をしたいと思います。
みなさんのシェルスクリプトライフの一助になれば幸いです。
標準入力を加工するシェル関数、例えば、
- 入力を本文とするメールを送るシェル関数
- ログを入力として受け取り、集計や解析をするシェル関数
を書く場合、みなさんどういう風に書いているでしょうか。
私は今までこんな風に書いていました…
send_mail() { from='sender@example.org' to='receiver@example.org' { echo "From: ${from}" echo "To: ${to}" echo "Subject: test" echo cat - } | qmail-inject -A -f${from} } make_mail_body | send_mail
これの何が問題かというと、もし、send_mailの入力を提供するmake_mail_body関数が何も出力しない場合、send_mailのcat -のところでえんえんと入力を待つことになってしまいます。
つまり、send_mail関数が返らず、ここでスクリプトが止まってしまいます。
cat -のところで止まるケースがあるのは認識していたのですが、うまい回避策が思いつかず、いままで放置してしまっていました。
が、いいかげんどうにかせねばなーと思い、社内のIRCチャンネルで質問してみました。
19:38:34 <#tech> (CurrySan) 質問す:シェルスクリプトで、標準入力を 加工したくてkazoerukun() { cat - | wc } とかすると、標準入力が ない場合に ブロックして戻ってこないわけなんすけど、どやって 回避するのがいいですかねー? ※個人名は仮名です
というわけで、以下では教えてもらった対処法を3つ紹介します。
(サンプルスクリプトでは、メールを送るのではなく行数を数えるようにしています)
readのタイムアウトを使う
まず1つめはreadの-tオプションでタイムアウトを指定する方法です。
read_timeout() { while read -t 3 line; do echo $line done | wc -l }
readはシェル自身が持つ組み込みコマンドなので、-tオプションをサポートしているかどうかはシェルに依存します。ですのでこの方法はちょっといまいちです。
さらに、入力がなかった場合はタイムアウト秒数だけ無駄に待たされるので、やっぱりこの方法はいまいちです。
ですのでこの方法ではなく、次に紹介する方法をおすすめします。
testの-tを使う
2つめはtestコマンドの-tオプションをつかって、標準入力(ファイル記述子0)を検査する方法です。
マニュアルを紐解いてみると、
The Single UNIX ® Specification, Version 2
- -t file_descriptor
- True if the file whose file descriptor number is file_descriptor is open and is associated with a terminal.
と書かれていました。
この-tを使って、標準入力(0)が、対話的なターミナルと関連づけられている場合はechoでダミーの出力をし、そうでない場合はcat -で標準入力をそのまま出力してあげればよさそうです。
fd_opened() { if [ -t 0 ]; then echo else cat - fi | wc -l } printf "foo\nbar\nbaz\n" | fd_opened # 3 lines fd_opened # 0 line
といったコードで期待通りの動作をしました。
testの-pを使う
最後もtestコマンドなのですが、今度は-pオプションをつかって、標準入力がパイプかどうかを検査します。
これまた先ほどのマニュアルを紐解いてみると、
The Single UNIX ® Specification, Version 2
- -p file
- True if file is a named pipe (FIFO).
と書かれていました。
この-pを使って、標準入力が、名前付きパイプならばcat -でそれを読んでそのまま出力し、そうでない場合はダミーのechoをすればOKです。
is_pipe() { if [ -p /dev/stdin ]; then # if [ -p /dev/fd/0 ]; then # if [ -p /proc/self/fd/0 ]; then cat - else echo fi | wc -l } printf "foo\nbar\nbaz\n" | is_pipe # 3 lines is_pipe # 0 line
コメントアウトされている部分がありますが、手元の環境(Linux)ではどれでも期待した動作をしました。
今回はシェルスクリプトで、(あるかどうかわからない)標準入力を処理する方法を3つ(うち1つはおすすめしませんが…)を紹介しました。
ほかにもステキな方法がありましたら、コメントやトラックバックで教えてもらえるとうれしいです!
トラックバックURL
この記事へのトラックバック
この記事へのコメント
$ cat ~/t
#!/bin/sh
kazoerukun() {
cat - |wc
}
true |kazoerukun
$ bash ~/t
0 0 0
$ dash ~/t
0 0 0
$ busybox sh ~/t
0 0 0
標準入力がないならブロックするわけないですし、`kazoerukun` を単独で呼んだ場合にシェルの標準入力を待つのは当たり前だし、何が問題なのかわかりませんでした。