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

[インデックス 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

元コミット内容

正規表現の置換関数であるReplaceAllStringReplaceAllにおいて、置換文字列内でキャプチャグループ(サブマッチ)を参照する機能(substitution)を許可する変更です。また、このsubstitution機能に直接アクセスするための新しい関数ExpandExpandStringが追加されました。この変更は、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パッケージの置換関数にサブマッチ参照機能を追加し、そのためのヘルパー関数を導入したことです。

  1. ReplaceAllStringおよびReplaceAllの変更:

    • これらの関数は、置換文字列repl内で$記号を特別に解釈するようになりました。
    • $1, $2, ... は対応するキャプチャグループの内容に展開されます。
    • ${name}形式で名前付きキャプチャグループを参照できます。
    • $$はリテラルの$として扱われます。
    • この展開処理は、新しく追加された内部関数expandによって行われます。
  2. ExpandおよびExpandString関数の追加:

    • これらの新しい公開関数は、正規表現のマッチ結果(match []int)とテンプレート文字列(template)を受け取り、テンプレート内の$記号によるサブマッチ参照を展開した結果を返します。
    • これにより、ユーザーはReplaceAll系関数を使わずに、正規表現のマッチ結果を基に任意の文字列を構築できるようになります。
  3. ReplaceAllLiteralStringおよびReplaceAllLiteral関数の追加:

    • これらの関数は、従来のReplaceAllStringおよびReplaceAllの挙動を維持します。つまり、置換文字列repl内の$記号を特別に解釈せず、リテラル文字列として扱います。
    • これにより、ユーザーはサブマッチ参照を意図しない場合に、安全にリテラル置換を行うことができます。
  4. 内部ヘルパー関数expandおよびextractの導入:

    • expand関数は、テンプレート文字列とマッチ情報に基づいて、実際にサブマッチの展開を行うロジックを実装しています。
    • extract関数は、テンプレート文字列から$nameまたは${name}形式の変数名を解析し、それが数値インデックスなのか名前付きグループなのかを判断します。この関数は、$記号の後の文字列を解析し、有効な変数名(数字、文字、アンダースコアの組み合わせ)を抽出します。
  5. replaceAll内部関数の汎用化:

    • ReplaceAllString, ReplaceAllStringFunc, ReplaceAllLiteralStringなどの共通の置換ロジックを処理するために、replaceAllという内部関数が導入されました。
    • この関数は、マッチした部分をどのように置換するかを決定するreplという関数型引数を受け取ることで、様々な置換戦略に対応できるようになりました。これにより、コードの重複が削減され、保守性が向上しています。

これらの変更により、Goのregexpパッケージは、より一般的な正規表現エンジンの置換機能に近づき、開発者にとってより強力で柔軟なツールとなりました。

コアとなるコードの変更箇所

src/pkg/regexp/all_test.go

  • replaceTests変数に、新しい置換機能($0, $1, ${1}, $noun, $$など)をテストするための多数の新しいテストケースが追加されました。
  • replaceLiteralTests変数も追加され、リテラル置換($が特別に解釈されない場合)のテストケースが定義されました。
  • TestReplaceAll関数がReplaceAllStringReplaceAllのテストを行うように修正され、新しいTestReplaceAllLiteral関数がReplaceAllLiteralStringReplaceAllLiteralのテストを行うように追加されました。

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
    }
    

    ReplaceAllexpandを呼び出すように変更されました。

  • 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関数は、ReplaceAllStringReplaceAllといった複数の置換関数で共通して使用される内部ロジックをカプセル化しています。

  • これは、入力文字列(srcまたはbsrc)を走査し、正規表現にマッチする部分を見つけます。
  • マッチが見つかるたびに、マッチしていない部分を結果バッファに追加し、その後、replという関数型引数を使ってマッチした部分をどのように置換するかを決定します。
  • このrepl関数が、expandを呼び出すことでサブマッチ参照を処理したり、単にリテラル文字列を挿入したりする役割を担います。
  • 特に重要なのは、空文字列のマッチングに関する処理です。正規表現が空文字列にマッチする場合(例: a*が「b」にマッチする)、無限ループを防ぐために、マッチ後に少なくとも1文字は進むようにロジックが組まれています。

extract関数

extract関数は、expand関数から呼び出され、置換テンプレート内の$記号の後に続く変数名を解析します。

  • $name形式の場合、nameは文字、数字、アンダースコアの最長シーケンスとして解釈されます。
  • ${name}形式の場合、name{}の間の文字列として解釈されます。
  • 数値のみのname(例: 1, 10)は、キャプチャグループのインデックスとして扱われます。
  • それ以外のnameは、名前付きキャプチャグループとして扱われます。
  • 不正な形式(例: $, ${の後に閉じ括弧がない)の場合は、$をリテラルとして扱うように指示します。

これらの関数が連携することで、Goのregexpパッケージは、より高度で柔軟な文字列置換機能を提供できるようになりました。

関連リンク

参考にした情報源リンク