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

[インデックス 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言語およびファイルシステム関連の概念の理解が不可欠です。

  1. Goパッケージとインポートパス:
    • Go言語のコードは「パッケージ」という単位で組織されます。各パッケージは通常、ファイルシステム上のディレクトリに対応します。
    • パッケージは、他のパッケージからインポートされる際に「インポートパス」で識別されます。例えば、"fmt""net/http"などです。
    • ローカルファイルシステム上のパッケージを参照する場合、相対パスや特殊なパターンを使用できます。
  2. go listコマンド:
    • go listは、Goのパッケージに関する情報を表示するためのコマンドです。
    • 引数としてパッケージのインポートパスやパターンを受け取ります。
    • . (ドット): カレントディレクトリにあるパッケージを指します。
    • ... (エリプシス): パッケージパスの末尾に付加されると、そのプレフィックスを持つすべてのパッケージ(再帰的にサブディレクトリを含む)を意味します。例えば、io/...ioパッケージとそのすべてのサブパッケージ(io/ioutilなど)を指します。
    • ./...: カレントディレクトリとそのすべてのサブディレクトリにあるパッケージを指します。
  3. filepathパッケージ:
    • Goの標準ライブラリpath/filepathは、ファイルパスを操作するための関数を提供します。
    • filepath.Walk(root string, walkFn WalkFunc) error: 指定されたルートディレクトリからファイルツリーを再帰的に走査します。走査中に見つかった各ファイルやディレクトリに対してwalkFn(コールバック関数)が呼び出されます。
    • filepath.Clean(path string) string: パスを正規化します。例えば、a/b/../ca/cに、./aaに、a//ba/bに変換されます。これは、パスの比較や一貫した処理のために重要です。
    • filepath.Split(path string) (dir, file string): パスをディレクトリ部分とファイル(または最後のディレクトリ)部分に分割します。
    • filepath.SkipDir: filepath.WalkwalkFnがこのエラーを返すと、現在のディレクトリのサブディレクトリへの再帰的な走査がスキップされます。これは、特定のディレクトリツリーを無視したい場合に便利です。
  4. ファイルシステム上の隠しディレクトリと特殊ディレクトリ:
    • Unix系システムでは、ファイル名が.で始まるディレクトリ(例: .git)は通常「隠しディレクトリ」と見なされます。
    • .はカレントディレクトリ、..は親ディレクトリを指す特殊なディレクトリ名です。

技術的詳細

このバグは、src/cmd/go/main.go内のmatchPackagesInFS関数に存在していました。この関数は、与えられたパターンに基づいてファイルシステムからGoパッケージを探索する役割を担っています。

問題の根本原因は二つありました。

  1. filepath.Walkの初期パス処理の不整合: filepath.Walkは、走査を開始するルートディレクトリ(dir変数)をwalkFnの最初の呼び出しでpath引数として渡します。この最初のpathは、filepath.Joinによって生成される後続のpathとは異なり、filepath.Cleanが適用されていませんでした。 例えば、cd $GOROOT/src/pkg; go list ./io/...のようなコマンドの場合、matchPackagesInFS./iodirとして受け取ります。filepath.Walkの最初の呼び出しではpath./ioとなります。しかし、内部のパッケージマッチングロジックでは、正規化されたパス(例: io)を期待していました。この不整合により、./ioのようなパスが正しくマッチせず、カレントディレクトリのパッケージがスキップされる原因となっていました。

  2. 特殊ディレクトリ(...)の誤ったスキップ: matchPackagesInFS関数内には、.foo_footestdataといった特定のディレクトリツリーをスキップするためのロジックがありました。これは、これらのディレクトリが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
 		}
 

コアとなるコードの解説

  1. 初期パスの正規化:

    -		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と誤って解釈され、マッチングに失敗するのを防ぎます。
  2. 特殊ディレクトリのスキップロジックの改善:

    -		// 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コマンドは./...のような再帰的なパス指定に対して、より正確で期待通りのパッケージリストを返すようになりました。

関連リンク

参考にした情報源リンク