Skip to content

Commit e6d8bfe

Browse files
committed
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
1 parent 4723308 commit e6d8bfe

File tree

2 files changed

+53
-14
lines changed

2 files changed

+53
-14
lines changed

src/pkg/os/exec/exec.go

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ type Cmd struct {
3333
// Path is the path of the command to run.
3434
//
3535
// This is the only field that must be set to a non-zero
36-
// value.
36+
// value. If Path is relative, it is evaluated relative
37+
// to Dir.
3738
Path string
3839

3940
// Args holds command line arguments, including the command as Args[0].
@@ -84,7 +85,7 @@ type Cmd struct {
8485
// available after a call to Wait or Run.
8586
ProcessState *os.ProcessState
8687

87-
err error // last error (from LookPath, stdin, stdout, stderr)
88+
lookPathErr error // LookPath error, if any.
8889
finished bool // when Wait was called
8990
childFiles []*os.File
9091
closeAfterStart []io.Closer
@@ -96,8 +97,7 @@ type Cmd struct {
9697
// Command returns the Cmd struct to execute the named program with
9798
// the given arguments.
9899
//
99-
// It sets Path and Args in the returned structure and zeroes the
100-
// other fields.
100+
// It sets only the Path and Args in the returned structure.
101101
//
102102
// If name contains no path separators, Command uses LookPath to
103103
// resolve the path to a complete name if possible. Otherwise it uses
@@ -107,19 +107,31 @@ type Cmd struct {
107107
// followed by the elements of arg, so arg should not include the
108108
// command name itself. For example, Command("echo", "hello")
109109
func Command(name string, arg ...string) *Cmd {
110-
aname, err := LookPath(name)
111-
if err != nil {
112-
aname = name
113-
}
114-
return &Cmd{
115-
Path: aname,
110+
cmd := &Cmd{
111+
Path: name,
116112
Args: append([]string{name}, arg...),
117-
err: err,
118113
}
114+
if !containsPathSeparator(name) {
115+
if lp, err := LookPath(name); err != nil {
116+
cmd.lookPathErr = err
117+
} else {
118+
cmd.Path = lp
119+
}
120+
}
121+
return cmd
122+
}
123+
124+
func containsPathSeparator(s string) bool {
125+
for i := 0; i < len(s); i++ {
126+
if os.IsPathSeparator(s[i]) {
127+
return true
128+
}
129+
}
130+
return false
119131
}
120132

121133
// interfaceEqual protects against panics from doing equality tests on
122-
// two interfaces with non-comparable underlying types
134+
// two interfaces with non-comparable underlying types.
123135
func interfaceEqual(a, b interface{}) bool {
124136
defer func() {
125137
recover()
@@ -235,10 +247,10 @@ func (c *Cmd) Run() error {
235247

236248
// Start starts the specified command but does not wait for it to complete.
237249
func (c *Cmd) Start() error {
238-
if c.err != nil {
250+
if c.lookPathErr != nil {
239251
c.closeDescriptors(c.closeAfterStart)
240252
c.closeDescriptors(c.closeAfterWait)
241-
return c.err
253+
return c.lookPathErr
242254
}
243255
if c.Process != nil {
244256
return errors.New("exec: already started")

src/pkg/os/exec/exec_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ func TestEcho(t *testing.T) {
4444
}
4545
}
4646

47+
func TestCommandRelativeName(t *testing.T) {
48+
// Run our own binary as a relative path
49+
// (e.g. "_test/exec.test") our parent directory.
50+
base := filepath.Base(os.Args[0]) // "exec.test"
51+
dir := filepath.Dir(os.Args[0]) // "/tmp/go-buildNNNN/os/exec/_test"
52+
if dir == "." {
53+
t.Skip("skipping; running test at root somehow")
54+
}
55+
parentDir := filepath.Dir(dir) // "/tmp/go-buildNNNN/os/exec"
56+
dirBase := filepath.Base(dir) // "_test"
57+
if dirBase == "." {
58+
t.Skipf("skipping; unexpected shallow dir of %q", dir)
59+
}
60+
61+
cmd := exec.Command(filepath.Join(dirBase, base), "-test.run=TestHelperProcess", "--", "echo", "foo")
62+
cmd.Dir = parentDir
63+
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
64+
65+
out, err := cmd.Output()
66+
if err != nil {
67+
t.Errorf("echo: %v", err)
68+
}
69+
if g, e := string(out), "foo\n"; g != e {
70+
t.Errorf("echo: want %q, got %q", e, g)
71+
}
72+
}
73+
4774
func TestCatStdin(t *testing.T) {
4875
// Cat, testing stdin and stdout.
4976
input := "Input string\nLine 2"

0 commit comments

Comments
 (0)