Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 は、コマンドを実行する際に以下の優先順位でファイルを検索します。

    1. フルパス指定: コマンド名にディレクトリパスとファイル拡張子の両方が含まれている場合(例: C:\Windows\System32\notepad.exe)、cmd.exe はそのパスのファイルを直接実行しようとします。
    2. パス指定(拡張子なし): コマンド名にディレクトリパスは含まれるが、ファイル拡張子が含まれない場合(例: C:\Windows\System32\notepad)、cmd.exe は指定されたディレクトリ内で PATHEXT に基づいて拡張子を試行します。
    3. カレントディレクトリ検索: コマンド名にパスが含まれない場合(例: notepad)、cmd.exe はまずカレントディレクトリ内でコマンドを探します。この際も PATHEXT が適用されます。
    4. PATH 環境変数検索: カレントディレクトリで見つからなかった場合、cmd.exePATH 環境変数にリストされた各ディレクトリを順番に検索します。各ディレクトリ内で、PATHEXT に基づいて拡張子を試行します。重要なのは、cmd.exe はあるディレクトリ内で PATHEXT の全ての拡張子を試行し終えてから、次の PATH ディレクトリに移るという点です。

このコミットは、Goの os/exec.LookPath が、特に PATHEXT の適用順序と、ファイル名に既に拡張子が含まれている場合の挙動において、cmd.exe のロジックと一致するように調整することを目的としています。

技術的詳細

このコミットでは、src/pkg/os/exec/lp_windows.go ファイル内の findExecutable 関数が変更され、新たに hasExt 関数が追加されています。

変更の核心は、findExecutable 関数が実行可能ファイルを検索する際のロジック、特にファイル拡張子の扱いを改善することにあります。

  1. hasExt 関数の追加: この新しい関数 hasExt(file string) bool は、与えられたファイル名に「有効な」ファイル拡張子が含まれているかどうかを判断します。ここでいう「有効な」とは、ファイル名にドット (.) が含まれており、かつそのドットがパス区切り文字 (:/\) よりも後ろにあることを意味します。これにより、C:\path\to\file.txt のようなパスは拡張子を持つと判断されますが、C:\path.to\file のようなディレクトリ名にドットが含まれるケースは拡張子を持たないと判断されます。

  2. 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.exePATHEXT を利用して実行可能ファイルを検索する挙動に合致します。

これらの変更により、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 文字列がファイル拡張子を持っているかどうかを判定します。

  1. strings.LastIndex(file, "."): ファイル名の中で最後のドット (.) のインデックスを探します。
  2. if i < 0: ドットが見つからなければ、拡張子はないと判断し false を返します。
  3. strings.LastIndexAny(file, :/): ファイル名の中で最後のパス区切り文字 (: または \ または /) のインデックスを探します。Windowsでは \ が一般的ですが、Goのパス処理では / も考慮されます。: はドライブレター(例: C:)や代替データストリーム(例: file:stream)の可能性を考慮しています。
  4. 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 に関するドキュメントや議論:
  • 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とは異なるため注意が必要です。)