[インデックス 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 build
や go test
などの go
コマンドは、引数としてインポートパスを受け取ります。これらのコマンドは、単一のパッケージパスだけでなく、複数のパッケージを一度に指定するための「パッケージパターン」もサポートしています。
最も一般的なパッケージパターンは、ワイルドカード ...
を使用するものです。
-
x/...
パターン:- このパターンは、指定されたパス
x
のサブディレクトリにあるすべてのパッケージにマッチします。 - このコミット以前:
net/...
はnet/http
やnet/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
自体にはマッチしませんでした(net
は net/
で終わらないため)。
変更後:
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
という新しい関数が導入されました。この関数は、...
ワイルドカードの展開を行わず、all
やstd
といった特殊なキーワードの処理のみを行います。 - 元の
importPaths
関数は、importPathsNoDotExpansion
の結果を受け取り、その上で...
ワイルドカードの展開を行うように変更されました。これにより、ロジックがより明確に分離されました。
- 元の
-
allPackages
とallPackagesInFS
のラッパー関数導入:allPackages
とallPackagesInFS
は、それぞれGOPATH全体または特定のファイルシステムパス内のパッケージを検索し、パターンにマッチするものを返す関数です。- これらの関数は、マッチするパッケージが見つからなかった場合に警告メッセージを出力するロジックを含んでいました。
- このコミットでは、
matchPackages
とmatchPackagesInFS
という新しい関数が導入され、実際のパッケージマッチングロジックがこれらの関数に移動しました。 allPackages
とallPackagesInFS
は、これらの新しい関数を呼び出し、その結果に対して警告メッセージのロジックを適用するラッパー関数となりました。これにより、警告ロジックとコアなマッチングロジックが分離され、コードの再利用性と保守性が向上しました。
新しいテストファイルの追加
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)
allPackages
と allPackagesInFS
のラッパー関数導入
--- 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.QuoteMeta
と strings.Replace
の後で net/.*
になります。この条件に合致するため、re
は net(/.*)?
に書き換えられます。
net
はnet
にマッチします。net/http
はnet
にマッチし、/http
が(/.*)?
の部分にマッチします。 これにより、x/...
パターンがx
自体と、x
のすべてのサブパッケージにマッチするようになります。
importPaths
関数の分割とロジック変更
-
importPathsNoDotExpansion
の導入: この新しい関数は、コマンドライン引数を処理し、all
やstd
といった特殊なキーワードをパッケージリストに展開しますが、...
ワイルドカードの展開は行いません。これにより、importPaths
のロジックがよりシンプルになり、責任が明確に分離されました。 -
新しい
importPaths
のロジック: 新しいimportPaths
関数は、まずimportPathsNoDotExpansion
を呼び出して基本的なパスのリストを取得します。その後、そのリストをループし、各パスに...
が含まれている場合にのみ、allPackages
またはallPackagesInFS
を呼び出してワイルドカードを展開します。この二段階の処理により、パッケージ展開のフローがより明確になりました。
allPackages
と allPackagesInFS
のラッパー関数導入
-
matchPackages
とmatchPackagesInFS
の導入: これらの新しい関数は、実際にGOPATHやファイルシステムを走査してパターンにマッチするパッケージを見つけるコアなロジックを含んでいます。以前はallPackages
とallPackagesInFS
の中に直接含まれていた警告メッセージの出力ロジックは、これらの新しい関数からは分離されました。 -
allPackages
とallPackagesInFS
の役割変更: これらの関数は、matchPackages
とmatchPackagesInFS
を呼び出し、その結果が空の場合にのみ警告メッセージを出力するラッパー関数となりました。これにより、パッケージマッチングのロジックと、ユーザーへのフィードバック(警告)のロジックが分離され、コードのモジュール性が向上しました。
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
ディレクトリ内の関連ファイル) - 正規表現に関する一般的な情報