[インデックス 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のドキュメント