2024-03-23

Emacs Lispで文字列内の特定の位置が正規表現にマッチしているか判定する

Emacs Lispで、文字列の中の特定の位置が正規表現とマッチしているか判定するにはどうしたらよいでしょうか。

例えば "{ name: 'Taro' }" という文字列があったとして、3文字目の位置が正規表現 "\\([a-z]+\\)" にマッチしているか判定するにはどうしたら良いでしょうか(もちろんこの文字列や位置は色々と変わるものとします)。

Emacs Lispの正規表現マッチングの関数には大きく分けてバッファ用と文字列用があります。POSIX版ではない標準的なマッチング関数だと次のものがあります(Emacs 29.2時点でのRegexp Search (GNU Emacs Lisp Reference Manual)より):

  • バッファ用
    • re-search-forward
    • re-search-backward
    • looking-at
    • looking-at-p
    • looking-back
  • 文字列用
    • string-match
    • string-match-p

バッファ上のテキストであれば、現在のポイントから続くテキストが正規表現とマッチしているかを判定するにはlooking-atやlooking-at-pが使えます。他にも正規表現にポイントの位置とマッチするバックスラッシュ記法 \= があるので、それを使って (re-search-forward "\\=[a-z]+") などと書くことも出来ます(計測してみるとre-search-forwardの方が若干遅いようです)。

しかしながら文字列版の方にはそのような関数がありません。string-matchは文字列版のre-search-forwardです。検索を開始する位置こそ指定出来ますが、末尾までの間に正規表現がマッチする部分を探し、見つかったらその先頭位置を返します。例えば (string-match "\\([a-z]+\\)" "{ },{name: 'Taro' }" 2) を評価したら、 },{ の部分を飛ばして6文字目にマッチしてしまうわけです。指定した位置そのものがマッチしているかを判定するようなstring-looking-atのような関数はありません。 \= もバッファのポイントに対するもので、文字列には効きません。文字列の先頭を示す \` は本当に文字列の先頭(index=0)にしかマッチしませんし、 ^ も文字列の先頭か行頭(\nの直後)にしかマッチしません。

Emacs Lispというのは基本的にはバッファ用の関数の方が充実していて文字列用の関数が貧弱な傾向にあるような気がします。

仕方ないのでこれまで私はどうしていたかというと、

(eq (string-match "\\([a-z]+\\)" text pos) pos)

のようにしてマッチした位置が検索を開始した位置と同じであることを確認したり、

(string-match "\\`\\([a-z]+\\)" (substring text pos))

substringを使って検索開始位置を文字列の先頭に持ってきた上で \\` を使うなどしていたわけです(実際には前者ばかりで後者はほとんどしていないと思います)。

後はまぁ、

(string-match (format "\\`.\\{%d\\}\\([a-z]+\\)" pos) text)

みたいな手はあるかもしれません。

しかし、最近edraw-dom-svg.elでstyle属性の解析をしていたときに次のようなコードを書きました。

(defconst edraw-css-re-token
  (concat
   edraw-css-re-comment "*"
   "\\(?:\\(" edraw-css-re-ws "\\)"
   "\\|\\(" edraw-css-re-string "\\)"  ;; " '
   "\\|\\(" edraw-css-re-hash "\\)"  ;; #
   "\\|\\(" edraw-css-re-at-keyword "\\)"  ;; @
   "\\|\\(" edraw-css-re-dimension "\\)"
   "\\|\\(" edraw-css-re-percentage "\\)"
   "\\|\\(" edraw-css-re-number "\\)"
   "\\|\\(" edraw-css-re-function "\\)"
   "\\|\\(" edraw-css-re-ident "\\)"
   "\\|\\(" "[]({}),:;[]" "\\)"
   "\\|\\(" "." "\\)" ;; delim
   "\\)"))

(defun edraw-css-token (str pos)
  (unless (eq (string-match edraw-css-re-token str pos) pos)
    (error "CSS Syntax Error: %s `%s'" pos str))
  (cond
   ((match-beginning 1) ... )
   ((match-beginning 2) ... )
   ((match-beginning 3) ... )
   ...))

そのときに、「あれ、これって最後に . が入ってるんだから(末尾以外)必ずposの位置でマッチするんじゃないの?」と思ったわけです。

正規表現の最後が . ではなく空文字列の "略\\|" で終わっていれば、本当に必ずposの位置でマッチすることになります。

なので、最初の問いの答えは、

(progn
  (string-match "\\([a-z]+\\)\\|" text pos)
  (match-beginning 1))

でいいじゃないかということになったわけです。要するに必ずマッチさせてしまうわけです。これなら末尾まで無駄な検索は起こりませんし、substringで新しい文字列を作る必要もありません(substringがCopy-on-Writeだったりはしませんよね?)。で、実際に肝心の部分がマッチしているかは (match-beginning 1) が非nil(この場合はpos)を返すかどうかで確かめればいいわけです。正規表現に括弧が無いなら (match-end 0) がpos(つまり (match-beginning 0))と同じかどうかで確認しても良いでしょう。

分かっている人から見ればなんだ当たり前じゃ無いか、こんなことが分からないなんてバカなんじゃないか? と思われるかもしれませんが、何だかキツネにつままれたような不思議な気分になってしまいました。

大丈夫ですよね? 一応計測もしていてパフォーマンスも悪くは無さそうです。

ちなみにEmacsの \| は左がダメだったら右を試すという意味です。実際の実装がどうなってるのかは知りませんので、左が優先されると言った方が良いでしょうか。どちらか長い方という意味ではないのでご注意ください。

ところでel-easydrawの方は現在インポートまわりの作業をしていて、dvisvgmやdot、Inkscapeが出力したSVGを取りこんで遊んだりしています。org-babelのような仕組みが出来ると良いのですが……。