Rubyで標準出力を文字列で乗っ取る
移転しました →
この間書いたこの記事のコードがDRYじゃないので少し修正した。
GrapeのAPIのエンドポイントをrake routes的に出力する - PILOG
やりたかったのは記事タイトルの通りで、Grapeで定義したAPIの結果をrake routesの結果と一緒に出力するというもの。
ソースを深く読んで行くと既存のrake routesタスクの乗っ取りはそう簡単にはいかなかったため、別のタスクでrake routesと同じような処理をした上、さらにGrapeの情報も出力するってことをやっていた。
この、rake routesと同じような処理の書き方がまずくて、このタスクの該当部分をそのまま持ってくるといういけていない書き方をしてしまっていた。
task my_routes: :environment do # この4行はRailsの中からほぼコピペしている all_routes = Rails.application.routes.routes require 'action_dispatch/routing/inspector' inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes) output = inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, ENV['CONTROLLER']) # このあとoutput を使って何かする、みたいな。 end
これはDRYの原則に反するし、気持ち悪い。そこでなんとかする方法を考えた。
まず、このmy_routesタスクからデフォルトのroutesタスクを呼ぶことを考えた。これはとても簡単で、Rake::Task["routes"].executeというコードで呼び出すことができる。
これでできたと思いたいところだが、実はこのroutesタスク、内部で出力用データを作ってそのまま標準出力にputsしている。my_routesでやりたかったのはroutesの結果を受け取って、その内容を元に出力テキストの調整をするのでこれでは困る。
まあきっとRubyだから標準出力乗っ取るぐらい余裕だろうと思ったが、その通りだった。
capture_io
minitestの中にcapture_ioというメソッドがあって、これを使うと標準出力と標準エラーを則って文字列として取り出すことができる。
module Minitest::Assertions - minitest-5.3.4 Documentation
使い方
まず使い方から。
task my_routes: :environment do out, err = capture_io do Raks::Task["routes"].execute end # このあとout を使って何かする、みたいな。 end
capture_ioのソース
このためだけにminitestをrequireするのはどうかと思ったのでcapture_ioのコードは適当に貼り付けた。ああ。またDRYじゃない。
ソースはこんな感じ。
# File lib/minitest/assertions.rb, line 399 def capture_io _synchronize do begin require 'stringio' captured_stdout, captured_stderr = StringIO.new, StringIO.new orig_stdout, orig_stderr = $stdout, $stderr $stdout, $stderr = captured_stdout, captured_stderr yield return captured_stdout.string, captured_stderr.string ensure $stdout = orig_stdout $stderr = orig_stderr end end end