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

# 環境
macOS 10.13.4
Ruby 2.5.1

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

```ruby:sample.rb
5.times do |i|
  puts i * 2
  sleep(1)
end
```
実行して、コンソールに表示される出力は以下になります。

```console
$ ruby sample.rb
0
2
4
6
8
```

## 標準出力をファイルに出力するには
### リダイレクト

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

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

```console
$ cat output.txt
0
2
4
6
8
```
ちゃんと保存されています。

### $stdoutを変更
今度は別の方法を試してみます。

Rubyプログラム内で、標準出力の出力先を変更する方法です。この方法は`$stdout`のリファレンスに書いていました。[$stdout](https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=3e.html)
>標準出力です。 組み込み関数 Kernel.#print、Kernel.#puts や Kernel.#p などのデフォルトの出力先となります。 初期値は Object::STDOUT です。

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

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

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

実行してみます。

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

```console
$ cat output.txt
0
2
4
6
8
```
こちらもちゃんと保存されています。

# 本題
ここから本題です。

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

## 調べてみた

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

## 参考にしたプログラム
どうやら、まつもとゆきひろさんが書かれたようです。でも2006年のもので結構古めです。
[puts,printの出力をファイルにも出力するには](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/42076)
以下が載っていたプログラムです。コメントを加えてみました。

```ruby
# 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](https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=3e.html)のリファレンスには以下が書いています。

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

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

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

```ruby: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
```

実行してみます。

```console
$ ruby sample.rb
0
2
4
6
8
```
コンソールには表示されました。
output.txtの方も確認してみます。

```console
$ cat output.txt
0
2
4
6
8
```
ファイルにも保存されています。
両方に出力させることに成功しました。

# ppでエラー発生
sample.rbではなく、実際に本番のプログラムで使ってみるとエラーが出てしまいました。
どうやら`pp`でエラーになっていました。[Kernel.#pp](https://docs.ruby-lang.org/ja/latest/method/Kernel/m/pp.html)
sample.rbでもppを使ってみて、エラーを再現してみます。

```ruby: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
```

実行してみます。

```console
$ 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](https://docs.ruby-lang.org/ja/latest/method/Object/c/STDOUT.html)

なら`defout`は、ObjectクラスでなくてもIOクラスでいいのでは。IOクラスなら`<<`メソッドが定義されています。
でもIO.newのリファレンスには、引数について
> [PARAM] fd:
    ファイルディスクリプタである整数を指定します。 

と書いています。

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

## 最終的なプログラム
ということで結果的に以下のようになりました。

```ruby: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](https://kitak.hatenablog.jp/entry/2013/01/06/201112)


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

```console
$ ruby sample.rb
0
2
4
6
8
```
エラーは出ず、コンソールに表示されました。
output.txtの方も確認してみます。

```console
$ cat output.txt
0
2
4
6
8
```
成功しました。
本番のプログラムでも正しく動作しました。😀

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