[インデックス 18407] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/exec
パッケージにおける Command
関数の挙動を修正し、相対パスの扱いを改善するものです。具体的には、Command
関数がプログラムのパスを解決する際に、ドキュメントに記載されている通りのロジック(パスセパレータを含まない場合にのみ LookPath
を使用する)を厳密に適用するように変更されました。これにより、相対パスで指定されたコマンドが正しく実行できるようになり、以前は手動で *Cmd
構造体を構築する必要があったケースが解消されました。
コミット
commit e6d8bfe218a5f387d7aceddcaee5067a59181838
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Feb 3 16:32:13 2014 -0500
os/exec: fix Command with relative paths
Command was (and is) documented like:
"If name contains no path separators, Command uses LookPath to
resolve the path to a complete name if possible. Otherwise it
uses name directly."
But that wasn't true. It always did LookPath, and then
set a sticky error that the user couldn't unset.
And then if cmd.Dir was changed, Start would still fail
due to the earlier sticky error being set.
This keeps LookPath in the same place as before (so no user
visible changes in cmd.Path after Command), but only does
it when the documentation says it will happen.
Also, clarify the docs about a relative Dir path.
No change in any existing behavior, except using Command
is now possible with relative paths. Previously it only
worked if you built the *Cmd by hand.
Fixes #7228
LGTM=iant
R=iant
CC=adg, golang-codereviews
https://golang.org/cl/59580044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e6d8bfe218a5f387d7aceddcaee5067a59181838
元コミット内容
os/exec: fix Command with relative paths
Command
関数は、ドキュメントでは「名前がパスセパレータを含まない場合、LookPath
を使用して完全なパスを解決する。それ以外の場合は、名前を直接使用する」と記述されていた。
しかし、実際には常に LookPath
を実行し、ユーザーが解除できない「スティッキーエラー」を設定していた。その結果、cmd.Dir
が変更された場合でも、以前設定されたスティッキーエラーのために Start
が失敗していた。
この変更は、LookPath
の位置を以前と同じに保ちつつ(Command
呼び出し後の cmd.Path
にユーザーが認識できる変更はない)、ドキュメントに記載されている場合にのみ LookPath
を実行するようにする。
また、相対 Dir
パスに関するドキュメントを明確にする。
既存の動作に変更はないが、Command
を使用して相対パスでコマンドを実行することが可能になった。以前は、*Cmd
を手動で構築した場合にのみ機能していた。
Fixes #7228
変更の背景
Go言語の os/exec
パッケージは、外部コマンドを実行するための機能を提供します。このパッケージの Command
関数は、実行するプログラムの名前と引数を受け取り、*Cmd
構造体を返します。この構造体には、実行するプログラムのパス (Path
フィールド) や引数 (Args
フィールド) などが含まれます。
コミットメッセージによると、Command
関数のドキュメントには、プログラム名にパスセパレータが含まれない場合にのみ LookPath
(システムのPATH環境変数から実行可能ファイルを探す関数) を使用し、それ以外の場合は指定された名前をそのままパスとして使用すると明記されていました。しかし、実際の挙動は異なり、Command
関数は常に LookPath
を呼び出していました。
この不一致は、特に相対パスでコマンドを指定した場合に問題を引き起こしていました。例えば、./myprogram
のように相対パスでコマンドを指定すると、Command
関数は LookPath
を実行しようとします。しかし、LookPath
は通常、PATH環境変数に登録されたディレクトリ内から実行可能ファイルを探すため、カレントディレクトリの相対パスは解決できません。この結果、LookPath
はエラーを返し、そのエラーが *Cmd
構造体内部に「スティッキーエラー」として保持されていました。
さらに悪いことに、ユーザーが cmd.Dir
(コマンドを実行する作業ディレクトリ) を後から変更した場合でも、このスティッキーエラーが原因で cmd.Start()
が失敗するという問題がありました。これは、ユーザーが意図的に相対パスを指定し、cmd.Dir
を設定してその相対パスを解決しようとしても、Command
関数が内部で発生させたエラーが邪魔をして実行できないという、直感的ではない挙動でした。
このコミットは、このドキュメントと実装の乖離を修正し、ユーザーが Command
関数をより柔軟に、特に相対パスを意図通りに扱えるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とオペレーティングシステムの知識が必要です。
-
os/exec
パッケージ:- Go言語で外部コマンドを実行するための標準ライブラリパッケージです。
exec.Command(name string, arg ...string) *Cmd
は、指定されたプログラムと引数で*Cmd
構造体を作成します。*Cmd
構造体には、実行するプログラムのパス (Path
)、引数 (Args
)、標準入力/出力/エラー (Stdin
,Stdout
,Stderr
)、実行ディレクトリ (Dir
) などの情報が含まれます。cmd.Start()
はコマンドを非同期で開始します。cmd.Run()
はコマンドを開始し、完了するまで待機します。cmd.Output()
はコマンドを実行し、標準出力をバイトスライスとして返します。
-
exec.LookPath(file string) (path string, err error)
:- この関数は、指定された
file
(実行可能ファイル名) をシステムのPATH
環境変数で指定されたディレクトリ群から検索し、最初に見つかった実行可能ファイルの絶対パスを返します。 PATH
環境変数は、オペレーティングシステムがコマンドを探すディレクトリのリストです。例えば、Linuxでは/usr/local/bin:/usr/bin:/bin
のようになります。LookPath
は、ファイル名にパスセパレータ(例:/
や\
)が含まれていない場合にのみ意味を持ちます。絶対パスや相対パスが指定された場合は、通常LookPath
を使う必要はありません。
- この関数は、指定された
-
パスセパレータ (Path Separator):
- ファイルパスのディレクトリを区切る文字です。Unix系システムでは
/
(スラッシュ)、Windowsでは\
(バックスラッシュ) が使われます。 - Go言語の
os
パッケージにはos.PathSeparator
という定数があり、現在のOSに応じたパスセパレータを提供します。また、os.IsPathSeparator(c uint8)
関数は、指定されたバイトがパスセパレータであるかを判定します。
- ファイルパスのディレクトリを区切る文字です。Unix系システムでは
-
相対パスと絶対パス:
- 絶対パス: ファイルシステムのルートディレクトリから始まる完全なパスです(例:
/home/user/documents/file.txt
)。 - 相対パス: 現在の作業ディレクトリを基準としたパスです(例:
./myprogram
、../data/config.json
)。
- 絶対パス: ファイルシステムのルートディレクトリから始まる完全なパスです(例:
-
cmd.Dir
フィールド:*Cmd
構造体のDir
フィールドは、コマンドを実行する作業ディレクトリを指定します。このフィールドが設定されている場合、コマンドは指定されたディレクトリ内で実行されます。- 相対パスで指定されたコマンドは、この
Dir
フィールドで指定されたディレクトリを基準に解決されます。
-
スティッキーエラー (Sticky Error):
- このコミットメッセージで使われている「スティッキーエラー」という表現は、一度設定されると、その後の操作で解除されず、オブジェクトのライフサイクル全体にわたって影響を与え続けるエラー状態を指します。この文脈では、
Command
関数がLookPath
の失敗によって内部的に設定したエラーが、cmd.Dir
の変更後もStart
メソッドの実行を妨げていたことを意味します。
- このコミットメッセージで使われている「スティッキーエラー」という表現は、一度設定されると、その後の操作で解除されず、オブジェクトのライフサイクル全体にわたって影響を与え続けるエラー状態を指します。この文脈では、
これらの知識を前提として、このコミットが os/exec
パッケージの使いやすさと正確性をどのように向上させたかを理解することができます。
技術的詳細
このコミットの技術的な核心は、os/exec
パッケージの Command
関数が、プログラムのパスを解決するロジックを、ドキュメントの記述と一致させるように変更した点にあります。
変更前は、Command
関数は常に exec.LookPath
を呼び出してプログラムのパスを解決しようとしていました。これは、プログラム名が ls
のような単純な名前であっても、./myprogram
のような相対パスであっても同様でした。
問題は、./myprogram
のような相対パスが LookPath
に渡された場合です。LookPath
は PATH
環境変数に基づいて実行可能ファイルを検索するため、通常はカレントディレクトリの相対パスを解決できません。このため、LookPath
はエラーを返し、そのエラーが *Cmd
構造体の内部フィールド (err
という名前でした) に格納されていました。このエラーは「スティッキーエラー」となり、たとえユーザーが後から cmd.Dir
を設定して相対パスが解決可能になるようにしても、cmd.Start()
を呼び出すと、この以前のエラーが原因で即座に失敗していました。
このコミットでは、以下の変更が行われました。
-
Cmd
構造体の変更:Path
フィールドのコメントが更新され、「Path
が相対パスの場合、Dir
に対して評価される」という点が明確化されました。これは、cmd.Dir
を設定することで相対パスのコマンドを実行できるという意図を強調しています。- 内部エラーフィールドの名前が
err
からlookPathErr
に変更され、その目的がLookPath
からのエラーに限定されることが明確になりました。これにより、他の種類のエラーと区別しやすくなります。
-
Command
関数のロジック変更:- 最も重要な変更点です。
Command
関数は、まず引数として渡されたname
をそのままcmd.Path
に設定します。 - 次に、新しいヘルパー関数
containsPathSeparator(name)
を呼び出して、name
にパスセパレータが含まれているかどうかをチェックします。 - パスセパレータが含まれていない場合(例:
ls
,go
)にのみ、exec.LookPath(name)
を呼び出します。LookPath
が成功した場合、cmd.Path
はLookPath
が返した完全なパスに更新されます。LookPath
が失敗した場合、そのエラーはcmd.lookPathErr
に格納されます。
- パスセパレータが含まれている場合(例:
./myprogram
,/usr/bin/ls
)は、LookPath
は呼び出されず、cmd.Path
は初期設定されたname
のままになります。この場合、cmd.lookPathErr
は設定されません。
- 最も重要な変更点です。
-
containsPathSeparator
ヘルパー関数の追加:- この新しい関数は、文字列
s
を走査し、os.IsPathSeparator(s[i])
を使用して、OS固有のパスセパレータが含まれているかを効率的に判定します。
- この新しい関数は、文字列
-
Start
関数の変更:Start
関数は、コマンドの実行を開始する前に、以前はc.err
をチェックしていましたが、この変更によりc.lookPathErr
をチェックするように変わりました。これにより、LookPath
に関連するエラーのみがStart
の即時失敗を引き起こすようになります。
これらの変更により、Command
関数はドキュメント通りの挙動をするようになり、./myprogram
のような相対パスが指定された場合でも、LookPath
が不必要に呼び出されてエラーが設定されることがなくなりました。これにより、ユーザーは cmd.Dir
を設定することで、相対パスのコマンドを意図通りに実行できるようになりました。既存の絶対パスや単純なコマンド名に対する挙動は維持されつつ、相対パスの柔軟性が向上した点がこのコミットの大きな成果です。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/os/exec/exec.go
ファイルと src/pkg/os/exec/exec_test.go
ファイルに集中しています。
src/pkg/os/exec/exec.go
-
Cmd
構造体の定義変更:--- a/src/pkg/os/exec/exec.go +++ b/src/pkg/os/exec/exec.go @@ -33,7 +33,8 @@ type Cmd struct { // Path is the path of the command to run. // // This is the only field that must be set to a non-zero - // value. + // value. If Path is relative, it is evaluated relative + // to Dir. Path string // Args holds command line arguments, including the command as Args[0]. @@ -84,7 +85,7 @@ type Cmd struct { // available after a call to Wait or Run. ProcessState *os.ProcessState - err error // last error (from LookPath, stdin, stdout, stderr) + lookPathErr error // LookPath error, if any. finished bool // when Wait was called childFiles []*os.File closeAfterStart []io.Closer
Path
フィールドのコメントが更新され、相対パスがDir
に対して評価されることが明記されました。- 内部エラーフィールド
err
がlookPathErr
にリネームされ、その役割がLookPath
からのエラーに限定されました。
-
Command
関数のロジック変更:--- a/src/pkg/os/exec/exec.go +++ b/src/pkg/os/exec/exec.go @@ -107,19 +107,31 @@ func Command(name string, arg ...string) *Cmd { // followed by the elements of arg, so arg should not include the // command name itself. For example, Command("echo", "hello") func Command(name string, arg ...string) *Cmd { - aname, err := LookPath(name) - if err != nil { - aname = name - } - return &Cmd{\ - Path: aname, + cmd := &Cmd{ + Path: name, Args: append([]string{name}, arg...),\ - err: err,\ }\ + if !containsPathSeparator(name) { + if lp, err := LookPath(name); err != nil { + cmd.lookPathErr = err + } else { + cmd.Path = lp + } + } + return cmd +} + +func containsPathSeparator(s string) bool { + for i := 0; i < len(s); i++ { + if os.IsPathSeparator(s[i]) { + return true + } + } + return false } // interfaceEqual protects against panics from doing equality tests on -// two interfaces with non-comparable underlying types +// two interfaces with non-comparable underlying types. func interfaceEqual(a, b interface{}) bool { defer func() { \trecover()\ @@ -235,10 +247,10 @@ func (c *Cmd) Run() error { // Start starts the specified command but does not wait for it to complete. func (c *Cmd) Start() error { - if c.err != nil { + if c.lookPathErr != nil { c.closeDescriptors(c.closeAfterStart) c.closeDescriptors(c.closeAfterWait) - return c.err + return c.lookPathErr } if c.Process != nil { return errors.New("exec: already started")
Command
関数内でLookPath
を呼び出すロジックが変更され、containsPathSeparator
関数による条件分岐が導入されました。- 新しいヘルパー関数
containsPathSeparator
が追加されました。 Start
関数がc.err
の代わりにc.lookPathErr
をチェックするように変更されました。
src/pkg/os/exec/exec_test.go
- 新しいテストケース
TestCommandRelativeName
の追加:--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -44,6 +44,33 @@ func TestEcho(t *testing.T) { }\ }\ +func TestCommandRelativeName(t *testing.T) { + // Run our own binary as a relative path + // (e.g. "_test/exec.test") our parent directory. + base := filepath.Base(os.Args[0]) // "exec.test" + dir := filepath.Dir(os.Args[0]) // "/tmp/go-buildNNNN/os/exec/_test" + if dir == "." { + t.Skip("skipping; running test at root somehow") + } + parentDir := filepath.Dir(dir) // "/tmp/go-buildNNNN/os/exec" + dirBase := filepath.Base(dir) // "_test" + if dirBase == "." { + t.Skipf("skipping; unexpected shallow dir of %q", dir)\ + } + + cmd := exec.Command(filepath.Join(dirBase, base), "-test.run=TestHelperProcess", "--", "echo", "foo") + cmd.Dir = parentDir + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + + out, err := cmd.Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(out), "foo\\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + func TestCatStdin(t *testing.T) { // Cat, testing stdin and stdout. input := "Input string\\nLine 2"
- このテストは、自身のバイナリを相対パスで実行し、
cmd.Dir
を設定することで、新しい相対パスの挙動が正しく機能することを確認します。
- このテストは、自身のバイナリを相対パスで実行し、
コアとなるコードの解説
src/pkg/os/exec/exec.go
Cmd
構造体の変更
-
Path
フィールドのコメント更新:// Path is the path of the command to run. // // This is the only field that must be set to a non-zero // value. If Path is relative, it is evaluated relative // to Dir. Path string
この変更は、
Path
が相対パスである場合に、Cmd.Dir
フィールドで指定されたディレクトリを基準として解決されるという、os/exec
パッケージの意図された挙動を明確にしています。これは、ユーザーが相対パスでコマンドを指定し、Dir
を設定することでそのコマンドを実行できるというシナリオをサポートするための重要な説明です。 -
err
フィールドからlookPathErr
へのリネーム:lookPathErr error // LookPath error, if any.
以前は
err
という汎用的な名前だったフィールドがlookPathErr
に変更されました。これにより、このフィールドがexec.LookPath
関数によって発生した特定のエラーのみを保持することが明確になります。この区別は重要で、Command
関数がLookPath
を呼び出すべきではない状況で発生したエラーが、後続のStart
メソッドの実行を不必要に妨げないようにするために役立ちます。
Command
関数のロジック変更
func Command(name string, arg ...string) *Cmd {
cmd := &Cmd{
Path: name,
Args: append([]string{name}, arg...),
}
if !containsPathSeparator(name) {
if lp, err := LookPath(name); err != nil {
cmd.lookPathErr = err
} else {
cmd.Path = lp
}
}
return cmd
}
これがこのコミットの最も重要な変更点です。
cmd := &Cmd{Path: name, Args: append([]string{name}, arg...)}
- まず、引数として渡された
name
をそのままcmd.Path
に設定し、Args
も初期化します。これは、name
が絶対パスや相対パスである場合に、そのパスを直接使用するという意図を反映しています。
- まず、引数として渡された
if !containsPathSeparator(name)
- ここで新しいヘルパー関数
containsPathSeparator
が使用されます。この条件は、name
がls
やgo
のようにパスセパレータを含まない単純なコマンド名である場合にtrue
となります。 - この条件が
true
の場合のみ、以下のLookPath
処理が実行されます。if lp, err := LookPath(name); err != nil
exec.LookPath
を呼び出して、PATH
環境変数から実行可能ファイルを検索します。LookPath
がエラーを返した場合(例: コマンドが見つからない)、そのエラーはcmd.lookPathErr
に格納されます。このエラーは、後でStart
メソッドが呼び出されたときにチェックされます。LookPath
が成功した場合、lp
(見つかった実行可能ファイルの絶対パス) がcmd.Path
に設定されます。
- この条件が
false
の場合(つまり、name
にパスセパレータが含まれている場合、例:./myprogram
,/usr/bin/ls
)、LookPath
は呼び出されません。cmd.Path
は初期設定されたname
のままとなり、cmd.lookPathErr
も設定されません。これにより、相対パスが指定された場合に不必要なLookPath
の呼び出しと、それに伴う「スティッキーエラー」の発生が回避されます。
- ここで新しいヘルパー関数
containsPathSeparator
ヘルパー関数
func containsPathSeparator(s string) bool {
for i := 0; i < len(s); i++ {
if os.IsPathSeparator(s[i]) {
return true
}
}
return false
}
この関数は、与えられた文字列 s
がOS固有のパスセパレータ(os.PathSeparator
)を含んでいるかどうかを効率的にチェックします。これは、Command
関数が LookPath
を呼び出すべきかどうかを判断するためのシンプルなユーティリティです。
Start
関数の変更
func (c *Cmd) Start() error {
if c.lookPathErr != nil {
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return c.lookPathErr
}
// ... (rest of the Start function)
}
Start
メソッドは、コマンドの実行を開始する前に、c.lookPathErr
が設定されているかどうかをチェックするようになりました。これにより、Command
関数が LookPath
の失敗によって設定したエラーがある場合にのみ、Start
が即座に失敗するようになります。相対パスが指定された場合に lookPathErr
が設定されないようになったため、cmd.Dir
を設定して相対パスを解決するシナリオが正しく機能するようになります。
src/pkg/os/exec/exec_test.go
TestCommandRelativeName
テストケース
func TestCommandRelativeName(t *testing.T) {
// Run our own binary as a relative path
// (e.g. "_test/exec.test") our parent directory.
base := filepath.Base(os.Args[0]) // "exec.test"
dir := filepath.Dir(os.Args[0]) // "/tmp/go-buildNNNN/os/exec/_test"
if dir == "." {
t.Skip("skipping; running test at root somehow")
}
parentDir := filepath.Dir(dir) // "/tmp/go-buildNNNN/os/exec"
dirBase := filepath.Base(dir) // "_test"
if dirBase == "." {
t.Skipf("skipping; unexpected shallow dir of %q", dir)
}
cmd := exec.Command(filepath.Join(dirBase, base), "-test.run=TestHelperProcess", "--", "echo", "foo")
cmd.Dir = parentDir
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
out, err := cmd.Output()
if err != nil {
t.Errorf("echo: %v", err)
}
if g, e := string(out), "foo\n"; g != e {
t.Errorf("echo: want %q, got %q", e, g)
}
}
この新しいテストケースは、このコミットによって修正された主要なシナリオを検証します。
- テストバイナリ自身のパス (
os.Args[0]
) を利用して、相対パス (_test/exec.test
のような形式) を構築します。 exec.Command
にこの相対パスを渡し、cmd.Dir
を適切に設定します。これにより、コマンドが実行されるべきディレクトリが指定されます。cmd.Output()
を呼び出してコマンドを実行し、エラーがないこと、および期待される出力 (foo\n
) が得られることを確認します。
このテストは、Command
関数が相対パスを正しく処理し、cmd.Dir
と組み合わせて期待通りに動作することを示す重要な回帰テストです。
関連リンク
- Go Issue #7228: https://github.com/golang/go/issues/7228
- このコミットが修正した元のバグ報告です。相対パスで
exec.Command
を使用した際にLookPath
がエラーを返す問題が議論されています。
- このコミットが修正した元のバグ報告です。相対パスで
- Go Code Review CL 59580044: https://golang.org/cl/59580044
- このコミットのコードレビューページです。変更内容の詳細な議論や、レビュアーからのコメントを確認できます。
参考にした情報源リンク
- Go言語
os/exec
パッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語
os
パッケージのドキュメント (特にos.IsPathSeparator
): https://pkg.go.dev/os - Go言語
path/filepath
パッケージのドキュメント: https://pkg.go.dev/path/filepath PATH
環境変数に関する一般的な情報 (オペレーティングシステム): https://ja.wikipedia.org/wiki/PATH