[インデックス 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.go に lookExtensions という新しい関数が導入され、それが Cmd.Start メソッド内でWindows環境でのみ利用されるようになったことです。
lookExtensions 関数の役割
lookExtensions(path, dir string) (string, error) 関数は、Windows環境において、指定された path(コマンド名)と dir(作業ディレクトリ)に基づいて、実行可能ファイルの完全なパスを解決することを目的としています。この関数は os.LookPath を内部的に利用しますが、その前にいくつかの前処理と後処理を行います。
- 相対パスの処理:
- もし
pathがファイル名のみ(例:myprogram)である場合、filepath.Join(".", path)を使って.\\myprogramのように現在のディレクトリを示すプレフィックスを追加します。これにより、LookPathがPATH環境変数を検索する前に、現在のディレクトリ(またはdirで指定されたディレクトリ)を優先的に検索するよう促します。
- もし
dirとpathの結合:dirが空でない場合、filepath.Join(dir, path)を使ってdirとpathを結合します。これにより、指定された作業ディレクトリ内でのコマンド検索を可能にします。
LookPathの利用:- 結合されたパスを
os.LookPathに渡します。os.LookPathはWindowsのPATHとPATHEXT環境変数を考慮して、実行可能ファイルのフルパスを検索します。
- 結合されたパスを
- 拡張子の抽出:
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
}
このコードブロックは、以下の処理を行います。
runtime.GOOS == "windows"で、現在のOSがWindowsであるかを確認します。- Windowsである場合、
CmdオブジェクトのPath(実行するコマンド)とDir(作業ディレクトリ)をlookExtensionsに渡して、実行可能ファイルのフルパスを解決します。 lookExtensionsがエラーを返した場合(実行可能ファイルが見つからなかった場合)、Cmd.Startは直ちにエラーを返します。lookExtensionsが成功した場合、返されたフルパス(拡張子を含む)でc.Pathを更新します。これにより、後続のシステムコールが正しい実行可能ファイルを指すようになります。
テストの変更
src/pkg/os/exec/exec_test.go と src/pkg/os/exec/lp_windows_test.go には、新しい lookExtensions 関数と Cmd.Start の変更を検証するための広範なテストが追加・修正されています。
TestHelperProcessにexecとlookpathという新しいヘルパーコマンドが追加され、テスト内で外部コマンドの実行やLookPathの挙動をより細かく制御できるようになりました。lp_windows_test.goでは、lookPathTestとcommandTestという構造体が定義され、様々な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のように相対パス形式に変換します。これは、LookPathがPATH環境変数を検索する前に、現在のディレクトリを優先的に探すようにするためのヒントとなります。if dir == "",if filepath.VolumeName(path) != "",if len(path) > 1 && os.IsPathSeparator(path[0]): これらの条件は、pathが既に絶対パス、ボリューム名を含むパス、またはルートディレクトリからのパスである場合に、そのままLookPathを呼び出すことを意味します。これらのケースでは、dirを結合する必要がないためです。dirandpath := filepath.Join(dir, path):dirが指定されている場合、dirとpathを結合して、指定された作業ディレクトリ内での検索パスを作成します。lp, err := LookPath(dirandpath): ここで、Goの標準ライブラリ関数os.LookPathが呼び出されます。LookPathは、WindowsのPATHとPATHEXT環境変数を考慮して、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.exe や myprogram.bat などを自動的に探し出し、正しい実行可能ファイルを起動できるようになります。これは、Windowsユーザーがコマンドラインで期待する挙動とGoプログラムの挙動を一致させ、より直感的で堅牢なコマンド実行を可能にします。
src/cmd/pack/pack_test.go の変更
この変更は、go build コマンドの -o オプションを削除しています。これは、go build がデフォルトで現在のディレクトリに実行可能ファイルを生成する挙動に依存するようにテストを変更したものです。この変更自体は os/exec の機能変更とは直接関係ありませんが、新しい os/exec の挙動(特にWindowsでの拡張子補完)と整合性を取るためのテストの調整である可能性があります。例えば、-o pack と明示的に指定すると、Windowsでも pack という名前のファイルが生成され、拡張子補完のテストが難しくなるため、デフォルトの挙動に任せることで、より現実的なテストシナリオを構築していると考えられます。
関連リンク
- Go言語
os/execパッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語
os.LookPath関数のドキュメント: https://pkg.go.dev/os#LookPath - Go言語
path/filepathパッケージのドキュメント: https://pkg.go.dev/path/filepath - Windowsの
PATHEXT環境変数に関する情報: https://learn.microsoft.com/ja-jp/windows-server/administration/windows-commands/path (PATH環境変数のドキュメント内にPATHEXTの記述があります)
参考にした情報源リンク
- GitHubのコミットページ: https://github.com/golang/go/commit/df8ec65b3abcdc8566176d6dae756273d8641706
- Go CL 83020043: https://golang.org/cl/83020043 (Goのコードレビューシステム)
- Go Issue #7362: https://github.com/golang/go/issues/7362
- Go Issue #7377: https://github.com/golang/go/issues/7377
- Go Issue #7570: https://github.com/golang/go/issues/7570
web_fetchツールで取得したGitHubコミットページのコンテンツ。- Go言語の公式ドキュメント。
- Windowsの環境変数に関する一般的な知識。
- Go言語の
os/execパッケージの動作に関する一般的な知識。