Ruby の最近の変更で間違って使われていることが多い ENV.dup で例外が発生するようになり、 ENV.clone で警告がでるようになりました。 問題なくこのままリリースされれば Ruby 3.1 からこの挙動になります。
問題点
gem-codesearch や github での検索で調べると ENV.dup や ENV.clone はテストで一時的に環境変数をまるごと保存しておいて戻したい、という用途で使われているようにみえました。 しかし、実際にはそういう動作はしていません。
ENV.dup
ENV の特殊な挙動は特異メソッドで定義されていて、特異メソッドをコピーしない ENV.dup は Object.new とほぼ同義で、テスト後に ENV を ENV.dup の返り値に戻すと ENV#[] や ENV#[]= などがなく、元の ENV のようには全く動かなくて意味がない状態でした。
そのため、 ENV.dup の時点で例外になっても互換性に問題はなさそうということで Cannot dup ENV, use ENV.to_h to get a copy of ENV as a hash (TypeError) という例外が発生するようになりました。
ENV.clone
ENV#[] や ENV#[]= は getenv(3) や setenv(3) のような OS の環境変数を直接扱うメソッドで、オブジェクトの中には情報を保存していません。
そのため、 ENV.clone の返り値のオブジェクトからの操作でも OS の環境変数が書き換えられてしまい、 ENV 定数がどちらのオブジェクトを指すようにしても、環境変数の一時保存などはできません。
しかし、意図とは違うかもしれないとはいえ、動いているものが壊れることになるので、まずは警告になりました。
% ruby -we 'ENV.clone'
-e:1: warning: ENV.clone is deprecated; use ENV.to_h instead
正しい一時保存方法
例外や警告のメッセージにあるように、環境変数全部を一時的に保存しておきたいときは ENV.to_h を使います。 そして、戻すときは ENV.replace を使います。
% ruby -e 'env = ENV.to_h; ENV.clear; p ENV["LANG"]; ENV.replace(env); p ENV["LANG"]'
nil
"ja_JP.UTF-8"
まとめ
ENV で間違った使われ方をすることが多い ENV.dup と ENV.clone の変更を紹介しました。
ENV は他にも freeze が Ruby 2.7 から組み込みで唯一例外になる (freeze していても setenv(3) できるため) など、誤解の元になる挙動は変更されてきた経緯があります。
この記事が ENV の間違った使い方を減らす参考になれば幸いです。