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

[インデックス 19038] ファイルの概要

このコミットは、Go言語の os/exec パッケージがWindows環境で外部コマンドを実行する際の挙動を改善することを目的としています。具体的には、コマンド名に拡張子(例: .exe, .bat)が含まれていない場合でも、適切な実行可能ファイルを自動的に見つけ出して実行できるようにする変更が加えられています。これにより、Windows上でのコマンド実行の信頼性と一貫性が向上します。

コミット

commit df8ec65b3abcdc8566176d6dae756273d8641706
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Apr 4 16:26:15 2014 +1100

    os/exec: always try appropriate command extensions during Cmd.Start on windows
    
    Update #7362
    Fixes #7377
    Fixes #7570
    
    LGTM=rsc
    R=golang-codereviews, rsc
    CC=golang-codereviews
    https://golang.org/cl/83020043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/df8ec65b3abcdc8566176d6dae756273d8641706

元コミット内容

os/exec: always try appropriate command extensions during Cmd.Start on windows

このコミットメッセージは、Windows上で Cmd.Start が実行される際に、常に適切なコマンド拡張子を試行することを示しています。これは、コマンド名が完全なファイル名(例: program.exe)でなくても、システムが自動的に拡張子を補完して実行可能ファイルを見つけられるようにするための変更です。

変更の背景

この変更の背景には、WindowsとUnix系OSにおける実行可能ファイルの検索・実行メカニズムの違いがあります。Unix系OSでは、通常、実行可能ファイルに拡張子は不要であり、PATH 環境変数に指定されたディレクトリ内を検索してコマンドを見つけます。しかし、Windowsでは、実行可能ファイルには通常 .exe, .bat, .cmd などの拡張子が付与されており、コマンドプロンプトやPowerShellなどのシェルは、コマンド名に拡張子がない場合でも PATHEXT 環境変数に定義された拡張子リストを基に自動的に補完して実行可能ファイルを探します。

Goの os/exec パッケージがWindows上で外部コマンドを実行する際、このWindows特有の拡張子補完の挙動が考慮されていなかったため、以下のような問題が発生していました。

  • Issue #7362: コマンド名に拡張子がない場合に os/exec.Command が実行可能ファイルを見つけられない。
  • Issue #7377: Cmd.Dir が設定されている場合に、相対パスで指定されたコマンドが正しく解決されない。
  • Issue #7570: LookPath がWindowsの PATHEXT 環境変数を適切に利用しない。

これらの問題により、GoプログラムがWindows上で外部コマンドを起動する際に、ユーザーが期待する動作と異なる結果になったり、エラーが発生したりすることがありました。このコミットは、これらの既知のバグを修正し、Windows上での os/exec の堅牢性を高めることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が役立ちます。

  • Go言語の os/exec パッケージ: Go言語で外部コマンドを実行するためのパッケージです。exec.Command でコマンドオブジェクトを作成し、Start, Run, Output などのメソッドでコマンドを実行します。
  • Windowsの実行可能ファイル: Windowsでは、実行可能ファイルは通常 .exe、バッチファイルは .bat.cmd といった拡張子を持ちます。
  • PATH 環境変数: オペレーティングシステムが実行可能ファイルを探すディレクトリのリストです。コマンド名が絶対パスで指定されていない場合、システムはこのリストを順に検索します。
  • PATHEXT 環境変数 (Windows固有): Windowsに固有の環境変数で、実行可能ファイルと見なされるファイル拡張子のリスト(例: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC)を定義します。コマンド名に拡張子がない場合、システムはこのリストの拡張子を順に試行して実行可能ファイルを見つけます。
  • os.LookPath 関数: Go言語の os パッケージにある関数で、PATH 環境変数とシステム固有のルール(Windowsでは PATHEXT)に基づいて、指定された実行可能ファイルのフルパスを検索します。
  • filepath.Join: パス要素を結合して、オペレーティングシステムに適した形式のパスを生成する関数です。
  • filepath.Base: パス文字列の最後の要素(ファイル名またはディレクトリ名)を返します。
  • filepath.VolumeName: パス文字列のボリューム名(例: Windowsの C:)を返します。
  • os.IsPathSeparator: 指定された文字がパス区切り文字(Windowsでは \ または /)であるかどうかを判定します。

技術的詳細

このコミットの主要な技術的変更点は、os/exec/exec.golookExtensions という新しい関数が導入され、それが Cmd.Start メソッド内でWindows環境でのみ利用されるようになったことです。

lookExtensions 関数の役割

lookExtensions(path, dir string) (string, error) 関数は、Windows環境において、指定された path(コマンド名)と dir(作業ディレクトリ)に基づいて、実行可能ファイルの完全なパスを解決することを目的としています。この関数は os.LookPath を内部的に利用しますが、その前にいくつかの前処理と後処理を行います。

  1. 相対パスの処理:
    • もし path がファイル名のみ(例: myprogram)である場合、filepath.Join(".", path) を使って .\\myprogram のように現在のディレクトリを示すプレフィックスを追加します。これにより、LookPathPATH 環境変数を検索する前に、現在のディレクトリ(または dir で指定されたディレクトリ)を優先的に検索するよう促します。
  2. dirpath の結合:
    • dir が空でない場合、filepath.Join(dir, path) を使って dirpath を結合します。これにより、指定された作業ディレクトリ内でのコマンド検索を可能にします。
  3. LookPath の利用:
    • 結合されたパスを os.LookPath に渡します。os.LookPath はWindowsの PATHPATHEXT 環境変数を考慮して、実行可能ファイルのフルパスを検索します。
  4. 拡張子の抽出:
    • LookPath が返したパスから、元の dirandpath に追加された拡張子部分を strings.TrimPrefix を使って抽出します。
    • 最終的に、元の path にこの抽出された拡張子を付加して返します。これにより、Cmd.Path が常に完全な実行可能ファイルのパス(拡張子を含む)を持つようになります。

Cmd.Start メソッドの変更

Cmd.Start メソッドは、プロセスを起動する直前に、Windows環境である場合にのみ lookExtensions 関数を呼び出すように変更されました。

if runtime.GOOS == "windows" {
    lp, err := lookExtensions(c.Path, c.Dir)
    if err != nil {
        c.closeDescriptors(c.closeAfterStart)
        c.closeDescriptors(c.closeAfterWait)
        return err
    }
    c.Path = lp
}

このコードブロックは、以下の処理を行います。

  1. runtime.GOOS == "windows" で、現在のOSがWindowsであるかを確認します。
  2. Windowsである場合、Cmd オブジェクトの Path(実行するコマンド)と Dir(作業ディレクトリ)を lookExtensions に渡して、実行可能ファイルのフルパスを解決します。
  3. lookExtensions がエラーを返した場合(実行可能ファイルが見つからなかった場合)、Cmd.Start は直ちにエラーを返します。
  4. lookExtensions が成功した場合、返されたフルパス(拡張子を含む)で c.Path を更新します。これにより、後続のシステムコールが正しい実行可能ファイルを指すようになります。

テストの変更

src/pkg/os/exec/exec_test.gosrc/pkg/os/exec/lp_windows_test.go には、新しい lookExtensions 関数と Cmd.Start の変更を検証するための広範なテストが追加・修正されています。

  • TestHelperProcessexeclookpath という新しいヘルパーコマンドが追加され、テスト内で外部コマンドの実行や LookPath の挙動をより細かく制御できるようになりました。
  • lp_windows_test.go では、lookPathTestcommandTest という構造体が定義され、様々な PATH, PATHEXT, ファイル構成、作業ディレクトリの組み合わせでコマンドの検索と実行が正しく行われるかを検証しています。特に、拡張子なしのコマンド名、相対パス、Cmd.Dir の設定など、以前問題となっていたシナリオが網羅的にテストされています。

コアとなるコードの変更箇所

src/pkg/os/exec/exec.go

// lookExtensions finds windows executable by its dir and path.
// It uses LookPath to try appropriate extensions.
// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`.
func lookExtensions(path, dir string) (string, error) {
	if filepath.Base(path) == path {
		path = filepath.Join(".", path)
	}
	if dir == "" {
		return LookPath(path)
	}
	if filepath.VolumeName(path) != "" {
		return LookPath(path)
	}
	if len(path) > 1 && os.IsPathSeparator(path[0]) {
		return LookPath(path)
	}
	dirandpath := filepath.Join(dir, path)
	// We assume that LookPath will only add file extension.
	lp, err := LookPath(dirandpath)
	if err != nil {
		return "", err
	}
	ext := strings.TrimPrefix(lp, dirandpath)
	return path + ext, nil
}

// Start starts the specified command but does not wait for it to complete.
// ...
func (c *Cmd) Start() error {
	// ... (既存のコード)
	if c.lookPathErr != nil {
		c.closeDescriptors(c.closeAfterStart)
		c.closeDescriptors(c.closeAfterWait)
		return c.lookPathErr
	}
	if runtime.GOOS == "windows" { // このブロックが追加
		lp, err := lookExtensions(c.Path, c.Dir)
		if err != nil {
			c.closeDescriptors(c.closeAfterStart)
			c.closeDescriptors(c.closeAfterWait)
			return err
		}
		c.Path = lp
	}
	if c.Process != nil {
		return errors.New("exec: already started")
	}
	// ... (既存のコード)
}

src/cmd/pack/pack_test.go

--- a/src/cmd/pack/pack_test.go
+++ b/src/cmd/pack/pack_test.go
@@ -218,7 +218,7 @@ func TestHello(t *testing.T) {
 		t.Fatal("cannot find GOCHAR in 'go env' output:\n", out)
 	}
 	char := fields[1]
-	run("go", "build", "-o", "pack", "cmd/pack") // writes pack binary to dir
+	run("go", "build", "cmd/pack") // writes pack binary to dir
 	run("go", "tool", char+"g", "hello.go")
 	run("./pack", "grc", "hello.a", "hello."+char)
 	run("go", "tool", char+"l", "-o", "a.out", "hello.a")

コアとなるコードの解説

lookExtensions 関数

この関数は、Windows上での実行可能ファイルの検索ロジックをカプセル化しています。

  • if filepath.Base(path) == path: これは、path がディレクトリ情報を含まない、単なるファイル名(例: notepad)であるかをチェックします。もしそうであれば、filepath.Join(".", path) を使って .\notepad のように相対パス形式に変換します。これは、LookPathPATH 環境変数を検索する前に、現在のディレクトリを優先的に探すようにするためのヒントとなります。
  • if dir == "", if filepath.VolumeName(path) != "", if len(path) > 1 && os.IsPathSeparator(path[0]): これらの条件は、path が既に絶対パス、ボリューム名を含むパス、またはルートディレクトリからのパスである場合に、そのまま LookPath を呼び出すことを意味します。これらのケースでは、dir を結合する必要がないためです。
  • dirandpath := filepath.Join(dir, path): dir が指定されている場合、dirpath を結合して、指定された作業ディレクトリ内での検索パスを作成します。
  • lp, err := LookPath(dirandpath): ここで、Goの標準ライブラリ関数 os.LookPath が呼び出されます。LookPath は、Windowsの PATHPATHEXT 環境変数を考慮して、dirandpath に対応する実行可能ファイルのフルパスを検索します。
  • ext := strings.TrimPrefix(lp, dirandpath): LookPath が見つけたフルパス lp から、元の dirandpath を取り除くことで、LookPath が自動的に付加した拡張子(例: .exe)を抽出します。
  • return path + ext, nil: 最後に、元の path に抽出した拡張子を付加して返します。これにより、Cmd.Path が常に完全な実行可能ファイルのパスを持つことが保証されます。

Cmd.Start メソッド内の変更

Cmd.Start メソッドは、外部プロセスを起動するGoの主要なインターフェースです。このメソッド内に if runtime.GOOS == "windows" ブロックが追加されたことで、Windows環境でのみ lookExtensions が呼び出されるようになりました。

この変更により、Goプログラムが exec.Command("myprogram") のように拡張子なしでコマンドを指定した場合でも、os/exec パッケージが内部的に myprogram.exemyprogram.bat などを自動的に探し出し、正しい実行可能ファイルを起動できるようになります。これは、Windowsユーザーがコマンドラインで期待する挙動とGoプログラムの挙動を一致させ、より直感的で堅牢なコマンド実行を可能にします。

src/cmd/pack/pack_test.go の変更

この変更は、go build コマンドの -o オプションを削除しています。これは、go build がデフォルトで現在のディレクトリに実行可能ファイルを生成する挙動に依存するようにテストを変更したものです。この変更自体は os/exec の機能変更とは直接関係ありませんが、新しい os/exec の挙動(特にWindowsでの拡張子補完)と整合性を取るためのテストの調整である可能性があります。例えば、-o pack と明示的に指定すると、Windowsでも pack という名前のファイルが生成され、拡張子補完のテストが難しくなるため、デフォルトの挙動に任せることで、より現実的なテストシナリオを構築していると考えられます。

関連リンク

参考にした情報源リンク