環境
macOS 10.13.4
Ruby 2.5.1
はじめに
まず、なにかしら出力させるRubyプログラムの簡単な例として以下の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 に代入すれば十分です。
# 標準出力の出力先を変更
$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を以下のように変更しました。
参考にしたプログラムの部分はメソッドにしました。
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を使ってみて、エラーを再現してみます。
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オブジェクトを持たせなくてもいいのでは。
最終的なプログラム
ということで結果的に以下のようになりました。
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
成功しました。
本番のプログラムでも正しく動作しました。😀
最後に
どこか見落としがあるかもしれませんが、一応やりたかったことができました。
参考にしたものが古いバージョンだったので、もっといい方法があるかも。