Ruby の STDOUT と $stdout

ちょっと前の Ruby-dev office hourSTDOUT$stdout について話題になってたので書いてみる。

発端は Ractor で $stdout は使えるけど STDOUT は使えないというものだったようだけど、まあ Ractor についてはよくわからないんで置いておく。

Ruby には標準入出力エラー出力を表すものとして、定数(STDIN, STDOUT, STDERR)とグローバル変数($stdin, $stdout, $stderr)がある。

以下で STDOUT / $stdout と書いてるけど、STDIN / $stdin, STDERR / $stderr でも同じ。

Ruby プロセス起動直後は同じオブジェクトを指している。

STDOUT.__id__   # => 7984
$stdout.__id__  # => 7984

$stdout は変数なので代入できる。そうすると当然異なるオブジェクトになる。

$stdout = File.open('/tmp/hoge.out', 'a')
$stdout == STDOUT  #=> false

Ruby の pputs$stdout に出力するので、$stdout に IO オブジェクトを設定するだけでリダイレクトのようなことができる。

なので普通は $stdout を使えばいい。

じゃあ STDOUT は何のためにあるのか? 定数なので代入できない(いや Ruby なのでできちゃうんだけど流石にそんなことやる人はいないだろう)。

つまり $stdout を代入して変更したとしても STDOUT はファイル記述子1番が維持されてる(同様に STDIN は 0番、STDERR は 2番が維持されている)。

$stdout = File.open('/tmp/hoge.out', 'a')
$stdout.fileno  #=> 5
STDOUT.fileno   #=> 1

なので、$stdout を元に戻すのに使える。

$stdout = STDOUT

ところで、Ruby プログラムから別のプログラムを起動するときには $stdout は引き継がれない。

これ↓を実行すると ls の出力は hoge.out には出ずに端末に表示される。ファイル記述子1は変わってないので。

$stdout = File.open('/tmp/hoge.out', 'a')
system('ls')

リダイレクトを別プログラムに引き継ぎたい場合はファイル記述子1に対応している STDOUT を変える必要がある。 昔はこんな感じで書いたりしてた:

pid = fork do
  STDOUT.reopen('/tmp/hoge.out', 'a')
  exec('ls')
end
Process.wait(pid)

普通は $stdout.reopen でもいいのだけども、$stdout は他で変更されてる可能性があるので、STDOUT の方が無難。

でも現代の Ruby は fork & exec なんてしなくても system のオプションでリダイレクトを指定できるので簡単にできちゃう:

system('ls', out: ['/tmp/hoge.out', 'a'])

なので、今はもうあんまり定数の方を使う場面はないのかもしれない。

噂によると「プログラム中でグローバル変数は使うべきじゃない」というのに惑わされて $stdout は絶対使いたくないという人もいるみたいだけど、プログラムの中でグローバルなオブジェクトなんだからグローバル変数を使うのは理にかなってるんで気にせずに使おう。