準備
文字列に関する数学的な記法
-
正規表現(regular expression)は,元来,文字列の集合を数学的に定義するためのものです.このノートでは,POSIX.EREの意味を,それが表現している文字列の集合によって示します.そのため次のような数学的な記法を使用します.以下の$x$や$y$は文字列を表し,$A$や$B$は文字列からなる集合を表しています.
記法 意味 $\Sigma$ すべての文字からなる集合(文字の全体集合) $\Sigma^*$ すべての文字列からなる集合(文字列の全体集合) $x \cdot y$ $x$のうしろに$y$を連結する.Schemeのstring-appendのこと.
連結演算に関して $(x\cdot{}y)\cdot{}z = x\cdot(y\cdot{}z)$(結合律)が成り立つので,
連結演算の間の優先順位を指定する丸括弧は省略します.$\varepsilon$ 空文字列(長さが0の文字列).Schemeにおいて "" と表される文字列のこと.
空文字列と任意の文字列$x$に対して $x\cdot\varepsilon = \varepsilon\cdot{}x = x$ が成り立ちます.$x^n$ $x\cdot{}x\cdot{}\,\ldots\,\cdot{}x$ ($n$個の$x$を連結)
正確な定義は次の通り.- $x^0=\varepsilon$; $x^k=x^{k-1} \cdot x$($k \geq 1$)
$A \cup B$ $A$と$B$の和集合 $A \cap B$ $A$と$B$の共通部分 $A - B$ $A$と$B$の差集合 $A \cdot B$ $\{ x \cdot y \mid x \in A, y \in B \}$ $A^n$ $\{ x_1 \cdot x_2 \cdot ~...~ \cdot x_n \mid x_1 \in A, x_2 \in A, ~...~, x_n \in A \}$
正確な定義は次の通り.- $A^0=\{\varepsilon\}$; $A^k = A^{k-1} \cdot A$($k \geq 1$)
$A^*$ $\bigcup_{n \geq 0} A^n = A^0 \cup A^1 \cup A^2 \cup ~...$ $A^+$ $\bigcup_{n \geq 1} A^n = A^1 \cup A^2 \cup A^3 \cup ~...$ $L(\alpha)$ 正規表現$\alpha$が表す文字列の集合 - $\Sigma$はロケールに応じて変化します.筆者の環境(日本語版のDebian 11)ではja_JP.UTF-8で定義されているすべての文字からなる集合になります.
正規表現の色分けについて
- 正規表現の中で使用する文字(例えば,英字のaなど)は,単なる文字を表しているのではなく,その文字からなる集合(例えば,$\{$a$\}$など)を表しています.プログラミングに比喩して言うと,正規表現の中で使用する文字と文字列データの中で使用する文字はデータ型が異なるのです.そのため,オートマトン理論や形式言語理論の教科書では,データ型の違いを明記するために,正規表現の中の文字を太字で示したりしています.
- このノートでは,正規表現を色を変えて示します.例えば,abcという文字列を正規表現として使うときには abc といったように色を変えます.
マッチについて
- 正規表現$\alpha$に文字列$z$がマッチするとは,$z$が$L(\alpha)$に属すること($z \in L(\alpha)$が成り立つこと)を言います.
- 正規表現$\alpha$に文字列$x$をマッチさせるとは,$\alpha$にマッチする$x$の部分列$z$があるかどうかを探索することを言います.
- 正規表現に文字列をマッチさせたとき,マッチが成功する(失敗する)とは,正規表現にマッチする部分列が見つかる(見つからない)ことを言います.
-
文字列$x$を正規表現$\alpha$にマッチさせたとき,次のような規則に則って部分列の探索を行います.
- 空文字列だけが$\alpha$にマッチするときには空文字列を返します.
- 空文字列以外の部分列が$\alpha$にマッチするとき,$\alpha$にマッチする部分列の中で最も左に位置する部分列を返します.
- ただし,「最も左に位置する部分列」が複数存在するときには,最も長い部分列を返します.
- 最左最長原則は正規表現の部分式に対しても適用されます.ただし,前方参照(back-reference)を使わないのであれば,上で述べた正規表現全体に関する最左最長原則だけを気にしていればよいと言えます.詳細は省略します.
先頭,末尾,境界
-
以下の説明の中で,「文字列の先頭にマッチする」,「文字列の末尾にマッチする」,「境界にマッチする」といった言葉が出てきます.これらは次のように考えます.検索対象の文字列 $x=c_0c_1c_2 \,\cdots\, c_n$(各$c_i$は文字)が与えられたとき,各文字の前後に空文字列($\varepsilon$)が付加されていて,さらに,それぞれの空文字列に文字列内の位置情報が付随していると仮定します.つまり,
$x = \varepsilon_{(0)}\cdot{}c_0\cdot\varepsilon_{(1)}\cdot{}c_1\cdot\varepsilon_{(2)}\cdot{}c_2\cdot\varepsilon_{(3)} \,\cdots\, \varepsilon_{(n)}\cdot{}c_n\cdot\varepsilon_{(n+1)}$と仮定します.ここで,$\varepsilon_{(i)}$の添字$i$は空文字列の位置を示しています.この仮定のもとで:
- 文字列$x$の先頭にマッチするとは,文字列$x$の先頭にある空文字列($\varepsilon_{(0)}$)にマッチすることを言います.
- 文字列$x$の末尾にマッチするとは,文字列$x$の末尾にある空文字列($\varepsilon_{(n+1)}$)にマッチすることを言います.
- 文字列$x$の境界にマッチするとは,どこかの文字の間にある空文字列($\varepsilon_{(i)}$)にマッチすることを言います.
- 単語の先頭や末尾にマッチするとは,単語として認識される部分列の直前や直後にある空文字列にマッチすることを言います.
- ただし,境界にマッチするときの「どこか(空文字列の位置$i$)」は機能ごとに変化します.例えば.「単語の境界にマッチする」と言ったとき,単語(と認識される部分列)の直前にある空文字列か,単語の直後にある空文字列にマッチすることを言います.
-
参考のために,簡単な実行例を示します.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "^" "#word#") $1 = #("#word#" (0 . 0)) guile> (string-match "$" "#word#") $2 = #("#word#" (6 . 6)) guile> (string-match "\\b" "#word#") $3 = #("#word#" (1 . 1)) guile>
1番目の実行例は,「文字列の先頭にマッチする」ことを意味する正規表現 ^ を文字列 "#word#" にマッチ(string-match)させています.その結果として0番目の空文字列($\varepsilon_{(0)}$)がマッチしたことを示す (0 . 0) が返ってきています.2番目の実行例は「文字列の末尾にマッチする」ことを意味する正規表現 $ を同じ文字列にマッチ(string-match)させていて,その結果として6番目の空文字列($\varepsilon_{(6)}$)がマッチしたことを示す (6 . 6) が返ってきています.3番目の実行例は「単語の境界にマッチする」ことを意味する正規表現 \b を同じ文字列にマッチ(string-match)させています(注:バックスラッシュが2重になっているのは,Guileのreaderにバックスラッシュをエスケープさせるためです).この場合,"word"が単語として認識されていて,その直前にある空文字列($\varepsilon_{(1)}$)がマッチしたことを示す (1 . 1) が返ってきています.
POSIX拡張正規表現(POSIX.ERE)
POSIX.EREにおける文字
-
POSIX.EREは,文字を通常文字(ordinary character)と特殊文字(special character)に分類しています.通常文字は特殊文字以外の文字のことです.特殊文字はPOSIX.EREの中で特別な機能を果たす文字のことで,以下のものがあります.
名称 文字 機能 ハット記号 ^ 文字列の先頭にマッチする. ピリオド . 任意の1文字にマッチする. 左角括弧 [ ブラケット表現を開始する. ドル記号 $ 文字列の末尾にマッチする. 左丸括弧 ( グループ化(演算の結合順序を指定)を開始する. 右丸括弧 ) グループ化(演算の結合順序を指定)を終了する. 縦棒 | 選択する(和集合を作る). 星印 * 0回以上の繰り返しを指定する. プラス記号 + 1回以上の繰り返しを指定する. 疑問符 ? 0回または1回を指定する. 左中括弧 { 有限回の繰り返しを指定する. バックスラッシュ \ クォート(エスケープ)する. - クォート(または,エスケープ)するとは,特殊文字を通常文字に変えることを言います.特殊文字以外の通常文字をクォートしたときの意味については クォート(エスケープ)について を参照して下さい.
- 右角括弧(])や右中括弧(})は特殊文字ではないことに注意して下さい.POSIX.1(仕様書)も POSIX.2(オンラインマニュアル)も,これらを通常文字として分類しています. これらは,対応する左角括弧や左丸括弧がないとき,通常文字として処理されます.一方,対応する左角括弧や左丸括弧があるときには,それぞれ,ブラケット表現を終了したり,有限回の繰り返し指定を終了するといった機能を果たします.
POSIX.EREの記述形式
-
POSIX.EREは,一般に
$E_{11}E_{12}~\ldots~E_{1m_1}$|$E_{21}E_{22}~\ldots~E_{2m_2}$| $\ldots\ldots$ |$E_{n1}E_{n2}~\ldots~E_{nm_n}$ ($n \geq 1$,$m_i \geq 1$)という形式をしていて,これは\( \begin{array}{cl} & L(E_{11})\cdot{}L(E_{12})\cdot\ldots\cdot{}L(E_{1m_1}) \\ \cup & L(E_{21})\cdot{}L(E_{22})\cdot\ldots\cdot{}L(E_{2m_2}) \\ \cup & ~~~~~~\cdots\cdots \\ \cup & L(E_{n1})\cdot{}L(E_{n2})\cdot\ldots\cdot{}L(E_{nm_n}) \\ \end{array} \)という文字列の集合を定義しています.
-
各$E_{ij}$は以下のいずれかを指定します.以下の$\Sigma_{s}$は特殊文字の集合を表し,$\alpha$は任意のPOSIX.EREを表し,$E$は下記の表に示した任意の表現を表しています.さらに,$m$と$n$は0以上の整数です.
名称 $E_{ij}$ 意味($L(E_{ij})$) 備考(条件) 通常文字 $a$ $\{a\}$ $a \in \Sigma-\Sigma_{s}$ クォートされた特殊文字 \$c$ $\{c\}$ $c \in \Sigma_{s}$ ピリオド . $\Sigma$ ハット記号 ^ 文字列の先頭にある空文字列の集合 $\{\varepsilon\}$ ドル記号 $ 文字列の末尾にある空文字列の集合 $\{\varepsilon\}$ グループ化 ($\alpha$) $L(\alpha)$ 0回以上の繰り返し $E$* $L(E)^*$ 1回以上の繰り返し $E$+ $L(E)^+$ 0回または1回 $E$? $\{\varepsilon\} \cup L(E)$ ちょうど$m$回の繰り返し $E${$m$} $L(E)^m$ $m \geq 0$ $m$回以上の繰り返し $E${$m$,} $L(E)^m \cup L(E)^{m+1} \cup L(E)^{m+2} \ldots$ $m \geq 0$ $m$回以上$n$回以下の繰り返し $E${$m$,$n$} $L(E)^m \cup L(E)^{m+1} \cup \ldots \cup L(E)^n$ $m \leq n$ ブラケット表現 (後述) (後述) - 上記の繰り返し回数を表す整数($m$や$n$)は0以上RE_DUP_MAX以下の整数であるとされています.ここで,RE_DUP_MAXはGnu C Libraryのマクロです.Guile 3.0.7のソースコード(regex.h)を見ると,その値は$2^{15}-1$(int16_tの最大値)に設定されています.
- 繰り返し表現の適用対象が任意のPOSIX.ERE($\alpha$)ではなく上記の表に示した表現($E$)に制限されているのは,繰り返し演算が選択演算(|)や連結演算($\cdot$)よりも優先順位が高いためです.選択演算や連結演算を含む正規表現を繰り返したいときには丸括弧で囲んでグループ化しなければなりません.
-
各種演算の優先順位は以下のように定義されています.上のほうが順位が高く,下のほうが低くなります.
名称 演算 照合記号など [. ... .], [= ... =], [: ... :] クォート(エスケープ) \$c$ ブラケット表現 [ ... ] グループ化 ( ... ) 繰り返し指定 *, +, ?, {$m$} {$m$,} {$m$,$n$} 文字列の連結 (注)連結演算を表す記号はありません. 先頭と末尾 ^, $ 選択(和集合) | -
繰り返し表現を2重に指定したときの動作について,POSIX.1(仕様書)は未定義としていて,POSIX.2(オンラインマニュアル)は何も述べていません.Guileでは問題なく使えるようです.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "(ab){3}{1,2}" "abab") $1 = #f guile> (string-match "(ab){3}{1,2}" "ababab") $2 = #("ababab" (0 . 6) (4 . 6)) guile> (string-match "(ab){3}{1,2}" "ababababab") $3 = #("ababababab" (0 . 6) (4 . 6)) guile> (string-match "(ab){3}{1,2}" "abababababab") $4 = #("abababababab" (0 . 12) (10 . 12)) guile>
この実行例では,正規表現 (ab){3}{1,2} を4つの文字列にマッチ(string-match)させています.この正規表現は,数学的には$\{$ ((ab)$^3$)$^1$ $,$ ((ab)$^3$)$^2$ $\}$ $=$ $\{$ (ab)$^3$ $,$ (ab)$^6$ $\}$ $=$ $\{$ ababab $,$ abababababab $\}$という文字列の集合を定義しているので, "(ab)$^3$"$=$"ababab"や "(ab)$^6$"$=$"abababababab"といった部分列がマッチするはずです.上の実行結果を見るとその通りになっています.1番目の文字列は"(ab)$^2$"なのでマッチする部分列がありません.2番目の文字列は"(ab)$^3$"なので文字列全体がマッチしています.3番目の文字列は"(ab)$^5$"なので,(最左最長原則に則って)0文字目〜5文字目までの部分列"(ab)$^3$"がマッチしています.4番目の文字列は"(ab)$^6$"なので,(最左最長原則に則って)文字列全体がマッチしています.
POSIX.EREの文法
- POSIX.1(仕様書)に掲載されているPOSIX.EREの文法を示します.
;; -------------------------------------------------------------------------- ;; 拡張正規表現(POSIX extended regular expression):文字列の集合を表現する. ;; -------------------------------------------------------------------------- extended_reg_exp ::= ERE_branch | extended_reg_exp '|' ERE_branch ERE_branch ::= ERE_expression | ERE_branch ERE_expression ERE_expression ::= one_char_or_coll_elem_ERE | '^' | '$' | '(' extended_reg_exp ')' | ERE_expression ERE_dupl_symbol one_char_or_coll_elem_ERE ::= ORD_CHAR | QUOTED_CHAR | '.' | bracket_expression ERE_dupl_symbol ::= '*' | '+' | '?' | '{' DUP_COUNT '}' | '{' DUP_COUNT ',' '}' | '{' DUP_COUNT ',' DUP_COUNT '}'
;; --------------------------------------------------------------
;; 通常文字(特殊文字以外の文字)
;; --------------------------------------------------------------
ORD_CHAR ::= 下記の特殊文字以外の文字
;; --------------------------------------------------------------
;; 特殊文字
;; --------------------------------------------------------------
SPEC_CHAR ::= '^' ;; 文字列の先頭
| '.' ;; 任意の文字
| '[' ;; ブラケット表現の開始
| '$' ;; 文字列の末尾
| '(' ;; グループ化
| ')' ;; グループ化
| '|' ;; 選択(和集合)
| '*' ;; 0回以上の繰り返し(クリーネ閉包)
| '+' ;; 1回以上の繰り返し
| '?' ;; 0回または1回
| '{' ;; 有限回の繰り返し指定の開始
| '\' ;; クォート(エスケープ)
;; --------------------------------------------------------------
;; クォート(エスケープ)された文字(特殊文字を通常文字にする)
;; --------------------------------------------------------------
QUOTED_CHAR ::= '\^' | '\.' | '\[' | '\$' | '\(' | '\)'
| '\|' | '\*' | '\+' | '\?' | '\{' | '\\'
- (参考)特殊文字以外の通常文字をクォートしたときの意味については クォート(エスケープ)について を参照して下さい.
;; --------------------------------------------------------------
;; 繰り返し指定の整数
;; --------------------------------------------------------------
DUP_COUNT ::= 0以上RE_DUP_MAX以下の整数
- (参考)Guile 3.0.7のソースコード(regex.h)では RE_DUP_MAX $= 2^{15}-1$ です.
ブラケット表現
ブラケット表現の記述形式
-
ブラケット表現(bracket expression)は,文字集合($\Sigma$の部分集合)を定義するための正規表現です.以下のいずれかの形式によって記述します.
記述形式($\alpha$) 意味($L(\alpha)$) 備考(条件) [$C_0C_1 \cdots C_n$] $L(C_0) \cup L(C_1) \cup \cdots \cup L(C_n)$ $C_0\not=$^ [^$C_1 \cdots C_n$] $\Sigma - (L(C_1) \cup \cdots \cup L(C_n))$ - 左角括弧([)と右角括弧(])は,それぞれ,ブラケット表現の開始と終了を表します.
- ハット(^)は,$\Sigma$を全体集合としたときの補集合演算を表します.
- ブラケット表現の先頭(1番目の形式の$C_0$,または,2番目の形式の$C_1$)以外の$C_i$は右角括弧(])ではないとします. 右角括弧(])はブラケット表現の終了を示すためのものなので,ブラケット表現の途中で指定できないのは明らかでしょう.ただし,ブラケット表現の先頭には右角括弧を指定できます(下記の右角括弧を参照).
-
各$C_i$は以下のいずれかを指定します.以下では,$\Sigma_1$$=\Sigma-\{$'^'$,$'-'$,$']'$\}$とします.ここで,ハット('^'),ハイフン('-'),右角括弧(']')はブラケット表現におけるメタ文字(後述)です.さらに,
$\Gamma = \{$[.$c$.] $\mid$ $c \in \Sigma \}$ (注:以下に示す照合記号表現の全体)とします.
名称 $C_i$ 意味($L(C_i)$) 備考(条件) 文字 $a$ $\{a\}$ $a \in \Sigma_1$ ハット記号 ^ $\{$'^'$\}$ 下記参照 ハイフン - $\{$'-'$\}$ 下記参照 右角括弧 ] $\{$']'$\}$ 下記参照 照合記号表現 [.$c$.] $\{c\}$ $c \in \Sigma$ 等価クラス表現 [=$c$=] $c$と等価な文字の全体 $c \in \Sigma$ 文字クラス表現 [:alnum:] など 詳細は後述. 範囲表現 $d$-$e$ $d$以上$e$以下の文字の全体 $d\in \Sigma_1 \cup \{$'^'$\} \cup \Gamma$
$e \in \Sigma_1 \cup \{$'^'$,$'-'$\} \cup \Gamma$ - 上記の メタ文字(ハット記号,ハイフン,右角括弧)は,それらが普通の文字として処理される場合を示しています.
- 範囲表現の始点($d$)に関する条件は,もっとも一般的な場合の条件を示しています.文脈によっては,始点($d$)にハイフンや右角括弧を指定できます.なお,終点($e$)に関する条件は文脈に依存して変化することはありません.
- ブラケット表現の中で一つの文字を複数回指定してもエラーにはなりません.何回指定しても1回指定したものとして処理されます.例えば,ブラケット表現の[aaa]は,文字'a'を3回指定していますが,$\{$'a'$\}$という文字集合を表します.
ブラケット表現における文字の分類
- ブラケット表現の中では,通常文字/特殊文字といった分類や機能はキャンセルされ,文字の分類・機能がリセットされます.つまり,ブラケット表現の中と外では文字の処理の仕方(文字の分類・機能)がまったく異なります.
- ブラケット表現の中では,以下に述べるメタ文字以外のすべての文字は普通の文字として処理されます.そのため,例えば,星印(*)やプラス記号(+)やピリオド(.)やバックスラッシュ(\)は普通の文字として処理されます.さらに,メタ文字も,文脈に応じて普通の文字として処理されます(下記参照).
-
メタ文字(meta character)は以下の3つがあり,ブラケット表現の中で特殊な機能を果たします.
名称 文字 機能 ハット ^ $\Sigma$を全体集合とした補集合を取る ハイフン - 文字の範囲を指定する 右角括弧 ] ブラケット表現を終了する - ハット記号(^)は,ブラケット表現の$C_0$のところに指定したときにはメタ文字(補集合演算)として処理されます(つまり,そのときのブラケット表現は2番目の形式として処理されます).一方,$C_1$〜$C_n$のところに指定したときには普通の文字として処理されます. 例えば,ブラケット表現 [^^] は,$\Sigma-\{$^$\}$という文字集合(ハット記号以外の文字からなる集合)を表します.
- ハイフン(-)は,ブラケット表現の先頭(1番目の形式の$C_0$,または,2番目の形式の$C_1$)や,ブラケット表現の末尾($C_n$)や,範囲表現の終点(上記の$e$)に指定したときには,普通の文字として処理されます.これら以外のところに指定したときには,メタ文字(範囲指定)として処理されます.メタ文字として処理されるとき,ハイフンは単独で(つまり,$C_i$の一つとして)指定することはできません.範囲表現の一部として指定しなければなりません. このノートのあとのほう(ハイフンについて)で,ハイフンの解釈の仕方について説明します.
- 右角括弧(])は,ブラケット表現の先頭(1番目の形式の$C_0$,または,2番目の形式の$C_1$)に指定したときには普通の文字として処理されます.一方,先頭以外に指定したときにはメタ文字(ブラケット表現の終了)として処理されます.例えば,正規表現 []abc]] は, ブラケット表現 []abc] と正規表現 ] を連結したものです.つまり,1番目の右角括弧はブラケット表現の中の普通の文字として処理され,2番目の右角括弧はブラケット表現の終了として処理され,3番目の右角括弧は(ブラケット表現とは無関係な)普通の文字として処理されます. 従って, この正規表現は,$\{$]$,$a$,$b$,$c$\}\cdot\{$]$\}$ $=$ $\{$]]$,$a]$,$b]$,$c]$\}$ という集合を表します.
照合記号表現
-
POSIX.EREの照合記号表現は,ブラケット表現の中で単一の照合要素からなる集合を指定するためのものです.次の形式によって表現します.
[.$\,c\,$.]ここで:
- [.は左角括弧とピリオドの組み合わせです.
- .]はピリオドと右角括弧の組み合わせです.
- $c$は任意の照合要素です(注:すべての文字を指定できます).
- 照合要素は,文字かまたは文字として機能する文字列のことです(詳細は省略します).文字以外の照合要素(2文字以上で構成される照合要素)は実行環境が採用しているロケール(の定義ファイル)において定義されていなければなりません.ロケールにおいて定義されていないものは使用できません.残念ながら,筆者の環境(Debian 11)のロケール(ja_JP)は,文字以外の照合要素を定義していないため,文字以外の照合要素は使用できません.
- 一方,照合要素にはメタ文字も含まれるので,ブラケット表現の中でメタ文字を普通の文字として指定するために利用できます.つまり,照合記号表現の中ではメタ文字も普通の文字として処理されます.例えば,ブラケット表現 [[.^.][.-.][.].]] は,メタ文字からなる集合$\{$^$,$-$,$]$\}$を表します.
等価クラス表現
-
POSIX.EREの等価クラス表現は,ブラケット表現の中で等価な照合要素からなる集合を指定するためのものです.次の形式によって表現します.
[=$c$=]ここで:
- [=は左角括弧と等号の組み合わせです.
- =]は等号と右角括弧の組み合わせです.
- $c$は任意の照合要素です.
- 任意の照合要素$c$に対して,$c$は$c$自身と等価です(同値関係の反射性).従って,上記の集合には$c$自身が必ず入ります.一方,自分自身以外の等価性はロケール(の定義ファイル)の中で定義しなけばなりません.従って,上記の集合に$c$以外に何が入るかはロケール(の定義ファイル)に依存して決まります.
- 残念ながら,筆者の環境(Debian 11)のロケール(ja_JP)は,2つの異なる照合要素間の等価性を何も定義していません.従って,筆者の環境では,$c$が何であろうとも,上記の等価クラスは$c$だけからなる集合$\{c\}$を表すことになります.
文字クラス表現
-
POSIX.EREは以下に示すような文字集合を用意しています.これらを文字クラス(character class)と呼んでいます.
文字クラス 意味 [:alnum:] $L($[:alpha:]$) \cup L($[:digit:]$)$ [:alpha:] アルファベットに相当する文字の集合 [:blank:] 空白とタブの集合 [:cntrl:] 制御文字の集合 [:digit:] 数字の集合 [:graph:] 印字可能な文字の集合(注:空白を除く) [:lower:] 小文字に相当する文字の集合 [:print:] 印字可能な文字の集合(注:空白を含む) [:punct:] $L($[:graph:]$)-L($[:alnum:]$)$ [:space:] スペースに相当する文字の集合 [:upper:] 大文字に相当する文字の集合 [:xdigit:] 16進数で使用する文字の集合 -
各文字クラスの中身はロケールに応じて変化します.筆者の環境(Debian 11)のロケール(ja_JP)では,次のファイルの中で定義されています.
- /usr/share/i18n/locales/i18n_ctype
- /usr/share/i18n/locales/ja_JP
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "[[:alpha:]]" " ア ") $1 = #(" ア " (1 . 2)) guile> (string-match "[[:alpha:]]" " あ ") $2 = #(" あ " (1 . 2)) guile> (string-match "[[:alpha:]]" " ア ") $3 = #(" ア " (1 . 2)) guile> (string-match "[[:alpha:]]" " 亜 ") $4 = #(" 亜 " (1 . 2)) guile> (string-match "[[:alpha:]]" " @ ") $5 = #f guile> (string-match "[[:alpha:]]" " @ ") $6 = #f guile>
この実行例では,カタカナなどの前後に空白を入れた文字列を文字クラス [[:alpha:]] にマッチ(string-match)させています.半角のカタカナや全角のひらがな・カタカナ・漢字はマッチしていますが,半角や全角の特殊記号はマッチしていません.
範囲表現
- 範囲表現 $d$-$e$ は$d$以上$e$以下の文字からなる集合を表しています.$d$を範囲表現の始点(starting point)と呼び,$e$を終点(ending point)と呼びます.始点と終点の間はハイフン(マイナス記号)です.
- 始点($d$)と終点($e$)には,メタ文字を除いて,あらゆる文字が指定できます.文脈によってメタ文字も指定できるのですが,その規則についてはメタ文字(各メタ文字の解釈)を参照して下さい.
- 始点($d$)と終点($e$)には,あらゆる照合記号表現が指定できます.従って,メタ文字を始点や終点に指定したいときには,照合記号表現を利用するとより安全と言えるでしょう.
- 始点($d$)に指定した文字の順位は終点($e$)に指定した文字の順位よりも小さくなければなりません.ただし,文字の間の順序はロケールに応じて変化します ...... と言いたいところですが,厳密にはよく分かりません.この点については ロケールについて(文字の順序)を参照して下さい.
- (注意)文字の順序は文字コードの大小順とは限りません.
ハイフンについて
- ブラケット表現の中でハイフンを見たら,ほぼ直ちに範囲表現を表していると考えてしまいます.しかし,そういった考えがしばしば混乱を引き起こすことがあります.
-
正規表現の中でハイフンを見たら,範囲表現のことはいったん忘れて,まずハイフンそのものを解釈することが肝要です.つまり,範囲表現のことはいったん忘れて,
メタ文字のハイフンの解釈のところで述べた規則に則って,そのハイフンが普通の文字として処理されるのかメタ文字として処理されるのかを判断します.そのハイフンがメタ文字として処理されるときに改めて範囲表現として認識するようにします.例えば:
- ブラケット表現[--@]において,1番目のハイフンは(ブラケット表現の先頭なので)普通の文字として処理され,2番目のハイフンはメタ文字として処理されます.従って,このブラケット表現は'-'以上'@'以下の文字からなる集合を表します.
- ブラケット表現[+--@]において,1番目のハイフンはメタ文字として処理され,2番目のハイフンは(範囲表現の終点なので)普通の文字として処理されます.従って,このブラケット表現は'+'以上'-'以下の文字と'@'からなる集合を表します.
- ブラケット表現[+---]において,1番目のハイフンはメタ文字として処理され,2番目のハイフンは(範囲表現の終点なので)普通の文字として処理され,3番目のハイフンは(ブラケット表現の末尾なので)普通の文字として処理されます.従って,このブラケット表現は'+'以上'-'以下の文字と'-'からなる集合を表します(注:末尾の'-'は誤りではありませんがムダです).
- ブラケット表現[a-h-z]において,両方のハイフンはメタ文字として処理されます.従って,これらのハイフンは範囲表現 a-h と h-z の一部になっています.しかし,これら2つの範囲表現は終点と始点が重なっていて(それぞれが単独の$C_i$として成立していないので)文法的に正しくありません.
- ブラケット表現[^-a-h-]において,1番目のハイフンは(ブラケット表現の先頭なので)普通の文字として処理され,2番目のハイフンはメタ文字として処理され,3番目のハイフンは(ブラケット表現の末尾なので)普通の文字として処理されます.従って,このブラケット表現は'-'と'a'以上'h'以下の文字と'-'からなる集合の補集合(ただし,全体集合は文字集合$\Sigma$)を表します(注:ハイフンを2度指定することは誤りではありませんがムダです).
ブラケット表現の文法
- POSIX.1(仕様書)に掲載されているブラケット表現の文法を示します.
;; -------------------------------------------------------------- ;; ブラケット表現(bracket expression):文字集合を表現する. ;; -------------------------------------------------------------- bracket_expression ::= '[' matching_list ']' | '[' non-matching_list ']' matching_list ::= bracket_list non-matching_list ::= '^' bracket_list bracket_list ::= follow_list | follow_list '-' follow_list ::= expression_term | follow_list expression_term expression_term ::= single_expression | range_expression single_expression ::= end_range | character_class | equivalence_class range_expression ::= start_range end_range | start_range '-' start_range ::= end_range '-' end_range ::= COLL_ELEM_SINGLE | collating_symbol collating_symbol ::= Open_dot COLL_ELEM_SINGLE Dot_close | Open_dot COLL_ELEM_MULTI Dot_close | Open_dot META_CHAR Dot_close equivalence_class ::= Open_equal COLL_ELEM_SINGLE Equal_close | Open_equal COLL_ELEM_MULTI Equal_close character_class ::= Open_colon class_name Colon_close
;; -------------------------------------------------------------- ;; 照合要素(collating element): ;; -------------------------------------------------------------- COLL_ELEM_SINGLE ::= メタ文字以外の文字(照合要素) COLL_ELEM_MULTI ::= 2文字以上からなる照合要素
;; --------------------------------------------------------------
;; メタ文字(meta characters):
;; --------------------------------------------------------------
META_CHAR ::= '^' ;; 文字集合の補集合演算を表す
| '-' ;; 範囲指定
| ']' ;; ブラケット表現の終了
-
(参考)上記の文法はPOSIX.1(仕様書)の本文の説明との間に以下のような齟齬があります.
- 上記の文法に従うと,ブラケット表現の先頭に右角括弧(']')やハイフン('-')を指定できません.
- 上記の文法に従うと,$C_1$〜$C_n$のところにハット記号('^')を指定できません.
- 上記の文法に従うと,等価クラスにメタ文字を指定できません.でも,仕様書の本文には,そういった禁止事項は述べられていません.
- ハイフンの扱い方や構文カテゴリー(品詞)の名前が雑だったりして,ちょっと分かりにくい文法です.
その他色々
- (注意)以下の内容はあやふやです.あまり信用しないで下さい.あやふやと言わざるを得ない理由は,正確な仕様が(少なくとも今の筆者には)見つけられないからです.
ロケールについて
-
正規表現の機能のうち,少なくとも次の機能はロケール(の言語に関する定義)に依存します.
- 文字の順序
- 照合要素
- 等価クラス
- 正規表現の中で利用可能な既存の文字クラス
-
文字の順序は範囲表現の中で使用します.その範囲表現に関してPOSIX.1(仕様書)は次のように述べています(下記の強調は筆者によるものです).
In the POSIX locale, a range expression represents the set of collating elements that fall between two elements in the collation sequence, inclusive. In other locales, a range expression has unspecified behavior: strictly conforming applications shall not rely on whether the range expression is valid, or on the set of collating elements matched.
この記述をそのまま信じるならば,例えば筆者のロケール(ja_JP)の場合,範囲表現に関してどういった処理が行われるかは不明ということになります. -
Debian 11の場合,ロケール定義ファイルは次の通りです.
- /usr/share/i18n/locale/ja_jp
-
jspaceなどの文字クラスが定義されているのですが,残念ながら,これらは利用できないようです.以下に実行例を示します.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "[[:jspace:]]" " ") ice-9/boot-9.scm:1669:16: In procedure raise-exception: In procedure make-regexp: 無効な文字クラス名です Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. guile [1]>
利用できない理由は不明です.
クォート(エスケープ)について
- 1〜9の数字をクォートした場合(\1,\2,..., \9), POSIX.1(仕様書)もPOSIX.2(オンラインマニュアル)もともに,それを前方参照(back-reference)として処理すると定めています.ただし,これは基本正規表現に関する機能として述べられていて,拡張正規表現のところでは何も述べていません.そのため,厳密に仕様に従うのであれば,拡張正規表現において前方参照(back-reference)は使えないことになります.ただ,Guileで試してみると使えます(前方参照(back-reference)についてを参照して下さい).
-
その他の通常文字をクォートした場合(例えば,\a,\b,\c,... など),
POSIX.1(仕様書)は「その解釈は未定義である」としていて,POSIX.2(オンラインマニュアル)は「通常文字として処理される(ただし,実装依存)」としています.一方,Guile 3.0.7のソースコード(regcomp.c内のpeek_token関数)を見ると,下記のクォートされた文字は特別な機能を発揮します.
クォート文字 意味(機能) 備考 \b 単語の境界にマッチする \B 単語の境界以外(の文字境界)にマッチする \w [_[:alnum:]]と等価 _ はアンダースコア \W [^_[:alnum:]]と等価 同上 \< 単語の先頭にマッチする \> 単語の末尾にマッチする \s [[:space:]]と等価 \S [^[:space:]]と等価 \` 文字列の先頭にマッチする ^と等価 \' 文字列の末尾にマッチする $と等価 \$k$ 前方参照(back-reference) $k=$1$,$2$, \ldots ,$9 - 上記以外のクォートされた文字は普通の文字として処理されます(下記参照).
-
各機能を判断するにあたって,Guileを使って試してみた経験の他に,grepのオンラインマニュアルを参照しています.そのgrepのオンラインマニュアルによれば,単語(word)とは,数字,英字,アンダースコアーによって構成される文字列のことを言います.ただ,どのように構成すべきかについは何も述べていません.Guileで試してみると,数字から始まっていても,あるいは,数字列(数字だけの文字列)だったとしても単語として認識するようです.以下は簡単な実行例です.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "\\b" "#123#") $1 = #("#123#" (1 . 1)) guile> (string-match "\\>" "#123#") $2 = #("#123#" (4 . 4)) guile>
1番目の実行例は,部分列"123"の直前の境界にマッチしています.2番目の実行例は,部分列"123"の直後の境界にマッチしています.これらの結果から"123"を単語として扱っていることが分かります. -
back-referenceを「後方参照」と訳すのが一般的なようです.しかし,以下に示すようにGuileのエラーメッセージは「前方参照」と表示します.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "\\1" "a") ice-9/boot-9.scm:1669:16: In procedure raise-exception: In procedure make-regexp: 無効な前方参照です Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. guile [1]>
このノートはGuileに関するものなので,Guileの訳語を使っています. -
(参考)以下のプログラムは,
ASCIIコード表の印字可能な文字を対象に,
エスケープしたときに特殊な機能を果たす文字が上記の一覧表に示したものだけであることを確認するためのものです.
下記のtest-quote手続きは,文字コードchcodeを受け取って,
そのコードの文字$c$(下記プログラム内のch)に対して以下のような処理を行います.
- 正規表現 \$c$(下記プログラム内のregex)を作ります.
- 文字コードがchcode$-1$の文字 $c'$,文字$c$,文字コードがchcode$+1$の文字 $c''$の3つの文字からなる文字列 $c'cc''$(下記プログラム内のstr)を作ります.なお,$c'$と$c''$は単に$c$と異なる文字を適当に用意しているだけで,それ以外の意図はありません.
- 上記の文字列(str)を正規表現(regex)にマッチ(string-match)させ,その結果を適当な形式(format)にまとめて返します.
- regex:\$c$ str:$c'cc''$ result:#f
この形式は文字列$c'cc''$が正規表現\$c$にマッチしなかったことを表します.従って,この場合には,正規表現 \$c$ が普通の文字として処理されなかったこと(何らかの特別な機能を発揮したこと)を示します. - regex:\$c$ str:$c'cc''$ result:($s$ . $t$) "$d$"
この形式は文字列$c'cc''$が正規表現 \$c$ にマッチしたことを示しています.$s$と$t$は,それぞれ,文字列$c'cc''$内のマッチした開始位置と終了位置を表します.さらに,"$d$"はマッチした部分列を表しています(ただし,これは参考情報にすぎません).従って,正規表現 \$c$ が普通の文字として処理されるときには,文字列$c'cc''$の文字$c$(先頭から見て1文字目の文字)にマッチするので,その場合には$s=1$かつ$t=2$(かつ$d=c$)となります.逆に言えば,$s\not=1$または$t\not=2$のとき,正規表現 \$c$ が普通の文字として処理されなかったこと(何らかの特別な機能を発揮したこと)を示します.
#!/usr/bin/guile \ -e main -s !# ;; test-quote.scm (use-modules (ice-9 regex) (srfi srfi-11)) (define DISPLAY-ALL #f) (define (main args) (let ((alllst '()) (anomalst '())) (let loop ((chcode #x20)) (when (and (< chcode #x7f)) (if (not (<= #x31 chcode #x39)) (let-values (((form flag) (test-quote chcode))) (set! alllst (cons form alllst)) (when flag (set! anomalst (cons form anomalst))))) (loop (1+ chcode)))) (when DISPLAY-ALL (display alllst) (display "\n----------------------\n")) (display (reverse anomalst)) (newline))) (define (test-quote chcode) (let* ((ch (integer->char chcode)) (regex (string-append "\\" (string ch))) (str (string (integer->char (1- chcode)) ch (integer->char (1+ chcode)))) (match (string-match regex str))) (values (format #f "~a~a~a~s~a~a\n" "regex:" regex " str:" str " result:" (if match (format #f "~a ~s" (cons (match:start match) (match:end match)) (match:substring match)) match)) (not (and match (= (match:start match) 1) (= (match:end match) 2))))))
main手続きは,ASCIIコード表の数字(1〜9)を除く印字可能な文字(ASCIIコードで #x20〜#x30 と #x3a〜#x7e)に関してtest-quote手続きを実行します.DISPLAY-ALLが#tのときには,これらのすべての文字に関する結果を表示します.DISPLAY-ALLが#fのときには,\$c$が普通の文字として処理されなかった文字$c$の結果だけを表示します.以下は上記のプログラムの実行結果です.$ ./test-quote.scm (regex:\' str:"&'(" result:(3 . 3) "" regex:\< str:";<=" result:#f regex:\> str:"=>?" result:#f regex:\B str:"ABC" result:(1 . 1) "" regex:\S str:"RST" result:(0 . 1) "R" regex:\W str:"VWX" result:#f regex:\` str:"_`a" result:(0 . 0) "" regex:\b str:"abc" result:(0 . 0) "" regex:\s str:"rst" result:#f regex:\w str:"vwx" result:(0 . 1) "v" )
上記の結果(文字の一覧)は先に示した一覧表と一致します. なお,1〜9の数字については「無効な前方参照」といったエラーが発生します. 例えば:$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "\\1" "abc") ice-9/boot-9.scm:1669:16: In procedure raise-exception: In procedure make-regexp: 無効な前方参照です Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. guile [1]>
前方参照(back-reference)について
- 以下は余談(negligible)です.
-
前方参照(back-reference)の定義を掲載します.なお,前方参照(back-reference)は基本正規表現(BRE)の機能なので,丸括弧はエスケープされた文字として示されています.
POSIX.1(仕様書)
The back-reference expression '\n' shall match the same (possibly empty) string of characters as was matched by a subexpression enclosed between "\(" and "\)" preceding the '\n'. The character 'n' shall be a digit from 1 through 9, specifying the nth subexpression (the one that begins with the nth "\(" from the beginning of the pattern and ends with the corresponding paired "\)" ). The expression is invalid if less than n subexpressions precede the '\n'. The string matched by a contained subexpression shall be within the string matched by the containing subexpression. If the containing subexpression does not match, or if there is no match for the contained subexpression within the string matched by the containing subexpression, then back-reference expressions corresponding to the contained subexpression shall not match. When a subexpression matches more than one string, a back-reference expression corresponding to the subexpression shall refer to the last matched string.POSIX.2(オンラインマニュアル)
Finally, there is one new type of atom, a back reference: '\' followed by a nonzero decimal digit d matches the same sequence of characters matched by the dth parenthesized subexpression (numbering subexpressions by the positions of their opening parentheses, left to right), so that, for example, "\([bc]\)\1" matches "bb" or "cc" but not "bc". -
前方参照(back-reference)は面白いアイデアだと思いますが,
POSIX.2(オンラインマニュアル)は次のようなことを述べています(注:正規表現の形式を拡張版に変更しています).
Back references are a dreadful botch, posing major problems for efficient implementations. They are also somewhat vaguely defined (does a((b)*\2)*d match "abbbd"?). Avoid using them.
「dreadful botch」のニュアンス(ひどさの度合い)は分かりませんが,「Avoid using them」はかなり強い提言だと思います.前方参照(back-reference)は使わないほうがよいようです. - 数学的な観点から言うと,前方参照(back-reference)は(数学的な意味の)正規表現の能力を超えています.例えば,正規表現 (.*)\1 は $\{ ww \mid w \in \Sigma^* \}$ という文字列の集合を定義していて,この集合は文脈自由ですらありません(これについては形式言語理論の教科書を参照して下さい).このようなことから,前方参照(back-reference)は正規表現の元来の意図を超えていると言えます.
-
ちなみに,前方参照(back-reference)の定義より,正規表現 a((b)*\2)*d は a((b)*b)*d (注:\2をbに置き換えたもの)と等価です.さらに,正規表現 ((b)*b)* は b* と数学的に等価です.従って,数学的な解釈では,正規表現 a((b)*\2)*d は ab*d と等価なので,
$\{$ ab$^k$d $\mid$ $k \geq 0$ $\}$ $=$ $\{$ ad$,$ abd$,$ abbd$,$ abbbd$,$ $\ldots$ $\}$といった集合を定義しています.従って,文字列"abbbd"は正規表現 a((b)*\2)*d に(数学的には)明らかにマッチします.実際に試してみると次のようになります.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "a((b)*\\2)*d" "abbbd") $1 = #("abbbd" (0 . 5) (1 . 4) (2 . 3)) guile>
なので,一見したところでは,上の丸括弧内のコメントは何か勘違いしているように筆者には見えます.いま一つ,何を言いたいのか理解できません.おそらく,筆者が理解していない技術的な背景があるのだろうと思います.とにもかくにも,筆者は,数学的に解釈した結果と一致してくれれば文句はありません. -
前方参照(back-reference)はプロでも間違い易いもののようです.POSIX.1(仕様書)の前方参照(bac-reference)の説明のところに次のような具体例があります
(ただし,基本正規表現を拡張版に書き換えています).
..., the expression (a(b)*)*\2 fails to match 'abab',...
正規表現 (a(b)*)*\2 は (a(b)*)*b と等価(なはず)なので,以下に示すように,文字列"abab"はこの正規表現にマッチします.guile> (string-match "(a(b)*)*\\2" "abab") $4 = #("abab" (0 . 4) (2 . 3) (1 . 2)) guile>
ハット記号(^)やドル記号($)を途中に含む正規表現
-
POSIX.EREは,
a^b
のような,
特殊文字としてのハット記号(^)を途中に指定するような表現も構文的に許しています.常識的には,途中に先頭があるといった文字列はあり得ないので,
そのような正規表現はどんな文字列ともマッチすることはありません ... と言いたいところですが,実はそうでもありません.
ハット記号(^)やドル記号($)を途中に含む正規表現は,
改行文字を含む文字列に対して有効に機能するようです.
ただし,改行文字もマッチの対象になります.
以下に,ちょっとした実行例を示します.
$ guile GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (string-match "b$.^c" "ab\ncd") $1 = #("ab\ncd" (1 . 4)) guile> (substring "ab\ncd" 1 4) $2 = "b\nc" guile>
この実行例では, 正規表現 b$.^c が部分列 "b\nc" にマッチしています. つまり,正規表現 $.^ が改行文字 "\n" にマッチしています. この実行例が示すように, 改行文字をマッチさせるように正規表現を設計すれば, ハット記号(^)やドル記号($)を途中に含んでいても有効に機能するようです.
(おしまい)