gsub における特殊変数の扱いとその回避策

完璧なソースコードなどといったものは存在しない、完璧な絶望が存在しないようにね。

こんにちは!ご無沙汰の hikaru-kimi です!

みなさまは Ruby でどのように文字列置換を行っていますでしょうか?

最初に一致したもののみ置換する String#sub や、該当したパターンをすべて置換する String#gsub なんかを利用することが多いと思います!

本日は、みんな大好き sub, gsub の意外な落とし穴について紹介していきたいと思います!

(便宜的に String#gsub で紹介しますが、String#sub も同様の技術仕様です)

gsub における特殊変数の扱いと回避策

早速ですが、以下の実行結果はどうなりますでしょうか?

'Hello Ingage'.gsub(/Ingage/, 'Japan')

答えは簡単ですね!

'Hello Japan'

となります!

では、以下の処理の実行結果はどうなりますでしょうか?

"Hello Ingage".gsub(/Ingage/, "Mr \\0 is a programmer.")

Ingage がそのまま Mr \\0 is a programmer. に置換されるだけと思いきや、正解は以下の通りとなります。

=> "Hello Mr Ingage is a programmer."

なぜこのような処理結果となるのかと言いますと、gsub には第二引数内に特殊変数が存在すると特殊な動きをする仕様があるからです。

実際に Ruby の String#gsub のリファレンスを参照してみましょう。

https://docs.ruby-lang.org/ja/latest/method/String/i/gsub.html

gsub(pattern, replace) -> String

という用例に対して、以下のような説明書きがあります。

置換文字列 replace 中の & と \0 はマッチした部分文字列に、 \1 ... \9 は n 番目の括弧の内容に置き換えられます。置換文字列内では `、\'、+ も使えます。これらは $`、$'、$+ に対応します。

第二引数内の特殊の種類にもよるのですが、\& で解説していきたいと思います。

gsub の第二引数内に「\&」(正確にはエスケープの都合上 \\&)が存在すると、レシーバに指定した文字列内の第一引数にマッチする部分に対して、「\&」に第一引数を割り当てた第二引数で置換します。

一言で説明すると難解なので、再度具体例をお示しします。

"Hello Ingage".gsub(/Ingage/, "Mr \\& is a programmer.")
=> "Hello Mr Ingage is a programmer."

まず第二引数の "Mr \\& is a programmer."\& の部分を、第一引数の Ingage で置換します。

置換によって生成された "Mr Ingage is a programmer." を、レシーバ内の第一引数に該当した Ingage と置換します。

よって結果は "Hello Mr Ingage is a programmer." となります。

では、第二引数に外部入力された文字列が入り得るコードにおいて、このような特殊変数特有の動作を回避したい場合はどうしたらいいのでしょうか?

リファレンスによりますと、第二引数に置換したい文字を指定するのではなく、ブロックで渡す形で対応すると特殊変数特有の動作を回避できるとのことです。

以下がその例となります。

"Hello Ingage".gsub(/Ingage/) { "Mr \\& is a programmer."}
=> "Hello Mr \\& is a programmer."

これで、特殊変数を考慮せずとも文字列置換を実現できましたね!

経緯

弊社サービス「Re:lation」には、外部入力された文字列を gsub の第二引数として指定し、レシーバ内の第一引数に該当した文字列を第二引数で置換するという処理があります。

目的としてはレシーバの文字列内の特定の文字を置換したいからに他ならないのですが、意図せぬ形で第一引数が gsub の結果に複数個残ってしまい、想定外の処理としてエラーを惹起するという結果となってしまいました。

実際にエラー調査に臨んだ際も、上記の様なよもや直感に反する技術仕様を知らなかったが故に、 gsub のレシーバの第一引数に該当した箇所をそのまま第二引数に置換するという既知の仕様に拘泥してしまい、結果調査にしばしば時間を要してしまいました。

ただ、上記に挙げたリファレンスをよく読むと確かにそのような仕様の記載があるんですよね。また、特殊変数はプログラミング一般においても概して同様の処理をするそうです。不可解なエラーが発生した際はドキュメントを丹念に読むことこそ問題解決の捷径であると肺腑にしみました。 また同時に、使い慣れた gsub の仕様に対する己の一知半解さを自覚させられました。

実装時は正しく動作すると信じて疑わなかったですし、エラー調査時ですらエラーなぞ発生するはずのないコードだと確信していたのですが…

今回ご紹介した gsub における特殊変数の扱いなんぞ当然知っているという博覧強記なそこのあなた!是非以下のリンクから弊社インゲージの採用サイトよりご応募の程よろしくお願いいたします!!