Ruby(Windows)で改行付き文字列データを扱う場合はテキストモード(バイナリモード)に気をつけろ!!

はじまり

Rubyに限った話ではないのかもしれないですが、Rubyで改行を含むテキストデータを送受信するようなプログラムをWindowsで書いていた時に思った通りに処理されなくて嵌ってしまったので、自戒を込めてブログに残しておきます。

結論

Windowsで CR/LF をそのまま受け渡したいのであれば、IO#binmodeを設定しよう!!

詳細

例えば以下のようなプログラムがあったとします。

write.rb

$stdout.write "1\r2\n3\r\n"
$stdout.flush

read.rb

while char = $stdin.getc
  p char
end

以下のようにコマンドプロンプトから実行してみます。

> ruby write.rb | ruby read.rb

出力は以下のようになります。

"1"
"\n"
"2"
"\n"
"3"
"\n"
"\n"

\r がすべて \n に変換されてしまっています。 これは IO#getc がそのような変換をかけるために発生します。これに対応するには2種類の方法があります。一つは IO#read を length 1 で呼び出すようにコードを置き換える方法、もう一つは IO#binmode を設定する方法です。 read.rb (IO#read使用)

while char = $stdin.read(1)
  p char
end

read.rb (IO#binmode使用)

$stdin.binmode
while char = $stdin.getc
  p char
end

IO#binmode は IOオブジェクトをバイナリモードに設定するためのメソッドになります。バイナリモードというのは Windows 特有のモードのようで、通常はテキストモードになっています。このモードを変更すると改行の処理が変化します。テキストモードの場合は puts "\n" のようなコードを実行すると \r\n が出力され、getc に対して \r\n が入力されると \n が入力されたものとして処理されます。バイナリモードにすることで、コードに記述した通りの出力となり、入力も \r\n を別々に処理されるようになります。Rubyのリファレンスマニュアルにも記載されています。

docs.ruby-lang.org

Windows の IO にはテキストモードとバイナリモードという2種類のモードが存在します。これらのモードは上で説明した IO のエンコーディングとは独立です。改行の変換にしか影響しません。

さて、上記のコードを実行した出力を見てみましょう。どちらも以下の通り同じ出力になります。

"1"
"\r"
"2"
"\r"
"\n"
"3"
"\r"
"\r"
"\n"

なにかおかしいですね。1 の後の \r がそのまま出力されているのはいいのですが、今度は \n\r\n と出力されています。これは先ほど説明したWindowsのIOモードの問題が出力側に残っているのが原因です。出力側に対策を入れていないので、出力側で $stdout.write "\n" というコードを実行すると、\n\r\n に変換して出力します。この問題に対応するためには 出力側にも IO#binmode を追加します。

write.rb (IO#binmode使用)

$stdout.binmode
$stdout.write "1\r2\n3\r\n"
$stdout.flush

出力は以下のようになります。

"1"
"\r"
"2"
"\n"
"3"
"\r"
"\n"

期待した出力になりました。めでたしめでたし。

注意点

今回は標準入出力にbinmodeを設定して対策しましたが、一度binmodeを設定すると、closeするまでバイナリモードのままとなります。標準入出力は再オープンできないので、一度binmodeを設定してしまうとそのプログラム内では二度とテキストモードに戻すことができませんので注意が必要です。

まとめ

  • 入出力をパイプでつなぐような処理を記載するとき改行が入るようなデータを扱う場合、かつ Windows が関係する場合は極力バイナリモードでデータの受け渡しは行うこと。
  • 出力側は IO#binmode を使用することでバイナリモードとなる
  • 入力側は IO#getc の代わりに IO#read(1) を使用するか、出力側と同様に IO#binmode を使用することでバイナリモードとなる