2013年11月22日金曜日

\L と \U と Perl

Perl スクリプトに文字列を渡すのに

count -t '\t'

のような感じでエスケープを使いたいけれど、Perl コアでそのものの機能は提供されていないということで String-Unescape というモジュールを作成中です。もちろん eval 使えば済むのは分かってるんですけど、文字列 eval はなるたけ避けたいということで。その中でぶつかった(自分的には)驚きの Perl 仕様について書いてみます。

ダブルクウォート内で \Q と \E で囲った範囲の英数アンダーバー以外をエスケープできる(quotemeta がかかる)というのは Perl 使いではそれなりに知られた事実だと思います。

say "\Q[|]\E";
# \[\|\+\]

同様に範囲で効く変換として \L, \U も存在は知っているという方も多いでしょう(最近は \F と言うのもあります)。で、これらは重ねることができると perldoc perlop には書いてあります。

\L, \U, \F, and \Q can stack, in which case you need one \E for each. For example:
 say"This \Qquoting \ubusiness \Uhere isn't quite\E done yet,\E is it?";
# This quoting\ Business\ HERE\ ISN\'T\ QUITE\ done\ yet\, is it?

さて、では問題です。以下はどのように出力されるでしょうか。

say "\Q[A\U[a\L[A\E[a\E[A\E";

正解は \[A\[A\[a\[a[A です。この時点で「は?」って感じですが、続いて第 2 問。

say "\U[a\Q[a\L[A\E[a\E[a\E";

正解は [A\[A[a[a[a です。もう訳がわからないという感じですが、真っ当に追っかける気を無くすコードを感覚で読み取ったところ、\U 中の \L や \L 中の \U があると、その前の \U, \L まで(途中に \Q とかがあればそれも)無効になった後で、新しく \L, \U が有効になるようです。\L や \U が一回しか有効にならないように \E が適宜補われると考えると分かりやすいかもしれません。

say "\Q[A\U[a\L[A\E[a\E[A\E";
say "\Q[A\U[a\E\L[A\E[a\E[A\E"; # 直前の \U が無効
# \[A\[A\[a\[a[A
# \[A\[A\[a\[a[A
say "\U[a\Q[a\L[A\E[a\E[a\E";
say "\U[a\Q[a\E\E\L[A\E[a\E[a\E"; # \U\Q が無効
# [A\[A[a[a[a
# [A\[A[a[a[a

\L...\U...\E...\E ってやっても外側の \L しか効かないから無駄じゃねーか、という意図であろうというのは推測できますが、個人的には「いや、書かれたとおりに実行しろよ」と言いたくなる挙動です。

もう一つ、エスケープ関連でお節介じゃないの?という機能として、\L\u と \U\l をそれぞれ \u\L と \l\U に置き換えるというものがあります。

say "\L\uaAA\E \U\lAaa\E";
# Aaa aAA

\u してから \L したら結局 \L だけじゃねーかってのは分かりますが、「勝手に書き換えるな、警告出せ」と思うわけです。

ちょっと気を利かせたつもりで全体として訳が分からなくなる、というのはある意味とても Perl らしいという気もしますが。元々 String-Unescape は Perl の処理と一致させることを目指そうと思っていたわけですが、この 2 つの挙動(特に前者)は無理して再現する必要はないだろうということで実装しないつもりでいます。