[インデックス 11697] ファイルの概要
このコミットは、Go言語の標準ライブラリregexp
パッケージにおける正規表現の置換機能に、キャプチャグループ(サブマッチ)の参照機能(substitution)を追加するものです。具体的には、ReplaceAllString
およびReplaceAll
関数が置換文字列内で$1
や${name}
のような形式でサブマッチを参照できるようになり、さらにこの置換機能に直接アクセスするためのExpand
およびExpandString
関数が追加されました。
コミット
commit 7201ba2171a9b15d3de3f705335d37afc7e7c85a
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 7 23:46:47 2012 -0500
regexp: allow substitutions in Replace, ReplaceString
Add Expand, ExpandString for access to the substitution functionality.
Fixes #2736.
R=r, bradfitz, r, rogpeppe, n13m3y3r
CC=golang-dev
https://golang.org/cl/5638046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7201ba2171a9b15d3de3f705335d37afc7e7c85a
元コミット内容
正規表現の置換関数であるReplaceAllString
とReplaceAll
において、置換文字列内でキャプチャグループ(サブマッチ)を参照する機能(substitution)を許可する変更です。また、このsubstitution機能に直接アクセスするための新しい関数Expand
とExpandString
が追加されました。この変更は、Go issue #2736を修正するものです。
変更の背景
このコミットは、Go言語の正規表現パッケージregexp
の機能拡張として行われました。特に、Fixes #2736
という記述から、GoのIssueトラッカーに登録されていた特定の課題を解決するために導入されたことがわかります。
Issue #2736は、regexp
パッケージのReplaceAllString
関数が、置換文字列内で正規表現のキャプチャグループを参照する機能(例: $1
, $2
など)をサポートしていないという要望でした。多くのプログラミング言語や正規表現エンジンでは、この「後方参照」または「置換」機能が標準的に提供されており、マッチした部分文字列やそのサブマッチを基に、より柔軟な文字列置換を行うことが可能です。
この機能がないため、ユーザーは正規表現でマッチした部分を複雑なロジックで置換したい場合に、一度マッチした文字列を取得し、手動でサブマッチを抽出し、それらを組み合わせて新しい文字列を生成するという、より冗長なコードを書く必要がありました。このコミットは、このような手間を省き、regexp
パッケージの利便性と表現力を向上させることを目的としています。
前提知識の解説
正規表現 (Regular Expression)
正規表現は、文字列のパターンを記述するための強力なツールです。特定の文字の並び、繰り返し、選択肢などを簡潔に表現できます。プログラミングにおいて、文字列の検索、置換、検証などに広く利用されます。
キャプチャグループ (Capturing Groups)
正規表現において、括弧()
で囲まれた部分は「キャプチャグループ」と呼ばれます。これは、マッチした文字列全体の一部を「キャプチャ」(捕捉)し、後で参照できるようにする機能です。キャプチャグループは左から順に番号が振られ($1
, $2
, ...)、また名前を付けることもできます((?P<name>...)
)。
置換 (Substitution)
文字列置換は、あるパターンにマッチした文字列を別の文字列に置き換える操作です。正規表現を用いた置換では、単に固定の文字列に置き換えるだけでなく、キャプチャグループの内容を置換文字列に挿入する機能がよく提供されます。これを「後方参照による置換」または単に「置換(substitution)」と呼びます。
例えば、正規表現/(\w+)\s(\w+)/
が「Hello World」にマッチした場合、$1
は「Hello」を、$2
は「World」を参照します。置換文字列に$2 $1
を指定すると、「World Hello」という結果が得られます。
Go言語のregexp
パッケージ
Go言語の標準ライブラリには、正規表現を扱うためのregexp
パッケージが用意されています。このパッケージは、RE2という高速な正規表現エンジンに基づいています。RE2は、バックトラッキングを伴う正規表現の脆弱性(ReDoS)を防ぐために設計されており、線形時間でのマッチングを保証します。
このコミット以前のregexp
パッケージのReplaceAllString
関数は、置換文字列内で$1
のようなサブマッチ参照をサポートしていませんでした。そのため、固定の文字列でしか置換できませんでした。
技術的詳細
このコミットの主要な技術的変更点は、regexp
パッケージの置換関数にサブマッチ参照機能を追加し、そのためのヘルパー関数を導入したことです。
-
ReplaceAllString
およびReplaceAll
の変更:- これらの関数は、置換文字列
repl
内で$
記号を特別に解釈するようになりました。 $1
,$2
, ... は対応するキャプチャグループの内容に展開されます。${name}
形式で名前付きキャプチャグループを参照できます。$$
はリテラルの$
として扱われます。- この展開処理は、新しく追加された内部関数
expand
によって行われます。
- これらの関数は、置換文字列
-
Expand
およびExpandString
関数の追加:- これらの新しい公開関数は、正規表現のマッチ結果(
match []int
)とテンプレート文字列(template
)を受け取り、テンプレート内の$
記号によるサブマッチ参照を展開した結果を返します。 - これにより、ユーザーは
ReplaceAll
系関数を使わずに、正規表現のマッチ結果を基に任意の文字列を構築できるようになります。
- これらの新しい公開関数は、正規表現のマッチ結果(
-
ReplaceAllLiteralString
およびReplaceAllLiteral
関数の追加:- これらの関数は、従来の
ReplaceAllString
およびReplaceAll
の挙動を維持します。つまり、置換文字列repl
内の$
記号を特別に解釈せず、リテラル文字列として扱います。 - これにより、ユーザーはサブマッチ参照を意図しない場合に、安全にリテラル置換を行うことができます。
- これらの関数は、従来の
-
内部ヘルパー関数
expand
およびextract
の導入:expand
関数は、テンプレート文字列とマッチ情報に基づいて、実際にサブマッチの展開を行うロジックを実装しています。extract
関数は、テンプレート文字列から$name
または${name}
形式の変数名を解析し、それが数値インデックスなのか名前付きグループなのかを判断します。この関数は、$
記号の後の文字列を解析し、有効な変数名(数字、文字、アンダースコアの組み合わせ)を抽出します。
-
replaceAll
内部関数の汎用化:ReplaceAllString
,ReplaceAllStringFunc
,ReplaceAllLiteralString
などの共通の置換ロジックを処理するために、replaceAll
という内部関数が導入されました。- この関数は、マッチした部分をどのように置換するかを決定する
repl
という関数型引数を受け取ることで、様々な置換戦略に対応できるようになりました。これにより、コードの重複が削減され、保守性が向上しています。
これらの変更により、Goのregexp
パッケージは、より一般的な正規表現エンジンの置換機能に近づき、開発者にとってより強力で柔軟なツールとなりました。
コアとなるコードの変更箇所
src/pkg/regexp/all_test.go
replaceTests
変数に、新しい置換機能($0
,$1
,${1}
,$noun
,$$
など)をテストするための多数の新しいテストケースが追加されました。replaceLiteralTests
変数も追加され、リテラル置換($
が特別に解釈されない場合)のテストケースが定義されました。TestReplaceAll
関数がReplaceAllString
とReplaceAll
のテストを行うように修正され、新しいTestReplaceAllLiteral
関数がReplaceAllLiteralString
とReplaceAllLiteral
のテストを行うように追加されました。
src/pkg/regexp/regexp.go
-
ReplaceAllString
の変更:// Old: // func (re *Regexp) ReplaceAllString(src, repl string) string { // return re.ReplaceAllStringFunc(src, func(string) string { return repl }) // } // New: func (re *Regexp) ReplaceAllString(src, repl string) string { n := 2 if strings.Index(repl, "$") >= 0 { n = 2 * (re.numSubexp + 1) } b := re.replaceAll(nil, src, n, func(dst []byte, match []int) []byte { return re.expand(dst, repl, nil, src, match) }) return string(b) }
ReplaceAllString
が内部的にexpand
を呼び出すように変更され、置換文字列内の$
記号がサブマッチ参照として解釈されるようになりました。 -
ReplaceAllLiteralString
の追加:func (re *Regexp) ReplaceAllLiteralString(src, repl string) string { return string(re.replaceAll(nil, src, 2, func(dst []byte, match []int) []byte { return append(dst, repl...) })) }
$
記号をリテラルとして扱う新しい置換関数が追加されました。 -
ReplaceAllStringFunc
の変更:// Old: // func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string { // lastMatchEnd := 0 // ... (rest of the old implementation) // } // New: func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string { b := re.replaceAll(nil, src, 2, func(dst []byte, match []int) []byte { return append(dst, repl(src[match[0]:match[1]])...) }) return string(b) }
内部的に
replaceAll
を呼び出すように変更され、コードの共通化が図られました。 -
replaceAll
内部関数の追加:func (re *Regexp) replaceAll(bsrc []byte, src string, nmatch int, repl func(dst []byte, m []int) []byte) []byte { // ... (implementation for common replacement logic) }
ReplaceAll
系の関数が共通して利用する内部ロジックがここに集約されました。 -
ReplaceAll
の変更:// Old: // func (re *Regexp) ReplaceAll(src, repl []byte) []byte { // return re.ReplaceAllFunc(src, func([]byte) []byte { return repl }) // } // New: func (re *Regexp) ReplaceAll(src, repl []byte) []byte { n := 2 if bytes.IndexByte(repl, '$') >= 0 { n = 2 * (re.numSubexp + 1) } srepl := "" b := re.replaceAll(src, "", n, func(dst []byte, match []int) []byte { if len(srepl) != len(repl) { srepl = string(repl) } return re.expand(dst, srepl, src, "", match) }) return b }
ReplaceAll
もexpand
を呼び出すように変更されました。 -
ReplaceAllLiteral
の追加:func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte { return re.replaceAll(src, "", 2, func(dst []byte, match []int) []byte { return append(dst, repl...) }) }
バイトスライス版のリテラル置換関数が追加されました。
-
ReplaceAllFunc
の変更:// Old: // func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte { // lastMatchEnd := 0 // ... (rest of the old implementation) // } // New: func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte { return re.replaceAll(src, "", 2, func(dst []byte, match []int) []byte { return append(dst, repl(src[match[0]:match[1]])...) }) }
内部的に
replaceAll
を呼び出すように変更されました。 -
Expand
およびExpandString
関数の追加:func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte { return re.expand(dst, string(template), src, "", match) } func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte { return re.expand(dst, template, nil, src, match) }
サブマッチ展開機能を提供する新しい公開関数が追加されました。
-
expand
内部関数の追加:func (re *Regexp) expand(dst []byte, template string, bsrc []byte, src string, match []int) []byte { // ... (implementation for expanding substitutions) }
$
記号によるサブマッチ展開の実際のロジックがここに実装されています。 -
extract
内部関数の追加:func extract(str string) (name string, num int, rest string, ok bool) { // ... (implementation for extracting variable names from $name or ${name}) }
テンプレート文字列から変数名を解析するためのヘルパー関数が追加されました。
コアとなるコードの解説
このコミットの核心は、正規表現の置換処理において、マッチした部分文字列だけでなく、その中の特定のキャプチャグループ(サブマッチ)を参照して置換文字列を動的に生成する機能を追加した点にあります。
expand
関数
expand
関数は、この動的な置換の心臓部です。
template
文字列を走査し、$
記号を見つけると、その後の文字を解析して変数名(例:$1
,${name}
,$$
)を特定します。extract
関数を使って変数名を解析し、それが数値インデックス($1
など)なのか、名前付きキャプチャグループ(${name}
など)なのかを判断します。- 対応するキャプチャグループがマッチしていれば、その内容を
dst
(結果のバイトスライス)に追加します。 $$
の場合は、リテラルの$
を追加します。- マッチしないインデックスや存在しない名前付きグループが参照された場合は、空文字列が挿入されます。
replaceAll
関数
replaceAll
関数は、ReplaceAllString
やReplaceAll
といった複数の置換関数で共通して使用される内部ロジックをカプセル化しています。
- これは、入力文字列(
src
またはbsrc
)を走査し、正規表現にマッチする部分を見つけます。 - マッチが見つかるたびに、マッチしていない部分を結果バッファに追加し、その後、
repl
という関数型引数を使ってマッチした部分をどのように置換するかを決定します。 - この
repl
関数が、expand
を呼び出すことでサブマッチ参照を処理したり、単にリテラル文字列を挿入したりする役割を担います。 - 特に重要なのは、空文字列のマッチングに関する処理です。正規表現が空文字列にマッチする場合(例:
a*
が「b」にマッチする)、無限ループを防ぐために、マッチ後に少なくとも1文字は進むようにロジックが組まれています。
extract
関数
extract
関数は、expand
関数から呼び出され、置換テンプレート内の$
記号の後に続く変数名を解析します。
$name
形式の場合、name
は文字、数字、アンダースコアの最長シーケンスとして解釈されます。${name}
形式の場合、name
は{
と}
の間の文字列として解釈されます。- 数値のみの
name
(例:1
,10
)は、キャプチャグループのインデックスとして扱われます。 - それ以外の
name
は、名前付きキャプチャグループとして扱われます。 - 不正な形式(例:
$
,${
の後に閉じ括弧がない)の場合は、$
をリテラルとして扱うように指示します。
これらの関数が連携することで、Goのregexp
パッケージは、より高度で柔軟な文字列置換機能を提供できるようになりました。
関連リンク
- Go Issue 2736: https://github.com/golang/go/issues/2736
- Go CL 5638046: https://golang.org/cl/5638046 (このコミットに対応するGoのコードレビュー変更リスト)
参考にした情報源リンク
- Go言語
regexp
パッケージ公式ドキュメント: https://pkg.go.dev/regexp - 正規表現の置換に関する一般的な情報 (例: MDN Web Docs - String.prototype.replace()): https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace
- RE2正規表現エンジン: https://github.com/google/re2
- Go言語の正規表現に関するブログ記事やチュートリアル (一般的な知識として)