ぴよログ

↓に移転したのでこっちは更新されません、多分。

Rubyで標準出力を文字列で乗っ取る

移転しました →

この間書いたこの記事のコードがDRYじゃないので少し修正した。

GrapeのAPIのエンドポイントをrake routes的に出力する - PILOGGrapeの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