Ruby
Perl
Bash

標準入力にデータを与えられているかどうか調べる方法

コマンドライン引数で任意個の情報を与えるスクリプトを書いたとします。

$ ./address-exists.pl foo@example.com bar@example.com
foo@example.com FOUND
bar@example.com NOT_FOUND

コマンドラインが長くならなければいいのですが、それこそ端末の一画面にすら収まらない膨大な量になったりすると、指定しづらいだけでなく bash から一行が長すぎと怒られて実行できないケースもあります。

このような場合、一行一データとして標準入力からデータ群を与えられないか考えたりもします。

xargs で解決?

もっとも、シェルの知識が豊富な方は xargs を思い出すでしょう。コマンドライン引数で任意個の情報を与えて動作するスクリプトがあれば、 xargs はほとんどの場合に最良の答えを与えてくれます

$ cat addresses.txt | xargs ./address-exists.pl
foo@example.com FOUND
bar@example.com NOT_FOUND
...

上記 addressse.txt が膨大な行であったとしても、xargs は bash に一行が長すぎと怒られないように適当に分割して実行してくれます。

$ seq 1 500 | xargs perl -E '($first, $last) = (shift, pop); say "$first .. $last";'
1 .. 500

$ seq 1 100000 | xargs perl -E '($first, $last) = (shift, pop); say "$first .. $last";'
1 .. 5000
5001 .. 10000
10001 .. 15000
15001 .. 20000
20001 .. 25000
25001 .. 30000
30001 .. 35000
35001 .. 40000
40001 .. 45000
45001 .. 50000
50001 .. 55000
55001 .. 60000
60001 .. 65000
65001 .. 70000
70001 .. 75000
75001 .. 80000
80001 .. 85000
85001 .. 90000
90001 .. 95000
95001 .. 100000

(上記は私の macOS High Sierra の bash 環境での実行結果です)

私もこれでいいかなと思ったのですが、なんとなく

  • スクリプトは実行ごとに初期化処理があってそれなりに重い場合 は xargs に複数回起動されるか考えたりするのも煩わしい
    • といっても上記だと5000個に1回の初期化処理ですが
  • ストリーム的に流れてくる標準入力を自分でハンドリングしたい
    • xargs に気を使ってもらわなくてもいい
    • パイプの左側のコマンドによって STDIN の流速が変わる場合も、それを手に取るように知りたい場合がある

といった場合もあるかなと思いました。

標準入力にデータが流されているか確認する方法

なお、「標準入力から何かデータが来ているか読んでみよう」と思って、以下のようなコードを書くと、自分がパイプの右側にいたり < ファイルリダイレクトを指定されていない場合は端末が固まります(正確にはキーボード入力待ちにななっています)。

read.pl
while (my $line = <STDIN>) {
    print $line;
}
read.rb
STDIN.each do |line|
  puts line
end

今回の場合

$ address-exists.pl

$ cat file.txt | address-exists.pl
または
$ address-exists.pl < file.txt

を区別する方法があるといいなというのが記事の本題。

これは 標準入力がパイプによって与えられているかの検査 を行うと良いです。

Perl の場合は -p 演算子

choice-argv-stdin.pl
my @addresses;
if ( @ARGV ) {
    # 引数が指定されていればそちらを採用
    @addresses = @ARGV;
} elsif ( -p STDIN ) {
    # 引数が指定されておらず、標準入力からデータが流されている場合はそちらを採用
    @addresses = <STDIN>;
    chomp @addresses;
} else {
    die "no data";
}

Ruby の場合は FileTest.pipe?(file) メソッド

choice-argv-stdin.rb
if ARGV.length > 0
    addresses = [ *ARGV ]
elsif FileTest.pipe?(STDIN)
    addresses = STDIN.readlines.map { |line| line.chomp }
else
    raise "no data"
end

です。

単純にその効力を見るには

$ perl -e 'printf "STDIN %s\n", -p STDIN ? "open" : "close";'
STDIN close
$ echo hello | perl -e 'printf "STDIN %s\n", -p STDIN ? "open" : "close";'
STDIN open
$ ruby -e 'printf "STDIN %s\n", FileTest.pipe?(STDIN) ? "open" : "close"'
STDIN close
$ echo hello | ruby -e 'printf "STDIN %s\n", FileTest.pipe?(STDIN) ? "open" : "close"'
STDIN open

といったワンライナーがわかりやすいでしょう。

シェルスクリプト(Bash)の場合にも test コマンドに -p があるので、 test -p /dev/stdin のように書けば良さそうです。

$ bash -c 'test -p /dev/stdin ; echo $?'
1
$ echo foo | bash -c 'test -p /dev/stdin ; echo $?'
0

そもそもこれって良いスタイルなの?

上記のようにすることで、標準入力にパイプやファイルリダイレクトでデータが流し込まれていたらそちらを採用ということができるようになりました。

とはいえ、コマンドライン引数があればそちらを参照、そうでなければ標準入力を参照というのは、コマンドによる暗黙のおせっかいとも言えるわけで、マニュアルをしっかり書いたりしないと容易に混乱を生みそうです。

既存のコマンドラインでよくあるスタイルの一つは

  • コマンドライン引数でファイル名を指定可能
  • コマンドライン引数で - という「ファイル名」を指定された場合、コマンドはファイルの代わりに標準入力を読みに行く

というもの。gzip などのアーカイブコマンドだったり、curl などのコマンドがこれを採用しています。

コマンドが暗黙的にデータソースをコマンドライン引数か標準入力か切り替えるのではなく、コマンド使用者が明示的にコマンドに指示を与える方が素直かもしれません。

他にどんなときにこの確認方法は役に立つの?

データをどちらから取るかといった上記手法は、例えば引数を含んだコマンドを引数に取るコマンドを作成するときにも使えるでしょう。そのようなコマンドの具体例は ssh や xargs など。

例えば、load average が指定の数以上になったら負荷が落ち着くまで指定コマンドを一時停止するスクリプト limit-loadavg.pl を書く場合

$ limit-loadavg.pl --max-loadavg1=5 tar zcf huge.tar.gz huge/

と呼び出されたときと

$ tar cf - huge/ | limit-loadavg.pl --max-loadavg1=5 gzip -c > huge.tar.gz

と呼び出されたときで limit-loadavg.pl が標準入力を扱うか否かをハンドリングする必要が出てきます。このときに一つの手法として前述の確認方法が活躍しそうです。

なお、左側にパイプをつながれた limit-loadavg.pl は、自分だけでなく自分が所属するプロセスグループ全体を止める必要があるでしょう。