Skip to content

Commit

Permalink
testscript: respect TestScript env PATH variable for exec command (#49)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
myitcv committed Jan 17, 2019
1 parent 7113be1 commit f22f413
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 14 deletions.
8 changes: 8 additions & 0 deletions internal/os/execpath/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package execpath

import "os/exec"

type Error = exec.Error

// ErrNotFound is the error resulting if a path search failed to find an executable file.
var ErrNotFound = exec.ErrNotFound
16 changes: 16 additions & 0 deletions internal/os/execpath/lp_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build js,wasm

package execpath

// Look searches for an executable named file, using getenv to look up
// environment variables. If getenv is nil, os.Getenv will be used. If file
// contains a slash, it is tried directly and getenv will not be called. The
// result may be an absolute path or a path relative to the current directory.
func Look(file string, getenv func(string) string) (string, error) {
// Wasm can not execute processes, so act as if there are no executables at all.
return "", &Error{file, ErrNotFound}
}
54 changes: 54 additions & 0 deletions internal/os/execpath/lp_plan9.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package execpath

import (
"os"
"path/filepath"
"strings"
)

func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return os.ErrPermission
}

// Look searches for an executable named file, using getenv to look up
// environment variables. If getenv is nil, os.Getenv will be used. If file
// contains a slash, it is tried directly and getenv will not be called. The
// result may be an absolute path or a path relative to the current directory.
func Look(file string, getenv func(string) string) (string, error) {
if getenv == nil {
getenv = os.Getenv
}

// skip the path lookup for these prefixes
skip := []string{"/", "#", "./", "../"}

for _, p := range skip {
if strings.HasPrefix(file, p) {
err := findExecutable(file)
if err == nil {
return file, nil
}
return "", &Error{file, err}
}
}

path := getenv("path")
for _, dir := range filepath.SplitList(path) {
path := filepath.Join(dir, file)
if err := findExecutable(path); err == nil {
return path, nil
}
}
return "", &Error{file, ErrNotFound}
}
58 changes: 58 additions & 0 deletions internal/os/execpath/lp_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build aix darwin dragonfly freebsd linux nacl netbsd openbsd solaris

package execpath

import (
"os"
"path/filepath"
"strings"
)

func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return os.ErrPermission
}

// Look searches for an executable named file, using getenv to look up
// environment variables. If getenv is nil, os.Getenv will be used. If file
// contains a slash, it is tried directly and getenv will not be called. The
// result may be an absolute path or a path relative to the current directory.
func Look(file string, getenv func(string) string) (string, error) {
if getenv == nil {
getenv = os.Getenv
}

// NOTE(rsc): I wish we could use the Plan 9 behavior here
// (only bypass the path if file begins with / or ./ or ../)
// but that would not match all the Unix shells.

if strings.Contains(file, "/") {
err := findExecutable(file)
if err == nil {
return file, nil
}
return "", &Error{file, err}
}
path := getenv("PATH")
for _, dir := range filepath.SplitList(path) {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := filepath.Join(dir, file)
if err := findExecutable(path); err == nil {
return path, nil
}
}
return "", &Error{file, ErrNotFound}
}
92 changes: 92 additions & 0 deletions internal/os/execpath/lp_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package execpath

import (
"os"
"path/filepath"
"strings"
)

func chkStat(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if d.IsDir() {
return os.ErrPermission
}
return nil
}

func hasExt(file string) bool {
i := strings.LastIndex(file, ".")
if i < 0 {
return false
}
return strings.LastIndexAny(file, `:\/`) < i
}

func findExecutable(file string, exts []string) (string, error) {
if len(exts) == 0 {
return file, chkStat(file)
}
if hasExt(file) {
if chkStat(file) == nil {
return file, nil
}
}
for _, e := range exts {
if f := file + e; chkStat(f) == nil {
return f, nil
}
}
return "", os.ErrNotExist
}

// Look searches for an executable named file, using getenv to look up
// environment variables. If getenv is nil, os.Getenv will be used. If file
// contains a slash, it is tried directly and getenv will not be called. The
// result may be an absolute path or a path relative to the current directory.
// Look also uses PATHEXT environment variable to match
// a suitable candidate.
func Look(file string, getenv func(string) string) (string, error) {
if getenv == nil {
getenv = os.Getenv
}
var exts []string
x := getenv(`PATHEXT`)
if x != "" {
for _, e := range strings.Split(strings.ToLower(x), `;`) {
if e == "" {
continue
}
if e[0] != '.' {
e = "." + e
}
exts = append(exts, e)
}
} else {
exts = []string{".com", ".exe", ".bat", ".cmd"}
}

if strings.ContainsAny(file, `:\/`) {
if f, err := findExecutable(file, exts); err == nil {
return f, nil
} else {
return "", &Error{file, err}
}
}
if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
return f, nil
}
path := getenv("path")
for _, dir := range filepath.SplitList(path) {
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
return f, nil
}
}
return "", &Error{file, ErrNotFound}
}
5 changes: 3 additions & 2 deletions testscript/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ func (ts *TestScript) cmdEnv(neg bool, args []string) {
if len(args) == 0 {
printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once
for _, kv := range ts.env {
k := kv[:strings.Index(kv, "=")]
k := envvarname(kv[:strings.Index(kv, "=")])
if !printed[k] {
printed[k] = true
ts.Logf("%s=%s\n", k, ts.envMap[k])
}
}
Expand All @@ -194,7 +195,7 @@ func (ts *TestScript) cmdEnv(neg bool, args []string) {
i := strings.Index(env, "=")
if i < 0 {
// Display value instead of setting it.
ts.Logf("%s=%s\n", env, ts.envMap[env])
ts.Logf("%s=%s\n", env, ts.Getenv(env))
continue
}
ts.Setenv(env[:i], env[i+1:])
Expand Down
7 changes: 7 additions & 0 deletions testscript/envvarname.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !windows

package testscript

func envvarname(k string) string {
return k
}
7 changes: 7 additions & 0 deletions testscript/envvarname_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package testscript

import "strings"

func envvarname(k string) string {
return strings.ToLower(k)
}
25 changes: 25 additions & 0 deletions testscript/scripts/exec_path_change.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# If the PATH environment variable is set in the testscript.Params.Setup phase
# or set directly within a script, exec should honour that PATH

[!windows] env HOME=$WORK/home
[windows] env HOME=$WORK\home
[windows] env USERPROFILE=$WORK\home
[windows] env LOCALAPPDATA=$WORK\appdata

cd go
exec go$exe version
stdout 'go version'
exec go$exe build
[!windows] env PATH=$WORK/go${:}$PATH
[windows] env PATH=$WORK\go${:}$PATH
exec go$exe version
stdout 'This is not go'

-- go/main.go --
package main

import "fmt"

func main() {
fmt.Println("This is not go")
}
6 changes: 3 additions & 3 deletions testscript/scripts/execguard.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[exec:nosuchcommand] exec nosuchcommand
[exec:cat] exec cat foo
[exec:cat] stdout 'foo\n'
[!exec:cat] grep 'foo\n' foo
[!exec:cat] stop
exec cat foo
stdout 'foo\n'

-- foo --
foo

0 comments on commit f22f413

Please sign in to comment.