LoginSignup
18
7

More than 5 years have passed since last update.

Rubyの標準出力をコンソールとファイルの両方に出力させる

Last updated at Posted at 2018-10-18

環境

macOS 10.13.4
Ruby 2.5.1

はじめに

まず、なにかしら出力させるRubyプログラムの簡単な例として以下のsample.rbを用意しました。

sample.rb
5.times do |i|
  puts i * 2
  sleep(1)
end

実行して、コンソールに表示される出力は以下になります。

$ ruby sample.rb
0
2
4
6
8

標準出力をファイルに出力するには

リダイレクト

コンソールに表示される標準出力をファイルに保存したい場合、実行時に>(リダイレクト)を使えば簡単にできます。
試しに、output.txtに保存してみます。

$ ruby sample.rb > output.txt

コンソールには、なにも表示されません。
catコマンドでoutput.txtの中身を確認してみます。

$ cat output.txt
0
2
4
6
8

ちゃんと保存されています。

$stdoutを変更

今度は別の方法を試してみます。

Rubyプログラム内で、標準出力の出力先を変更する方法です。この方法は$stdoutのリファレンスに書いていました。$stdout

標準出力です。 組み込み関数 Kernel.#print、Kernel.#puts や Kernel.#p などのデフォルトの出力先となります。 初期値は Object::STDOUT です。

自プロセスの標準出力をリダイレクトしたいときには、 以下のように $stdout に代入すれば十分です。

sample.rb
# 標準出力の出力先を変更
$stdout = File.open('output.txt', 'w')
# ここ以降は標準出力がファイルに出力される

5.times do |i|
  puts i * 2
  sleep(1)
end

実行してみます。

$ ruby sample.rb

今回もコンソールには、なにも表示されません。
同様にcatコマンドでoutput.txtの中身を確認してみます。

$ cat output.txt
0
2
4
6
8

こちらもちゃんと保存されています。

本題

ここから本題です。

やりたいこと

ところで、標準出力をコンソールとファイルの両方に出力したい場合どうすればいいでしょうか。
両方に出力できれば、どこまで出力されているかを確認しつつ、ファイルにも保存できます。

調べてみた

ググってみると、同様の記事がありました。
Rubyでstdoutl/errを標準出力/エラーとファイルの両方に出力する
しかし載っているコードの記号がうまく表示されていなかったり、インデントが整っていなかったので、参考している元ページを見てみました。

参考にしたプログラム

どうやら、まつもとゆきひろさんが書かれたようです。でも2006年のもので結構古めです。
puts,printの出力をファイルにも出力するには
以下が載っていたプログラムです。コメントを加えてみました。

# Objectクラスのインスタンス(defaou)生成
defout = Object.new

# defoutのインスタンス変数としてFileオブジェクトを持たせる
defout.instance_eval{@ofile=open("/path/to/log/file", "w")}

# defoutに特異メソッドを定義
class << defout
  def write(str)
    STDOUT.write(str) # コンソールに表示
    @ofile.write(str) # ファイルに書き込み
  end
end

# 特異のwriteを定義したオブジェクトを$stdoutに代入
$stdout = defout

基本的には、$stdoutを変更しているのですが、
$stdoutのリファレンスには以下が書いています。

$stdout に代入するオブジェクトには write という名前のメソッドが定義されていなければいけません。

おそらく、標準出力のたびにwriteが呼ばれるのでしょう。
なのでwriteメソッドを定義しているようです。

試してみた

sample.rbを以下のように変更しました。
参考にしたプログラムの部分はメソッドにしました。

sample.rb
module Output
  def self.console_and_file(output_file)
    defout = Object.new
    defout.instance_eval { @ofile = open(output_file, 'w') }
    class << defout
      def write(str)
        STDOUT.write(str)
        @ofile.write(str)
      end
    end
    $stdout = defout
  end
end

Output.console_and_file('output.txt')

5.times do |i|
  puts i * 2
  sleep(1)
end

実行してみます。

$ ruby sample.rb
0
2
4
6
8

コンソールには表示されました。
output.txtの方も確認してみます。

$ cat output.txt
0
2
4
6
8

ファイルにも保存されています。
両方に出力させることに成功しました。

ppでエラー発生

sample.rbではなく、実際に本番のプログラムで使ってみるとエラーが出てしまいました。
どうやらppでエラーになっていました。Kernel.#pp
sample.rbでもppを使ってみて、エラーを再現してみます。

sample_error.rb
module Output
  def self.console_and_file(output_file)
    defout = Object.new
    defout.instance_eval { @ofile = open(output_file, 'w') }
    class <<defout
      def write(str)
        STDOUT.write(str)
        @ofile.write(str)
      end
    end
    $stdout = defout
  end
end

Output.console_and_file('output.txt')

5.times do |i|
  pp i * 2 # ppを使ってみる
  sleep(1)
end

実行してみます。

$ ruby sample.rb
/Users/username/.rbenv/versions/2.5.1/lib/ruby/2.5.0/prettyprint.rb:182:in `text': undefined method `<<' for #<Object:0x00007fbb9b0e1010 @ofile=#<File:output.txt>> (NoMethodError)

$stdoutに代入したdefout<<が定義されていないためのエラーのようです。

解決案

ところで、もとの$stdoutはObject::STDOUTがデフォルト値になっています。Object::STDOUTのクラスはIOです。
Object::STDOUT

ならdefoutは、ObjectクラスでなくてもIOクラスでいいのでは。IOクラスなら<<メソッドが定義されています。
でもIO.newのリファレンスには、引数について

[PARAM] fd:
ファイルディスクリプタである整数を指定します。

と書いています。

よくわからないので、サブクラスのFileクラスでやってみました。
File.newの引数には書き先のファイルパスを指定してみます。
そうなると、わざわざインスタンス変数でFileオブジェクトを持たせなくてもいいのでは。

最終的なプログラム

ということで結果的に以下のようになりました。

sample.rb
module Output
  def self.console_and_file(output_file)
    defout = File.new(output_file, 'w')
    class << defout
      alias_method :write_org, :write
      def write(str)
        STDOUT.write(str)
        self.write_org(str)
      end
    end
    $stdout = defout
  end
end

Output.console_and_file('output.txt')

5.times do |i|
  pp i * 2
  sleep(1)
end

一度何も考えず書いて実行してみると、writeの中でself.writeを呼ぶと無限ループになってしました。
それを避けるために、alias_methodで一度write_orgという同名のメソッドをつくってから、writeの中ではwrite_orgを呼ぶようにしました。
参考:Rubyの==演算子を再定義する(ネタ) - kitak's blog

正しく動くか実行してみます。

$ ruby sample.rb
0
2
4
6
8

エラーは出ず、コンソールに表示されました。
output.txtの方も確認してみます。

$ cat output.txt
0
2
4
6
8

成功しました。
本番のプログラムでも正しく動作しました。😀

最後に

どこか見落としがあるかもしれませんが、一応やりたかったことができました。
参考にしたものが古いバージョンだったので、もっといい方法があるかも。

18
7
3

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
18
7