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

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

このコミットは、Go言語の標準ライブラリである path/filepath パッケージにおいて、Windows環境でのパス操作関数 Base および Dir の実装を改善するものです。具体的には、Windows特有のパス形式(ドライブレターやUNCパスなど)を正確に処理できるように修正が加えられています。

コミット

  • コミットハッシュ: 5962ef2c008e10b21ad73a7bdc08713225f90297
  • 作者: Alex Brainman alex.brainman@gmail.com
  • コミット日時: Fri Dec 23 13:23:07 2011 +1100
  • コミットメッセージ:
    path/filepath: implement Base and Dir for windows
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5501069
    

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

https://github.com/golang/go/commit/5962ef2c008e10b21ad73a7bdc08713225f90297

元コミット内容

commit 5962ef2c008e10b21ad73a7bdc08713225f90297
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Dec 23 13:23:07 2011 +1100

    path/filepath: implement Base and Dir for windows
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5501069
---
 src/pkg/path/filepath/path.go      | 12 +++++++---\
 src/pkg/path/filepath/path_test.go | 48 ++++++++++++++++++++++++++++++++++----\
 2 files changed, 53 insertions(+), 7 deletions(-)\

diff --git a/src/pkg/path/filepath/path.go b/src/pkg/path/filepath/path.go
index f1cda7c530..3dc52aab46 100644
--- a/src/pkg/path/filepath/path.go
+++ b/src/pkg/path/filepath/path.go
@@ -426,6 +426,8 @@ func Base(path string) string {
 	for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) {
 		path = path[0 : len(path)-1]
 	}\n+\t// Throw away volume name\n+\tpath = path[len(VolumeName(path)):]\n 	// Find the last element\n 	i := len(path) - 1\n 	for i >= 0 && !os.IsPathSeparator(path[i]) {\
@@ -447,8 +449,12 @@ func Base(path string) string {
 // If the path consists entirely of separators, Dir returns a single separator.
 // The returned path does not end in a separator unless it is the root directory.
 func Dir(path string) string {
-\tdir, _ := Split(path)\n-\tdir = Clean(dir)\n+\tvol := VolumeName(path)\n+\ti := len(path) - 1\n+\tfor i >= len(vol) && !os.IsPathSeparator(path[i]) {\
+\t\ti--\n+\t}\n+\tdir := Clean(path[len(vol) : i+1])\n \tlast := len(dir) - 1\n \tif last > 0 && os.IsPathSeparator(dir[last]) {\
 \t\tdir = dir[:last]\n@@ -456,5 +462,5 @@ func Dir(path string) string {\
 \tif dir == \"\" {\
 \t\tdir = \".\"\n \t}\n-\treturn dir\n+\treturn vol + dir\n }\ndiff --git a/src/pkg/path/filepath/path_test.go b/src/pkg/path/filepath/path_test.go
index 49a7135b4a..966b08e4f8 100644
--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -423,9 +423,29 @@ var basetests = []PathTest{\
 	{\"a/b/c.x\", \"c.x\"},\
 }\n \
+var winbasetests = []PathTest{\
+\t{`c:\\`, `\\`},\
+\t{`c:.`, `.`},\
+\t{`c:\\a\\b`, `b`},\
+\t{`c:a\\b`, `b`},\
+\t{`c:a\\b\\c`, `c`},\
+\t{`\\\\host\\share\\`, `\\`},\
+\t{`\\\\host\\share\\a`, `a`},\
+\t{`\\\\host\\share\\a\\b`, `b`},\
+}\n+\n func TestBase(t *testing.T) {\
-\tfor _, test := range basetests {\
-\t\tif s := filepath.ToSlash(filepath.Base(test.path)); s != test.result {\
+\ttests := basetests\n+\tif runtime.GOOS == \"windows\" {\
+\t\t// make unix tests work on windows\n+\t\tfor i, _ := range tests {\
+\t\t\ttests[i].result = filepath.Clean(tests[i].result)\n+\t\t}\n+\t\t// add windows specific tests\n+\t\ttests = append(tests, winbasetests...)\n+\t}\n+\tfor _, test := range tests {\
+\t\tif s := filepath.Base(test.path); s != test.result {\
 \t\t\tt.Errorf(\"Base(%q) = %q, want %q\", test.path, s, test.result)\
 \t\t}\n \t}\
@@ -446,9 +466,29 @@ var dirtests = []PathTest{\
 	{\"a/b/c.x\", \"a/b\"},\
 }\n \
+var windirtests = []PathTest{\
+\t{`c:\\`, `c:\\`},\
+\t{`c:.`, `c:.`},\
+\t{`c:\\a\\b`, `c:\\a`},\
+\t{`c:a\\b`, `c:a`},\
+\t{`c:a\\b\\c`, `c:a\\b`},\
+\t{`\\\\host\\share\\`, `\\\\host\\share\\`},\
+\t{`\\\\host\\share\\a`, `\\\\host\\share\\`},\
+\t{`\\\\host\\share\\a\\b`, `\\\\host\\share\\a`},\
+}\n+\n func TestDir(t *t.T) {\
-\tfor _, test := range dirtests {\
-\t\tif s := filepath.ToSlash(filepath.Dir(test.path)); s != test.result {\
+\ttests := dirtests\n+\tif runtime.GOOS == \"windows\" {\
+\t\t// make unix tests work on windows\n+\t\tfor i, _ := range tests {\
+\t\t\ttests[i].result = filepath.Clean(tests[i].result)\n+\t\t}\n+\t\t// add windows specific tests\n+\t\ttests = append(tests, windirtests...)\n+\t}\n+\tfor _, test := range tests {\
+\t\tif s := filepath.Dir(test.path); s != test.result {\
 \t\t\tt.Errorf(\"Dir(%q) = %q, want %q\", test.path, s, test.result)\
 \t\t}\n \t}\

変更の背景

Go言語はクロスプラットフォーム対応を重視しており、異なるオペレーティングシステム(OS)間で一貫した動作を提供することを目指しています。しかし、ファイルパスの構造や処理方法はOSによって大きく異なります。特にWindowsは、Unix系OSとは異なる独自のパス表現(ドライブレター、UNCパス、バックスラッシュをセパレータとして使用するなど)を持っています。

このコミット以前の path/filepath パッケージの Base および Dir 関数は、Windows特有のパス形式を適切に処理できていなかったと考えられます。例えば、C:\foo\bar のようなパスに対して Base 関数が bar を返すのは正しいですが、C:\\\server\share のようなパスに対しては、期待される結果と異なる動作をしていた可能性があります。これは、Unix系のパス処理ロジックがそのまま適用されていたためと考えられます。

このコミットの目的は、Base および Dir 関数がWindows環境下で、Windowsのファイルシステム規則に則った正しい結果を返すように修正することです。これにより、GoアプリケーションがWindows上でファイルパスを扱う際に、より堅牢で予測可能な動作が保証されるようになります。

前提知識の解説

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

path/filepath パッケージは、Go言語の標準ライブラリの一部であり、ファイルパスを操作するためのユーティリティ関数を提供します。このパッケージは、OS固有のパス表現を考慮して設計されており、クロスプラットフォームなファイルパス操作を可能にします。

  • filepath.Base(path string) string: 与えられたパスの最後の要素(ファイル名またはディレクトリ名)を返します。パスがディレクトリセパレータで終わる場合、そのセパレータは無視されます。パスが空文字列の場合、"." を返します。 例:

    • filepath.Base("a/b/c") -> "c"
    • filepath.Base("a/b/") -> "b"
    • filepath.Base("a/b/c.txt") -> "c.txt"
  • filepath.Dir(path string) string: 与えられたパスのディレクトリ部分を返します。パスがファイル名のみの場合、"." を返します。パスがルートディレクトリの場合、ルートディレクトリを返します。返されるパスは、末尾にディレクトリセパレータを持ちません(ルートディレクトリの場合を除く)。 例:

    • filepath.Dir("a/b/c") -> "a/b"
    • filepath.Dir("a/b/") -> "a/b"
    • filepath.Dir("c.txt") -> "."
    • filepath.Dir("/usr/local") -> "/usr"
  • filepath.VolumeName(path string) string: Windows環境でのみ意味を持つ関数で、与えられたパスのボリューム名(ドライブレターやUNCパスのサーバー/共有名部分)を返します。Unix系OSでは常に空文字列を返します。 例 (Windows):

    • filepath.VolumeName("C:\\foo\\bar") -> "C:"
    • filepath.VolumeName("\\\\server\\share\\file") -> "\\\\server\\share"
    • filepath.VolumeName("foo\\bar") -> ""
  • os.IsPathSeparator(c uint8) bool: 指定された文字が現在のOSのパスセパレータであるかどうかを判定します。Windowsでは \/ の両方がパスセパレータとして認識されます。

  • filepath.Clean(path string) string: パスを簡潔な形式に変換します。冗長なセパレータ(//)、... などを解決し、正規化されたパスを返します。

Windowsのファイルパスの特性

Windowsのファイルパスは、Unix系OSのパスと比べていくつかの重要な違いがあります。

  1. パスセパレータ: 主にバックスラッシュ (\) を使用しますが、フォワードスラッシュ (/) も多くのAPIで許容されます。
  2. ドライブレター: C:\, D:\ のように、パスの先頭にドライブレターとコロンが付きます。これはパスの「ボリューム名」の一部です。
  3. UNC (Universal Naming Convention) パス: ネットワーク上の共有リソースを指すパス形式で、\\ServerName\ShareName\path\to\file のように \\ で始まります。これもパスの「ボリューム名」の一部と見なされます。
  4. カレントディレクトリ指定: ドライブレターのみのパス (C:) は、そのドライブのカレントディレクトリを指します。

これらの特性を BaseDir のようなパス操作関数が正しく理解し、処理することが、Windows環境でのGoアプリケーションの正確な動作には不可欠です。

技術的詳細

このコミットでは、path/filepath/path.go 内の Base 関数と Dir 関数が、Windowsのパス構造をより適切に扱うように変更されました。主な変更点は、filepath.VolumeName 関数を導入し、パスのボリューム部分を正確に識別・処理することです。

Base 関数の変更

Base 関数は、パスの末尾のセパレータを取り除く既存のロジックに加えて、パスの先頭にあるボリューム名を「破棄」する処理が追加されました。これにより、C:\foo\bar のようなパスから bar を正しく抽出する際に、C: の部分が邪魔にならないようになります。また、C:\ のようなパスに対しては \ を返すなど、Windowsの慣習に合わせた動作を実現します。

Dir 関数の変更

Dir 関数は、パスからディレクトリ部分を抽出する際に、まず VolumeName を取得します。その後、ボリューム名を除いたパスの残りの部分に対してディレクトリの検索を行い、最後に取得したボリューム名を結合して返します。これにより、C:\foo\bar から C:\foo を、\\host\share\a\b から \\host\share\a を正しく抽出できるようになります。特に、C:\\\host\share\ のようなルートパスに対しては、ボリューム名自体がディレクトリとして返されるように処理されます。

テストの追加

変更の正しさを検証するために、path/filepath/path_test.go にWindows固有のテストケース (winbasetestswindirtests) が追加されました。これらのテストは、ドライブレター付きパス、UNCパス、およびカレントディレクトリ指定のパスなど、Windows特有の様々なシナリオをカバーしています。また、既存のUnix系テストケースもWindows上で動作するように調整されています。runtime.GOOS == "windows" のチェックにより、これらのWindows固有のテストはWindows環境でのみ実行されるようになっています。

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

src/pkg/path/filepath/path.go

--- a/src/pkg/path/filepath/path.go
+++ b/src/pkg/path/filepath/path.go
@@ -426,6 +426,8 @@ func Base(path string) string {
 	for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) {
 		path = path[0 : len(path)-1]
 	}
+	// Throw away volume name
+	path = path[len(VolumeName(path)):]
 	// Find the last element
 	i := len(path) - 1
 	for i >= 0 && !os.IsPathSeparator(path[i]) {
@@ -447,8 +449,12 @@ func Base(path string) string {
 // If the path consists entirely of separators, Dir returns a single separator.
 // The returned path does not end in a separator unless it is the root directory.
 func Dir(path string) string {
-	dir, _ := Split(path)
-	dir = Clean(dir)
+	vol := VolumeName(path)
+	i := len(path) - 1
+	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
+		i--
+	}
+	dir := Clean(path[len(vol) : i+1])
 	last := len(dir) - 1
 	if last > 0 && os.IsPathSeparator(dir[last]) {
 		dir = dir[:last]
@@ -456,5 +462,5 @@ func Dir(path string) string {
 	if dir == "" {
 		dir = "."
 	}
-	return dir
+	return vol + dir
 }

src/pkg/path/filepath/path_test.go

--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -423,9 +423,29 @@ var basetests = []PathTest{\
 	{"a/b/c.x", "c.x"},
 }
 
+var winbasetests = []PathTest{
+	{`c:\`, `\`},
+	{`c:.`, `.`},
+	{`c:\a\b`, `b`},
+	{`c:a\b`, `b`},
+	{`c:a\b\c`, `c`},
+	{`\\host\share\`, `\`},
+	{`\\host\share\a`, `a`},
+	{`\\host\share\a\b`, `b`},
+}
+
 func TestBase(t *testing.T) {
-	for _, test := range basetests {
-		if s := filepath.ToSlash(filepath.Base(test.path)); s != test.result {
+	tests := basetests
+	if runtime.GOOS == "windows" {
+		// make unix tests work on windows
+		for i, _ := range tests {
+			tests[i].result = filepath.Clean(tests[i].result)
+		}
+		// add windows specific tests
+		tests = append(tests, winbasetests...)
+	}
+	for _, test := range tests {
+		if s := filepath.Base(test.path); s != test.result {
 			t.Errorf("Base(%q) = %q, want %q", test.path, s, test.result)
 		}
 	}
@@ -446,9 +466,29 @@ var dirtests = []PathTest{\
 	{"a/b/c.x", "a/b"},
 }
 
+var windirtests = []PathTest{
+	{`c:\`, `c:\`},
+	{`c:.`, `c:.`},
+	{`c:\a\b`, `c:\a`},
+	{`c:a\b`, `c:a`},
+	{`c:a\b\c`, `c:a\b`},
+	{`\\host\share\`, `\\host\share\`},
+	{`\\host\share\a`, `\\host\share\`},
+	{`\\host\share\a\b`, `\\host\share\a`},
+}
+
 func TestDir(t *testing.T) {
-	for _, test := range dirtests {
-		if s := filepath.ToSlash(filepath.Dir(test.path)); s != test.result {
+	tests := dirtests
+	if runtime.GOOS == "windows" {
+		// make unix tests work on windows
+		for i, _ := range tests {
+			tests[i].result = filepath.Clean(tests[i].result)
+		}
+		// add windows specific tests
+		tests = append(tests, windirtests...)
+	}
+	for _, test := range tests {
+		if s := filepath.Dir(test.path); s != test.result {
 			t.Errorf("Dir(%q) = %q, want %q", test.path, s, test.result)
 		}
 	}

コアとなるコードの解説

Base 関数の変更点 (src/pkg/path/filepath/path.go)

func Base(path string) string {
	// ... (既存の末尾セパレータ除去ロジック) ...

	// Throw away volume name
	// パスの先頭からボリューム名(例: "C:", "\\server\share")の長さに応じてパスを切り詰める。
	// これにより、Base関数がファイル名やディレクトリ名を抽出する際に、ボリューム名が考慮されなくなる。
	path = path[len(VolumeName(path)):]

	// Find the last element
	// ... (既存の最後の要素を見つけるロジック) ...
}

Base 関数では、まずパスの末尾のセパレータを削除する既存の処理が行われます。その直後に、path = path[len(VolumeName(path)):] という行が追加されました。これは、VolumeName(path) 関数を使ってパスのボリューム名(WindowsのドライブレターやUNCパスのサーバー/共有名部分)を取得し、その長さ分だけパスの先頭を切り捨てることを意味します。これにより、Base 関数はボリューム名を除いた残りのパスに対して、最後の要素を抽出する処理を行うようになります。例えば、C:\foo\bar の場合、VolumeName("C:\foo\bar")C: を返し、その長さは2です。したがって、パスは \foo\bar となり、そこから bar が正しく抽出されます。

Dir 関数の変更点 (src/pkg/path/filepath/path.go)

func Dir(path string) string {
	vol := VolumeName(path) // パスのボリューム名を取得

	i := len(path) - 1
	// パスの末尾からボリューム名の終わりまで、パスセパレータでない文字をスキップ
	// これにより、最後のパスセパレータの位置を見つける
	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
		i--
	}
	// ボリューム名を除いたパスのディレクトリ部分をCleanして取得
	dir := Clean(path[len(vol) : i+1])

	last := len(dir) - 1
	// ... (既存の末尾セパレータ除去ロジック) ...

	if dir == "" {
		dir = "."
	}
	// 最後にボリューム名をディレクトリ部分に結合して返す
	return vol + dir
}

Dir 関数では、まず vol := VolumeName(path) でパスのボリューム名を取得します。次に、for i >= len(vol) && !os.IsPathSeparator(path[i]) { i-- } ループによって、パスの末尾からボリューム名の終わりまで(つまり、ボリューム名以外の部分で)最後のパスセパレータの位置を逆順に探索します。dir := Clean(path[len(vol) : i+1]) では、ボリューム名を除いたパスの残りの部分(path[len(vol):])から、見つかった最後のセパレータまでの部分をディレクトリとして抽出し、Clean 関数で正規化します。最後に、return vol + dir で、取得したボリューム名と正規化されたディレクトリ部分を結合して返します。これにより、C:\foo\barDirC:\foo となり、\\host\share\a\bDir\\host\share\a となります。また、C:\\\host\share\ のようなボリューム名のみのパスに対しては、ボリューム名自体がディレクトリとして返されるようになります。

テストファイルの変更点 (src/pkg/path/filepath/path_test.go)

TestBaseTestDir 関数内で、runtime.GOOS == "windows" の条件分岐が追加されました。 Windows環境の場合、以下の処理が行われます。

  1. 既存のUnix系テストケース (basetests, dirtests) の resultfilepath.Clean で正規化します。これは、Unix系テストの期待値がWindowsのパス規則に合わない場合があるため、Windowsのパス規則に合うように調整するためです。
  2. winbasetests および windirtests というWindows固有のテストケーススライスを既存のテストケースに追加します。これらのスライスには、c:\, c:., c:\a\b, \\host\share\ など、Windows特有のパス形式に対する期待される Base および Dir の結果が定義されています。

これにより、Windows環境でテストを実行する際には、Unix系パスとWindows系パスの両方に対して、Base および Dir 関数が正しく動作するかどうかが検証されるようになります。

関連リンク

参考にした情報源リンク