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

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

このコミットは、Go言語のgoコマンドにおいて、パッケージのインポートパス引数に...(ワイルドカード)パターンを追加する機能の実装です。これにより、ユーザーは特定のディレクトリツリー内のすべてのパッケージや、パターンに一致するパッケージを簡単に指定できるようになります。

コミット

  • コミットハッシュ: b8615a0931b05c37c7d8cf3c0e11a858888483de
  • 作者: Russ Cox rsc@golang.org
  • 日付: Mon Jan 9 16:23:00 2012 -0800

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

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

元コミット内容

go: add ... patterns in import path arguments

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5530058
---
 src/cmd/go/help.go |   7 ++++\n src/cmd/go/main.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++-------\n 2 files changed, 110 insertions(+), 15 deletions(-)

変更の背景

このコミット以前のgoコマンド(例: go build, go install)では、パッケージを指定する際に、個々のパッケージパスを明示的に記述するか、あるいは特別なキーワード(allstd)を使用する必要がありました。しかし、これは大規模なプロジェクトや、特定のサブディレクトリ以下にある多数のパッケージに対して一括で操作を行いたい場合に不便でした。

例えば、encodingパッケージとそのサブパッケージ(encoding/json, encoding/xmlなど)すべてに対してgo testを実行したい場合、以前はそれぞれのパッケージパスを列挙する必要がありました。このコミットは、encoding/...のようなワイルドカードパターンを導入することで、この問題を解決し、ユーザーがより柔軟かつ効率的にパッケージを指定できるようにすることを目的としています。これにより、開発者は関連するパッケージ群に対して、より簡潔なコマンドで操作を実行できるようになりました。

前提知識の解説

Go言語のパッケージ管理

Go言語では、コードは「パッケージ」という単位で整理されます。パッケージは関連する機能の集合であり、他のパッケージからimport文を使って利用されます。各パッケージはファイルシステム上のディレクトリに対応し、そのディレクトリパスがパッケージのインポートパスとなります。例えば、fmtパッケージはGoの標準ライブラリの一部であり、import "fmt"として利用されます。

GOPATHGOROOT

Goのワークスペースは、主に以下の2つの環境変数によって構成されます。

  • GOROOT: Goのインストールディレクトリを指します。Goの標準ライブラリのソースコード(例: fmt, net/httpなど)は$GOROOT/src以下に配置されています。
  • GOPATH: ユーザーが開発するプロジェクトのワークスペースを指します。通常、複数のディレクトリパスを設定できます。go getコマンドでダウンロードされた外部パッケージや、ユーザー自身のプロジェクトのソースコードは$GOPATH/src以下に配置されます。

goコマンドは、これらのGOROOTGOPATHのパスを探索して、指定されたパッケージを見つけ出します。

goコマンド

goコマンドは、Go言語のビルド、テスト、依存関係管理などを行うための主要なツールです。例えば、go buildはパッケージをコンパイルし、go installはコンパイルしたバイナリをインストールします。これらのコマンドは、引数としてパッケージのインポートパスを受け取ります。

ワイルドカードパターン(...

一般的なファイルシステムやシェルでは、*?などのワイルドカードがファイル名やパスのパターンマッチングに利用されます。Goのgoコマンドにおける...は、これに似た概念ですが、Goのパッケージパスに特化したワイルドカードです。

  • ...は「任意の文字列」にマッチします。これには空文字列やスラッシュ(/)を含む文字列も含まれます。
  • 例えば、encoding/...encodingディレクトリ以下にあるすべてのパッケージ(encoding/json, encoding/xmlなど)にマッチします。
  • .../xのようなパターンも可能で、パスの途中に...を置くことで、任意の深さのディレクトリを横断してxという名前のパッケージにマッチさせることができます。

このコミットは、この...パターンをgoコマンドの引数として解釈し、対応するパッケージ群を自動的に特定する機能を追加するものです。

技術的詳細

このコミットの主要な変更は、src/cmd/go/main.goファイルに集中しており、特にimportPathsallPackages、そして新しく導入されたmatchPatternallPackagesInFS関数が中心となります。

  1. src/cmd/go/help.goの更新: go help packagesの出力に、...ワイルドカードパターンの説明が追加されました。これにより、ユーザーはこの新機能の存在と使い方を公式ドキュメントを通じて知ることができます。

    --- a/src/cmd/go/help.go
    +++ b/src/cmd/go/help.go
    @@ -30,6 +30,13 @@ lists all the packages on the local system.
     The special import path "std" is like all but expands to just the
     packages in the standard Go library.
    
    +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 the encoding tree.
    +
     An import path can also name a package to be downloaded from
     a remote repository.  Run 'go help remote' for details.
    
  2. src/cmd/go/main.goの変更:

    • importPaths関数の変更: この関数は、goコマンドに渡された引数を解析し、実際に処理すべきパッケージのインポートパスのリストを返します。変更前は、引数がallまたはstdの場合にallPackagesを呼び出すか、単に引数をそのまま返すだけでした。 変更後は、引数に...が含まれる場合、または./../で始まる相対パスと...が組み合わされた場合に、allPackagesまたは新しく導入されたallPackagesInFSを呼び出すように拡張されました。これにより、ワイルドカードパターンが適切に展開されるようになりました。

      func importPaths(args []string) []string {
          var out []string
          for _, a := range args {
              // ./... or ../... の形式のパターンを処理
              if (strings.HasPrefix(a, "./") || strings.HasPrefix(a, "../")) && strings.Contains(a, "...") {
                  out = append(out, allPackagesInFS(a)...)
                  continue
              }
              // all, std, または ... を含むパターンを処理
              if a == "all" || a == "std" || strings.Contains(a, "...") {
                  out = append(out, allPackages(a)...)
                  continue
              }
              // それ以外の通常のパス
              out = append(out, a)
          }
          return out
      }
      
    • matchPattern関数の導入: この新しいヘルパー関数は、...を含むパターン文字列を受け取り、そのパターンに名前がマッチするかどうかを判定する関数を返します。内部的には、...を正規表現の.*に変換し、regexpパッケージを使用してパターンマッチングを行います。これにより、柔軟なワイルドカードマッチングが可能になります。

      // matchPattern(pattern)(name) reports whether
      // name matches pattern.  Pattern is a limited glob
      // pattern in which '...' means 'any string' and there
      // is no other special syntax.
      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)
          }
      }
      
    • allPackages関数の変更: この関数は、GOROOTGOPATH以下からすべてのパッケージ、または指定されたパターンにマッチするパッケージを検索します。変更前はallまたはstdキーワードのみを扱っていましたが、変更後はmatchPattern関数を利用して、...を含む任意のパターンにマッチするパッケージをフィルタリングできるようになりました。また、.で始まるディレクトリ(例: .git)やtestdataディレクトリをスキップするロジックも追加され、不要なディレクトリの走査を避けるようになりました。

      func allPackages(pattern string) []string {
          match := func(string) bool { return true } // デフォルトはすべてマッチ
          if pattern != "all" && pattern != "std" {
              match = matchPattern(pattern) // パターンが指定された場合はmatchPatternを使用
          }
          // ... (既存のパッケージ探索ロジック) ...
          // パッケージ名がパターンにマッチする場合のみ追加
          if match(name) {
              pkgs = append(pkgs, name)
          }
          // ... (ディレクトリ走査時のフィルタリング) ...
          // .foo や testdata ディレクトリを避ける
          _, elem := filepath.Split(path)
          if strings.HasPrefix(elem, ".") || elem == "testdata" {
              return filepath.SkipDir
          }
          // ...
      }
      
    • allPackagesInFS関数の導入: この関数は、./または../で始まる相対パスと...を組み合わせたパターン(例: ./myproject/...)を処理するために導入されました。これは、GOPATHGOROOT全体ではなく、特定のファイルシステム上のディレクトリツリーを起点としてパッケージを検索します。filepath.Walkを使用して指定されたディレクトリ以下を再帰的に走査し、matchPatternでフィルタリングを行います。

      // allPackagesInFS is like allPackages but is passed a pattern
      // beginning ./ or ../, meaning it should scan the tree rooted
      // at the given directory.  There are ... in the pattern too.
      func allPackagesInFS(pattern string) []string {
          // スキャンを開始するディレクトリを特定
          i := strings.Index(pattern, "...")
          dir, _ := path.Split(pattern[:i])
      
          prefix := ""
          if strings.HasPrefix(pattern, "./") {
              prefix = "./" // ./ を保持してマッチングと結果パスに利用
          }
          match := matchPattern(pattern)
      
          var pkgs []string
          filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
              if err != nil || !fi.IsDir() {
                  return nil
              }
      
              // .foo や testdata ディレクトリを避ける
              _, elem := filepath.Split(path)
              if strings.HasPrefix(elem, ".") || elem == "testdata" {
                  return filepath.SkipDir
              }
      
              name := prefix + filepath.ToSlash(path) // パッケージ名を構築
              if !match(name) { // パターンにマッチしない場合はスキップ
                  return nil
              }
              if _, err = build.ScanDir(path); err != nil { // パッケージとして有効かチェック
                  return nil
              }
              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/help.go:

    • go help packagesの出力に、...ワイルドカードパターンに関する説明が7行追加されました。
  • src/cmd/go/main.go:

    • import文にpathregexpパッケージが追加されました。
    • importPaths関数が大幅に修正され、...パターンや相対パス(./, ../)を含む引数を処理するロジックが追加されました。
    • matchPattern関数が新しく追加されました。これは、...を含むパターン文字列を正規表現に変換し、文字列がそのパターンにマッチするかどうかを判定するクロージャを返します。
    • allPackages関数が修正され、matchPatternを利用して指定されたパターンにマッチするパッケージのみを返すようになりました。また、.で始まるディレクトリやtestdataディレクトリをスキップするロジックが改善されました。
    • allPackagesInFS関数が新しく追加されました。これは、./../で始まる相対パスと...を組み合わせたパターンを処理し、ファイルシステム上の特定のディレクトリツリーを走査してパッケージを見つけます。

コアとなるコードの解説

importPaths関数の拡張

importPaths関数は、goコマンドの引数として渡されたパッケージパスを解釈する中心的な役割を担います。このコミットにより、引数が単なるパッケージパスだけでなく、allstd、そして新しい...パターンを含むことができるようになりました。

  • strings.HasPrefix(a, "./") || strings.HasPrefix(a, "../"): 引数が相対パス(カレントディレクトリまたは親ディレクトリからのパス)で始まるかどうかをチェックします。
  • strings.Contains(a, "..."): 引数にワイルドカード...が含まれているかどうかをチェックします。
  • これらの条件に基づいて、allPackagesInFS(相対パスと...の組み合わせ)またはallPackagesall, std, または絶対パスの...パターン)を呼び出し、適切なパッケージリストを取得します。

matchPattern関数の実装

この関数は、...ワイルドカードを正規表現の.*に変換することで、柔軟なパターンマッチングを実現しています。

  1. regexp.QuoteMeta(pattern): 入力されたpattern文字列内の正規表現の特殊文字(例: ., *, +など)をエスケープします。これにより、...以外の文字がリテラルとして扱われるようになります。
  2. strings.Replace(re, ., .*, -1): エスケープされた...\.)を正規表現の「任意の0文字以上の文字」にマッチする.*に置換します。-1はすべての出現を置換することを意味します。
  3. regexp.MustCompile(^+ re +$): 変換された正規表現文字列をコンパイルします。^$は、パターンが文字列全体にマッチする必要があることを示します。
  4. 返される関数は、与えられたnameがコンパイルされた正規表現にマッチするかどうかを判定します。

allPackagesallPackagesInFSの連携

  • allPackages: 主にGOPATHGOROOT全体を対象にパッケージを探索します。matchPattern関数を利用して、指定されたpattern(例: encoding/...)に合致するパッケージのみを結果に含めます。また、filepath.SkipDirを利用して、.で始まる隠しディレクトリやtestdataディレクトリなど、パッケージとして扱われるべきではないディレクトリを走査から除外することで、パフォーマンスを向上させています。
  • allPackagesInFS: 相対パス(例: ./myutil/...)が指定された場合に、その相対パスが指すファイルシステム上のディレクトリツリーのみを対象にパッケージを探索します。これもfilepath.WalkmatchPatternを組み合わせて、効率的に目的のパッケージを見つけ出します。build.ScanDirを呼び出すことで、実際にGoのソースファイルが存在し、パッケージとして有効であるかどうかのチェックも行っています。

これらの変更により、goコマンドはより強力で柔軟なパッケージ指定機能を手に入れ、開発者の利便性が大幅に向上しました。

関連リンク

参考にした情報源リンク