[インデックス 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
パッケージのMatch
やGlob
を使用すると、バックスラッシュが意図せずエスケープ文字として解釈されたり、パス区切り文字として正しく機能しなかったりする問題が発生していました。
このコミットは、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?.txt
はfile1.txt
やfileA.txt
などにマッチします。[abc]
: 角括弧内のいずれか1文字にマッチします。例:[abc].txt
はa.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であるかどうかに応じて切り替える点にあります。
-
runtime.GOOS
の利用: Go言語の標準ライブラリであるruntime
パッケージは、現在の実行環境に関する情報を提供します。runtime.GOOS
は、プログラムが実行されているオペレーティングシステムの名前(例:"linux"
,"windows"
,"darwin"
)を文字列で返します。このコミットでは、この変数を利用して、コードの挙動をWindows環境とそれ以外の環境で条件分岐させています。 -
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 } }
-
-
テストコードの修正 (
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環境ではTestMatch
とTestGlob
のテストケース全体がreturn
でスキップされていました。このコミットでは、そのif runtime.GOOS == "windows" { return }
という行が削除され、Windowsでもこれらのテストが実行されるようになりました。 -
Windows固有のテストロジックの追加:
TestMatch
とTestGlob
のループ内で、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
の適用:pattern
とs
(またはresult
)に対してfilepath.Clean
が適用されます。filepath.Clean
は、パスを簡略化し、OS固有のパス区切り文字に正規化する関数です。これにより、テストがWindowsのパス慣習に沿って行われるようになり、テストの信頼性が向上します。
- バックスラッシュを含むパターンのスキップ: Windowsではバックスラッシュがエスケープ文字として機能しないため、テストパターンにリテラルのバックスラッシュが含まれている場合(例:
-
contains
関数からのToSlash
呼び出しの削除:contains
ヘルパー関数は、Glob
の結果が期待値に含まれているかを確認するために使用されます。以前は、比較のためにパスをスラッシュ形式に変換するToSlash
が呼び出されていましたが、Windowsでのパス処理の変更に伴い、この変換が不要になったため削除されました。
これらのテストコードの変更は、Match
およびGlob
関数のWindowsでの新しい挙動を正確に検証するために不可欠です。
関連リンク
- Go言語
path/filepath
パッケージのドキュメント: https://pkg.go.dev/path/filepath - Go言語
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語のコードレビューシステム (Gerrit) での変更セット: https://golang.org/cl/5825044 (コミットメッセージに記載されているリンク)
参考にした情報源リンク
- 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
パッケージのMatch
やGlob
を使用すると、バックスラッシュが意図せずエスケープ文字として解釈されたり、パス区切り文字として正しく機能しなかったりする問題が発生していました。
このコミットは、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?.txt
はfile1.txt
やfileA.txt
などにマッチします。[abc]
: 角括弧内のいずれか1文字にマッチします。例:[abc].txt
はa.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であるかどうかに応じて切り替える点にあります。
-
runtime.GOOS
の利用: Go言語の標準ライブラリであるruntime
パッケージは、現在の実行環境に関する情報を提供します。runtime.GOOS
は、プログラムが実行されているオペレーティングシステムの名前(例:"linux"
,"windows"
,"darwin"
)を文字列で返します。このコミットでは、この変数を利用して、コードの挙動をWindows環境とそれ以外の環境で条件分岐させています。 -
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 } }
-
-
テストコードの修正 (
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環境ではTestMatch
とTestGlob
のテストケース全体がreturn
でスキップされていました。このコミットでは、そのif runtime.GOOS == "windows" { return }
という行が削除され、Windowsでもこれらのテストが実行されるようになりました。 -
Windows固有のテストロジックの追加:
TestMatch
とTestGlob
のループ内で、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
の適用:pattern
とs
(またはresult
)に対してfilepath.Clean
が適用されます。filepath.Clean
は、パスを簡略化し、OS固有のパス区切り文字に正規化する関数です。これにより、テストがWindowsのパス慣習に沿って行われるようになり、テストの信頼性が向上します。
- バックスラッシュを含むパターンのスキップ: Windowsではバックスラッシュがエスケープ文字として機能しないため、テストパターンにリテラルのバックスラッシュが含まれている場合(例:
-
contains
関数からのToSlash
呼び出しの削除:contains
ヘルパー関数は、Glob
の結果が期待値に含まれているかを確認するために使用されます。以前は、比較のためにパスをスラッシュ形式に変換するToSlash
が呼び出されていましたが、Windowsでのパス処理の変更に伴い、この変換が不要になったため削除されました。
これらのテストコードの変更は、Match
およびGlob
関数のWindowsでの新しい挙動を正確に検証するために不可欠です。
関連リンク
- Go言語
path/filepath
パッケージのドキュメント: https://pkg.go.dev/path/filepath - Go言語
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語のコードレビューシステム (Gerrit) での変更セット: https://golang.org/cl/5825044 (コミットメッセージに記載されているリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Gitのコミットログと差分
- グロブパターンに関する一般的な情報 (例: Wikipedia, 各種プログラミング言語のドキュメント)
- WindowsとUnix系OSのファイルパスに関する一般的な情報
golang-dev
メーリングリストのアーカイブ (具体的な議論スレッドは特定できませんでしたが、コミットメッセージに言及があります)