Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 メソッドを導入し、正規表現の利用における利便性と可読性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. 正規表現 (Regular Expression):

    • 文字列のパターンを記述するための強力なツール。
    • キャプチャグループ (Capturing Group): 正規表現の一部を括弧 () で囲むことで、その部分にマッチした文字列を後で抽出できるようにする機能。
    • 名前付きキャプチャグループ (Named Capturing Group): (?P<name>...) のように、キャプチャグループに名前を付ける機能。これにより、インデックスではなく名前でマッチした部分を参照できるようになる。Goの regexp パッケージは、Pythonの構文 (?P<name>...) をサポートしています。
  2. Go言語の regexp パッケージ:

    • Go言語の標準ライブラリで、正規表現のコンパイル、マッチング、検索などの機能を提供します。
    • regexp.Compile または regexp.MustCompile: 正規表現文字列を *regexp.Regexp 型のオブジェクトにコンパイルします。
    • Regexp.FindStringSubmatch: 正規表現にマッチした文字列全体と、すべてのキャプチャグループにマッチした文字列を文字列スライスとして返します。
    • Regexp.NumSubexp: 正規表現内のキャプチャグループの総数を返します。
  3. Go言語の構造体とメソッド:

    • Go言語におけるデータ構造と、それに関連付けられた関数(メソッド)の基本的な概念。
  4. 抽象構文木 (Abstract Syntax Tree - AST):

    • プログラムのソースコードの抽象的な構文構造を木構造で表現したもの。正規表現エンジンは、入力された正規表現文字列を内部的にASTに変換して処理します。regexp/syntax パッケージは、このASTの構築と操作を担当します。

技術的詳細

このコミットの技術的な核心は、正規表現のコンパイル時に名前付きキャプチャグループの情報を抽出し、それを Regexp オブジェクトに保存し、後で取得できるようにすることです。

  1. regexp/syntax パッケージの変更:

    • 正規表現の構文解析を行う regexp/syntax パッケージに CapNames() メソッドが追加されました。
    • このメソッドは、正規表現のAST (syntax.Regexp 型) を再帰的に走査し、OpCapture (キャプチャグループ) オペレータを持つノードを見つけます。
    • OpCapture ノードには、キャプチャグループのインデックス (Cap) と名前 (Name) が含まれています。
    • CapNames() は、MaxCap() で取得できる最大キャプチャインデックスに基づいて []string スライスを初期化し、走査中に見つかった名前を対応するインデックスに格納します。名前がないキャプチャグループや、インデックス0(正規表現全体)には空文字列が格納されます。
  2. regexp パッケージの Regexp 構造体:

    • Regexp 構造体に subexpNames []string という新しいフィールドが追加されました。このフィールドは、コンパイルされた正規表現のキャプチャグループの名前を保持します。
  3. regexp パッケージの compile 関数:

    • 正規表現をコンパイルする compile 関数内で、syntax.Regexp オブジェクトの CapNames() メソッドが呼び出され、キャプチャグループの名前のスライスが取得されます。
    • 取得された capNames スライスは、新しく作成される Regexp オブジェクトの subexpNames フィールドに格納されます。
  4. 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 フィールドを追加し、正規表現のコンパイル時にこのフィールドにキャプチャグループの名前を格納するようにした点です。

  1. Regexp 構造体の拡張:

    • subexpNames []string フィールドが追加されました。これは、正規表現内の各キャプチャグループに対応する名前(もしあれば)を格納するための文字列スライスです。インデックス0は正規表現全体に対応するため、常に空文字列が格納されます。
  2. compile 関数の変更:

    • 正規表現のASTを生成した後、re.CapNames() を呼び出して、そのASTからキャプチャグループの名前のリストを取得します。
    • 取得した capNames スライスは、新しく作成される Regexp オブジェクトの subexpNames フィールドに代入されます。これにより、コンパイルされた正規表現オブジェクトが、そのキャプチャグループの名前に関する情報を内部的に保持するようになります。
  3. SubexpNames メソッドの追加:

    • Regexp 型に SubexpNames() メソッドが追加されました。このメソッドは非常にシンプルで、単に re.subexpNames フィールドの値を返します。
    • このメソッドのドキュメントは重要で、names[0] が常に空文字列であること、そして m[i] (マッチ結果のスライス) の名前が SubexpNames()[i] で取得できるという、インデックスとの対応関係を明確にしています。
  4. regexp/syntax パッケージの CapNames メソッド:

    • このメソッドは、正規表現のAST (syntax.Regexp) を再帰的に走査し、OpCapture (キャプチャグループ) オペレータを持つノードを見つけます。
    • OpCapture ノードには、キャプチャグループのインデックス (Cap) と、名前付きキャプチャグループの場合にその名前 (Name) が含まれています。
    • CapNames は、MaxCap() で得られる最大キャプチャインデックスに基づいて []string スライスを初期化し、見つかった名前を対応するインデックスに格納していきます。これにより、正規表現の構文レベルでキャプチャグループの名前を抽出し、それを上位の regexp パッケージに渡す準備が整います。

これらの変更により、Goの regexp パッケージは、名前付きキャプチャグループの情報を透過的に扱い、ユーザーがプログラム的にその名前を取得できる強力な機能を提供できるようになりました。これにより、正規表現の可読性と保守性が大幅に向上します。

関連リンク

  • Go Issue #2440: https://github.com/golang/go/issues/2440 - このコミットが解決した元のIssue。名前付きキャプチャグループの名前を取得する機能の要望が議論されています。
  • Go Code Review 5559043: https://golang.org/cl/5559043 - このコミットのコードレビューページ。変更内容に関する詳細な議論やコメントが含まれています。

参考にした情報源リンク