SRELL (std::regex-like library) はC++用の正規表現テンプレートライブラリです。
SRELLはECMAScript (JavaScript) 互換の正規表現エンジンを、"std::regex"(C++11で導入された正規表現ライブラリ)とクラス構成が同じになるようにラッピングしたものです。クラスデザインが同じですので、std::regexや、その基となったboost::regexと同じ感覚で扱えます(APIもJavaScript風にしたテンプレートライブラリもあります)。
SRELLはUnicodeに特化した正規表現ライブラリです。
'.'
がUTF-16文字列でサロゲートペアの片割れだけにマッチしたり、UTF-8文字列のコードユニットにマッチしたりするようなことがありません。[丈𠀋]
のように指定できます。また [\u{1b000}-\u{1b0ff}]
のような範囲指定もできます。※ちなみにC++11以降のstd::regexは、us-asciiやiso-8859-*のような1文字が固定長の文字コードを前提としています。そのため1文字が可変長であるUTF-8やUTF-16の文字列はうまく扱えません。
C++11に向けた改訂作業の中でもregexは比較的初期に提案された拡張であったことから、まったくと言って良いほどC++11の新機能に依存していません。そのためC++11より前のコンパイラであっても、C++のテンプレートを正しく解釈するものであればSRELLは利用可能です(動作確認済みコンパイラのうちもっとも古いものはVC++2005です)。
パスの通ったところに srell*.hpp(srell.hpp, srell_ucfdata2.hpp, srell_updata.hpp の3ファイル)を置いて srell.hpp をincludeするだけです。
// Example 01: #include <cstdio> #include <string> #include <iostream> #include "srell.hpp" int main() { srell::regex e; // 正規表現オブジェクト。 srell::cmatch m; // 結果を納めるオブジェクト。 e = "\\d+[^-\\d]+"; // 正規表現をコンパイル。 if (srell::regex_search("1234-5678-90ab-cdef", m, e)) { // printfを使うなら。 const std::string s(m[0].first, m[0].second); // 上は下のどちらかでも良い。 // const std::string s(m[0].str()); // const std::string s(m.str(0)); std::printf("result: %s\n", s.c_str()); // iostreamを使うなら。 std::cout << "result: " << m[0] << std::endl; } return 0; }
この例のように、SRELLを構成するクラスやアルゴリズムはすべてnamespace srellの下に置かれています。この点を除けば使い方はstd::regexに準じます。
現時点ではまだstd::regexに関する日本語の文書があまりないようですので、さしあたってSRELLを使ううえで必要となりそうな情報を次のページにまとめました。
Zipアーカイヴ内の reame_ja.txt も併せてご覧ください。
C++11以降で導入された機能のうち、SRELLが利用することもあるのは以下の4つです。
char16_t
型とchar32_t
型initializer_list
char8_t
型2020年 6月現在、SRELLではこれらの使用可否を次のようにして判定しています。
#ifdef __cpp_unicode_characters #ifndef SRELL_CPP11_CHAR1632_ENABLED #define SRELL_CPP11_CHAR1632_ENABLED // char16_t, char32_t用のtypedefを行う。 #endif #endif #ifdef __cpp_initializer_lists #include <initializer_list> #ifndef SRELL_CPP11_INITIALIZER_LIST_ENABLED #define SRELL_CPP11_INITIALIZER_LIST_ENABLED // 引数にinitializer_listを渡せるようにする。 #endif #endif #ifdef __cpp_rvalue_references #ifndef SRELL_CPP11_MOVE_ENABLED #define SRELL_CPP11_MOVE_ENABLED // コンストラクタや代入でmoveを有効にする。 #endif #endif #ifdef __cpp_char8_t #ifdef __cpp_lib_char8_t #define SRELL_CPP20_CHAR8_ENABLED 2 // char8_t対応とstd::u8string対応との両方を行う。 #else #define SRELL_CPP20_CHAR8_ENABLED 1 // char8_t対応のみ行う。 #endif #endif
実際には該当する機能が使えるにもかかわらず、コンパイラが__cpp_*
マクロを適切に定義しないためにC++11/C++20の機能が有効にならない場合、SRELLをincludeする前に上記マクロのうち必要なSRELL_CPP*
を定義しておくと、対応する機能を強制的にオンにすることが出来ます。
SRELLの動作確認には主にVCとMinGWを使用していますが、Compiler Explorer上で簡単にテストしてみたところによりますと、2019年5月現在、少なくとも次の環境でもSRELL 2.200を使ったサンプルコード(srell::u16regexを使用してUTF-16文字列に対する正規表現検索をするもの)からバイナリの生成ができるようです。
charやwchar_tのみを使う場合、Compiler Explorerで一番古いと思われるx86-64 gcc 4.1.2でもビルド可能なようです。
SRELL 2ではECMAScript 2020 (ES11.0) のRegExp互換の正規表現が使えます(u
フラグは常に指定されていると見なされる)。
具体的には次の通りです。
文字 | |
---|---|
. |
改行以外の文字にマッチ(ECMAScriptにおける改行文字は、U+000A, U+000D, U+2028, U+2029の4文字)。
Note: |
\0 |
NULL文字 ( |
\t |
水平タブ ( |
\n |
Line Feed ( |
\v |
垂直タブ ( |
\f |
Form Feed ( |
\r |
Carriage Return ( |
\cX |
|
\\ |
バックスラッシュそのもの ( |
\xHH |
UTF-16におけるコードユニット値が、2桁の16進数
UTF-16において |
\uHHHH |
Unicodeのコードポイント値が、4桁の16進数
SRELL 2.500以降:連続する |
\u{H...} |
1桁以上の16進数
Note: ECMAScript 6にて追加された表現です。提案書の段階では |
\ |
|
^$.*+?()[]{}|\/ 以外の文字 |
その文字そのものを表す。 |
選択 | |
A|B |
正規表現AまたはBにマッチ。 |
文字クラス | |
[] |
文字クラス。文字集合。
Perlの正規表現には「
|
定義済み文字クラス | |
\d |
|
\D |
|
\s |
Note: 厳密にはWhiteSpaceとLineTerminatorとに一致します。今後UnicodeカテゴリのZsに新たな文字が追加されることがあれば、その都度WhiteSpaceの右辺値は増えます。 |
\S |
|
\w |
|
\W |
|
\p{...} |
Note: ES2018/ES9.0でRegExpに導入された表現で、SRELL 2.000以降で使用可能です。 |
\P{...} |
Note: ES2018/ES9.0でRegExpに導入された表現で、SRELL 2.000以降で使用可能です。 |
量指定子(回数指定) | |
* *? |
直前の正規表現による照合を0回以上繰り返す。
先行する表現なしにいきなり回数指定が現れた時には |
+ +? |
直前の正規表現による照合を1回以上繰り返す。 |
? ?? |
直前の正規表現による照合を0回ないし1回繰り返す。 |
{n} |
直前の正規表現による照合をきっちり
|
{n,} {n,}? |
直前の正規表現による照合を |
{n,m} {n,m}? |
直前の正規表現による照合を
|
括弧・後方参照・グループ化 | |
(...) |
正規表現のグループ化および文字列の捕獲。正規表現全体において開き括弧 括弧自身またはその外側の正規表現に繰り返し指定がある場合、捕獲した文字列はループのたびに未定義値相当にクリアされる。そのためキャプチャした文字列を次のループに持ち越すことは出来ない。 |
\N (※Nは正の 整数) |
後方参照。
ECMAScriptの正規表現では、文字列を捕獲する括弧が対応する後方参照よりも先行している必要はない。そのため
対応する括弧が何も捕獲していない時、後方参照は未定義値 ( |
(?<NAME>...) |
名前付きの Note: ES2018/ES9.0でRegExpに導入された表現で、SRELL 2.000以降で使用可能です。 |
\k<NAME> |
Note: ES2018/ES9.0でRegExpに導入された表現で、SRELL 2.000以降で使用可能です。 |
(?:...) |
グループ化。 |
位置にマッチするもの | |
^ |
文字列の最初にマッチ。 |
$ |
文字列の最後にマッチ。 |
\b |
文字クラスの外側では |
\B |
文字クラスの外側では |
(?=...) |
肯定先読み。たとえば |
(?!...) |
否定先読み。たとえば |
(?<=...) |
肯定戻り読み。たとえば
Note: SRELL 1では、(...)内は |
(?<!...) |
否定戻り読み。たとえば
Note: SRELL 1では、(...)内は固定幅の文字列のみ指定可能。固定幅でない時は |
'\'
で終わっていたり '\'
がこの表にない組み合わせで使われたりした時は、error_escape
がthrow
されてきます。後者についてはSRELL 2.300まで「'\' に続く文字そのもの」として解釈されていましたが、2.301以降はECMAScriptの仕様に合わせてエラー扱いするようになりました。/\1\u0030/
のように数字のほうはコードポイントで書く」「/\1[0]/
のように、数字のほうはその1文字だけからなる文字クラスとして書く」の2つがあります。SRELLのパターンコンパイラはどちらの書き方も同じ内部表現に変換します。\ooo
や \0ooo
のような8進数表現が存在しません。ECMAScriptの仕様では、\
に0
が続く時は<NUL> (\u0000
) として解釈し、1-9で始まる数字が続く時は後方参照として解釈、その際対応する()
が正規表現中に存在しなければエラーと定められています。
[^]
(空集合の補集合)で代用できます。/(?=\p{sc=Latin})\p{Ll}/
→ ラテン文字の小文字にのみマッチ)。またこの応用で、否定先読みを使うと減算相当の処理も出来ます(例:/(?!\p{sc=Latin})\p{Ll}/
→ ラテン文字ではない小文字にマッチ)。Unicode対応のために、次のような型がSRELLには追加されています。
型 | basic_regex | match_results | sub_match |
文字列の解釈 | 備考 |
---|---|---|---|---|---|
char |
u8cregex |
u8ccmatch u8csmatch |
u8ccsub_match u8cssub_match |
UTF-8 | SRELL_CPP20_CHAR8_ENABLED マクロ非定義時は、それぞれu8regex , u8[cs]match , u8[cs]sub_match という別名も定義される。 |
wchar_t |
u16wregex |
u16wcmatch u16wsmatch |
u16wcsub_match u16wssub_match |
UTF-16 | WCHAR_MAX が0xFFFF 以上、0x10FFFF 未満の場合のみ。 |
u32wregex |
u32wcmatch u32wsmatch |
u32wcsub_match u32wssub_match |
UTF-32 | WCHAR_MAX が0x10FFFF 以上の場合のみ。 |
|
char8_t |
u8regex |
u8cmatch u8smatch |
u8csub_match u8ssub_match |
UTF-8 | SRELL_CPP20_CHAR8_ENABLED マクロ定義時のみchar8_t が使われる。非定義時は前出のu8c* 型の単なる別名となる。 |
char16_t |
u16regex |
u16cmatch u16smatch |
u16csub_match u16ssub_match |
UTF-16 | SRELL_CPP11_CHAR1632_ENABLED マクロ定義時のみ。 |
char32_t |
u32regex |
u32cmatch u32smatch |
u32csub_match u32ssub_match |
UTF-32 |
SRELL 2.630以降、正規表現中に不正なUTF-8のシークウェンスがあると basic_regex
はregex_error(regex_constants::error_utf8)
をthrow
するようになっています(regex_constants::error_utf8
はSRELLの拡張で<regex> にはありません)。
各prefixの意味するところは次の通りです。
char
型配列またはstd::string
型インスタンスをUTF-8文字列として扱う(SRELL 2.100で導入。SRELL 2.002まではこれがu8-というprefixを使用していました)。wchar_t
型配列またはstd::wstring
型インスタンスをUTF-16文字列として扱う(WCHAR_MAX
が0xFFFF
以上0x10FFFF
未満の時のみ定義される)。wchar_t
型配列またはstd::wstring
型インスタンスをUTF-32文字列として扱う(WCHAR_MAX
が0x10FFFF
以上の時のみ定義)。char8_t
型に対応しているかどうか(SRELL_CPP20_CHAR8_ENABLED
マクロ定義の有無で判断)で次のように変わります。
char8_t
対応なら:char8_t
型配列またはstd::u8string
型インスタンスをUTF-8文字列として扱う。char8_t
未対応なら:前出のu8c-に同じ。単なる別名としてtypedef
される。char16_t
型配列またはstd::u16string
型インスタンスをUTF-16文字列として扱う。char32_t
型配列またはstd::u32string
型インスタンスをUTF-32文字列として扱う。
先の表では省略しましたが、このルールに基づいてregex_iterator
やregex_token_iterator
でも同じようにu(8c?|16w?|32w?)
prefixの付いた型がtypedef
されています。
Unicode対応版の基本的な使い方は次の通りです。
srell::u8regex u8re(u8"UTF-8文字列による正規表現"); srell::u8cmatch u8cm; std::printf("%s\n", srell::regex_search(u8"検索対象となるUTF-8文字列", u8cm, u8re) ? "found!" : "not found..."); srell::u16regex u16re(u"UTF-16文字列による正規表現"); srell::u16cmatch u16cm; std::printf("%s\n", srell::regex_search(u"検索対象となるUTF-16文字列", u16cm, u16re) ? "found!" : "not found..."); srell::u32regex u32re(U"UTF-32文字列による正規表現"); srell::u32cmatch u32cm; std::printf("%s\n", srell::regex_search(U"検索対象となるUTF-32文字列", u32cm, u32re) ? "found!" : "not found..."); srell::u16wregex u16wre(L"UTF-16文字列による正規表現"); srell::u16wcmatch u16wcm; std::printf("%s\n", srell::regex_search(L"検索対象となるUTF-16文字列", u16wcm, u16wre) ? "found!" : "not found..."); // 上3行と下3行とは排他的。wchar_tが21ビット未満なら上、以上なら下。 srell::u32wregex u32wre(L"UTF-32文字列による正規表現"); srell::u32wcmatch u32wcm; std::printf("%s\n", srell::regex_search(L"検索対象となるUTF-32文字列", u32wcm, u32wre) ? "found!" : "not found...");
C++11より前のコンパイラでは、wchar_t
が16ビット以上かつ21ビット未満の環境ならu8c-型とu16w-型とが、wchar_t
が21ビット以上ある環境ならu8c-型とu32w-型とがそれぞれ利用可能です。もっともそのような環境でも、SRELLをincludeする前に次のようなコードを書いておけば、u8c-型、u16-型、u32-型を3つとも利用することができます。
typedef uint_least16_t char16_t; // 16ビット以上ある符号無しの型をtypedefする。 typedef uint_least32_t char32_t; // 32ビット以上ある符号無しの型をtypedefする。 namespace std { typedef basic_string<char16_t> u16string; typedef basic_string<char32_t> u32string; } #define SRELL_CPP11_CHAR1632_ENABLED // 手動でonにする。
ちなみに、UTF-8やUTF-16に固有の処理をしているのは、basic_regex
に対してテンプレート引数として渡されるu8regex_traits
、u16regex_traits
です。このことを利用して、たとえば basic_regex<uint32_t, u16regex_traits<uint32_t> >
のような「UTF-16文字列をuint32_t
型の配列で処理する型」なるものを作ることもできます。
SRELL 2では名前付きキャプチャ (named-capture) 用に、次のメンバ函数がmatch_results
クラスに追加されています。
difference_type length(const string_type &sub) const;
difference_type position(const string_type &sub) const;
string_type str(const string_type &sub) const;
const_reference operator[](const string_type &sub) const;
// 以下はSRELL 2.650以降。
difference_type length(const char_type *sub) const;
difference_type position(const char_type *sub) const;
string_type str(const char_type *sub) const;
const_reference operator[](const char_type *sub) const;
使い方は<regex>にある同名の函数と同じです。ただ引数として括弧の番号ではなく括弧の名前を渡すという点のみが異なります。
// 使用例。 srell::regex e("-(?<digits>\\d+)-"); srell::cmatch m; if (srell::regex_search("1234-5678-90ab-cdef", m, e)) { const std::string by_number(m.str(1)); // std::regexにもある括弧の番号を使ったアクセス。 const std::string by_name(m.str("digits")); // 名前を使って同じ括弧へアクセス。SRELL 2の独自拡張。 std::printf("results: bynumber=%s byname=%s\n", by_number.c_str(), by_name.c_str()); } // results: bynumber=5678 byname=5678 と表示される。
SRELL 2には次のフラグオプションが追加されています。
namespace regex_constants { static const syntax_option_type dotall; // シングルライン指定。 // シングルラインモード用フラグオプションを参照。 }
他のsyntax_option_type
型の値と同様に、この値はbasic_regex
内でも定義されています。
正規表現パターンのコンパイル時に dotall
フラグオプション(regex_constants::syntax_option_type
型またはbasic_regex::flag_type
型)が指定されていると '.' の挙動が変わります。
このフラグオプションはPerl 5の //s
に相当します。
SRELL 2.600以降、引数としてBidirectionalIterator
を3つ取るオーヴァーロードが追加されています。
template <class BidirectionalIterator, class Allocator, class charT, class traits> bool regex_search( BidirectionalIterator first, BidirectionalIterator last, BidirectionalIterator lookbehind_limit, match_results<BidirectionalIterator, Allocator> &m, const basic_regex<charT, traits> &e, const regex_constants::match_flag_type flags = regex_constants::match_default); template <class BidirectionalIterator, class charT, class traits> bool regex_search( BidirectionalIterator first, BidirectionalIterator last, BidirectionalIterator lookbehind_limit, const basic_regex<charT, traits> &e, const regex_constants::match_flag_type flags = regex_constants::match_default);
3つ目のイテレータであるlookbehind_limit
は、戻り読み (lookbehind) が行われる際の「逆行して良い限界位置」を指定するためのものです。
const char text[] = "0123456789abcdefghijklmnopqrstuvwxyz"; const char* const begin = text; const char* const end = text + std::strlen(text); const char* const first = text + 10; // 'a' の位置に合わせる。 const srell::regex re("(?<=^\\d+)."); srell::cmatch match; std::printf("matched %d\n", srell::regex_search(first, end, match, re)); // 戻り読みも [first, end) の範囲でのみ行われるのでマッチしない。 std::printf("matched %d\n", srell::regex_search(first, end, begin, match, re)); // beginまで逆行できるのでマッチする。 // 即ち3イテレータ版は、[begin, end) というシークウェンスに対して // firstよりsearchを始める。
上の例にもあります通り3イテレータ版では、^
は first
(第1引数として渡したほう)ではなく begin
(第3引数として渡したほう)にマッチするようになります。
また3イテレータ版使用時には、match_results
のposition()
は第3引数として渡した位置からの距離を返すようになります。これに対してmatch_results
のprefix().first
には、第1引数として渡した位置がセットされます。
※3イテレータ版追加に伴い、SRELL 2.300~2.500で導入していた指定方法は廃止しました。
<regex>は、次の6つの正規表現エンジンから選択する仕組みになっています(既定はECMAScript)。
ECMAScript (既定) |
ECMAScriptの正規表現からUnicodeに依存した要素(\sの定義など)を取り除き、代わりにlocale依存の処理とC++の規格書29.13 [re.grammar] にある拡張とを追加したもの。
|
---|---|
basic |
POSIXのbasic regular expression相当。 |
extended |
POSIXのextended regular expression相当。 |
awk |
POSIXのawk相当。 |
grep |
POSIXのgrep相当。 |
egrep |
POSIXのegrep相当。 |
一方SRELLにはこのような選択肢はなく、常にECMAScript互換のエンジンで動きます。この関係で、<regex>で定義されているフラグオプションのうち次のものは、たとえ指定してもSRELLでは無視されます。
syntax_option_type
(かつbasic_regex
のflag_type
)icase
とmultiline
以外のすべてmatch_flag_type
match_any
format_sed
<regex>のECMAScriptモードとSRELLとの違いは次の通りです。
同じECMAScriptベースでありながらも<regex>とSRELLとはどちらもどちらの上位互換になっていません。
u8regex
, u8cregex
, u16regex
, u16wregex
以外の型は、入力文字列をUnicode値の連続として解釈します。例えばCHAR_BIT
が8
である環境でsrell::regex
(srell::basic_regex<char>
) を使うと、入力文字列に現れる0x00
~0xFF
はそのままU+0000~U+00FFとして解釈されます。
UnicodeのU+0000~U+00FFはISO-8859-1と同じですので、結果的にsrell::regex
はISO-8859-1に対応しているとも言えます。
またsrell::regex
は、バイナリデータ中から特定のパターンを見つけ出すような場合にも使うことが出来ます。
srell::wregex
(srell::basic_regex<wchar_t>
) も同じで、文字列中の0x00
~WCHAR_MAX
までがそのまま同じ値のUnicode値として解釈されます。
ECMAScriptの正規表現(およびその元となったPerlの正規表現)では通常、バックトラッキングと呼ばれる方法を使って照合が行われます。このバックトラッキング方式には、「量指定子(回数指定)が入れ子になっている正規表現」や「量指定子を伴う文字ないし文字クラスが連続していて、かつそれらが互いに排他的な集合になっていない正規表現」で検索を行うと、オートマトンが著しい長考に入ってしまうことがあるという問題が存在します。
次のような例が有名です。
残念ながらこの現象に対しては、あらゆる状況に適用できる根本的な解決策というものが見つかっていません。そこで制御が長時間返ってこなくなる事態を避けるため、SRELLは特定の位置からの照合が一定回数以上失敗すると、regex_error(regex_constants::error_complexity)
を throw
するようになっています。
回数の既定値は16777216(256の3乗)ですが、アルゴリズム函数(regex_search()
やregex_match()
など)に渡すbasic_regex
型インスタンスのlimit_counter
変数に任意の値を代入することで変更することも出来ます。
SRELL 2.300~2.500には次のような拡張がありました。
match_results
: BidirectionalIterator lookbehind_limit
(Lookbehindの逆行限界指定)match_flag_type
: match_lblim_avail
(アルゴリズム函数に対して前記match_results.lookbehind_limit
が有効であることを伝えるためのフラグオプション)
match_lblim_avail
フラグがセットされると戻り読み (lookbehind) が行われる際、アルゴリズム函数に渡したmatch_results
型インスタンスのlookbehind_limit
メンバの値を「逆行できる限界」と見なすようになります。
const char text[] = "0123456789abcdefghijklmnopqrstuvwxyz"; const char* const begin = text; const char* const end = text + std::strlen(text); const char* const first = text + 10; // 'a' の位置に合わせる。 const srell::regex re("(?<=^\\d+)."); srell::cmatch match; match.lookbehind_limit = begin; std::printf("matched %d\n", srell::regex_search(first, end, match, re)); // 戻り読みも [first, end) の範囲でのみ行われるのでマッチしない。 std::printf("matched %d\n", srell::regex_search(first, end, match, re, srell::regex_constants::match_lblim_avail)); // match.lookbehind_limitまで逆行できるのでマッチする。 // 即ち match_lblim_avail指定時は [match.lookbehind_limit, end) というシークウェンスに対して // firstよりsearchを始める。
上の例にもあります通りmatch_lblim_avail
が指定された時は、^
は first
ではなく match.lookbehind_limit
にマッチするようになります。
SRELL 2.600以降では、regex_searchの引数としてlookbehindの逆行限界位置を指定できるようにしました。これに伴い上記の方法は廃止しました。
ちなみに2.600で導入した「3イテレータ方式」を最初から採用しなかった理由についてはこちらに記してあります。
SRELL 2.002までu8-というprefixは、「char型の文字列をUTF-8文字列として扱うクラス型」であることを意味していました。しかしC++20よりchar8_t
型が導入されることが決まり、今後u8-というprefixは標準ライブラリでもchar8_t
型用に使われるようになってゆくことが予想されます。
そこで標準ライブラリの命名規則との不整合を避けるため、SRELL 2.100以降では「char
型の文字列をUTF-8文字列として扱う」ことを意味するprefixをu8-からu8c-に変更しました。
basic_regex
: u8cregex
match_results
: u8ccmatch
, u8csmatch
sub_match
: u8ccsub_match
, u8cssub_match
regex_iterator
: u8ccregex_iterator
, u8csregex_iterator
regex_token_iterator
: u8ccregex_token_iterator
, u8csregex_token_iterator
一方u8-というprefixはSRELL 2.100以降、char8_t
型と関連づけられるようになっています。ただしお使いのコンパイラがchar8_t
に未対応である場合、後方互換のためu8-というprefix付きのクラスは、対応するu8c-付きクラスの単なるtypedef
として定義されるようになっています。
Prefix | SRELL 2.002まで | SRELL 2.100以降 | |
---|---|---|---|
char8_t未対応コンパイラ | char8_t対応コンパイラ | ||
u8- | char 型の文字列をUTF-8として扱う |
char8_t 型の文字列をUTF-8として扱う |
|
u8c- | (Prefix自体未使用) | char 型の文字列をUTF-8として扱う |
SRELLのversion 1.xxxでは、ECMAScript 2017 (ES8) 規格書の21.2 RegExp (Regular Expression) Objects で定義されている正規表現に固定幅の戻り読み (lookbehind assertions) を加えたものが利用できました。
ただ次のような経緯により、SRELL 1とSRELL 2とでは戻り読みの挙動に違いがあります。
ECMAScript (JavaScript) の仕様を策定しているTC39はRegExpに追加する戻り読みについて、Perl5, Pythonなど多くのスクリプト言語が採用している固定幅限定の戻り読みではなく、制限のない可変幅の戻り読みを採用しました。
一見すると後者は前者の上位互換のように思われますが、実のところこれらは次のような場合に異なる結果をもたらします。
"abcd" =~ /(?<=(.){2})./
// 固定幅の戻り読みなら: $& = "c", $1 = "b".
// オートマトンは戻り読み内でも通常通り左から右へと文字列を照合してゆくため、
// "c" の直前にある "b" が「$1によって最後にキャプチャされたもの」となる。
// 可変幅の戻り読みなら: $& = "c", $1 = "a".
// オートマトンは戻り読み内では右から左へと走るため、"c" から2つ離れた
// "a"が「$1によって最後にキャプチャされたもの」となる。
SRELL 1が独自拡張として採用していたのは固定幅の戻り読みでした。対してSRELL 2には、RegExpの機能拡張に追随して可変幅の戻り読みが実装されています。このためSRELL 1 → SRELL 2では breaking change が発生しています。
以下は外部のサイトです。