Ruby 2.5 系で CSV.generate のバグ
Ruby 2.5 系で CSV.generate
を使用しようとしたら意図しない動作をして、調べてみたらバグだったのでそのまとめ。
しかし、これ、結構クリティカルなバグだと思うんですけど、全然話題になってないのが不思議(当時は話題になっていたのかもしれないけど。
CSV.generate
とは
以下のような感じで CSV 形式で文字列を構築する事が出来ます。
# Ruby 2.4 で実行 require "csv" require "pp" result = CSV.generate do |csv| csv << [1, 2, 3] csv << ["homu", "mami", "mado"] end pp result # => "1,2,3\n" + "homu,mami,mado\n"
https://wandbox.org/permlink/jL6prEzNrYZ0g9m0
また CSV.generate
の第一引数で『その CSV の先頭文字列』を指定する事が出来ます。
require "csv" require "pp" csv = CSV.generate("prefix") do |csv| csv << [1, 2, 3] end pp csv # => "prefix1,2,3\n" # 既存の csv に対して追記したり csv = CSV.generate(csv) do |csv| csv << ["homu", "mami", "mado"] end pp csv # => "1,2,3\n" + "homu,mami,mado\n"
https://wandbox.org/permlink/afzhyZti9dAAyukB
CSV.generate
のバグ
で、件のバグなんですが、先ほど説明した『CSV.generate
の第一引数で『その CSV の先頭文字列』を指定する』が Ruby 2.5 では動作しません。
# Ruby 2.5 で実行した場合 require "csv" csv = CSV.generate("prefix") do |csv| csv << ["homu", "mami", "mado"] end pp csv # => "homu,mami,mado\n"
https://wandbox.org/permlink/jWYqvmnnWIRdH2X7
上記のようなコードではすぐに『何かおかしい』とわかるんですが、例えば次のように『CSV データに BOM を追加する』みたいな事をしたい場合はほぼ気づきません。
require "csv" # BOM 付き CSV データを生成したいが追加されない… bom = "\uFEFF" csv = CSV.generate(bom) do |csv| csv << ["homu", "mami", "mado"] end # 出力先によっては BOM がついているかどうかが視覚的にわからないのでバグを見つけるのがむずかしい… pp csv # => "homu,mami,mado\n"
わたしも上記のような事をやりたかったんですが、うまく動作しなくて調べてみたら既知のバグでした。
『Ruby BOM CSV』でググると CSV.generate
を使ったやり方を書いているブログとかが結構ヒットするんですが、このバグに気づいてない人もいるんじゃないかなあ…。
Ruby 2.6 では修正済み
このバグは Ruby 2.6 では修正済みとなっています。
Ruby 2.5 系での対処方法
幸いにも CSV.generate
は Ruby で実装されているので次のようなモンキーパッチで対処する事が出来ます。
require "csv" # CSV.generate の実装を上書き class CSV def self.generate(str=nil, **options) # add a default empty String, if none was given if str str = StringIO.new(str) str.seek(0, IO::SEEK_END) else encoding = options[:encoding] str = String.new str.force_encoding(encoding) if encoding end csv = new(str, options) # wrap yield csv # yield for appending csv.string # return final String end end # これで BOM が追加される bom = "\uFEFF" csv = CSV.generate(bom) do |csv| csv << ["homu", "mami", "mado"] end pp csv # => "<feff>homu,mami,mado\n"
まとめ
Ruby 2.6 はよ〜