[インデックス 13048] ファイルの概要
このコミットは、Go言語のコマンドラインツールであるgo list
コマンドにおいて、再帰的なパス指定(例: ./...
)が現在のディレクトリ(.
)を正しく含まないというバグを修正したものです。具体的には、go list ./...
を実行した際に、カレントディレクトリのパッケージが結果に含まれず、そのサブパッケージのみがリストアップされる問題を解決しました。
コミット
commit c8332198f42d0c5eb4e6345fe3fc935283dd5a9d
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed May 9 10:43:15 2012 +1000
go: fix the import path "./..." not matching ".".
Tested manually.
Fixes #3554.
Before:
$ cd $GOROOT/src/pkg
$ go list io
io
$ go list io/...\n io
io/ioutil
$ cd $GOROOT/src/pkg/io
$ go list .
io
$ go list ./...
io/ioutil
After:
$ cd $GOROOT/src/pkg
$ go list io
io
$ go list io/...\n io
io/ioutil
$ cd $GOROOT/src/pkg/io
$ go list .
io
$ go list ./...
io
io/ioutil
$ go list ././...\n io
io/ioutil
$ go list ././.././io/...\n io
io/ioutil
$ go list ../image
image
$ go list ../image/...\n image
image/color
image/draw
image/gif
image/jpeg
image/png
$ go list ../.../template
html/template
text/template
$ cd $GOROOT/src/pkg
$ go list ./io
io
$ go list ./io/...\n io
io/ioutil
$ go list ./.../pprof
net/http/pprof
runtime/pprof
$ go list ./compress
can't load package: package compress: no Go source files in /home/nigeltao/go/src/pkg/compress
$ go list ./compress/...\n compress/bzip2
compress/flate
compress/gzip
compress/lzw
compress/zlib
$ cd $GOROOT/src/pkg/code.google.com
$ go list ./p/leveldb-go/...\n code.google.com/p/leveldb-go/leveldb
code.google.com/p/leveldb-go/leveldb/crc
code.google.com/p/leveldb-go/leveldb/db
code.google.com/p/leveldb-go/leveldb/memdb
code.google.com/p/leveldb-go/leveldb/memfs
code.google.com/p/leveldb-go/leveldb/record
code.google.com/p/leveldb-go/leveldb/table
code.google.com/p/leveldb-go/manualtest/filelock
$ go list ./p/.../truetype
code.google.com/p/freetype-go/example/truetype
code.google.com/p/freetype-go/freetype/truetype
$ go list ./p/.../example
warning: "./p/.../example" matched no packages
$ go list ./p/.../example/...\n code.google.com/p/freetype-go/example/freetype
code.google.com/p/freetype-go/example/gamma
code.google.com/p/freetype-go/example/raster
code.google.com/p/freetype-go/example/round
code.google.com/p/freetype-go/example/truetype
code.google.com/p/x-go-binding/example/imgview
code.google.com/p/x-go-binding/example/xgb
R=rsc
CC=golang-dev
https://golang.org/cl/6194056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c8332198f42d0c5eb4e6345fe3fc935283dd5a9d
元コミット内容
このコミットが修正する問題は、go list
コマンドがパッケージパスのパターンマッチングを行う際に、特定のケースで期待通りの動作をしないというものでした。特に、現在のディレクトリを示す./...
のような再帰的なパス指定が、カレントディレクトリ自体(.
)のパッケージを結果に含めないというバグがありました。
具体的な例として、$GOROOT/src/pkg/io
ディレクトリに移動し、go list ./...
を実行した場合、修正前はio/ioutil
のみがリストアップされ、io
パッケージ自体は含まれませんでした。これは、./...
が「現在のディレクトリとそのすべてのサブディレクトリにあるパッケージ」を意味するにもかかわらず、現在のディレクトリのパッケージが除外されてしまうという不整合を生んでいました。
変更の背景
この変更は、Go言語のIssue #3554「go list ./...
should include .
」を修正するために行われました。go list
コマンドは、Goのワークスペース内のパッケージ情報を取得するための重要なツールです。開発者はこのコマンドを使って、依存関係の確認、ビルド対象の特定、テストの実行範囲の指定など、様々なタスクを行います。
./...
というパターンは、カレントディレクトリとそのすべてのサブディレクトリにあるパッケージを対象とする、非常に一般的な指定方法です。しかし、このパターンがカレントディレクトリのパッケージ自体を含まないというバグは、開発者の期待に反する動作であり、混乱や誤ったビルド・テスト結果を引き起こす可能性がありました。例えば、カレントディレクトリのパッケージとサブパッケージの両方を対象にビルドやテストを行いたい場合、go list . ./...
のように冗長な指定をする必要がありました。
この修正は、go list
コマンドのセマンティクスをより直感的で一貫性のあるものにし、開発者の利便性を向上させることを目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語およびファイルシステム関連の概念の理解が不可欠です。
- Goパッケージとインポートパス:
- Go言語のコードは「パッケージ」という単位で組織されます。各パッケージは通常、ファイルシステム上のディレクトリに対応します。
- パッケージは、他のパッケージからインポートされる際に「インポートパス」で識別されます。例えば、
"fmt"
や"net/http"
などです。 - ローカルファイルシステム上のパッケージを参照する場合、相対パスや特殊なパターンを使用できます。
go list
コマンド:go list
は、Goのパッケージに関する情報を表示するためのコマンドです。- 引数としてパッケージのインポートパスやパターンを受け取ります。
.
(ドット): カレントディレクトリにあるパッケージを指します。...
(エリプシス): パッケージパスの末尾に付加されると、そのプレフィックスを持つすべてのパッケージ(再帰的にサブディレクトリを含む)を意味します。例えば、io/...
はio
パッケージとそのすべてのサブパッケージ(io/ioutil
など)を指します。./...
: カレントディレクトリとそのすべてのサブディレクトリにあるパッケージを指します。
filepath
パッケージ:- Goの標準ライブラリ
path/filepath
は、ファイルパスを操作するための関数を提供します。 filepath.Walk(root string, walkFn WalkFunc) error
: 指定されたルートディレクトリからファイルツリーを再帰的に走査します。走査中に見つかった各ファイルやディレクトリに対してwalkFn
(コールバック関数)が呼び出されます。filepath.Clean(path string) string
: パスを正規化します。例えば、a/b/../c
はa/c
に、./a
はa
に、a//b
はa/b
に変換されます。これは、パスの比較や一貫した処理のために重要です。filepath.Split(path string) (dir, file string)
: パスをディレクトリ部分とファイル(または最後のディレクトリ)部分に分割します。filepath.SkipDir
:filepath.Walk
のwalkFn
がこのエラーを返すと、現在のディレクトリのサブディレクトリへの再帰的な走査がスキップされます。これは、特定のディレクトリツリーを無視したい場合に便利です。
- Goの標準ライブラリ
- ファイルシステム上の隠しディレクトリと特殊ディレクトリ:
- Unix系システムでは、ファイル名が
.
で始まるディレクトリ(例:.git
)は通常「隠しディレクトリ」と見なされます。 .
はカレントディレクトリ、..
は親ディレクトリを指す特殊なディレクトリ名です。
- Unix系システムでは、ファイル名が
技術的詳細
このバグは、src/cmd/go/main.go
内のmatchPackagesInFS
関数に存在していました。この関数は、与えられたパターンに基づいてファイルシステムからGoパッケージを探索する役割を担っています。
問題の根本原因は二つありました。
-
filepath.Walk
の初期パス処理の不整合:filepath.Walk
は、走査を開始するルートディレクトリ(dir
変数)をwalkFn
の最初の呼び出しでpath
引数として渡します。この最初のpath
は、filepath.Join
によって生成される後続のpath
とは異なり、filepath.Clean
が適用されていませんでした。 例えば、cd $GOROOT/src/pkg; go list ./io/...
のようなコマンドの場合、matchPackagesInFS
は./io
をdir
として受け取ります。filepath.Walk
の最初の呼び出しではpath
も./io
となります。しかし、内部のパッケージマッチングロジックでは、正規化されたパス(例:io
)を期待していました。この不整合により、./io
のようなパスが正しくマッチせず、カレントディレクトリのパッケージがスキップされる原因となっていました。 -
特殊ディレクトリ(
.
と..
)の誤ったスキップ:matchPackagesInFS
関数内には、.foo
や_foo
、testdata
といった特定のディレクトリツリーをスキップするためのロジックがありました。これは、これらのディレクトリがGoパッケージとして扱われるべきではないためです。 しかし、このロジックは、ディレクトリ名が.
で始まる場合に無条件にfilepath.SkipDir
を返していました。これにより、カレントディレクトリを示す.
や親ディレクトリを示す..
といった特殊なディレクトリもスキップされてしまっていました。go list ./...
のようなパターンでは、カレントディレクトリ自体が.
として扱われるため、この誤ったスキップロジックがカレントディレクトリのパッケージがリストに含まれない原因となっていました。
このコミットは、これらの問題を解決するために、matchPackagesInFS
関数内のパス処理とディレクトリフィルタリングロジックを修正しました。
コアとなるコードの変更箇所
変更はsrc/cmd/go/main.go
ファイルに集中しています。
--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -500,13 +500,25 @@ func matchPackagesInFS(pattern string) []string {
var pkgs []string
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
- if err != nil || !fi.IsDir() || path == dir {
+ if err != nil || !fi.IsDir() {
return nil
}
+ if path == dir {
+ // filepath.Walk starts at dir and recurses. For the recursive case,
+ // the path is the result of filepath.Join, which calls filepath.Clean.
+ // The initial case is not Cleaned, though, so we do this explicitly.
+ //
+ // This converts a path like "./io/" to "io". Without this step, running
+ // "cd $GOROOT/src/pkg; go list ./io/...\" would incorrectly skip the io
+ // package, because prepending the prefix "./" to the unclean path would
+ // result in "././io", and match("././io") returns false.
+ path = filepath.Clean(path)
+ }
- // Avoid .foo, _foo, and testdata directory trees.
+ // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
_, elem := filepath.Split(path)
- if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
+ dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
+ if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
return filepath.SkipDir
}
コアとなるコードの解説
-
初期パスの正規化:
- if err != nil || !fi.IsDir() || path == dir { + if err != nil || !fi.IsDir() { return nil } + if path == dir { + // filepath.Walk starts at dir and recurses. For the recursive case, + // the path is the result of filepath.Join, which calls filepath.Clean. + // The initial case is not Cleaned, though, so we do this explicitly. + // + // This converts a path like "./io/" to "io". Without this step, running + // "cd $GOROOT/src/pkg; go list ./io/...\" would incorrectly skip the io + // package, because prepending the prefix "./" to the unclean path would + // result in "././io", and match("././io") returns false. + path = filepath.Clean(path) + }
- 元のコードでは、
path == dir
の場合にnil
を返していましたが、これはfilepath.Walk
が開始ディレクトリ自体をスキップしてしまう原因となっていました。 - 修正後、
path == dir
のチェックは独立したブロックに移されました。 - このブロック内で、
path = filepath.Clean(path)
が実行されます。これにより、filepath.Walk
が最初に渡すルートディレクトリのパスが正規化され、後続のfilepath.Join
によって生成されるパス(これらは自動的にfilepath.Clean
が適用される)との一貫性が保たれます。 - コメントにもあるように、この正規化は
./io
のようなパスが././io
と誤って解釈され、マッチングに失敗するのを防ぎます。
- 元のコードでは、
-
特殊ディレクトリのスキップロジックの改善:
- // Avoid .foo, _foo, and testdata directory trees. + // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". _, elem := filepath.Split(path) - if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { + dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." + if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { return filepath.SkipDir }
- 元のコードでは、ディレクトリ名(
elem
)が.
で始まる場合に無条件にスキップしていました。 - 新しいコードでは、
dot
という新しい変数が導入されました。これは、elem
が.
で始まり、かつelem
が.
でも..
でもない場合にのみtrue
となります。 - この
dot
変数を使用することで、.
や..
といった特殊なディレクトリが、隠しディレクトリ(例:.git
)と同じように誤ってスキップされるのを防ぎます。これにより、go list ./...
がカレントディレクトリのパッケージを正しく含めることができるようになりました。
- 元のコードでは、ディレクトリ名(
これらの変更により、go list
コマンドは./...
のような再帰的なパス指定に対して、より正確で期待通りのパッケージリストを返すようになりました。
関連リンク
- Go Issue #3554: https://github.com/golang/go/issues/3554
- Go CL 6194056: https://golang.org/cl/6194056 (このコミットに対応するGo Code Reviewのチェンジリスト)
参考にした情報源リンク
- Go Command Documentation (
go help list
): https://pkg.go.dev/cmd/go#hdr-List_packages path/filepath
package documentation: https://pkg.go.dev/path/filepathfilepath.Walk
documentation: https://pkg.go.dev/path/filepath#Walkfilepath.Clean
documentation: https://pkg.go.dev/path/filepath#Cleanfilepath.SkipDir
documentation: https://pkg.go.dev/path/filepath#SkipDir