[インデックス 18426] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/exec パッケージにおけるコマンド実行時のパス解決ロジックの改善に関するものです。具体的には、実行ファイル名がパスセパレータを含むかどうかを判定する内部関数 containsPathSeparator を廃止し、より堅牢な filepath.Base 関数を利用するように変更しています。これにより、特にWindows環境における多様なパス形式(例: d:hello.txt のようなドライブ相対パス)の取り扱いが正確になり、コマンドの検索(LookPath)の挙動が改善されました。
コミット
commit aac872e11806b7a66ab51f5efab7496a36e4f3da
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Feb 7 12:30:30 2014 +1100
os/exec: use filepath.Base in Command
filepath.Base covers all scenarios
(for example paths like d:hello.txt)
on windows
LGTM=iant, bradfitz
R=golang-codereviews, iant, bradfitz
CC=golang-codereviews
https://golang.org/cl/59740050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aac872e11806b7a66ab51f5efab7496a36e4f3da
元コミット内容
os/exec: use filepath.Base in Command
このコミットは、os/exec パッケージの Command 関数において、実行ファイル名がパスセパレータを含むかどうかの判定に filepath.Base を使用するように変更します。これにより、特にWindows環境における d:hello.txt のようなパスを含む、あらゆるシナリオが適切に処理されるようになります。
変更の背景
Goの os/exec パッケージは、外部コマンドを実行するための機能を提供します。exec.Command(name string, arg ...string) 関数は、与えられた name が単なるコマンド名(例: ls, notepad.exe)なのか、それとも実行ファイルへの完全なパス(例: /bin/ls, C:\Windows\System32\notepad.exe)なのかを区別する必要があります。
もし name が単なるコマンド名であれば、システムパス(PATH 環境変数)を検索して実行ファイルを見つける必要があります(これは LookPath 関数が行います)。しかし、name が既にパスを含んでいる場合、LookPath を呼び出す必要はなく、指定されたパスを直接使用すべきです。
この区別を行うために、以前は containsPathSeparator という内部関数が使用されていました。この関数は、文字列内にOS固有のパスセパレータ(Unix系では /、Windowsでは \ または /)が含まれているかを単純にチェックしていました。しかし、Windows環境では d:hello.txt のようなドライブ相対パスが存在します。これはパスセパレータを含まないものの、実際にはファイル名だけでなくドライブ指定を含む「パス」として扱われるべきものです。containsPathSeparator はこのようなケースを適切に処理できず、誤って LookPath を呼び出してしまう可能性がありました。
この問題を解決し、より堅牢なパス判定ロジックを導入するために、filepath.Base 関数への置き換えが決定されました。filepath.Base は、与えられたパスの最後の要素(ファイル名またはディレクトリ名)を返します。もし filepath.Base(name) の結果が元の name と同じであれば、それはパスセパレータを含まない(または Base がパスセパレータと認識しない)単純なファイル名であると判断できます。
前提知識の解説
-
os/execパッケージ: Go言語で外部コマンドを実行するための機能を提供する標準ライブラリです。exec.Command("command_name", "arg1", "arg2")のように使用し、指定されたコマンドを新しいプロセスとして起動します。 -
exec.Commandのname引数:exec.Commandの最初の引数nameは、実行するコマンドの名前またはパスです。- コマンド名の場合: 例:
ls,go,notepad.exe。この場合、os/execはシステムのPATH環境変数を参照して実行ファイルを探します。この検索はLookPath関数によって行われます。 - パスの場合: 例:
/bin/ls,C:\Windows\System32\notepad.exe,./my_script.sh。この場合、os/execは指定されたパスを直接使用します。
- コマンド名の場合: 例:
-
exec.LookPath関数:LookPath(file string)は、指定されたfileが実行可能ファイルであるかどうかをPATH環境変数に従って検索し、見つかった場合はその絶対パスを返します。見つからない場合はエラーを返します。 -
path/filepathパッケージ: ファイルパスを操作するためのユーティリティ関数を提供する標準ライブラリです。OS固有のパスセパレータ(os.PathSeparator)を考慮し、クロスプラットフォームで動作するように設計されています。 -
filepath.Base関数:filepath.Base(path string)は、パスの最後の要素を返します。これは通常、ファイル名またはディレクトリ名です。- 例:
filepath.Base("/foo/bar/baz.txt")は"baz.txt"を返します。 - 例:
filepath.Base("/foo/bar/")は"bar"を返します。 - 例:
filepath.Base("baz.txt")は"baz.txt"を返します。 - 例:
filepath.Base("d:hello.txt")は"hello.txt"を返します(Windowsの場合)。
- 例:
-
os.IsPathSeparator関数:os.IsPathSeparator(c uint8)は、指定されたバイトcがOS固有のパスセパレータである場合にtrueを返します。 -
Windowsのパス形式: Windowsでは、Unix系OSとは異なるパスの表記規則がいくつか存在します。
- ドライブレター:
C:\,D:\など。 - バックスラッシュ: パスセパレータとして
\が主に使用されますが、/も多くの場合で許容されます。 - ドライブ相対パス:
d:hello.txtのように、ドライブレターの後にパスセパレータなしでファイル名が続く形式。これはカレントディレクトリがD:ドライブにある場合にD:\current_dir\hello.txtを意味します。
- ドライブレター:
技術的詳細
このコミットの核心は、os/exec.Command 関数が name 引数をどのように解釈するかという点にあります。
変更前は、Command 関数内で containsPathSeparator(name) というヘルパー関数が呼び出されていました。この関数は、name 文字列を1文字ずつ走査し、os.IsPathSeparator を使ってパスセパレータ(Windowsでは \ や /)が含まれているかをチェックしていました。
// 変更前の containsPathSeparator 関数
func containsPathSeparator(s string) bool {
for i := 0; i < len(s); i++ {
if os.IsPathSeparator(s[i]) {
return true
}
}
return false
}
このロジックは、/path/to/command や C:\path\to\command.exe のような一般的なパス形式では正しく機能します。しかし、Windows特有の d:hello.txt のようなパス形式では問題が生じます。この形式は、ドライブ指定を含んでいますが、\ や / といった明示的なパスセパレータを含んでいません。したがって、containsPathSeparator は d:hello.txt を「パスセパレータを含まない」と誤って判断し、結果として LookPath を呼び出してシステムパスを検索しようとします。これは意図しない挙動であり、d:hello.txt が PATH 上に存在しない場合、コマンドが見つからないというエラーにつながる可能性があります。
この問題を解決するために、filepath.Base(name) == name という条件が導入されました。
filepath.Base(name) は、name がパスセパレータを含む場合、その最後の要素(ファイル名)を返します。例えば、filepath.Base("/bin/ls") は "ls" を返します。
一方、name がパスセパレータを含まない単純なファイル名である場合、filepath.Base(name) は name そのものを返します。例えば、filepath.Base("ls") は "ls" を返します。
そして重要な点として、filepath.Base("d:hello.txt") は "hello.txt" を返します。これは、filepath.Base がWindowsのドライブ相対パスを適切に解釈し、: をパスの一部としてではなく、ドライブ指定の一部として扱うためです。
したがって、filepath.Base(name) == name という条件は以下のように機能します。
nameが/bin/lsの場合:filepath.Base("/bin/ls")は"ls"。"ls" == "/bin/ls"はfalse。これはパスなのでLookPathは呼ばれない。nameがlsの場合:filepath.Base("ls")は"ls"。"ls" == "ls"はtrue。これは単純なコマンド名なのでLookPathが呼ばれる。nameがd:hello.txtの場合:filepath.Base("d:hello.txt")は"hello.txt"。"hello.txt" == "d:hello.txt"はfalse。これはパスなのでLookPathは呼ばれない。
このように、filepath.Base(name) == name という条件は、name がシステムパスを検索して見つけるべき単純なコマンド名であるか、それとも既にパス情報を含んでいるかを、containsPathSeparator よりも正確かつ堅牢に判定することができます。特にWindowsの特殊なパス形式を考慮に入れている点が重要です。
この変更に伴い、os/exec パッケージは path/filepath パッケージに依存するようになったため、src/pkg/go/build/deps_test.go 内の依存関係リストも更新されています。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
src/pkg/go/build/deps_test.go:os/execパッケージの依存関係にpath/filepathが追加されました。--- a/src/pkg/go/build/deps_test.go +++ b/src/pkg/go/build/deps_test.go @@ -125,7 +125,7 @@ var pkgDeps = map[string][]string{ "os": {"L1", "os", "syscall", "time"}, "path/filepath": {"L2", "os", "syscall"}, "io/ioutil": {"L2", "os", "path/filepath", "time"}, - "os/exec": {"L2", "os", "syscall"}, + "os/exec": {"L2", "os", "path/filepath", "syscall"}, "os/signal": {"L2", "os", "syscall"}, // OS enables basic operating system functionality, -
src/pkg/os/exec/exec.go:path/filepathパッケージがインポートされました。Command関数内の条件式が!containsPathSeparator(name)からfilepath.Base(name) == nameに変更されました。containsPathSeparator関数が完全に削除されました。
--- a/src/pkg/os/exec/exec.go +++ b/src/pkg/os/exec/exec.go @@ -12,6 +12,7 @@ import ( "errors" "io" "os" + "path/filepath" "strconv" "sync" "syscall" @@ -111,7 +112,7 @@ func Command(name string, arg ...string) *Cmd { Path: name, Args: append([]string{name}, arg...), } - if !containsPathSeparator(name) { + if filepath.Base(name) == name { if lp, err := LookPath(name); err != nil { cmd.lookPathErr = err } else { @@ -121,15 +122,6 @@ func Command(name string, arg ...string) *Cmd { return cmd } -func containsPathSeparator(s string) bool { - for i := 0; i < len(s); i++ { - if os.IsPathSeparator(s[i]) { - return true - } - } - return false -} - // interfaceEqual protects against panics from doing equality tests on // two interfaces with non-comparable underlying types. func interfaceEqual(a, b interface{}) bool {
コアとなるコードの解説
src/pkg/os/exec/exec.go の Command 関数内の変更がこのコミットの主要な部分です。
// 変更前
if !containsPathSeparator(name) {
// ... LookPath を呼び出すロジック ...
}
// 変更後
if filepath.Base(name) == name {
// ... LookPath を呼び出すロジック ...
}
この条件式は、name がシステムパスを検索して見つけるべき「単純なコマンド名」であるかどうかを判定しています。
-
変更前 (
!containsPathSeparator(name)):containsPathSeparatorは、name文字列内にOS固有のパスセパレータ(例:/や\)が含まれているかをチェックしていました。もし含まれていなければfalseを返し、!で反転されてtrueとなり、LookPathが呼び出されていました。このロジックは、d:hello.txtのようなWindows特有のパス形式を正しく「パス」として認識できず、誤ってLookPathを呼び出す原因となっていました。 -
変更後 (
filepath.Base(name) == name):filepath.Base(name)は、与えられたパスの最後の要素(ファイル名)を返します。- もし
nameが/usr/bin/lsのようなパスであれば、filepath.Base("/usr/bin/ls")は"ls"を返します。この場合、"ls" == "/usr/bin/ls"はfalseとなり、LookPathは呼び出されません。これは正しい挙動です。 - もし
nameがlsのような単純なファイル名であれば、filepath.Base("ls")は"ls"を返します。この場合、"ls" == "ls"はtrueとなり、LookPathが呼び出されます。これも正しい挙動です。 - そして、重要な
d:hello.txtのようなWindows特有のパスの場合、filepath.Base("d:hello.txt")は"hello.txt"を返します。この場合、"hello.txt" == "d:hello.txt"はfalseとなり、LookPathは呼び出されません。これにより、d:hello.txtが正しく「パス」として扱われるようになります。
- もし
この変更により、os/exec.Command は、コマンド名がパスセパレータを含むかどうかをより正確に、特にWindows環境の多様なパス形式を考慮して判断できるようになりました。これにより、コマンドの検索ロジックが堅牢になり、予期せぬエラーが減少します。
また、containsPathSeparator 関数が不要になったため、コードベースから削除され、全体的なコードの簡素化にも貢献しています。
関連リンク
- Go CL 59740050: https://golang.org/cl/59740050
- Go
os/execパッケージドキュメント: https://pkg.go.dev/os/exec - Go
path/filepathパッケージドキュメント: https://pkg.go.dev/path/filepath
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Windowsのパスに関する一般的な情報 (例: ドライブ相対パス)
filepath.Baseの挙動に関するGoのドキュメントとテストケースos.IsPathSeparatorの挙動に関するGoのドキュメント