[インデックス 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
パッケージの動作に関する一般的な知識。