LoginSignup
17
16

More than 5 years have passed since last update.

シェルのパイプ処理を行うスクリプトは標準入出力を使う

Last updated at Posted at 2014-11-22

タイトルが全てです。

パイプ処理するスクリプトってどう作るんだっけ…

 シェルでパイプ処理をする際に、次作のスクリプトなどをはさみたい場合がごくまれにあります。
 そのたびに「あれ、どうやって作るのだっけ」となるので覚書。

 例えば以下のような処理をしたい場合です。

$ ls | ./cnt_line.rb
**** start ****
1 cnt_line.rb
2 hoge.txt
3 huga.txt
**** end ****

これLinuxの教科書に載ってたやつだ!

 この記事のタイトルにもあるように、標準入出力を使うことでバイプ処理に対応させられます。
 Linuxの初歩でした……

cnt_line.rb
#!/usr/bin/env ruby

$stdout.puts '**** start ****'

# 標準入力に番号を付けて出力
idx = 1 
while str = $stdin.gets
  $stdout.print "#{idx} #{str}"
  idx += 1
end

$stdout.puts '**** end ****'

 スクリプトにはRubyを使っていますが、これがShellスクリプトだろうが、Perlだろうが、Cだろうが、標準入出力を使えばOK。お好みの言語を使ってください。
 個人的には以下のように使い分けるといいかもと思います。

  • 文字列操作.. Ruby, Perl
  • 数値計算.. Python
  • データ量が多い、時間かかる.. Go, C

一行入力するたびに出力される…

 おまけです。
 たとえばxargsのように、標準入力がある程度入力されたら、標準出力に吐き出すようなことがしたい場合があります。

$ # カレントディレクトリの内容について、権限を一行で出す
$ ls -l | cut -d" " -f1 | xargs echo
合計 -rw-r--r-- -rw-r--r-- -rwxr-xr-x

 例として`cat`コマンドで入力した文章を、バイプ処理で渡し、行頭に番号を付けて出力したいとします。
 先ほどのスクリプトだと、一行入力するごとに画面(標準出力)に文字が表示されるので嫌な感じです。

$ cat | ./cnt_line.rb 
**** start ****
ssss
1 ssss
aaaa
2 aaaa
bbbb
3 bbbb
hoge
4 hog
^D  // Ctrl+Dの入力。実際には表示されない
**** end ****

 なので、標準出力の入力が終わってから出力をするようにしましょう。
 catで開いた標準入力画面でCtrl+Dが入力されると、標準入力が終了します。

cnt_line_buffer.rb
#!/usr/bin/env ruby

# 標準入力をプール
buffer = []
while str = $stdin.gets
  buffer << str 
end

# 標準入力が終了したら以下を実行

$stdout.puts '**** start ****'

# ブールした内容を出力
idx = 0 
while str = buffer[idx]
  $stdout.print "#{idx + 1} #{str}"
  idx += 1
end

$stdout.puts '**** end ****'

 変更前のプログラムとの差がわかりやすいように、あえてRubyっぽくない書き方をしています。
 ではパイプで使ってみましょう。

$ cat | ./cnt_line_buffer.rb
こんにちは
今日はいい天気
です
さようなら
^D
**** start ****
1 こんにちは
2 今日はいい天気
3 です
4 さようなら
**** end ****

 やりました! ちゃんと入力が終わってから表示されています。

Rspecの結果を整形した話

 おまけその2。

 今回、何故パイプ処理をするスクリプトが欲しかったかというと、RspecやCucumberの実行後に表示される、失敗シナリオの表示を編集したかったからでした。

$ bundle exec rspec
F.F...F..
Finished in 54.25 seconds (files took 6.04 seconds to load)
50 examples, 5 failures, 12 pending

Failed examples:

rspec ./spec/hoge_spec.rb:30 # Hogeの30行目のテスト
rspec ./spec/hoge_spec.rb:40 # Hogeの40行目のテスト
rspec ./spec/fuga_spec.rb:10 # Fugaの10行目のテスト

 RspecやCucumberは失敗したテストを上記の最後のあたりのように表示します。
 これをそのままコピペすれば、任意のテストだけを実行できます。

$ bundle exec rspec ./spec/hoge_spec.rb:40    # 40行目だけ実行
$ bundle exec rspec ./spec/hoge_spec.rb:30:40 # 30行目、40行目を実行

 が、複数の行を指定してテストをする場合(上記の二行目)は、コピペ+違う行の指定を書き込むという作業が発生するので面倒くさいです。
 そこで、標準入出力を利用したスクリプトを使い

$ cat | ./format_f_test.rb
** plz Copy & Pasts : Rspec or Cucumber Faileds
rspec ./spec/hoge_spec.rb:30 # Hogeの30行目のテスト
rspec ./spec/hoge_spec.rb:40 # Hogeの40行目のテスト
rspec ./spec/fuga_spec.rb:10 # Fugaの10行目のテスト
^D

** Formatted !
./spec/hoge_spec.rb:30:40
./spec/fuga_spec.rb:10

** ALL
./spec/hoge_spec.rb:30:40 ./spec/fuga_spec.rb:10

 こうして標準入力を使う利点は、テスト結果をファイル出力しておけば、コピペを使わずリダイレクトでも入力が可能という点です。

$ bundle exec cucumber --out failed_tests.txt
$ ./format_f_test.rb < failed_tests.txt
** plz Copy & Pasts : Rspec or Cucumber Faileds

** Formatted !
features/hoge.feature:23:44:56:67:103
features/fuga.feature:13
features/fuga_moga.feature:15:14

** ALL
features/hoge.feature:23:44:56:67:103 features/fuga.feature:13 features/fuga_moga.feature:15:14

$ bundle exec cucumber features/fuga_moga.feature:15:14 # コピペで再実行しやすくなった

 Rubyは文字列のパターンマッチや、文字列操作がストレスなくできるので、このようなスクリプトを気軽に作れるのが素晴らしいです。

追記

stdbufコマンドで行バッファモードなどの変更ができると知りました。なるほど。

備考

*【翻訳】パイプとフィルタ ~ソフトウェア工学における有用なアーキテクチャ~ - POSTD パイプ処理の仕組みと利点がよくまとまっている

17
16
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
16