Ruby: 文字列の書式設定では引用符内で書式設定以外の`%`記号を`%%`とエスケープすること

ある日のSlackにて

前振り1: Rubyの%記号と式展開

今回の記事を書くために、まず%(パーセント)記号と式展開についてそれぞれ別記事にまとめましたので先に以下をご覧ください。そうしないと自分が混乱してしまうので。

Ruby: パーセント記号 `%` の使い方まとめ

Rubyの式展開(string interpolation)についてまとめ: `#{}`、`%`、Railsの`?`

前振り2: sprintfString#%メソッドを使う場合の%文字のエスケープ

この部分はRubyのバージョンに限らず共通です。

RubyのsprintfString#%メソッドを用いて書式設定を行う場合、引用符内で%そのものを出力したい場合は%%とすることでエスケープされます(エラーメッセージでも%%を使えと書かれてますね)。面白いことに、%%による%のエスケープは二重引用符でも一重引用符でも使えます。本記事では簡単のためString#%メソッドのみを用いています。

'%0.0f%%' % 45    #=> "45%"
"%0.0f%%" % 45    #=> "45%"

# 機能と関係ない%はエスケープしないとエラーになる
'%0.0f%' % 45     #=> ArgumentError: incomplete format specifier; use %% (double %) instead

逆に、バックスラッシュ%では二重引用符/一重引用符にかかわらず、%をエスケープできません。

"%0.0f%" % 45    #=> ArgumentError: incomplete format specifier; use %% (double %) instead
`%0.0f%` % 45    #=> ArgumentError: incomplete format specifier; use %% (double %) instead

ただしsprintfString#%メソッドを使っていなければ、二重引用符内で通常どおり%でエスケープできます。もちろん一重引用符では式展開もエスケープも無効になります。おなじみの挙動ですね。

value = 0.0
"#{value}%"      #=> "0.0%"
'#{value}%'      #=> "#{value}\%" (一重引用符なのでエスケープされない)

前振り3: SQLのLIKEで使われる%

さらにややこしいのが、SQLのLIKEでは%を一種のワイルドカード的に使えるという点です。

SELECT id FROM users WHERE name LIKE '%name%';

上はnameカラムが「なんちゃらnameなんちゃら」の行を取得します。「なんちゃら」は長さゼロも含みます。

Ruby 2.5で何が変わったのか

いろいろ書きましたが、Ruby 2.5で引用符内の%の挙動が変わったのはsprintfに関連する場合です(String#%で書式を設定する場合を含む)。

とりあえず以下の3つの場合が考えられます(簡単のためString#%の書式に揃えました)。

 # 1. 名前なし/順序のみの引数指定
'%%%d%%' % 99                    #=> "%99%"

# 2. 名前付き変数のハッシュでの指定(書式あり)
'%%%<value>d%%' % { value: 99}   #=> "%99%"

# 3. 名前付き変数のハッシュでの指定(書式なし)
'%%%{value}%%' % { value: 99}    #=> "%99%"

2.と3.の名前付き変数はハッシュ{ 変数: 値}で渡すことになっています。また、3.の%{名前付き変数}では書式は設定できないので、実は普通の式展開でやっても同じだったりします。詳しくは以下の記事をご覧ください。

Ruby: パーセント記号 `%` の使い方まとめ

ここまでを押さえたところで、冒頭のSlackに出てきたRubyコードを再録します。

Ruby 2.4.x以前の場合

'%%%{text}%' % { text: "aaa" }

%は左から順に以下の意味になります。

  • %%: SQLで使う
  • %{text}': 式展開の一種({ }なので書式は指定できない)
  • %: SQLで使う
  • %: String#%メソッド(書式設定用)

上ではSQLで使う%%%がどちらも使えてしまっている点にご注目ください。後述のツイートにあるように、これは本来の挙動ではありませんでした。ここでは%の後ろがたまたま%'だったおかげで、%が書式設定用ではないとRubyに認識されただけと考えられます。

したがって、以下のようにエスケープなしの%の後ろに文字があるとエラーになります。

'%%%{text}% decimal' % { text: "aaa" }  #=> ArgumentError: unnumbered(1) mixed with named
'%%%{text}%です' % { text: "aaa" }       #=> ArgumentError: unnumbered(1) mixed with named

つまり、String#%で書式を整える場合は、Rubyのバージョンにかかわらず、エスケープなしの%を引用符に書かない方がよいということになります。引用符内の単独の%は、Rubyのバージョンや文字列内の位置で挙動が変わるので当てにしない方がよいと思います。

Ruby 2.5以降の場合

'%%%{text}%%' % { text: "aaa" }

%は左から順に以下の意味になります。

  • %%: SQLで使う(%%としてエスケープしなければならない)
  • %{text}': 式展開の一種({ }なので書式は指定できない)
  • %%: SQLで使う(%%としてエスケープしなければならない)
  • %: String#%メソッド(書式設定用)

Ruby 2.4.x以前との違いは、%%というエスケープが必須になったことです。

'%%%{text}% decimal' % { text: "aaa" }  #=> ArgumentError: unnumbered(1) mixed with named
'%%%{text}%です' % { text: "aaa" }       #=> ArgumentError: unnumbered(1) mixed with named

ここからわかるように、引用符内では書式設定と無関係な%常に%%とエスケープすることで、バージョンや位置にかかわらず問題なく動作するようになります。

結論

String#%で書式を整える場合は、Rubyのバージョンにかかわらず、引用符の中で書式設定と無関係な%%%とエスケープすること。

あーややこしかった。

おまけ

↑やはり2.5で挙動が修正されたそうです。「ハッカソンのプルリク」がどれなのかはわからずじまい😭。記号はほんとググりにくい。

参考: バージョンごとの挙動

Rubyのバージョンごとの挙動を改めて調べてみました。

以前の挙動

  • Ruby 2.1.10
  • Ruby 2.2.10
  • Ruby 2.3.8
  • Ruby 2.4.4
'%%%%{text}%' % { text: "aaa" } # 1つ目のエスケープに余分な%がある
#=> "%%{text}%"

'%%%{text}%' % { text: "aaa" }  # 1つ目だけをエスケープ
#=> "%aaa%"

'%%%{text}%%' % { text: "aaa" } # 1つ目と2つ目をエスケープ
#=> "%aaa%"

現在の挙動

  • Ruby 2.5.3
  • Ruby 2.6.0、2.6.1
'%%%%{text}%' % { text: "aaa" }
# Traceback (most recent call last):
#        3: from /Users/hachi8833/.rbenv/versions/2.5.3/bin/irb:11:in `<main>'
#        2: from (irb):1
#        1: from (irb):1:in `%'
# ArgumentError (incomplete format specifier; use %% (double %) instead)

'%%%{text}%' % { text: "aaa" }
# Traceback (most recent call last):
#        3: from /Users/hachi8833/.rbenv/versions/2.5.3/bin/irb:11:in `<main>'
#        2: from (irb):3
#        1: from (irb):3:in `%'
# ArgumentError (incomplete format specifier; use %% (double %) instead)

'%%%{text}%%' % { text: "aaa" } #=> "%aaa%"

3つの中では、適切にエスケープした'%%%{text}%%' % { text: "aaa" }だけがすべてのRubyバージョンで正常に動作します。

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ

BPSアドベントカレンダー