[インデックス 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とは異なるため注意が必要です。)