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

[インデックス 12579] ファイルの概要

このコミットは、Goコマンドラインツール (cmd/go) のパッケージマッチングロジックを改善し、net/... のようなパターンが net パッケージ自体も含むように変更したものです。これにより、ユーザーが特定のパッケージとそのサブパッケージをまとめて指定する際の利便性が向上しました。

変更されたファイルは以下の通りです。

  • src/cmd/go/doc.go: Goコマンドのドキュメントファイル。パッケージパターンの説明が更新されました。
  • src/cmd/go/help.go: Goコマンドのヘルプメッセージファイル。パッケージパターンの説明が更新されました。
  • src/cmd/go/main.go: Goコマンドの主要なロジックが含まれるファイル。パッケージマッチングとパス展開の関数が変更されました。
  • src/cmd/go/match_test.go: 新規追加されたテストファイル。パッケージマッチングの新しい挙動を検証します。

合計で4つのファイルが変更され、81行が追加、21行が削除されました。

コミット

  • コミットハッシュ: b70925d6999bbe455cfa012401561fa19969153f
  • 作者: Russ Cox rsc@golang.org
  • コミット日時: 2012年3月12日 月曜日 16:34:24 -0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/b70925d6999bbe455cfa012401561fa19969153f

元コミット内容

cmd/go: make net/... match net too

Otherwise there's no good way to get both, and it comes up often.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5794064

変更の背景

この変更の背景には、Goコマンドのパッケージ指定におけるユーザーエクスペリエンスの改善があります。以前のGoコマンドでは、net/... のようなパターンは net パッケージ自体を含まず、net のサブパッケージ(例: net/http, net/rpc など)のみを対象としていました。しかし、多くのシナリオでユーザーは net パッケージとそのすべてのサブパッケージをまとめて操作したいと考えることが頻繁にありました。

コミットメッセージにある「Otherwise there's no good way to get both, and it comes up often.」(さもなければ両方を取得する良い方法がなく、それは頻繁に発生する)という記述が、この問題意識を明確に示しています。この変更により、net/... のようなパターンが net パッケージ自体も含むようになることで、ユーザーはより直感的かつ効率的にパッケージを指定できるようになりました。これは、Goエコシステムにおける開発者の生産性向上に寄与する重要な改善です。

前提知識の解説

このコミットを理解するためには、Go言語におけるパッケージの概念、特にGoコマンドがパッケージパスをどのように解釈し、パターンマッチングを行うかについての知識が必要です。

Goパッケージとインポートパス

Go言語では、コードは「パッケージ」という単位で整理されます。各パッケージはファイルシステム上のディレクトリに対応し、その場所は「インポートパス」によって識別されます。例えば、net/http は標準ライブラリの net パッケージのサブディレクトリにある http パッケージを指します。

go コマンドとパッケージパターン

go buildgo test などの go コマンドは、引数としてインポートパスを受け取ります。これらのコマンドは、単一のパッケージパスだけでなく、複数のパッケージを一度に指定するための「パッケージパターン」もサポートしています。

最も一般的なパッケージパターンは、ワイルドカード ... を使用するものです。

  • x/... パターン:

    • このパターンは、指定されたパス xサブディレクトリにあるすべてのパッケージにマッチします。
    • このコミット以前: net/...net/httpnet/rpc などにマッチしましたが、net パッケージ自体にはマッチしませんでした。
    • このコミット以後: net/...net パッケージ自体にもマッチするようになります。これは、x/...x とそのすべてのサブパッケージを意味する「特別なケース」として扱われるようになったためです。
  • x... パターン (非推奨/異なる挙動):

    • これは x で始まるすべてのパッケージパスにマッチします。例えば、net...net, net/http, netchan など、net で始まる任意のパッケージにマッチします。これはファイルシステム上の階層構造とは直接関係なく、文字列としてのプレフィックスマッチングです。
    • このパターンは、意図しないパッケージまでマッチしてしまう可能性があるため、通常は x/... の形式が推奨されます。

このコミットは、特に x/... パターンの挙動を変更し、より直感的で便利なものにすることを目的としています。

正規表現とパターンマッチング

Goコマンドの内部では、これらのパッケージパターンを正規表現に変換してマッチングを行っています。特に ... ワイルドカードは、正規表現の .* (任意の文字に0回以上マッチ) に変換されます。このコミットでは、x/... のようなパターンが x 自体にもマッチするように、正規表現の変換ロジックが調整されています。

技術的詳細

このコミットの技術的な核心は、Goコマンドがパッケージパターンを正規表現に変換するロジックと、そのパターンをパッケージリストに展開するロジックの変更にあります。

matchPattern 関数の変更

src/cmd/go/main.go 内の matchPattern 関数は、Goのパッケージパターン(例: net/...)を実際の正規表現に変換する役割を担っています。

変更前:

func matchPattern(pattern string) func(name string) bool {
	re := regexp.QuoteMeta(pattern)
	re = strings.Replace(re, `\\.\\.\\.`, `.*`, -1)
	reg := regexp.MustCompile(`^` + re + `$`)
	return func(name string) bool {
		return reg.MatchString(name)
	}
}

このコードでは、....* に単純に置換されていました。例えば、net/...^net/.*$ という正規表現に変換され、これは net 自体にはマッチしませんでした(netnet/ で終わらないため)。

変更後:

func matchPattern(pattern string) func(name string) bool {
	re := regexp.QuoteMeta(pattern)
	re = strings.Replace(re, `\\.\\.\\.`, `.*`, -1)
	// Special case: foo/... matches foo too.
	if strings.HasSuffix(re, `/.*`) {
		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
	}
	reg := regexp.MustCompile(`^` + re + `$`)
	return func(name string) bool {
		return reg.MatchString(name)
	}
}

追加された以下の行が重要です。

	// Special case: foo/... matches foo too.
	if strings.HasSuffix(re, `/.*`) {
		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
	}

このロジックは、変換された正規表現が /.* で終わる場合(つまり、元のパターンが x/... の形式であった場合)、その /.*(/.*)? に変更します。

  • (/.*)? は、(/.*) の部分が0回または1回出現することを示します。
  • これにより、net/...^net(/.*)?$ という正規表現に変換されます。
    • net には net の部分がマッチし、(/.*)? の部分は0回出現として扱われるため、マッチします。
    • net/http には net の部分がマッチし、/http(/.*)? の部分にマッチするため、マッチします。

この変更によって、x/... パターンが x パッケージ自体にもマッチするようになりました。

パッケージ展開ロジックの変更と関数の分割

src/cmd/go/main.go では、パッケージパスを展開するロジックも変更されました。

  • importPaths 関数の分割:

    • 元の importPaths 関数は、引数として与えられたパスを処理し、... ワイルドカードの展開も行っていました。
    • このコミットでは、importPathsNoDotExpansion という新しい関数が導入されました。この関数は、... ワイルドカードの展開を行わず、allstd といった特殊なキーワードの処理のみを行います。
    • 元の importPaths 関数は、importPathsNoDotExpansion の結果を受け取り、その上で ... ワイルドカードの展開を行うように変更されました。これにより、ロジックがより明確に分離されました。
  • allPackagesallPackagesInFS のラッパー関数導入:

    • allPackagesallPackagesInFS は、それぞれGOPATH全体または特定のファイルシステムパス内のパッケージを検索し、パターンにマッチするものを返す関数です。
    • これらの関数は、マッチするパッケージが見つからなかった場合に警告メッセージを出力するロジックを含んでいました。
    • このコミットでは、matchPackagesmatchPackagesInFS という新しい関数が導入され、実際のパッケージマッチングロジックがこれらの関数に移動しました。
    • allPackagesallPackagesInFS は、これらの新しい関数を呼び出し、その結果に対して警告メッセージのロジックを適用するラッパー関数となりました。これにより、警告ロジックとコアなマッチングロジックが分離され、コードの再利用性と保守性が向上しました。

新しいテストファイルの追加

src/cmd/go/match_test.go が新規追加され、matchPattern 関数の新しい挙動を検証するテストケースが多数追加されました。これにより、変更が意図通りに機能し、既存の挙動を壊していないことが保証されます。特に、net...net/... の違い、そして net/...net にマッチするようになったことがテストされています。

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

src/cmd/go/doc.go および src/cmd/go/help.go の変更

Goコマンドのドキュメントとヘルプメッセージが更新され、x/... パターンが x 自体も含むようになったことが明記されました。

--- a/src/cmd/go/doc.go
+++ b/src/cmd/go/doc.go
@@ -508,9 +508,8 @@ An import path is a pattern if it includes one or more "..." wildcards,
 each of which can match any string, including the empty string and
 strings containing slashes.  Such a pattern expands to all package
 directories found in the GOPATH trees with names matching the
-patterns.  For example, encoding/... expands to all packages
-in subdirectories of the encoding tree, while net... expands to
-net and all its subdirectories.
+patterns.  As a special case, x/... matches x as well as x's subdirectories.
+For example, net/... expands to net and packages in its subdirectories.

src/cmd/go/main.go の変更

matchPattern 関数の変更

--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -345,6 +359,10 @@ func runOut(dir string, cmdargs ...interface{}) []byte {
 func matchPattern(pattern string) func(name string) bool {
 	re := regexp.QuoteMeta(pattern)
 	re = strings.Replace(re, `\\.\\.\\.`, `.*`, -1)
+	// Special case: foo/... matches foo too.
+	if strings.HasSuffix(re, `/.*`) {
+		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
+	}
 	reg := regexp.MustCompile(`^` + re + `$`)\
 	return func(name string) bool {
 		return reg.MatchString(name)

importPaths 関数の分割とロジック変更

--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -247,8 +247,9 @@ func help(args []string) {
 	os.Exit(2) // failed at 'go help cmd'
 }
 
-// importPaths returns the import paths to use for the given command line.\
-func importPaths(args []string) []string {
+// importPathsNoDotExpansion returns the import paths to use for the given
+// command line, but it does no ... expansion.
+func importPathsNoDotExpansion(args []string) []string {
 	if len(args) == 0 {
 		return []string{"."}
 	}
@@ -270,13 +271,26 @@ func importPaths(args []string) []string {
 		} else {
 			a = path.Clean(a)
 		}
-
-		if build.IsLocalImport(a) && strings.Contains(a, "...") {
-			out = append(out, allPackagesInFS(a)...)
+		if a == "all" || a == "std" {
+			out = append(out, allPackages(a)...)
 			continue
 		}
-		if a == "all" || a == "std" || strings.Contains(a, "...") {
-			out = append(out, allPackages(a)...)
+		out = append(out, a)
+	}
+	return out
+}
+
+// importPaths returns the import paths to use for the given command line.
+func importPaths(args []string) []string {
+	args = importPathsNoDotExpansion(args)
+	var out []string
+	for _, a := range args {
+		if strings.Contains(a, "...") {
+			if build.IsLocalImport(a) {
+				out = append(out, allPackagesInFS(a)...)
+			} else {
+				out = append(out, allPackages(a)...)
+			}
 			continue
 		}
 		out = append(out, a)

allPackagesallPackagesInFS のラッパー関数導入

--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -356,6 +374,14 @@ func matchPattern(pattern string) func(name string) bool {
 // The pattern is either "all" (all packages), "std" (standard packages)
 // or a path including "...".
 func allPackages(pattern string) []string {
+	pkgs := matchPackages(pattern)
+	if len(pkgs) == 0 {
+		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\\n", pattern)
+	}
+	return pkgs
+}
+
+func matchPackages(pattern string) []string {
 	match := func(string) bool { return true }
 	if pattern != "all" && pattern != "std" {
 		match = matchPattern(pattern)
@@ -432,10 +458,6 @@ func allPackages(pattern string) []string {
 			return nil
 		})
 	}
-
-	if len(pkgs) == 0 {
-		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\\n", pattern)
-	}
 	return pkgs
 }
 
@@ -443,6 +465,14 @@ func allPackagesInFS(pattern string) []string {
 // beginning ./ or ../, meaning it should scan the tree rooted
 // at the given directory.  There are ... in the pattern too.
 func allPackagesInFS(pattern string) []string {
+	pkgs := matchPackagesInFS(pattern)
+	if len(pkgs) == 0 {
+		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\\n", pattern)
+	}
+	return pkgs
+}
+
+func matchPackagesInFS(pattern string) []string {
 	// Find directory to begin the scan.
 	// Could be smarter but this one optimization
 	// is enough for now, since ... is usually at the\
@@ -482,10 +512,6 @@ func allPackagesInFS(pattern string) []string {
 		pkgs = append(pkgs, name)
 		return nil
 	})
-
-	if len(pkgs) == 0 {
-		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\\n", pattern)
-	}
 	return pkgs
 }

src/cmd/go/match_test.go の新規追加

--- /dev/null
+++ b/src/cmd/go/match_test.go
@@ -0,0 +1,36 @@
+// Copyright 2012 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import "testing"
+
+var matchTests = []struct {
+	pattern string
+	path    string
+	match   bool
+}{
+	{"...", "foo", true},
+	{"net", "net", true},
+	{"net", "net/http", false},
+	{"net/http", "net", false},
+	{"net/http", "net/http", true},
+	{"net...", "netchan", true},
+	{"net...", "net", true},
+	{"net...", "net/http", true},
+	{"net...", "not/http", false},
+	{"net/...", "netchan", false},
+	{"net/...", "net", true},
+	{"net/...", "net/http", true},
+	{"net/...", "not/http", false},
+}
+
+func TestMatchPattern(t *testing.T) {
+	for _, tt := range matchTests {
+		match := matchPattern(tt.pattern)(tt.path)
+		if match != tt.match {
+			t.Errorf("matchPattern(%q)(%q) = %v, want %v", tt.pattern, tt.path, match, tt.match)
+		}
+	}
+}

コアとなるコードの解説

matchPattern 関数の変更

この変更は、net/... のようなパターンが net パッケージ自体にもマッチするようにするための核心的な部分です。

	// Special case: foo/... matches foo too.
	if strings.HasSuffix(re, `/.*`) {
		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
	}

このコードブロックは、正規表現 re/.* で終わる場合にのみ実行されます。これは、元のパターンが x/... の形式であったことを意味します。 例えば、net/...regexp.QuoteMetastrings.Replace の後で net/.* になります。この条件に合致するため、renet(/.*)? に書き換えられます。

  • netnet にマッチします。
  • net/httpnet にマッチし、/http(/.*)? の部分にマッチします。 これにより、x/... パターンが x 自体と、x のすべてのサブパッケージにマッチするようになります。

importPaths 関数の分割とロジック変更

  • importPathsNoDotExpansion の導入: この新しい関数は、コマンドライン引数を処理し、allstd といった特殊なキーワードをパッケージリストに展開しますが、... ワイルドカードの展開は行いません。これにより、importPaths のロジックがよりシンプルになり、責任が明確に分離されました。

  • 新しい importPaths のロジック: 新しい importPaths 関数は、まず importPathsNoDotExpansion を呼び出して基本的なパスのリストを取得します。その後、そのリストをループし、各パスに ... が含まれている場合にのみ、allPackages または allPackagesInFS を呼び出してワイルドカードを展開します。この二段階の処理により、パッケージ展開のフローがより明確になりました。

allPackagesallPackagesInFS のラッパー関数導入

  • matchPackagesmatchPackagesInFS の導入: これらの新しい関数は、実際にGOPATHやファイルシステムを走査してパターンにマッチするパッケージを見つけるコアなロジックを含んでいます。以前は allPackagesallPackagesInFS の中に直接含まれていた警告メッセージの出力ロジックは、これらの新しい関数からは分離されました。

  • allPackagesallPackagesInFS の役割変更: これらの関数は、matchPackagesmatchPackagesInFS を呼び出し、その結果が空の場合にのみ警告メッセージを出力するラッパー関数となりました。これにより、パッケージマッチングのロジックと、ユーザーへのフィードバック(警告)のロジックが分離され、コードのモジュール性が向上しました。

src/cmd/go/match_test.go の新規追加

このテストファイルは、matchPattern 関数の挙動を網羅的に検証します。特に重要なのは、net...net/... の違いを明確にし、net/...net にマッチするようになった新しい挙動をテストしている点です。これにより、変更が正しく実装され、将来のリグレッションを防ぐための安全網が提供されます。

関連リンク

  • Gerrit Change-Id: https://golang.org/cl/5794064 (GoプロジェクトのコードレビューシステムであるGerritへのリンク)

参考にした情報源リンク

  • Go言語の公式ドキュメント (パッケージとモジュールに関するセクション)
  • Go言語のソースコード (src/cmd/go ディレクトリ内の関連ファイル)
  • 正規表現に関する一般的な情報