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

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

このコミットは、Go言語の標準ライブラリであるpath/filepathパッケージにおける、ファイルパスのマッチング(Match関数)とグロブパターンによるファイル検索(Glob関数)のWindows環境での挙動を改善するものです。具体的には、Windowsのパス区切り文字であるバックスラッシュ(\)の扱いを、Unix系OSとは異なるWindowsの慣習に合わせて修正し、クロスプラットフォームでの一貫性と正確性を向上させています。

コミット

path/filepathパッケージにおいて、Match関数とGlob関数がWindows上で正しく動作するように実装を修正しました。これは、golang-devメーリングリストでの議論に基づき、Windowsではバックスラッシュ(\)をパス区切り文字として扱い、エスケープ文字としての機能は無効にするという方針に従ったものです。

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

https://github.com/golang/go/commit/2ef4a84022577ee3de1ecf91ef05603a527b9889

元コミット内容

commit 2ef4a84022577ee3de1ecf91ef05603a527b9889
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Mar 19 16:51:06 2012 +1100

    path/filepath: implement Match and Glob on windows
    
    As discussed on golang-dev, windows will use
    "\" as path separator. No escaping allowed.
    
    R=golang-dev, r, mattn.jp, rsc, rogpeppe, bsiegert, r
    CC=golang-dev
    https://golang.org/cl/5825044

変更の背景

Go言語のpath/filepathパッケージは、オペレーティングシステムに依存しないパス操作を提供することを目的としています。しかし、ファイルパスの表現方法はOSによって大きく異なります。特に、パス区切り文字に関しては、Unix系OS(Linux, macOSなど)ではスラッシュ(/)を使用するのに対し、Windowsではバックスラッシュ(\)を使用します。

Match関数やGlob関数は、ファイル名やパスが特定のパターン(グロブパターン)に一致するかどうかを判定するために使用されます。グロブパターンでは、*(任意の文字列)、?(任意の一文字)、[](文字集合)などのワイルドカードが使われますが、これらのワイルドカード文字自体をパターンの一部として扱いたい場合には、エスケープ文字(通常はバックスラッシュ\)を用いてその特殊な意味を打ち消す必要があります。

問題は、Windowsではバックスラッシュがパス区切り文字であると同時に、Unix系OSのグロブパターンではエスケープ文字としても機能するという二重の意味を持つ点にありました。このため、Windows環境でpath/filepathパッケージのMatchGlobを使用すると、バックスラッシュが意図せずエスケープ文字として解釈されたり、パス区切り文字として正しく機能しなかったりする問題が発生していました。

このコミットは、golang-devメーリングリストでの議論を経て、Windows環境ではバックスラッシュをパス区切り文字としてのみ扱い、エスケープ文字としての機能を無効にするという方針を決定し、その実装を反映したものです。これにより、Windowsユーザーが期待するパスの挙動と、グロブパターンのマッチングが両立できるようになりました。

前提知識の解説

Go言語のpath/filepathパッケージ

path/filepathパッケージは、Go言語においてファイルパスを操作するための機能を提供します。このパッケージは、オペレーティングシステム固有のパス表現の違いを抽象化し、クロスプラットフォームで動作するパス操作を可能にします。主な機能には、パスの結合、分割、クリーンアップ、絶対パスへの変換、そしてファイル名やディレクトリ名が特定のパターンにマッチするかどうかの判定などがあります。

  • Match(pattern, name string) (matched bool, err error): この関数は、指定されたname(ファイル名またはパス)がpattern(グロブパターン)に一致するかどうかを判定します。パターンにはワイルドカード文字(*, ?, [])を含めることができます。
  • Glob(pattern string) (matches []string, err error): この関数は、指定されたpatternに一致するファイルやディレクトリのパスを検索し、そのリストを返します。シェルにおけるls *.txtのような操作をプログラム的に実現するものです。

グロブパターン(Glob Pattern)

グロブパターンは、ファイル名やパスをマッチングさせるための簡易的なパターンマッチング構文です。正規表現に似ていますが、よりシンプルでファイルシステムパスの指定によく使われます。

  • *: 0個以上の任意の文字にマッチします。例: *.txt.txtで終わるすべてのファイルにマッチします。
  • ?: 任意の1文字にマッチします。例: file?.txtfile1.txtfileA.txtなどにマッチします。
  • [abc]: 角括弧内のいずれか1文字にマッチします。例: [abc].txta.txt, b.txt, c.txtにマッチします。
  • [!abc] または [^abc]: 角括弧内の文字以外にマッチします。
  • [a-z]: 範囲内の文字にマッチします。
  • \ (バックスラッシュ): 通常、特殊な意味を持つワイルドカード文字(*, ?, [])をエスケープするために使用されます。例えば、foo\*barはリテラルの*を含むファイル名foo*barにマッチします。

WindowsとUnix系OSのパス区切り文字の違い

  • Unix系OS (Linux, macOS): パス区切り文字はスラッシュ(/)です。例: /home/user/documents/report.txt
  • Windows: パス区切り文字はバックスラッシュ(\)です。例: C:\Users\user\Documents\report.txt

この違いが、クロスプラットフォームのパス操作において複雑さをもたらします。Goのpath/filepathパッケージは、内部的にOS固有のパス区切り文字を適切に処理するように設計されていますが、グロブパターンにおけるエスケープ文字としてのバックスラッシュの扱いは、このコミット以前はWindowsの慣習と衝突していました。

技術的詳細

このコミットの主要な変更点は、path/filepathパッケージ内のMatch関数の内部ロジック、特にバックスラッシュ(\)の処理方法を、実行中のOSがWindowsであるかどうかに応じて切り替える点にあります。

  1. runtime.GOOSの利用: Go言語の標準ライブラリであるruntimeパッケージは、現在の実行環境に関する情報を提供します。runtime.GOOSは、プログラムが実行されているオペレーティングシステムの名前(例: "linux", "windows", "darwin")を文字列で返します。このコミットでは、この変数を利用して、コードの挙動をWindows環境とそれ以外の環境で条件分岐させています。

  2. Match関数におけるバックスラッシュのエスケープ処理の無効化: Match関数の内部では、パターン文字列を走査し、ワイルドカードやエスケープ文字を解析するロジックが存在します。変更前は、バックスラッシュが見つかると、それが次の文字をエスケープするものとして処理されていました。 このコミットでは、このエスケープ処理の箇所にif runtime.GOOS != "windows"という条件が追加されました。これにより、Windows環境ではバックスラッシュがエスケープ文字として特別扱いされなくなり、通常の文字(この場合はパス区切り文字)として扱われるようになります。

    • Scanループ内でのパターン解析時:

      		case '\\':
      			if runtime.GOOS != "windows" { // <-- 追加された条件
      				// error check handled in matchChunk: bad pattern.
      				if i+1 < len(pattern) {
      					i++
      				}
      			}
      

      この変更により、Windowsでは\がエスケープ文字として消費されず、次の文字がそのままパターンの一部として解釈されます。

    • matchChunk関数内でのチャンク処理時: matchChunkは、パターンの一部と文字列の一部をマッチングさせる内部関数です。ここでも同様に、バックスラッシュのエスケープ処理がWindowsでのみ無効化されています。

      		case '\\':
      			if runtime.GOOS != "windows" { // <-- 追加された条件
      				chunk = chunk[1:]
      				if len(chunk) == 0 {
      					err = ErrBadPattern
      					return
      				}
      			}
      			fallthrough // <-- エスケープされない場合は、次の文字をリテラルとして扱う
      

      fallthroughキーワードは、switch文で次のcaseブロックのコードを実行するために使用されます。ここでは、Windowsで\がエスケープ文字として扱われない場合、その\自体をリテラル文字として次のマッチング処理に進めることを意味します。

    • getEsc関数内でのエスケープ文字の取得時: getEscは、エスケープされた文字を取得するためのヘルパー関数です。ここでも、Windowsではバックスラッシュをエスケープ文字として認識しないように条件が追加されています。

      	if chunk[0] == '\\' && runtime.GOOS != "windows" { // <-- 変更された条件
      		chunk = chunk[1:]
      		if len(chunk) == 0 {
      			err = ErrBadPattern
      			return
      		}
      	}
      
  3. テストコードの修正 (match_test.go): WindowsでのMatchおよびGlob関数の挙動が変更されたため、テストコードもそれに合わせて修正されました。

    • 以前はWindows環境ではテストをスキップしていましたが、そのスキップロジックが削除され、Windowsでもテストが実行されるようになりました。
    • Windows環境でのテスト実行時、パターンにバックスラッシュ(\)が含まれている場合は、そのテストケースをスキップするようになりました。これは、Windowsでは\がエスケープ文字ではなくパス区切り文字として扱われるため、エスケープを意図したパターンが正しくマッチしないためです。
    • filepath.Clean関数がテストパターンとテスト文字列に適用されるようになりました。filepath.Cleanは、パスを簡略化し、OS固有のパス区切り文字に正規化する関数です。これにより、テストがWindowsのパス慣習に沿って行われるようになります。
    • containsヘルパー関数からToSlashの呼び出しが削除されました。これは、パスをスラッシュに変換する処理が不要になったためです。

これらの変更により、path/filepathパッケージはWindows環境において、バックスラッシュをパス区切り文字として適切に処理し、グロブパターンにおけるエスケープ文字としての誤解釈を防ぐことができるようになりました。

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

src/pkg/path/filepath/match.go

--- a/src/pkg/path/filepath/match.go
+++ b/src/pkg/path/filepath/match.go
@@ -7,6 +7,7 @@ package filepath
 import (
 	"errors"
 	"os"
+	"runtime" // <-- 追加
 	"sort"
 	"strings"
 	"unicode/utf8"
@@ -37,6 +38,9 @@ var ErrBadPattern = errors.New("syntax error in pattern")
 // The only possible returned error is ErrBadPattern, when pattern
 // is malformed.
 //
+// On Windows, escaping is disabled. Instead, '\' is treated as
+// path separator.
+//
 func Match(pattern, name string) (matched bool, err error) {
 Pattern:
 	for len(pattern) > 0 {
@@ -95,9 +99,11 @@ Scan:
 	for i = 0; i < len(pattern); i++ {
 		switch pattern[i] {
 		case '\\':
-			// error check handled in matchChunk: bad pattern.
-			if i+1 < len(pattern) {
-				i++
+			if runtime.GOOS != "windows" { // <-- 追加
+				// error check handled in matchChunk: bad pattern.
+				if i+1 < len(pattern) {
+					i++
+				}
 			}
 		case '[':
 			inrange = true
@@ -167,10 +173,12 @@ func matchChunk(chunk, s string) (rest string, ok bool, err error) {
 			chunk = chunk[1:]
 
 		case '\\':
-			chunk = chunk[1:]
-			if len(chunk) == 0 {
-				err = ErrBadPattern
-				return
+			if runtime.GOOS != "windows" { // <-- 追加
+				chunk = chunk[1:]
+				if len(chunk) == 0 {
+					err = ErrBadPattern
+					return
+				}
 			}
 			fallthrough
 
@@ -191,7 +199,7 @@ func getEsc(chunk string) (r rune, nchunk string, err error) {
 		err = ErrBadPattern
 		return
 	}
-	if chunk[0] == '\\' {
+	if chunk[0] == '\\' && runtime.GOOS != "windows" { // <-- 変更
 		chunk = chunk[1:]
 		if len(chunk) == 0 {
 			err = ErrBadPattern

src/pkg/path/filepath/match_test.go

--- a/src/pkg/path/filepath/match_test.go
+++ b/src/pkg/path/filepath/match_test.go
@@ -7,21 +7,26 @@ package filepath_test
 import (
 	. "path/filepath"
 	"runtime"
+	"strings" // <-- 追加
 	"testing"
 )
 
@@ -76,21 +77,26 @@ func errp(e error) string {
 }
 
 func TestMatch(t *testing.T) {
-\tif runtime.GOOS == "windows" {
-\t\t// XXX: Don't pass for windows.
-\t\treturn
-\t}\n // <-- 削除
 	for _, tt := range matchTests {
-\t\tok, err := Match(tt.pattern, tt.s)\n+\t\tpattern := tt.pattern // <-- 追加
+\t\ts := tt.s // <-- 追加
+\t\tif runtime.GOOS == "windows" { // <-- 追加
+\t\t\tif strings.Index(pattern, "\\") >= 0 { // <-- 追加
+\t\t\t\t// no escape allowed on windows. // <-- 追加
+\t\t\t\tcontinue // <-- 追加
+\t\t\t} // <-- 追加
+\t\t\tpattern = Clean(pattern) // <-- 追加
+\t\t\ts = Clean(s) // <-- 追加
+\t\t} // <-- 追加
+\t\tok, err := Match(pattern, s) // <-- 変更
 	\tif ok != tt.match || err != tt.err {\
-\t\t\tt.Errorf("Match(%#q, %#q) = %v, %q want %v, %q", tt.pattern, tt.s, ok, errp(err), tt.match, errp(tt.err))\n+\t\t\tt.Errorf("Match(%#q, %#q) = %v, %q want %v, %q", pattern, s, ok, errp(err), tt.match, errp(tt.err)) // <-- 変更
 	\t}\
 	}\
 }
 
 // contains returns true if vector contains the string s.
 func contains(vector []string, s string) bool {
-\ts = ToSlash(s)\n // <-- 削除
 	for _, elem := range vector {
 	\tif elem == s {
 	\t\treturn true
@@ -109,18 +115,20 @@ var globTests = []struct {
 }\
 
 func TestGlob(t *testing.T) {
-\tif runtime.GOOS == "windows" {
-\t\t// XXX: Don't pass for windows.
-\t\treturn
-\t}\n // <-- 削除
 	for _, tt := range globTests {
-\t\tmatches, err := Glob(tt.pattern)\n+\t\tpattern := tt.pattern // <-- 追加
+\t\tresult := tt.result // <-- 追加
+\t\tif runtime.GOOS == "windows" { // <-- 追加
+\t\t\tpattern = Clean(pattern) // <-- 追加
+\t\t\tresult = Clean(result) // <-- 追加
+\t\t} // <-- 追加
+\t\tmatches, err := Glob(pattern) // <-- 変更
 	\tif err != nil {\
-\t\t\tt.Errorf("Glob error for %q: %s", tt.pattern, err)\n+\t\t\tt.Errorf("Glob error for %q: %s", pattern, err) // <-- 変更
 	\t\tcontinue
 	\t}\
-\t\tif !contains(matches, tt.result) {\
-\t\t\tt.Errorf("Glob(%#q) = %#v want %v", tt.pattern, matches, tt.result)\n+\t\t\tif !contains(matches, result) { // <-- 変更
+\t\t\t\tt.Errorf("Glob(%#q) = %#v want %v", pattern, matches, result) // <-- 変更
 	\t}\
 	}\
 	for _, pattern := []string{"no_match", "../*/no_match"} {

コアとなるコードの解説

src/pkg/path/filepath/match.goの変更点

  • import "runtime"の追加: runtime.GOOS変数を使用するために、runtimeパッケージがインポートされました。これにより、現在のオペレーティングシステムをプログラム内で判別できるようになります。

  • Match関数のコメント追加: Match関数のドキュメントに「On Windows, escaping is disabled. Instead, '' is treated as path separator.」という記述が追加されました。これは、Windowsでのバックスラッシュの挙動変更を明示的に示しています。

  • Scanループ内のcase '\\'の変更:

    		case '\\':
    			if runtime.GOOS != "windows" {
    				// ... (既存のエスケープ処理) ...
    			}
    

    この変更は、パターン文字列を走査する際にバックスラッシュが見つかった場合の処理を制御します。runtime.GOOS != "windows"という条件が追加されたことで、Windows以外のOSではこれまで通りバックスラッシュがエスケープ文字として扱われますが、Windowsではこのブロックがスキップされます。結果として、Windowsではバックスラッシュがエスケープ文字として消費されず、リテラル文字(パス区切り文字)として次のマッチング処理に進むことになります。

  • matchChunk関数内のcase '\\'の変更:

    		case '\\':
    			if runtime.GOOS != "windows" {
    				// ... (既存のエスケープ処理) ...
    			}
    			fallthrough
    

    matchChunkは、パターンと文字列のチャンク(部分)をマッチングさせる内部関数です。ここでも同様に、runtime.GOOS != "windows"の条件が追加されました。Windows以外のOSでは、バックスラッシュがエスケープ文字として処理され、その次の文字がリテラルとして扱われます。しかし、Windowsではこのブロックがスキップされ、fallthroughによって次のcase(この場合はデフォルトの文字マッチング)に処理が移ります。これにより、Windowsではバックスラッシュがリテラル文字としてマッチングの対象となります。

  • getEsc関数内の条件変更:

    	if chunk[0] == '\\' && runtime.GOOS != "windows" {
    		// ... (既存のエスケープ処理) ...
    	}
    

    getEscは、エスケープされた文字を取得するヘルパー関数です。この条件は、チャンクの最初の文字がバックスラッシュであり、かつOSがWindowsではない場合にのみ、エスケープ処理を行うように変更されました。Windowsでは、バックスラッシュがエスケープ文字として機能しないため、この関数はバックスラッシュをエスケープ文字として認識しなくなります。

これらの変更により、Windows環境では\がパス区切り文字として機能し、グロブパターンにおけるエスケープ文字としての特殊な意味を持たなくなりました。

src/pkg/path/filepath/match_test.goの変更点

  • import "strings"の追加: テストコード内で文字列操作を行うためにstringsパッケージがインポートされました。

  • TestMatchおよびTestGlobからのWindowsスキップロジックの削除: 変更前は、Windows環境ではTestMatchTestGlobのテストケース全体がreturnでスキップされていました。このコミットでは、そのif runtime.GOOS == "windows" { return }という行が削除され、Windowsでもこれらのテストが実行されるようになりました。

  • Windows固有のテストロジックの追加: TestMatchTestGlobのループ内で、if runtime.GOOS == "windows"という条件ブロックが追加されました。

    		if runtime.GOOS == "windows" {
    			if strings.Index(pattern, "\\") >= 0 {
    				// no escape allowed on windows.
    				continue
    			}
    			pattern = Clean(pattern)
    			s = Clean(s) // TestMatchの場合
    			// result = Clean(result) // TestGlobの場合
    		}
    
    • バックスラッシュを含むパターンのスキップ: Windowsではバックスラッシュがエスケープ文字として機能しないため、テストパターンにリテラルのバックスラッシュが含まれている場合(例: foo\*barのようなエスケープを意図したパターン)、そのテストケースはスキップされます。これは、変更後のMatch関数がそのようなパターンを正しく処理できないためです。
    • filepath.Cleanの適用: patterns(またはresult)に対してfilepath.Cleanが適用されます。filepath.Cleanは、パスを簡略化し、OS固有のパス区切り文字に正規化する関数です。これにより、テストがWindowsのパス慣習に沿って行われるようになり、テストの信頼性が向上します。
  • contains関数からのToSlash呼び出しの削除: containsヘルパー関数は、Globの結果が期待値に含まれているかを確認するために使用されます。以前は、比較のためにパスをスラッシュ形式に変換するToSlashが呼び出されていましたが、Windowsでのパス処理の変更に伴い、この変換が不要になったため削除されました。

これらのテストコードの変更は、MatchおよびGlob関数のWindowsでの新しい挙動を正確に検証するために不可欠です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Gitのコミットログと差分
  • グロブパターンに関する一般的な情報 (例: Wikipedia, 各種プログラミング言語のドキュメント)
  • WindowsとUnix系OSのファイルパスに関する一般的な情報
  • golang-devメーリングリストのアーカイブ (具体的な議論スレッドは特定できませんでしたが、コミットメッセージに言及があります)# [インデックス 12684] ファイルの概要

このコミットは、Go言語の標準ライブラリであるpath/filepathパッケージにおける、ファイルパスのマッチング(Match関数)とグロブパターンによるファイル検索(Glob関数)のWindows環境での挙動を改善するものです。具体的には、Windowsのパス区切り文字であるバックスラッシュ(\)の扱いを、Unix系OSとは異なるWindowsの慣習に合わせて修正し、クロスプラットフォームでの一貫性と正確性を向上させています。

コミット

path/filepathパッケージにおいて、Match関数とGlob関数がWindows上で正しく動作するように実装を修正しました。これは、golang-devメーリングリストでの議論に基づき、Windowsではバックスラッシュ(\)をパス区切り文字として扱い、エスケープ文字としての機能は無効にするという方針に従ったものです。

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

https://github.com/golang/go/commit/2ef4a84022577ee3de1ecf91ef05603a527b9889

元コミット内容

commit 2ef4a84022577ee3de1ecf91ef05603a527b9889
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Mar 19 16:51:06 2012 +1100

    path/filepath: implement Match and Glob on windows
    
    As discussed on golang-dev, windows will use
    "\" as path separator. No escaping allowed.
    
    R=golang-dev, r, mattn.jp, rsc, rogpeppe, bsiegert, r
    CC=golang-dev
    https://golang.org/cl/5825044

変更の背景

Go言語のpath/filepathパッケージは、オペレーティングシステムに依存しないパス操作を提供することを目的としています。しかし、ファイルパスの表現方法はOSによって大きく異なります。特に、パス区切り文字に関しては、Unix系OS(Linux, macOSなど)ではスラッシュ(/)を使用するのに対し、Windowsではバックスラッシュ(\)を使用します。

Match関数やGlob関数は、ファイル名やパスが特定のパターン(グロブパターン)に一致するかどうかを判定するために使用されます。グロブパターンでは、*(任意の文字列)、?(任意の一文字)、[](文字集合)などのワイルドカードが使われますが、これらのワイルドカード文字自体をパターンの一部として扱いたい場合には、エスケープ文字(通常はバックスラッシュ\)を用いてその特殊な意味を打ち消す必要があります。

問題は、Windowsではバックスラッシュがパス区切り文字であると同時に、Unix系OSのグロブパターンではエスケープ文字としても機能するという二重の意味を持つ点にありました。このため、Windows環境でpath/filepathパッケージのMatchGlobを使用すると、バックスラッシュが意図せずエスケープ文字として解釈されたり、パス区切り文字として正しく機能しなかったりする問題が発生していました。

このコミットは、golang-devメーリングリストでの議論を経て、Windows環境ではバックスラッシュをパス区切り文字としてのみ扱い、エスケープ文字としての機能を無効にするという方針を決定し、その実装を反映したものです。これにより、Windowsユーザーが期待するパスの挙動と、グロブパターンのマッチングが両立できるようになりました。

前提知識の解説

Go言語のpath/filepathパッケージ

path/filepathパッケージは、Go言語においてファイルパスを操作するための機能を提供します。このパッケージは、オペレーティングシステム固有のパス表現の違いを抽象化し、クロスプラットフォームで動作するパス操作を可能にします。主な機能には、パスの結合、分割、クリーンアップ、絶対パスへの変換、そしてファイル名やディレクトリ名が特定のパターンにマッチするかどうかの判定などがあります。

  • Match(pattern, name string) (matched bool, err error): この関数は、指定されたname(ファイル名またはパス)がpattern(グロブパターン)に一致するかどうかを判定します。パターンにはワイルドカード文字(*, ?, [])を含めることができます。
  • Glob(pattern string) (matches []string, err error): この関数は、指定されたpatternに一致するファイルやディレクトリのパスを検索し、そのリストを返します。シェルにおけるls *.txtのような操作をプログラム的に実現するものです。

グロブパターン(Glob Pattern)

グロブパターンは、ファイル名やパスをマッチングさせるための簡易的なパターンマッチング構文です。正規表現に似ていますが、よりシンプルでファイルシステムパスの指定によく使われます。

  • *: 0個以上の任意の文字にマッチします。例: *.txt.txtで終わるすべてのファイルにマッチします。
  • ?: 任意の1文字にマッチします。例: file?.txtfile1.txtfileA.txtなどにマッチします。
  • [abc]: 角括弧内のいずれか1文字にマッチします。例: [abc].txta.txt, b.txt, c.txtにマッチします。
  • [!abc] または [^abc]: 角括弧内の文字以外にマッチします。
  • [a-z]: 範囲内の文字にマッチします。
  • \ (バックスラッシュ): 通常、特殊な意味を持つワイルドカード文字(*, ?, [])をエスケープするために使用されます。例えば、foo\*barはリテラルの*を含むファイル名foo*barにマッチします。

WindowsとUnix系OSのパス区切り文字の違い

  • Unix系OS (Linux, macOS): パス区切り文字はスラッシュ(/)です。例: /home/user/documents/report.txt
  • Windows: パス区切り文字はバックスラッシュ(\)です。例: C:\Users\user\Documents\report.txt

この違いが、クロスプラットフォームのパス操作において複雑さをもたらします。Goのpath/filepathパッケージは、内部的にOS固有のパス区切り文字を適切に処理するように設計されていますが、グロブパターンにおけるエスケープ文字としてのバックスラッシュの扱いは、このコミット以前はWindowsの慣習と衝突していました。

技術的詳細

このコミットの主要な変更点は、path/filepathパッケージ内のMatch関数の内部ロジック、特にバックスラッシュ(\)の処理方法を、実行中のOSがWindowsであるかどうかに応じて切り替える点にあります。

  1. runtime.GOOSの利用: Go言語の標準ライブラリであるruntimeパッケージは、現在の実行環境に関する情報を提供します。runtime.GOOSは、プログラムが実行されているオペレーティングシステムの名前(例: "linux", "windows", "darwin")を文字列で返します。このコミットでは、この変数を利用して、コードの挙動をWindows環境とそれ以外の環境で条件分岐させています。

  2. Match関数におけるバックスラッシュのエスケープ処理の無効化: Match関数の内部では、パターン文字列を走査し、ワイルドカードやエスケープ文字を解析するロジックが存在します。変更前は、バックスラッシュが見つかると、それが次の文字をエスケープするものとして処理されていました。 このコミットでは、このエスケープ処理の箇所にif runtime.GOOS != "windows"という条件が追加されました。これにより、Windows環境ではバックスラッシュがエスケープ文字として特別扱いされなくなり、通常の文字(この場合はパス区切り文字)として扱われるようになります。

    • Scanループ内でのパターン解析時:

      		case '\\':
      			if runtime.GOOS != "windows" { // <-- 追加された条件
      				// error check handled in matchChunk: bad pattern.
      				if i+1 < len(pattern) {
      					i++
      				}
      			}
      

      この変更により、Windowsでは\がエスケープ文字として消費されず、次の文字がそのままパターンの一部として解釈されます。

    • matchChunk関数内でのチャンク処理時: matchChunkは、パターンの一部と文字列の一部をマッチングさせる内部関数です。ここでも同様に、バックスラッシュのエスケープ処理がWindowsでのみ無効化されています。

      		case '\\':
      			if runtime.GOOS != "windows" { // <-- 追加された条件
      				chunk = chunk[1:]
      				if len(chunk) == 0 {
      					err = ErrBadPattern
      					return
      				}
      			}
      			fallthrough // <-- エスケープされない場合は、次の文字をリテラルとして扱う
      

      fallthroughキーワードは、switch文で次のcaseブロックのコードを実行するために使用されます。ここでは、Windowsで\がエスケープ文字として扱われない場合、その\自体をリテラル文字として次のマッチング処理に進めることを意味します。

    • getEsc関数内でのエスケープ文字の取得時: getEscは、エスケープされた文字を取得するためのヘルパー関数です。ここでも、Windowsではバックスラッシュをエスケープ文字として認識しないように条件が追加されています。

      	if chunk[0] == '\\' && runtime.GOOS != "windows" { // <-- 変更された条件
      		chunk = chunk[1:]
      		if len(chunk) == 0 {
      			err = ErrBadPattern
      			return
      		}
      	}
      
  3. テストコードの修正 (match_test.go): WindowsでのMatchおよびGlob関数の挙動が変更されたため、テストコードもそれに合わせて修正されました。

    • 以前はWindows環境ではテストをスキップしていましたが、そのスキップロジックが削除され、Windowsでもテストが実行されるようになりました。
    • Windows環境でのテスト実行時、パターンにバックスラッシュ(\)が含まれている場合は、そのテストケースをスキップするようになりました。これは、Windowsでは\がエスケープ文字ではなくパス区切り文字として扱われるため、エスケープを意図したパターンが正しくマッチしないためです。
    • filepath.Clean関数がテストパターンとテスト文字列に適用されるようになりました。filepath.Cleanは、パスを簡略化し、OS固有のパス区切り文字に正規化する関数です。これにより、テストがWindowsのパス慣習に沿って行われるようになります。
    • containsヘルパー関数からToSlashの呼び出しが削除されました。これは、パスをスラッシュに変換する処理が不要になったためです。

これらの変更により、path/filepathパッケージはWindows環境において、バックスラッシュをパス区切り文字として適切に処理し、グロブパターンにおけるエスケープ文字としての誤解釈を防ぐことができるようになりました。

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

src/pkg/path/filepath/match.go

--- a/src/pkg/path/filepath/match.go
+++ b/src/pkg/path/filepath/match.go
@@ -7,6 +7,7 @@ package filepath
 import (
 	"errors"
 	"os"
+	"runtime" // <-- 追加
 	"sort"
 	"strings"
 	"unicode/utf8"
@@ -37,6 +38,9 @@ var ErrBadPattern = errors.New("syntax error in pattern")
 // The only possible returned error is ErrBadPattern, when pattern
 // is malformed.
 //
+// On Windows, escaping is disabled. Instead, '\' is treated as
+// path separator.
+//
 func Match(pattern, name string) (matched bool, err error) {
 Pattern:
 	for len(pattern) > 0 {
@@ -95,9 +99,11 @@ Scan:
 	for i = 0; i < len(pattern); i++ {
 		switch pattern[i] {
 		case '\\':
-			// error check handled in matchChunk: bad pattern.
-			if i+1 < len(pattern) {
-				i++
+			if runtime.GOOS != "windows" { // <-- 追加
+				// error check handled in matchChunk: bad pattern.
+				if i+1 < len(pattern) {
+					i++
+				}
 			}
 		case '[':
 			inrange = true
@@ -167,10 +173,12 @@ func matchChunk(chunk, s string) (rest string, ok bool, err error) {
 			chunk = chunk[1:]
 
 		case '\\':
-			chunk = chunk[1:]
-			if len(chunk) == 0 {
-				err = ErrBadPattern
-				return
+			if runtime.GOOS != "windows" { // <-- 追加
+				chunk = chunk[1:]
+				if len(chunk) == 0 {
+					err = ErrBadPattern
+					return
+				}
 			}
 			fallthrough
 
@@ -191,7 +199,7 @@ func getEsc(chunk string) (r rune, nchunk string, err error) {
 		err = ErrBadPattern
 		return
 	}
-	if chunk[0] == '\\' {
+	if chunk[0] == '\\' && runtime.GOOS != "windows" { // <-- 変更
 		chunk = chunk[1:]
 		if len(chunk) == 0 {
 			err = ErrBadPattern

src/pkg/path/filepath/match_test.go

--- a/src/pkg/path/filepath/match_test.go
+++ b/src/pkg/path/filepath/match_test.go
@@ -7,21 +7,26 @@ package filepath_test
 import (
 	. "path/filepath"
 	"runtime"
+	"strings" // <-- 追加
 	"testing"
 )
 
@@ -76,21 +77,26 @@ func errp(e error) string {
 }
 
 func TestMatch(t *testing.T) {
-\tif runtime.GOOS == "windows" {
-\t\t// XXX: Don't pass for windows.
-\t\treturn
-\t}\n // <-- 削除
 	for _, tt := range matchTests {
-\t\tok, err := Match(tt.pattern, tt.s)\n+\t\tpattern := tt.pattern // <-- 追加
+\t\ts := tt.s // <-- 追加
+\t\tif runtime.GOOS == "windows" { // <-- 追加
+\t\t\tif strings.Index(pattern, "\\") >= 0 { // <-- 追加
+\t\t\t\t// no escape allowed on windows. // <-- 追加
+\t\t\t\tcontinue // <-- 追加
+\t\t\t} // <-- 追加
+\t\t\tpattern = Clean(pattern) // <-- 追加
+\t\t\ts = Clean(s) // <-- 追加
+\t\t} // <-- 追加
+\t\tok, err := Match(pattern, s) // <-- 変更
 	\tif ok != tt.match || err != tt.err {\
-\t\t\tt.Errorf("Match(%#q, %#q) = %v, %q want %v, %q", tt.pattern, tt.s, ok, errp(err), tt.match, errp(tt.err))\n+\t\t\tt.Errorf("Match(%#q, %#q) = %v, %q want %v, %q", pattern, s, ok, errp(err), tt.match, errp(tt.err)) // <-- 変更
 	\t}\
 	}\
 }
 
 // contains returns true if vector contains the string s.
 func contains(vector []string, s string) bool {
-\ts = ToSlash(s)\n // <-- 削除
 	for _, elem := range vector {
 	\tif elem == s {
 	\t\treturn true
@@ -109,18 +115,20 @@ var globTests = []struct {
 }\
 
 func TestGlob(t *testing.T) {
-\tif runtime.GOOS == "windows" {
-\t\t// XXX: Don't pass for windows.
-\t\treturn
-\t}\n // <-- 削除
 	for _, tt := range globTests {
-\t\tmatches, err := Glob(tt.pattern)\n+\t\tpattern := tt.pattern // <-- 追加
+\t\tresult := tt.result // <-- 追加
+\t\tif runtime.GOOS == "windows" { // <-- 追加
+\t\t\tpattern = Clean(pattern) // <-- 追加
+\t\t\tresult = Clean(result) // <-- 追加
+\t\t} // <-- 追加
+\t\tmatches, err := Glob(pattern) // <-- 変更
 	\tif err != nil {\
-\t\t\tt.Errorf("Glob error for %q: %s", tt.pattern, err)\n+\t\t\tt.Errorf("Glob error for %q: %s", pattern, err) // <-- 変更
 	\t\tcontinue
 	\t}\
-\t\tif !contains(matches, tt.result) {\
-\t\t\tt.Errorf("Glob(%#q) = %#v want %v", tt.pattern, matches, tt.result)\n+\t\t\tif !contains(matches, result) { // <-- 変更
+\t\t\t\tt.Errorf("Glob(%#q) = %#v want %v", pattern, matches, result) // <-- 変更
 	\t}\
 	}\
 	for _, pattern := range []string{"no_match", "../*/no_match"} {

コアとなるコードの解説

src/pkg/path/filepath/match.goの変更点

  • import "runtime"の追加: runtime.GOOS変数を使用するために、runtimeパッケージがインポートされました。これにより、現在のオペレーティングシステムをプログラム内で判別できるようになります。

  • Match関数のコメント追加: Match関数のドキュメントに「On Windows, escaping is disabled. Instead, '' is treated as path separator.」という記述が追加されました。これは、Windowsでのバックスラッシュの挙動変更を明示的に示しています。

  • Scanループ内のcase '\\'の変更:

    		case '\\':
    			if runtime.GOOS != "windows" {
    				// ... (既存のエスケープ処理) ...
    			}
    

    この変更は、パターン文字列を走査する際にバックスラッシュが見つかった場合の処理を制御します。runtime.GOOS != "windows"という条件が追加されたことで、Windows以外のOSではこれまで通りバックスラッシュがエスケープ文字として扱われますが、Windowsではこのブロックがスキップされます。結果として、Windowsではバックスラッシュがエスケープ文字として消費されず、リテラル文字(パス区切り文字)として次のマッチング処理に進むことになります。

  • matchChunk関数内のcase '\\'の変更:

    		case '\\':
    			if runtime.GOOS != "windows" {
    				// ... (既存のエスケープ処理) ...
    			}
    			fallthrough
    

    matchChunkは、パターンと文字列のチャンク(部分)をマッチングさせる内部関数です。ここでも同様に、runtime.GOOS != "windows"の条件が追加されました。Windows以外のOSでは、バックスラッシュがエスケープ文字として処理され、その次の文字がリテラルとして扱われます。しかし、Windowsではこのブロックがスキップされ、fallthroughによって次のcase(この場合はデフォルトの文字マッチング)に処理が移ります。これにより、Windowsではバックスラッシュがリテラル文字としてマッチングの対象となります。

  • getEsc関数内の条件変更:

    	if chunk[0] == '\\' && runtime.GOOS != "windows" {
    		// ... (既存のエスケープ処理) ...
    	}
    

    getEscは、エスケープされた文字を取得するヘルパー関数です。この条件は、チャンクの最初の文字がバックスラッシュであり、かつOSがWindowsではない場合にのみ、エスケープ処理を行うように変更されました。Windowsでは、バックスラッシュがエスケープ文字として機能しないため、この関数はバックスラッシュをエスケープ文字として認識しなくなります。

これらの変更により、Windows環境では\がパス区切り文字として機能し、グロブパターンにおけるエスケープ文字としての特殊な意味を持たなくなりました。

src/pkg/path/filepath/match_test.goの変更点

  • import "strings"の追加: テストコード内で文字列操作を行うためにstringsパッケージがインポートされました。

  • TestMatchおよびTestGlobからのWindowsスキップロジックの削除: 変更前は、Windows環境ではTestMatchTestGlobのテストケース全体がreturnでスキップされていました。このコミットでは、そのif runtime.GOOS == "windows" { return }という行が削除され、Windowsでもこれらのテストが実行されるようになりました。

  • Windows固有のテストロジックの追加: TestMatchTestGlobのループ内で、if runtime.GOOS == "windows"という条件ブロックが追加されました。

    		if runtime.GOOS == "windows" {
    			if strings.Index(pattern, "\\") >= 0 {
    				// no escape allowed on windows.
    				continue
    			}
    			pattern = Clean(pattern)
    			s = Clean(s) // TestMatchの場合
    			// result = Clean(result) // TestGlobの場合
    		}
    
    • バックスラッシュを含むパターンのスキップ: Windowsではバックスラッシュがエスケープ文字として機能しないため、テストパターンにリテラルのバックスラッシュが含まれている場合(例: foo\*barのようなエスケープを意図したパターン)、そのテストケースはスキップされます。これは、変更後のMatch関数がそのようなパターンを正しく処理できないためです。
    • filepath.Cleanの適用: patterns(またはresult)に対してfilepath.Cleanが適用されます。filepath.Cleanは、パスを簡略化し、OS固有のパス区切り文字に正規化する関数です。これにより、テストがWindowsのパス慣習に沿って行われるようになり、テストの信頼性が向上します。
  • contains関数からのToSlash呼び出しの削除: containsヘルパー関数は、Globの結果が期待値に含まれているかを確認するために使用されます。以前は、比較のためにパスをスラッシュ形式に変換するToSlashが呼び出されていましたが、Windowsでのパス処理の変更に伴い、この変換が不要になったため削除されました。

これらのテストコードの変更は、MatchおよびGlob関数のWindowsでの新しい挙動を正確に検証するために不可欠です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Gitのコミットログと差分
  • グロブパターンに関する一般的な情報 (例: Wikipedia, 各種プログラミング言語のドキュメント)
  • WindowsとUnix系OSのファイルパスに関する一般的な情報
  • golang-devメーリングリストのアーカイブ (具体的な議論スレッドは特定できませんでしたが、コミットメッセージに言及があります)