2007年07月13日

シェルスクリプトで標準入力を加工する際の注意点

はてなブックマークに登録

シェルスクリプトで標準入力を加工する際に若干ハマリかけたので、今回はそのお話をしたいと思います。
みなさんのシェルスクリプトライフの一助になれば幸いです。

標準入力を加工するシェル関数、例えば、

  • 入力を本文とするメールを送るシェル関数
  • ログを入力として受け取り、集計や解析をするシェル関数

を書く場合、みなさんどういう風に書いているでしょうか。
私は今までこんな風に書いていました…

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)を検査する方法です。

マニュアルを紐解いてみると、

-t file_descriptor
True if the file whose file descriptor number is file_descriptor is open and is associated with a terminal.
The Single UNIX ® Specification, Version 2

と書かれていました。

この-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オプションをつかって、標準入力がパイプかどうかを検査します。

これまた先ほどのマニュアルを紐解いてみると、

-p file
True if file is a named pipe (FIFO).
The Single UNIX ® Specification, Version 2

と書かれていました。

この-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つはおすすめしませんが…)を紹介しました。
ほかにもステキな方法がありましたら、コメントやトラックバックで教えてもらえるとうれしいです!

klab_gijutsu2 at 08:00│Comments(1)TrackBack(1)

トラックバックURL

この記事へのトラックバック

シェルスクリプトで標準入力を加工する際に若干ハマリかけたので、今回はそのお話をしたいと思います。 みなさんのシェルスクリプトライフの一助になれば幸いです。 標準入力を加工するシェル関数、例えば、 入力を本文とするメールを送るシェル関数 ログを入力として受け取...

この記事へのコメント

1. Posted by fumiyas   2007年07月15日 22:22
最初に挙げていらっしゃるような動作はしないですよ。以下のように問題ないです。

$ 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` を単独で呼んだ場合にシェルの標準入力を待つのは当たり前だし、何が問題なのかわかりませんでした。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント