[インデックス 17525] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/exec パッケージにおけるWindows環境での LookPath 関数の挙動を、Windowsのコマンドプロンプト (cmd.exe) のそれと一致させるように変更するものです。具体的には、実行可能ファイルの検索ロジックが改善され、特にファイル拡張子の扱いがよりWindowsの慣習に沿うように修正されています。
コミット
commit a6149da08a6bcdca087977f053c529eea03032aa
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Sep 10 14:50:29 2013 +1000
os/exec: change windows LookPath so it works like cmd.exe
Fixes #6224
R=golang-dev, hcwfrichter, bradfitz
CC=golang-dev
https://golang.org/cl/13410045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a6149da08a6bcdca087977f053c529eea03032aa
元コミット内容
os/exec: change windows LookPath so it works like cmd.exe
Fixes #6224
R=golang-dev, hcwfrichter, bradfitz
CC=golang-dev
https://golang.org/cl/13410045
変更の背景
Go言語の os/exec.LookPath 関数は、指定された実行可能ファイルがシステムPATH上でどこにあるかを検索する役割を担っています。しかし、Windows環境においては、この LookPath の挙動が cmd.exe (Windowsのコマンドプロンプト) が実行可能ファイルを検索する際の挙動と完全に一致していませんでした。
cmd.exe は、実行可能ファイルを検索する際に、ファイル名に拡張子が含まれているかどうか、そして PATHEXT 環境変数に定義された拡張子リストをどのように適用するかについて、特定のルールを持っています。Goの LookPath がこれらのルールを正確に模倣していなかったため、Goプログラムから外部コマンドを実行しようとした際に、cmd.exe では見つかるコマンドが LookPath では見つからない、あるいは意図しないファイルが見つかるといった不整合が発生していました。
この不整合は、特に拡張子なしでコマンド名を指定した場合や、複数の拡張子を持つファイルが存在する場合に問題となりました。このコミットは、この挙動の不一致を解消し、GoプログラムがWindows上でより予測可能で、ネイティブなコマンド実行体験を提供できるようにすることを目的としています。コミットメッセージにある Fixes #6224 は、この問題がGoのIssue Trackerで報告されていたことを示しています。
前提知識の解説
このコミットの理解には、以下のWindowsの環境変数とコマンド検索の概念が不可欠です。
-
PATH 環境変数:
PATH環境変数は、オペレーティングシステムが実行可能ファイル(プログラム)を探す際に参照するディレクトリのリストをセミコロン (;) で区切って指定します。ユーザーがコマンドプロンプトでnotepadのようにファイル拡張子なしでコマンド名を入力すると、システムはPATHにリストされた各ディレクトリを順番に検索し、そのディレクトリ内に指定された名前の実行可能ファイルが存在するかどうかを確認します。 -
PATHEXT 環境変数:
PATHEXT環境変数は、Windowsが実行可能ファイルとみなすファイル拡張子のリストをセミコロン (;) で区切って指定します。一般的な値としては.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSCなどがあります。 ユーザーがコマンド名に拡張子を含めずにコマンドを入力した場合、システムはPATHにリストされた各ディレクトリを検索する際に、PATHEXTに定義された拡張子を順番に試行します。例えば、PATHEXTが.EXE;.BATで、ユーザーがmyprogと入力した場合、システムはまずmyprog.exeを探し、見つからなければmyprog.batを探します。 -
cmd.exeのコマンド検索ロジック:cmd.exeは、コマンドを実行する際に以下の優先順位でファイルを検索します。- フルパス指定: コマンド名にディレクトリパスとファイル拡張子の両方が含まれている場合(例:
C:\Windows\System32\notepad.exe)、cmd.exeはそのパスのファイルを直接実行しようとします。 - パス指定(拡張子なし): コマンド名にディレクトリパスは含まれるが、ファイル拡張子が含まれない場合(例:
C:\Windows\System32\notepad)、cmd.exeは指定されたディレクトリ内でPATHEXTに基づいて拡張子を試行します。 - カレントディレクトリ検索: コマンド名にパスが含まれない場合(例:
notepad)、cmd.exeはまずカレントディレクトリ内でコマンドを探します。この際もPATHEXTが適用されます。 - PATH 環境変数検索: カレントディレクトリで見つからなかった場合、
cmd.exeはPATH環境変数にリストされた各ディレクトリを順番に検索します。各ディレクトリ内で、PATHEXTに基づいて拡張子を試行します。重要なのは、cmd.exeはあるディレクトリ内でPATHEXTの全ての拡張子を試行し終えてから、次のPATHディレクトリに移るという点です。
- フルパス指定: コマンド名にディレクトリパスとファイル拡張子の両方が含まれている場合(例:
このコミットは、Goの os/exec.LookPath が、特に PATHEXT の適用順序と、ファイル名に既に拡張子が含まれている場合の挙動において、cmd.exe のロジックと一致するように調整することを目的としています。
技術的詳細
このコミットでは、src/pkg/os/exec/lp_windows.go ファイル内の findExecutable 関数が変更され、新たに hasExt 関数が追加されています。
変更の核心は、findExecutable 関数が実行可能ファイルを検索する際のロジック、特にファイル拡張子の扱いを改善することにあります。
-
hasExt関数の追加: この新しい関数hasExt(file string) boolは、与えられたファイル名に「有効な」ファイル拡張子が含まれているかどうかを判断します。ここでいう「有効な」とは、ファイル名にドット (.) が含まれており、かつそのドットがパス区切り文字 (:や/、\) よりも後ろにあることを意味します。これにより、C:\path\to\file.txtのようなパスは拡張子を持つと判断されますが、C:\path.to\fileのようなディレクトリ名にドットが含まれるケースは拡張子を持たないと判断されます。 -
findExecutable関数の変更: 変更前のfindExecutable関数は、ファイル名が既にexts(PATHEXTから取得された拡張子リスト) のいずれかのサフィックスを持っている場合、そのファイルをそのままchkStat(ファイルが存在し、実行可能であるかを確認する関数) に渡していました。これは、cmd.exeの挙動とは異なり、例えばprogram.comというファイルを探す際に、PATHEXTに.exeが含まれていてもprogram.exeを試す前にprogram.comを直接チェックしてしまう可能性がありました。変更後、
findExecutableは以下のように動作します。- まず、
hasExt(file)を呼び出して、検索対象のファイル名が既に拡張子を持っているかを確認します。 - もし拡張子を持っている場合、
chkStat(file)を呼び出して、そのファイルがそのまま存在し、実行可能であるかをチェックします。ここで見つかれば、そのパスが返されます。これは、cmd.exeが拡張子付きで指定されたコマンド名を直接検索する挙動を模倣しています。 - ファイル名に拡張子がない場合、または拡張子付きで指定されたファイルが見つからなかった場合、
PATHEXTに含まれる各拡張子 (exts) を順番にファイル名に付加し、chkStatで存在確認を行います。このループは、cmd.exeがPATHEXTを利用して実行可能ファイルを検索する挙動に合致します。
- まず、
これらの変更により、os/exec.LookPath は、Windowsの cmd.exe が実行可能ファイルを検索する際の複雑なルール、特に PATHEXT の適用と拡張子の有無による検索ロジックの違いをより正確に反映するようになりました。
また、src/pkg/os/exec/lp_windows_test.go という新しいテストファイルが追加され、LookPath のWindowsにおける挙動を検証するための広範なテストケースが導入されています。これにより、将来的な回帰を防ぎ、正しい挙動が保証されるようになります。
コアとなるコードの変更箇所
src/pkg/os/exec/lp_windows.go
--- a/src/pkg/os/exec/lp_windows.go
+++ b/src/pkg/os/exec/lp_windows.go
@@ -24,14 +24,21 @@ func chkStat(file string) error {
return nil
}
+func hasExt(file string) bool {
+ i := strings.LastIndex(file, ".")
+ if i < 0 {
+ return false
+ }
+ return strings.LastIndexAny(file, `:\/`) < i
+}
+
func findExecutable(file string, exts []string) (string, error) {
if len(exts) == 0 {
return file, chkStat(file)
}
- f := strings.ToLower(file)
- for _, e := range exts {
- if strings.HasSuffix(f, e) {
- return file, chkStat(file)
+ if hasExt(file) {
+ if chkStat(file) == nil {
+ return file, nil
}
}
for _, e := range exts {
src/pkg/os/exec/lp_windows_test.go
このファイルは新規追加されたため、差分ではなくファイル全体が変更箇所となります。非常に長いため、ここでは省略しますが、lookPathTest 構造体と lookPathTests 変数に定義された多数のテストケース、および testLookPath 関数が主要な変更点です。これらのテストは、cmd.exe の挙動を模倣した LookPath の新しいロジックを網羅的に検証します。
コアとなるコードの解説
hasExt 関数
func hasExt(file string) bool {
i := strings.LastIndex(file, ".")
if i < 0 {
return false
}
return strings.LastIndexAny(file, `:\/`) < i
}
この関数は、与えられた file 文字列がファイル拡張子を持っているかどうかを判定します。
strings.LastIndex(file, "."): ファイル名の中で最後のドット (.) のインデックスを探します。if i < 0: ドットが見つからなければ、拡張子はないと判断しfalseを返します。strings.LastIndexAny(file,:/): ファイル名の中で最後のパス区切り文字 (:または\または/) のインデックスを探します。Windowsでは\が一般的ですが、Goのパス処理では/も考慮されます。:はドライブレター(例:C:)や代替データストリーム(例:file:stream)の可能性を考慮しています。return strings.LastIndexAny(file,:/) < i: 最後のドットのインデックスが、最後のパス区切り文字のインデックスよりも大きい場合、それはファイル拡張子であると判断しtrueを返します。例えば、C:\path\to\file.txtの場合、.は\より後ろにあるためtrueです。しかし、C:\path.to\fileの場合、.は\より前にあるためfalseとなります。これにより、ディレクトリ名に含まれるドットとファイル拡張子を区別します。
findExecutable 関数の変更点
func findExecutable(file string, exts []string) (string, error) {
if len(exts) == 0 {
return file, chkStat(file)
}
- f := strings.ToLower(file)
- for _, e := range exts {
- if strings.HasSuffix(f, e) {
- return file, chkStat(file)
+ if hasExt(file) { // 新しいロジックの開始
+ if chkStat(file) == nil { // 拡張子付きで指定されたファイルを直接チェック
+ return file, nil
}
}
for _, e := range exts {
変更前は、file を小文字に変換し、exts (PATHEXTの拡張子) のいずれかで終わるかをチェックしていました。これは、program.exe のような拡張子付きのファイル名が与えられた場合でも、PATHEXT のリストを考慮せずに直接 chkStat を呼び出す可能性がありました。
変更後は、hasExt(file) を使って、まず file が既に拡張子を持っているかを厳密にチェックします。
- もし
hasExt(file)がtrueを返した場合(つまり、ファイル名に有効な拡張子が含まれている場合)、chkStat(file)を呼び出して、そのファイルがそのまま存在し、実行可能であるかを試みます。ここで見つかれば、そのパスを返します。これは、cmd.exeが拡張子付きで指定されたコマンド名を、PATHEXTを考慮せずに直接検索する挙動を模倣しています。 hasExt(file)がfalseを返した場合(拡張子がない場合)、または拡張子付きで指定されたファイルが見つからなかった場合、その後のループでexts(PATHEXTの拡張子リスト) を順番にfileに付加してchkStatを呼び出します。この部分は、cmd.exeが拡張子なしのコマンド名に対してPATHEXTを適用する挙動に合致します。
この修正により、os/exec.LookPath はWindowsの cmd.exe のコマンド検索ロジック、特に拡張子の有無による検索パスの違いを正確に反映し、より堅牢で予測可能な挙動を提供するようになりました。
関連リンク
- Go Issue Tracker: https://github.com/golang/go/issues
- コミットメッセージに記載されている
Fixes #6224は、GoのIssue Trackerにおける特定の課題を指しています。このコミットは、その課題を解決するために行われました。
- コミットメッセージに記載されている
参考にした情報源リンク
- Go言語の
os/exec.LookPathに関するドキュメントや議論:- https://pkg.go.dev/os/exec#LookPath
- https://go.dev/blog/path-security (Go 1.19以降の
LookPathのセキュリティに関する変更点も参考になりますが、このコミットはそれ以前のものです)
- Windowsの
PATHおよびPATHEXT環境変数、cmd.exeのコマンド検索ロジックに関する一般的な情報源:- Stack Overflowなどの開発者コミュニティの議論
- Microsoftの公式ドキュメント (例:
cmdコマンドのヘルプや、環境変数に関するドキュメント)
- Web検索: "Go os/exec LookPath Windows behavior cmd.exe PATHEXT"
- Web検索: "golang issue 6224 os/exec LookPath" (ただし、検索結果に表示されたCVE-2025-6224は本コミットのIssueとは異なるため注意が必要です。)