Skip to content

Commit f22f413

Browse files
authored
testscript: respect TestScript env PATH variable for exec command (#49)
Includes a partial snapshot of os/exec's platform-specific LookPath logic, as of 0456036e28b718d215f49abe83d3c49101f8a4c7. This is largely a copy and paste, with a type alias to os/exec.Error and an "alias" of os/exec.ErrNotFound.
1 parent 7113be1 commit f22f413

File tree

11 files changed

+300
-14
lines changed

11 files changed

+300
-14
lines changed

internal/os/execpath/exec.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package execpath
2+
3+
import "os/exec"
4+
5+
type Error = exec.Error
6+
7+
// ErrNotFound is the error resulting if a path search failed to find an executable file.
8+
var ErrNotFound = exec.ErrNotFound

internal/os/execpath/lp_js.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build js,wasm
6+
7+
package execpath
8+
9+
// Look searches for an executable named file, using getenv to look up
10+
// environment variables. If getenv is nil, os.Getenv will be used. If file
11+
// contains a slash, it is tried directly and getenv will not be called. The
12+
// result may be an absolute path or a path relative to the current directory.
13+
func Look(file string, getenv func(string) string) (string, error) {
14+
// Wasm can not execute processes, so act as if there are no executables at all.
15+
return "", &Error{file, ErrNotFound}
16+
}

internal/os/execpath/lp_plan9.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2011 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package execpath
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
)
12+
13+
func findExecutable(file string) error {
14+
d, err := os.Stat(file)
15+
if err != nil {
16+
return err
17+
}
18+
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
19+
return nil
20+
}
21+
return os.ErrPermission
22+
}
23+
24+
// Look searches for an executable named file, using getenv to look up
25+
// environment variables. If getenv is nil, os.Getenv will be used. If file
26+
// contains a slash, it is tried directly and getenv will not be called. The
27+
// result may be an absolute path or a path relative to the current directory.
28+
func Look(file string, getenv func(string) string) (string, error) {
29+
if getenv == nil {
30+
getenv = os.Getenv
31+
}
32+
33+
// skip the path lookup for these prefixes
34+
skip := []string{"/", "#", "./", "../"}
35+
36+
for _, p := range skip {
37+
if strings.HasPrefix(file, p) {
38+
err := findExecutable(file)
39+
if err == nil {
40+
return file, nil
41+
}
42+
return "", &Error{file, err}
43+
}
44+
}
45+
46+
path := getenv("path")
47+
for _, dir := range filepath.SplitList(path) {
48+
path := filepath.Join(dir, file)
49+
if err := findExecutable(path); err == nil {
50+
return path, nil
51+
}
52+
}
53+
return "", &Error{file, ErrNotFound}
54+
}

internal/os/execpath/lp_unix.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2010 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build aix darwin dragonfly freebsd linux nacl netbsd openbsd solaris
6+
7+
package execpath
8+
9+
import (
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
)
14+
15+
func findExecutable(file string) error {
16+
d, err := os.Stat(file)
17+
if err != nil {
18+
return err
19+
}
20+
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
21+
return nil
22+
}
23+
return os.ErrPermission
24+
}
25+
26+
// Look searches for an executable named file, using getenv to look up
27+
// environment variables. If getenv is nil, os.Getenv will be used. If file
28+
// contains a slash, it is tried directly and getenv will not be called. The
29+
// result may be an absolute path or a path relative to the current directory.
30+
func Look(file string, getenv func(string) string) (string, error) {
31+
if getenv == nil {
32+
getenv = os.Getenv
33+
}
34+
35+
// NOTE(rsc): I wish we could use the Plan 9 behavior here
36+
// (only bypass the path if file begins with / or ./ or ../)
37+
// but that would not match all the Unix shells.
38+
39+
if strings.Contains(file, "/") {
40+
err := findExecutable(file)
41+
if err == nil {
42+
return file, nil
43+
}
44+
return "", &Error{file, err}
45+
}
46+
path := getenv("PATH")
47+
for _, dir := range filepath.SplitList(path) {
48+
if dir == "" {
49+
// Unix shell semantics: path element "" means "."
50+
dir = "."
51+
}
52+
path := filepath.Join(dir, file)
53+
if err := findExecutable(path); err == nil {
54+
return path, nil
55+
}
56+
}
57+
return "", &Error{file, ErrNotFound}
58+
}

internal/os/execpath/lp_windows.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2010 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package execpath
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
)
12+
13+
func chkStat(file string) error {
14+
d, err := os.Stat(file)
15+
if err != nil {
16+
return err
17+
}
18+
if d.IsDir() {
19+
return os.ErrPermission
20+
}
21+
return nil
22+
}
23+
24+
func hasExt(file string) bool {
25+
i := strings.LastIndex(file, ".")
26+
if i < 0 {
27+
return false
28+
}
29+
return strings.LastIndexAny(file, `:\/`) < i
30+
}
31+
32+
func findExecutable(file string, exts []string) (string, error) {
33+
if len(exts) == 0 {
34+
return file, chkStat(file)
35+
}
36+
if hasExt(file) {
37+
if chkStat(file) == nil {
38+
return file, nil
39+
}
40+
}
41+
for _, e := range exts {
42+
if f := file + e; chkStat(f) == nil {
43+
return f, nil
44+
}
45+
}
46+
return "", os.ErrNotExist
47+
}
48+
49+
// Look searches for an executable named file, using getenv to look up
50+
// environment variables. If getenv is nil, os.Getenv will be used. If file
51+
// contains a slash, it is tried directly and getenv will not be called. The
52+
// result may be an absolute path or a path relative to the current directory.
53+
// Look also uses PATHEXT environment variable to match
54+
// a suitable candidate.
55+
func Look(file string, getenv func(string) string) (string, error) {
56+
if getenv == nil {
57+
getenv = os.Getenv
58+
}
59+
var exts []string
60+
x := getenv(`PATHEXT`)
61+
if x != "" {
62+
for _, e := range strings.Split(strings.ToLower(x), `;`) {
63+
if e == "" {
64+
continue
65+
}
66+
if e[0] != '.' {
67+
e = "." + e
68+
}
69+
exts = append(exts, e)
70+
}
71+
} else {
72+
exts = []string{".com", ".exe", ".bat", ".cmd"}
73+
}
74+
75+
if strings.ContainsAny(file, `:\/`) {
76+
if f, err := findExecutable(file, exts); err == nil {
77+
return f, nil
78+
} else {
79+
return "", &Error{file, err}
80+
}
81+
}
82+
if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
83+
return f, nil
84+
}
85+
path := getenv("path")
86+
for _, dir := range filepath.SplitList(path) {
87+
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
88+
return f, nil
89+
}
90+
}
91+
return "", &Error{file, ErrNotFound}
92+
}

testscript/cmd.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,9 @@ func (ts *TestScript) cmdEnv(neg bool, args []string) {
183183
if len(args) == 0 {
184184
printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once
185185
for _, kv := range ts.env {
186-
k := kv[:strings.Index(kv, "=")]
186+
k := envvarname(kv[:strings.Index(kv, "=")])
187187
if !printed[k] {
188+
printed[k] = true
188189
ts.Logf("%s=%s\n", k, ts.envMap[k])
189190
}
190191
}
@@ -194,7 +195,7 @@ func (ts *TestScript) cmdEnv(neg bool, args []string) {
194195
i := strings.Index(env, "=")
195196
if i < 0 {
196197
// Display value instead of setting it.
197-
ts.Logf("%s=%s\n", env, ts.envMap[env])
198+
ts.Logf("%s=%s\n", env, ts.Getenv(env))
198199
continue
199200
}
200201
ts.Setenv(env[:i], env[i+1:])

testscript/envvarname.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// +build !windows
2+
3+
package testscript
4+
5+
func envvarname(k string) string {
6+
return k
7+
}

testscript/envvarname_windows.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package testscript
2+
3+
import "strings"
4+
5+
func envvarname(k string) string {
6+
return strings.ToLower(k)
7+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# If the PATH environment variable is set in the testscript.Params.Setup phase
2+
# or set directly within a script, exec should honour that PATH
3+
4+
[!windows] env HOME=$WORK/home
5+
[windows] env HOME=$WORK\home
6+
[windows] env USERPROFILE=$WORK\home
7+
[windows] env LOCALAPPDATA=$WORK\appdata
8+
9+
cd go
10+
exec go$exe version
11+
stdout 'go version'
12+
exec go$exe build
13+
[!windows] env PATH=$WORK/go${:}$PATH
14+
[windows] env PATH=$WORK\go${:}$PATH
15+
exec go$exe version
16+
stdout 'This is not go'
17+
18+
-- go/main.go --
19+
package main
20+
21+
import "fmt"
22+
23+
func main() {
24+
fmt.Println("This is not go")
25+
}

testscript/scripts/execguard.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[exec:nosuchcommand] exec nosuchcommand
2-
[exec:cat] exec cat foo
3-
[exec:cat] stdout 'foo\n'
4-
[!exec:cat] grep 'foo\n' foo
2+
[!exec:cat] stop
3+
exec cat foo
4+
stdout 'foo\n'
55

66
-- foo --
77
foo

0 commit comments

Comments
 (0)