[インデックス 11256] ファイルの概要
このコミットは、Go言語の標準ライブラリである regexp
パッケージに SubexpNames
メソッドを追加するものです。これにより、正規表現内のキャプチャグループに付けられた名前を取得できるようになります。
コミット
commit 21d3721eb873c7a99e570e75deb9046a9aadd0bb
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 19 01:24:01 2012 -0500
regexp: add SubexpNames
Fixes #2440.
R=r, dsymonds
CC=golang-dev
https://golang.org/cl/5559043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21d3721eb873c7a99e570e75deb9046a9aadd0bb
元コミット内容
このコミットは、Go言語の regexp
パッケージに SubexpNames
という新しいメソッドを追加します。このメソッドは、正規表現内で定義された名前付きキャプチャグループの名前を文字列スライスとして返します。これにより、ユーザーは正規表現のマッチ結果を、インデックスだけでなく、より意味のある名前で参照できるようになります。
具体的には、以下のファイルが変更されています。
src/pkg/regexp/all_test.go
:SubexpNames
のテストケースが追加・修正されています。特に、名前付きキャプチャグループ(?P<name>...)
を含む正規表現のテストが追加されています。src/pkg/regexp/regexp.go
:Regexp
構造体にsubexpNames
フィールドが追加され、compile
関数内でこのフィールドが初期化されるようになります。また、SubexpNames
メソッドが実装されています。src/pkg/regexp/syntax/regexp.go
: 正規表現の構文解析を担当するパッケージにCapNames
メソッドが追加されています。これは、正規表現ツリーを走査してキャプチャグループの名前を収集する内部ヘルパー関数です。
この変更は、Issue #2440 を解決するために行われました。
変更の背景
この変更の背景には、Go言語の regexp
パッケージが提供する機能の拡張要求がありました。従来の regexp
パッケージでは、正規表現のマッチ結果からサブマッチ(キャプチャグループ)を取得する際に、インデックス番号(例: match[1]
, match[2]
)を使用する必要がありました。しかし、複雑な正規表現では、どのインデックスがどの部分に対応するのかを把握するのが困難になるという問題がありました。
他の多くの正規表現エンジン(Perl、Python、PCREなど)では、(?P<name>...)
のような構文を使ってキャプチャグループに名前を付ける機能が提供されており、これによりマッチ結果を名前で参照できるようになっています。Goの regexp
パッケージも、この名前付きキャプチャグループの構文自体はサポートしていましたが、その名前をプログラムから取得するAPIが提供されていませんでした。
Issue #2440 は、この機能の欠如を指摘し、名前付きキャプチャグループの名前を取得できるAPIの追加を求めていました。このコミットは、その要求に応える形で SubexpNames
メソッドを導入し、正規表現の利用における利便性と可読性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
正規表現 (Regular Expression):
- 文字列のパターンを記述するための強力なツール。
- キャプチャグループ (Capturing Group): 正規表現の一部を括弧
()
で囲むことで、その部分にマッチした文字列を後で抽出できるようにする機能。 - 名前付きキャプチャグループ (Named Capturing Group):
(?P<name>...)
のように、キャプチャグループに名前を付ける機能。これにより、インデックスではなく名前でマッチした部分を参照できるようになる。Goのregexp
パッケージは、Pythonの構文(?P<name>...)
をサポートしています。
-
Go言語の
regexp
パッケージ:- Go言語の標準ライブラリで、正規表現のコンパイル、マッチング、検索などの機能を提供します。
regexp.Compile
またはregexp.MustCompile
: 正規表現文字列を*regexp.Regexp
型のオブジェクトにコンパイルします。Regexp.FindStringSubmatch
: 正規表現にマッチした文字列全体と、すべてのキャプチャグループにマッチした文字列を文字列スライスとして返します。Regexp.NumSubexp
: 正規表現内のキャプチャグループの総数を返します。
-
Go言語の構造体とメソッド:
- Go言語におけるデータ構造と、それに関連付けられた関数(メソッド)の基本的な概念。
-
抽象構文木 (Abstract Syntax Tree - AST):
- プログラムのソースコードの抽象的な構文構造を木構造で表現したもの。正規表現エンジンは、入力された正規表現文字列を内部的にASTに変換して処理します。
regexp/syntax
パッケージは、このASTの構築と操作を担当します。
- プログラムのソースコードの抽象的な構文構造を木構造で表現したもの。正規表現エンジンは、入力された正規表現文字列を内部的にASTに変換して処理します。
技術的詳細
このコミットの技術的な核心は、正規表現のコンパイル時に名前付きキャプチャグループの情報を抽出し、それを Regexp
オブジェクトに保存し、後で取得できるようにすることです。
-
regexp/syntax
パッケージの変更:- 正規表現の構文解析を行う
regexp/syntax
パッケージにCapNames()
メソッドが追加されました。 - このメソッドは、正規表現のAST (
syntax.Regexp
型) を再帰的に走査し、OpCapture
(キャプチャグループ) オペレータを持つノードを見つけます。 OpCapture
ノードには、キャプチャグループのインデックス (Cap
) と名前 (Name
) が含まれています。CapNames()
は、MaxCap()
で取得できる最大キャプチャインデックスに基づいて[]string
スライスを初期化し、走査中に見つかった名前を対応するインデックスに格納します。名前がないキャプチャグループや、インデックス0(正規表現全体)には空文字列が格納されます。
- 正規表現の構文解析を行う
-
regexp
パッケージのRegexp
構造体:Regexp
構造体にsubexpNames []string
という新しいフィールドが追加されました。このフィールドは、コンパイルされた正規表現のキャプチャグループの名前を保持します。
-
regexp
パッケージのcompile
関数:- 正規表現をコンパイルする
compile
関数内で、syntax.Regexp
オブジェクトのCapNames()
メソッドが呼び出され、キャプチャグループの名前のスライスが取得されます。 - 取得された
capNames
スライスは、新しく作成されるRegexp
オブジェクトのsubexpNames
フィールドに格納されます。
- 正規表現をコンパイルする
-
regexp
パッケージのSubexpNames
メソッド:Regexp
型にSubexpNames() []string
メソッドが追加されました。- このメソッドは、単に
re.subexpNames
フィールドの値を返します。 - ドキュメントには、
names[0]
が常に空文字列であること、そしてスライスが変更されるべきではないことが明記されています。これは、FindStringSubmatch
などが返すマッチ結果のスライスm
と同様に、m[i]
の名前がSubexpNames()[i]
で取得できるという一貫性を提供するためです。
この一連の変更により、正規表現がコンパイルされる際に名前付きキャプチャグループの情報が適切に抽出・保存され、ユーザーは SubexpNames
メソッドを通じてその情報を取得できるようになります。
コアとなるコードの変更箇所
src/pkg/regexp/regexp.go
type Regexp struct {
expr string // original expression
prog *syntax.Prog // compiled program
prefix string // required prefix in match
prefixComplete bool // prefix is the entire match
prefixRune rune // first rune in prefix
cond syntax.EmptyOp // empty-width conditions required at start of match
numSubexp int
subexpNames []string // 追加: キャプチャグループの名前を保持するフィールド
longest bool
// cache of machines for running regexp
// (small optimization: avoid allcating new machine for each call)
matchMachine *machine
matchMachineMu sync.Mutex
}
func compile(expr string, mode syntax.Flags, longest bool) (*Regexp, error) {
re, err := syntax.Parse(expr, mode)
if err != nil {
return nil, err
}
maxCap := re.MaxCap()
capNames := re.CapNames() // 追加: syntaxパッケージからキャプチャグループ名を取得
re = re.Simplify()
prog, err := syntax.Compile(re)
if err != nil {
return nil, err
}
regexp := &Regexp{
expr: expr,
prog: prog,
numSubexp: maxCap,
subexpNames: capNames, // 追加: 取得した名前を構造体に設定
cond: prog.StartCond(),
longest: longest,
}
regexp.prefix, regexp.prefixComplete = prog.Prefix()
if regexp.prefix != "" {
regexp.prefixRune = []rune(regexp.prefix)[0]
}
return regexp, nil
}
// SubexpNames returns the names of the parenthesized subexpressions
// in this Regexp. The name for the first sub-expression is names[1],
// so that if m is a match slice, the name for m[i] is SubexpNames()[i].
// Since the Regexp as a whole cannot be named, names[0] is always
// the empty string. The slice should not be modified.
func (re *Regexp) SubexpNames() []string { // 追加: SubexpNamesメソッドの実装
return re.subexpNames
}
src/pkg/regexp/syntax/regexp.go
// CapNames walks the regexp to find the names of capturing groups.
func (re *Regexp) CapNames() []string { // 追加: キャプチャグループ名を収集するメソッド
names := make([]string, re.MaxCap()+1)
re.capNames(names)
return names
}
func (re *Regexp) capNames(names []string) { // 追加: CapNamesのヘルパー関数(再帰的にASTを走査)
if re.Op == OpCapture {
names[re.Cap] = re.Name
}
for _, sub := range re.Sub {
sub.capNames(names)
}
}
src/pkg/regexp/all_test.go
type subexpCase struct {
input string
num int
names []string // 追加: 期待されるキャプチャグループ名
}
var subexpCases = []subexpCase{
{``, 0, nil},
{`.*`, 0, nil},
{`abba`, 0, nil},
{`ab(b)a`, 1, []string{"", ""}},
{`ab(.*)a`, 1, []string{"", ""}},
{`(.*)ab(.*)a`, 2, []string{"", "", ""}},
{`(.*)(ab)(.*)a`, 3, []string{"", "", "", ""}},
{`(.*)((a)b)(.*)a`, 4, []string{"", "", "", "", ""}},
{`(.*)(\\(ab)(.*)a`, 3, []string{"", "", "", ""}},
{`(.*)(\\(a\\)b)(.*)a`, 3, []string{"", "", "", ""}},
{`(?P<foo>.*)(?P<bar>(a)b)(?P<foo>.*)a`, 4, []string{"", "foo", "bar", "", "foo"}}, // 名前付きキャプチャグループのテストケース
}
func TestSubexp(t *testing.T) { // NumSubexpとSubexpNamesの両方をテストするように変更
for _, c := range subexpCases {
re := MustCompile(c.input)
n := re.NumSubexp()
if n != c.num {
t.Errorf("%q: NumSubexp = %d, want %d", c.input, n, c.num)
continue
}
names := re.SubexpNames() // SubexpNamesの呼び出し
if len(names) != 1+n {
t.Errorf("%q: len(SubexpNames) = %d, want %d", c.input, len(names), 1+n)
continue
}
if c.names != nil {
for i := 0; i < 1+n; i++ {
if names[i] != c.names[i] {
t.Errorf("%q: SubexpNames[%d] = %q, want %q", c.input, i, names[i], c.names[i])
}
}
}
}
}
コアとなるコードの解説
このコミットのコアとなる変更は、regexp
パッケージの Regexp
構造体に subexpNames
フィールドを追加し、正規表現のコンパイル時にこのフィールドにキャプチャグループの名前を格納するようにした点です。
-
Regexp
構造体の拡張:subexpNames []string
フィールドが追加されました。これは、正規表現内の各キャプチャグループに対応する名前(もしあれば)を格納するための文字列スライスです。インデックス0は正規表現全体に対応するため、常に空文字列が格納されます。
-
compile
関数の変更:- 正規表現のASTを生成した後、
re.CapNames()
を呼び出して、そのASTからキャプチャグループの名前のリストを取得します。 - 取得した
capNames
スライスは、新しく作成されるRegexp
オブジェクトのsubexpNames
フィールドに代入されます。これにより、コンパイルされた正規表現オブジェクトが、そのキャプチャグループの名前に関する情報を内部的に保持するようになります。
- 正規表現のASTを生成した後、
-
SubexpNames
メソッドの追加:Regexp
型にSubexpNames()
メソッドが追加されました。このメソッドは非常にシンプルで、単にre.subexpNames
フィールドの値を返します。- このメソッドのドキュメントは重要で、
names[0]
が常に空文字列であること、そしてm[i]
(マッチ結果のスライス) の名前がSubexpNames()[i]
で取得できるという、インデックスとの対応関係を明確にしています。
-
regexp/syntax
パッケージのCapNames
メソッド:- このメソッドは、正規表現のAST (
syntax.Regexp
) を再帰的に走査し、OpCapture
(キャプチャグループ) オペレータを持つノードを見つけます。 OpCapture
ノードには、キャプチャグループのインデックス (Cap
) と、名前付きキャプチャグループの場合にその名前 (Name
) が含まれています。CapNames
は、MaxCap()
で得られる最大キャプチャインデックスに基づいて[]string
スライスを初期化し、見つかった名前を対応するインデックスに格納していきます。これにより、正規表現の構文レベルでキャプチャグループの名前を抽出し、それを上位のregexp
パッケージに渡す準備が整います。
- このメソッドは、正規表現のAST (
これらの変更により、Goの regexp
パッケージは、名前付きキャプチャグループの情報を透過的に扱い、ユーザーがプログラム的にその名前を取得できる強力な機能を提供できるようになりました。これにより、正規表現の可読性と保守性が大幅に向上します。
関連リンク
- Go Issue #2440: https://github.com/golang/go/issues/2440 - このコミットが解決した元のIssue。名前付きキャプチャグループの名前を取得する機能の要望が議論されています。
- Go Code Review 5559043: https://golang.org/cl/5559043 - このコミットのコードレビューページ。変更内容に関する詳細な議論やコメントが含まれています。
参考にした情報源リンク
- Go言語
regexp
パッケージ公式ドキュメント: https://pkg.go.dev/regexp - Go言語
regexp/syntax
パッケージ公式ドキュメント: https://pkg.go.dev/regexp/syntax - Python
re
モジュール (名前付きキャプチャグループの構文の参考): https://docs.python.org/3/library/re.html#re.match.groupdict - 正規表現のチュートリアル (キャプチャグループについて): https://www.regular-expressions.info/brackets.html (一般的な正規表現の概念理解のため)
- Go言語の正規表現に関するブログ記事やチュートリアル: (具体的なURLは省略しますが、Goの正規表現の基本的な使い方や概念を理解するために参照しました。)