[インデックス 15322] ファイルの概要
このコミットは、Go言語の標準ライブラリであるpath/filepath
パッケージのSplitList
関数と、os/exec
パッケージのLookPath
関数におけるWindows環境でのPATH
環境変数の扱いを改善するものです。具体的には、WindowsのPATH
要素に含まれる引用符("
)と、セパレータであるセミコロン(;
)の解釈に関する問題を修正し、より堅牢なパス解析を実現しています。
コミット
commit b4109f801a2b51978e1ddc1918a4558a8d8ba36c
Author: Péter Surányi <speter.go1@gmail.com>
Date: Wed Feb 20 16:19:52 2013 +1100
path/filepath, os/exec: unquote PATH elements on Windows
On Windows, directory names in PATH can be fully or partially quoted
in double quotes ('"'), but the path names as used by most APIs must
be unquoted. In addition, quoted names can contain the semicolon
(';') character, which is otherwise used as ListSeparator.
This CL changes SplitList in path/filepath and LookPath in os/exec
to only treat unquoted semicolons as separators, and to unquote the
separated elements.
(In addition, fix harmless test bug I introduced for LookPath on Unix.)
Related discussion thread:
https://groups.google.com/d/msg/golang-nuts/PXCr10DsRb4/sawZBM7scYgJ
R=rsc, minux.ma, mccoyst, alex.brainman, iant
CC=golang-dev
https://golang.org/cl/7181047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b4109f801a2b51978e1ddc1918a4558a8d8ba36c
元コミット内容
Windows環境において、PATH
環境変数内のディレクトリ名が二重引用符("
)で完全に、または部分的に囲まれている場合があるが、ほとんどのAPIで使用されるパス名は引用符なしでなければならないという問題がありました。さらに、引用符で囲まれたパス名には、通常リストセパレータとして使用されるセミコロン(;
)文字が含まれる可能性がありました。
このコミットは、path/filepath
パッケージのSplitList
関数とos/exec
パッケージのLookPath
関数を変更し、引用符で囲まれていないセミコロンのみをセパレータとして扱い、分離された要素から引用符を削除するように修正します。
(加えて、Unix版のLookPath
テストで導入した無害なテストバグも修正しています。)
変更の背景
WindowsのPATH
環境変数は、実行可能ファイルを探すためのディレクトリのリストをセミコロンで区切って保持しています。しかし、Windowsのパスにはスペースや特殊文字が含まれることがあり、これらを正しく扱うためにパス全体または一部を二重引用符で囲む慣習があります。
Go言語のpath/filepath.SplitList
関数は、このようなパス文字列を個々のディレクトリパスに分割するために使用されます。また、os/exec.LookPath
関数は、PATH
環境変数を利用して実行可能ファイルのフルパスを検索します。
このコミット以前のGoのこれらの関数は、WindowsのPATH
における引用符の扱いや、引用符内のセミコロンの解釈に関して不完全な挙動を示していました。具体的には、引用符で囲まれたパス内のセミコロンを誤ってセパレータとして認識してしまったり、引用符自体をパスの一部として扱ってしまったりする問題がありました。これにより、GoアプリケーションがWindows上で外部コマンドを正しく見つけられない、または予期しないパスを解決してしまう可能性がありました。
この問題は、Goコミュニティのメーリングリストで議論されており、特にhttps://groups.google.com/d/msg/golang-nuts/PXCr10DsRb4/sawZBM7scYgJの議論スレッドで詳細が述べられています。このコミットは、その議論の結果として、WindowsのPATH
環境変数のより正確な解析を実現するために導入されました。
前提知識の解説
PATH環境変数
PATH
環境変数は、オペレーティングシステムが実行可能ファイル(コマンド)を探す際に参照するディレクトリのリストです。ユーザーがコマンド名を入力した際、OSはこのPATH
に指定されたディレクトリを順番に検索し、最初に見つかった実行可能ファイルを実行します。
- Windows:
PATH
内のディレクトリはセミコロン(;
)で区切られます。例:C:\Windows;C:\Windows\System32;C:\Program Files\Git\cmd
- Unix/Linux/macOS:
PATH
内のディレクトリはコロン(:
)で区切られます。例:/usr/local/bin:/usr/bin:/bin
Windowsにおけるパスの引用符
Windowsのファイルシステムでは、パスにスペースや特殊文字(例: C:\Program Files
)が含まれることがよくあります。コマンドラインやスクリプトでこのようなパスを扱う場合、パス全体を二重引用符("
)で囲むのが一般的です。
例: C:\Program Files\My App
は C:\"Program Files"\My App
や "C:\Program Files\My App"
のように表現されることがあります。
path/filepath.SplitList
Go言語のpath/filepath
パッケージは、ファイルパスを操作するためのユーティリティを提供します。SplitList
関数は、PATH
環境変数のようなリスト形式のパス文字列を、個々のパス要素の文字列スライスに分割するために使用されます。
os/exec.LookPath
Go言語のos/exec
パッケージは、外部コマンドの実行をサポートします。LookPath
関数は、与えられたコマンド名(例: git
、node
)がPATH
環境変数内のどのディレクトリに存在するかを検索し、そのコマンドのフルパスを返します。
問題点と解決策の概要
従来のGoの実装では、WindowsのPATH
文字列を分割する際に、引用符の有無に関わらずセミコロンをセパレータとして扱っていました。これにより、例えば"C:\Program Files\My;App"
のようなパスがあった場合、これをC:\Program Files\My
とApp
の2つの要素に誤って分割してしまう可能性がありました。また、引用符自体がパスの一部として残ってしまう問題もありました。
このコミットの目的は、以下の挙動を実現することです。
- 引用符で囲まれていないセミコロンのみをセパレータとして認識する。
- 分割された各パス要素から引用符を正しく除去する。
これにより、WindowsのPATH
環境変数が持つ複雑な形式(引用符、引用符内のセミコロン)を正確に解析し、Goアプリケーションが外部コマンドをより確実に実行できるようになります。
技術的詳細
このコミットの主要な変更点は、Windows環境におけるPATH
文字列の解析ロジックを改善するために、splitList
という新しいヘルパー関数を導入し、それをos/exec.LookPath
とpath/filepath.SplitList
の両方で利用するようにしたことです。
splitList
関数の導入
以前は、path/filepath.SplitList
は単純にstrings.Split(path, string(ListSeparator))
を使用していました。しかし、これは引用符や引用符内のセミコロンを考慮しないため、WindowsのPATH
の特殊な要件を満たせませんでした。
新しいsplitList
関数(src/pkg/os/exec/lp_windows.go
とsrc/pkg/path/filepath/path_windows.go
に実装)は、この問題を解決するために、パス文字列を文字ごとに走査し、引用符の状態を追跡するステートマシンを実装しています。
splitList
のロジック概要:
-
初期化:
list := []string{}
: 分割されたパス要素を格納するスライス。start := 0
: 現在のパス要素の開始インデックス。quo := false
: 現在、引用符の内部にいるかどうかを示すブールフラグ。
-
文字列の走査:
- パス文字列を先頭から末尾まで1文字ずつ走査します。
- 各文字
c
に対してswitch
文を使用します。
-
引用符の処理:
case c == '"'
: 引用符が見つかった場合、quo
フラグを反転させます。これにより、引用符の開始と終了を検出します。
-
セパレータの処理:
case c == os.PathListSeparator && !quo
: 現在の文字がパスリストセパレータ(Windowsでは;
)であり、かつ引用符の内部にいない場合のみ、そのセミコロンをセパレータとして扱います。list = append(list, path[start:i])
:start
から現在のインデックスi
までの部分文字列を1つのパス要素としてlist
に追加します。start = i + 1
: 次のパス要素の開始インデックスを更新します。
-
最後の要素の追加:
- ループが終了した後、残りの部分文字列(
path[start:]
)を最後のパス要素としてlist
に追加します。
- ループが終了した後、残りの部分文字列(
-
引用符の除去:
list
内の各パス要素に対して、strings.Contains(s,
")
で引用符が含まれているかを確認し、strings.Replace(s,
", ``, -1)
を使用してすべての引用符を空文字列に置換することで除去します。
os/exec.LookPath
の変更
src/pkg/os/exec/lp_windows.go
内のLookPath
関数は、os.Getenv("PATH")
で取得したPATH
文字列を分割する際に、従来のstrings.Split
の代わりに新しく定義されたsplitList
関数を使用するように変更されました。
// 変更前
// for _, dir := range strings.Split(pathenv, `;`) {
// 変更後
for _, dir := range splitList(pathenv) {
path/filepath.SplitList
の変更
src/pkg/path/filepath/path.go
内のSplitList
関数も、Windows環境では新しく定義されたsplitList
関数を呼び出すように変更されました。
// 変更前
// func SplitList(path string) []string {
// if path == "" {
// return []string{}
// }
// return strings.Split(path, string(ListSeparator))
// }
// 変更後
func SplitList(path string) []string {
return splitList(path) // Windows固有のsplitListを呼び出す
}
プラットフォームごとの実装
このコミットでは、splitList
関数の実装がWindows (src/pkg/path/filepath/path_windows.go
, src/pkg/os/exec/lp_windows.go
) とUnix/Plan9 (src/pkg/path/filepath/path_unix.go
, src/pkg/path/filepath/path_plan9.go
) で異なるようにしています。Unix/Plan9では、引用符の問題がないため、従来のstrings.Split
を使用するシンプルなsplitList
が引き続き使われます。これにより、プラットフォーム固有の挙動を適切に分離しています。
テストの追加と修正
src/pkg/path/filepath/path_test.go
には、Windows特有のPATH
文字列の分割に関する新しいテストケースwinsplitlisttests
が追加されました。これには、引用符で囲まれたパス、部分的に引用符で囲まれたパス、引用符内のセミコロンを含むパスなど、様々なエッジケースが含まれています。
また、src/pkg/path/filepath/path_windows_test.go
という新しいテストファイルが追加され、TestWinSplitListTestsAreValid
関数が導入されました。このテストは、winsplitlisttests
で定義された各テストケースが、実際にWindowsのコマンドプロンプト(ComSpec
)でPATH
を設定して実行可能ファイルを検索した場合に期待される挙動と一致するかどうかを検証します。これにより、GoのSplitList
とLookPath
がWindowsのネイティブなPATH
解析と互換性を持つことを保証しています。
Unix版のLookPath
テスト (src/pkg/os/exec/lp_unix_test.go
) における無害なテストバグ(f.Close()
のエラーチェック漏れ)も修正されています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルと関数に集中しています。
-
src/pkg/os/exec/lp_windows.go
:LookPath
関数内でos.Getenv("PATH")
の分割にstrings.Split
の代わりに新しいsplitList
関数を使用するように変更。- Windows固有の
splitList
ヘルパー関数が新しく追加されました。
-
src/pkg/path/filepath/path.go
:SplitList
関数が、プラットフォーム固有のsplitList
ヘルパー関数を呼び出すように変更。
-
src/pkg/path/filepath/path_windows.go
:- Windows固有の
splitList
ヘルパー関数が新しく追加されました。この実装はos/exec/lp_windows.go
のものと同一です。
- Windows固有の
-
src/pkg/path/filepath/path_test.go
:- Windows特有の
SplitList
のテストケースを定義するwinsplitlisttests
変数が追加されました。 TestSplitList
関数が、Windows環境の場合にwinsplitlisttests
も実行するように変更されました。
- Windows特有の
-
src/pkg/path/filepath/path_windows_test.go
:- 新規追加されたテストファイル。
TestWinSplitListTestsAreValid
関数が、winsplitlisttests
の各ケースがWindowsの実際のPATH
挙動と一致するかを検証します。
-
src/pkg/path/filepath/path_unix.go
:- Unix固有の
splitList
ヘルパー関数が追加されました。これは従来のstrings.Split
のラッパーです。
- Unix固有の
-
src/pkg/path/filepath/path_plan9.go
:- Plan9固有の
splitList
ヘルパー関数が追加されました。これも従来のstrings.Split
のラッパーです。
- Plan9固有の
コアとなるコードの解説
最も重要な変更は、Windows環境におけるsplitList
関数の実装です。この関数は、PATH
文字列を正確に解析するための中心的なロジックを含んでいます。
以下は、src/pkg/path/filepath/path_windows.go
(およびsrc/pkg/os/exec/lp_windows.go
)に追加されたsplitList
関数の簡略化された解説です。
func splitList(path string) []string {
if path == "" {
return []string{} // 空文字列の場合は空のスライスを返す
}
list := []string{} // 結果のパス要素を格納するスライス
start := 0 // 現在のパス要素の開始インデックス
quo := false // 引用符の内部にいるかどうかのフラグ
// パス文字列を1文字ずつ走査
for i := 0; i < len(path); i++ {
switch c := path[i]; {
case c == '"': // 引用符が見つかった場合
quo = !quo // 引用符の状態を反転させる
case c == ListSeparator && !quo: // セパレータ(;)が見つかり、かつ引用符の内部にいない場合
list = append(list, path[start:i]) // 現在の要素をリストに追加
start = i + 1 // 次の要素の開始位置を更新
}
}
list = append(list, path[start:]) // 最後の要素をリストに追加
// 各要素から引用符を除去
for i, s := range list {
if strings.Contains(s, `"`) {
list[i] = strings.Replace(s, `"`, ``, -1) // すべての引用符を削除
}
}
return list
}
このsplitList
関数は、WindowsのPATH
環境変数の特殊性を考慮して設計されています。
- 引用符の追跡:
quo
フラグによって、現在の走査位置が引用符の内部にあるかどうかが正確に追跡されます。これにより、引用符で囲まれた部分内のセミコロンがセパレータとして誤って解釈されることを防ぎます。 - 条件付きセパレータ認識:
c == ListSeparator && !quo
の条件により、セミコロンがセパレータとして機能するのは、それが引用符の外部にある場合のみとなります。 - 引用符の除去: 最後に、分割された各パス要素から不要な引用符がすべて削除されます。これにより、GoのAPIが期待する「引用符なし」のパス名が提供されます。
このロジックにより、例えばPATH
がC:\foo;"C:\Program Files\My App";C:\bar
のような場合でも、C:\foo
、C:\Program Files\My App
、C:\bar
という3つの正しいパス要素に分割されるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/b4109f801a2b51978e1ddc1918a4558a8d8ba36c
- Go CL (Change List) 7181047: https://golang.org/cl/7181047
- 関連するGoコミュニティの議論スレッド: https://groups.google.com/d/msg/golang-nuts/PXCr10DsRb4/sawZBM7scYgJ
参考にした情報源リンク
- 上記の「関連リンク」セクションに記載されているGoの公式リポジトリのコミット情報、CL、およびGoコミュニティの議論スレッド。