| // Copyright 2013 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. | |
| // Use an external test to avoid os/exec -> internal/testenv -> os/exec | |
| // circular dependency. | |
| package exec_test | |
| import ( | |
| "errors" | |
| "fmt" | |
| "internal/testenv" | |
| "io" | |
| "io/fs" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "slices" | |
| "strings" | |
| "testing" | |
| ) | |
| func init() { | |
| registerHelperCommand("printpath", cmdPrintPath) | |
| } | |
| func cmdPrintPath(_ ...string) { | |
| fmt.Println(testenv.Executable(nil)) | |
| } | |
| // makePATH returns a PATH variable referring to the | |
| // given directories relative to a root directory. | |
| // | |
| // The empty string results in an empty entry. | |
| // Paths beginning with . are kept as relative entries. | |
| func makePATH(root string, dirs []string) string { | |
| paths := make([]string, 0, len(dirs)) | |
| for _, d := range dirs { | |
| switch { | |
| case d == "": | |
| paths = append(paths, "") | |
| case d == "." || (len(d) >= 2 && d[0] == '.' && os.IsPathSeparator(d[1])): | |
| paths = append(paths, filepath.Clean(d)) | |
| default: | |
| paths = append(paths, filepath.Join(root, d)) | |
| } | |
| } | |
| return strings.Join(paths, string(os.PathListSeparator)) | |
| } | |
| // installProgs creates executable files (or symlinks to executable files) at | |
| // multiple destination paths. It uses root as prefix for all destination files. | |
| func installProgs(t *testing.T, root string, files []string) { | |
| for _, f := range files { | |
| dstPath := filepath.Join(root, f) | |
| dir := filepath.Dir(dstPath) | |
| if err := os.MkdirAll(dir, 0755); err != nil { | |
| t.Fatal(err) | |
| } | |
| if os.IsPathSeparator(f[len(f)-1]) { | |
| continue // directory and PATH entry only. | |
| } | |
| if strings.EqualFold(filepath.Ext(f), ".bat") { | |
| installBat(t, dstPath) | |
| } else { | |
| installExe(t, dstPath) | |
| } | |
| } | |
| } | |
| // installExe installs a copy of the test executable | |
| // at the given location, creating directories as needed. | |
| // | |
| // (We use a copy instead of just a symlink to ensure that os.Executable | |
| // always reports an unambiguous path, regardless of how it is implemented.) | |
| func installExe(t *testing.T, dstPath string) { | |
| src, err := os.Open(testenv.Executable(t)) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer src.Close() | |
| dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o777) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer func() { | |
| if err := dst.Close(); err != nil { | |
| t.Fatal(err) | |
| } | |
| }() | |
| _, err = io.Copy(dst, src) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| // installBat creates a batch file at dst that prints its own | |
| // path when run. | |
| func installBat(t *testing.T, dstPath string) { | |
| dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o777) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer func() { | |
| if err := dst.Close(); err != nil { | |
| t.Fatal(err) | |
| } | |
| }() | |
| if _, err := fmt.Fprintf(dst, "@echo %s\r\n", dstPath); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| type lookPathTest struct { | |
| name string | |
| PATHEXT string // empty to use default | |
| files []string | |
| PATH []string // if nil, use all parent directories from files | |
| searchFor string | |
| want string | |
| wantErr error | |
| skipCmdExeCheck bool // if true, do not check want against the behavior of cmd.exe | |
| } | |
| var lookPathTests = []lookPathTest{ | |
| { | |
| name: "first match", | |
| files: []string{`p1\a.exe`, `p2\a.exe`, `p2\a`}, | |
| searchFor: `a`, | |
| want: `p1\a.exe`, | |
| }, | |
| { | |
| name: "dirs with extensions", | |
| files: []string{`p1.dir\a`, `p2.dir\a.exe`}, | |
| searchFor: `a`, | |
| want: `p2.dir\a.exe`, | |
| }, | |
| { | |
| name: "first with extension", | |
| files: []string{`p1\a.exe`, `p2\a.exe`}, | |
| searchFor: `a.exe`, | |
| want: `p1\a.exe`, | |
| }, | |
| { | |
| name: "specific name", | |
| files: []string{`p1\a.exe`, `p2\b.exe`}, | |
| searchFor: `b`, | |
| want: `p2\b.exe`, | |
| }, | |
| { | |
| name: "no extension", | |
| files: []string{`p1\b`, `p2\a`}, | |
| searchFor: `a`, | |
| wantErr: exec.ErrNotFound, | |
| }, | |
| { | |
| name: "directory, no extension", | |
| files: []string{`p1\a.exe`, `p2\a.exe`}, | |
| searchFor: `p2\a`, | |
| want: `p2\a.exe`, | |
| }, | |
| { | |
| name: "no match", | |
| files: []string{`p1\a.exe`, `p2\a.exe`}, | |
| searchFor: `b`, | |
| wantErr: exec.ErrNotFound, | |
| }, | |
| { | |
| name: "no match with dir", | |
| files: []string{`p1\b.exe`, `p2\a.exe`}, | |
| searchFor: `p2\b`, | |
| wantErr: exec.ErrNotFound, | |
| }, | |
| { | |
| name: "extensionless file in CWD ignored", | |
| files: []string{`a`, `p1\a.exe`, `p2\a.exe`}, | |
| searchFor: `a`, | |
| want: `p1\a.exe`, | |
| }, | |
| { | |
| name: "extensionless file in PATH ignored", | |
| files: []string{`p1\a`, `p2\a.exe`}, | |
| searchFor: `a`, | |
| want: `p2\a.exe`, | |
| }, | |
| { | |
| name: "specific extension", | |
| files: []string{`p1\a.exe`, `p2\a.bat`}, | |
| searchFor: `a.bat`, | |
| want: `p2\a.bat`, | |
| }, | |
| { | |
| name: "mismatched extension", | |
| files: []string{`p1\a.exe`, `p2\a.exe`}, | |
| searchFor: `a.com`, | |
| wantErr: exec.ErrNotFound, | |
| }, | |
| { | |
| name: "doubled extension", | |
| files: []string{`p1\a.exe.exe`}, | |
| searchFor: `a.exe`, | |
| want: `p1\a.exe.exe`, | |
| }, | |
| { | |
| name: "extension not in PATHEXT", | |
| PATHEXT: `.COM;.BAT`, | |
| files: []string{`p1\a.exe`, `p2\a.exe`}, | |
| searchFor: `a.exe`, | |
| want: `p1\a.exe`, | |
| }, | |
| { | |
| name: "first allowed by PATHEXT", | |
| PATHEXT: `.COM;.EXE`, | |
| files: []string{`p1\a.bat`, `p2\a.exe`}, | |
| searchFor: `a`, | |
| want: `p2\a.exe`, | |
| }, | |
| { | |
| name: "first directory containing a PATHEXT match", | |
| PATHEXT: `.COM;.EXE;.BAT`, | |
| files: []string{`p1\a.bat`, `p2\a.exe`}, | |
| searchFor: `a`, | |
| want: `p1\a.bat`, | |
| }, | |
| { | |
| name: "first PATHEXT entry", | |
| PATHEXT: `.COM;.EXE;.BAT`, | |
| files: []string{`p1\a.bat`, `p1\a.exe`, `p2\a.bat`, `p2\a.exe`}, | |
| searchFor: `a`, | |
| want: `p1\a.exe`, | |
| }, | |
| { | |
| name: "ignore dir with PATHEXT extension", | |
| files: []string{`a.exe\`}, | |
| searchFor: `a`, | |
| wantErr: exec.ErrNotFound, | |
| }, | |
| { | |
| name: "ignore empty PATH entry", | |
| files: []string{`a.bat`, `p\a.bat`}, | |
| PATH: []string{`p`}, | |
| searchFor: `a`, | |
| want: `p\a.bat`, | |
| // If cmd.exe is too old it might not respect NoDefaultCurrentDirectoryInExePath, | |
| // so skip that check. | |
| skipCmdExeCheck: true, | |
| }, | |
| { | |
| name: "return ErrDot if found by a different absolute path", | |
| files: []string{`p1\a.bat`, `p2\a.bat`}, | |
| PATH: []string{`.\p1`, `p2`}, | |
| searchFor: `a`, | |
| want: `p1\a.bat`, | |
| wantErr: exec.ErrDot, | |
| }, | |
| { | |
| name: "suppress ErrDot if also found in absolute path", | |
| files: []string{`p1\a.bat`, `p2\a.bat`}, | |
| PATH: []string{`.\p1`, `p1`, `p2`}, | |
| searchFor: `a`, | |
| want: `p1\a.bat`, | |
| }, | |
| } | |
| func TestLookPathWindows(t *testing.T) { | |
| // Not parallel: uses Chdir and Setenv. | |
| // We are using the "printpath" command mode to test exec.Command here, | |
| // so we won't be calling helperCommand to resolve it. | |
| // That may cause it to appear to be unused. | |
| maySkipHelperCommand("printpath") | |
| // Before we begin, find the absolute path to cmd.exe. | |
| // In non-short mode, we will use it to check the ground truth | |
| // of the test's "want" field. | |
| cmdExe, err := exec.LookPath("cmd") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, tt := range lookPathTests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| if tt.want == "" && tt.wantErr == nil { | |
| t.Fatalf("test must specify either want or wantErr") | |
| } | |
| root := t.TempDir() | |
| installProgs(t, root, tt.files) | |
| if tt.PATHEXT != "" { | |
| t.Setenv("PATHEXT", tt.PATHEXT) | |
| t.Logf("set PATHEXT=%s", tt.PATHEXT) | |
| } | |
| var pathVar string | |
| if tt.PATH == nil { | |
| paths := make([]string, 0, len(tt.files)) | |
| for _, f := range tt.files { | |
| dir := filepath.Join(root, filepath.Dir(f)) | |
| if !slices.Contains(paths, dir) { | |
| paths = append(paths, dir) | |
| } | |
| } | |
| pathVar = strings.Join(paths, string(os.PathListSeparator)) | |
| } else { | |
| pathVar = makePATH(root, tt.PATH) | |
| } | |
| t.Setenv("PATH", pathVar) | |
| t.Logf("set PATH=%s", pathVar) | |
| t.Chdir(root) | |
| if !testing.Short() && !(tt.skipCmdExeCheck || errors.Is(tt.wantErr, exec.ErrDot)) { | |
| // Check that cmd.exe, which is our source of ground truth, | |
| // agrees that our test case is correct. | |
| cmd := testenv.Command(t, cmdExe, "/c", tt.searchFor, "printpath") | |
| out, err := cmd.Output() | |
| if err == nil { | |
| gotAbs := strings.TrimSpace(string(out)) | |
| wantAbs := "" | |
| if tt.want != "" { | |
| wantAbs = filepath.Join(root, tt.want) | |
| } | |
| if gotAbs != wantAbs { | |
| // cmd.exe disagrees. Probably the test case is wrong? | |
| t.Fatalf("%v\n\tresolved to %s\n\twant %s", cmd, gotAbs, wantAbs) | |
| } | |
| } else if tt.wantErr == nil { | |
| if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { | |
| t.Fatalf("%v: %v\n%s", cmd, err, ee.Stderr) | |
| } | |
| t.Fatalf("%v: %v", cmd, err) | |
| } | |
| } | |
| got, err := exec.LookPath(tt.searchFor) | |
| if filepath.IsAbs(got) { | |
| got, err = filepath.Rel(root, got) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| if got != tt.want { | |
| t.Errorf("LookPath(%#q) = %#q; want %#q", tt.searchFor, got, tt.want) | |
| } | |
| if !errors.Is(err, tt.wantErr) { | |
| t.Errorf("LookPath(%#q): %v; want %v", tt.searchFor, err, tt.wantErr) | |
| } | |
| }) | |
| } | |
| } | |
| type commandTest struct { | |
| name string | |
| PATH []string | |
| files []string | |
| dir string | |
| arg0 string | |
| want string | |
| wantPath string // the resolved c.Path, if different from want | |
| wantErrDot bool | |
| wantRunErr error | |
| } | |
| var commandTests = []commandTest{ | |
| // testing commands with no slash, like `a.exe` | |
| { | |
| name: "current directory", | |
| files: []string{`a.exe`}, | |
| PATH: []string{"."}, | |
| arg0: `a.exe`, | |
| want: `a.exe`, | |
| wantErrDot: true, | |
| }, | |
| { | |
| name: "with extra PATH", | |
| files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2", "p"}, | |
| arg0: `a.exe`, | |
| want: `a.exe`, | |
| wantErrDot: true, | |
| }, | |
| { | |
| name: "with extra PATH and no extension", | |
| files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2", "p"}, | |
| arg0: `a`, | |
| want: `a.exe`, | |
| wantErrDot: true, | |
| }, | |
| // testing commands with slash, like `.\a.exe` | |
| { | |
| name: "with dir", | |
| files: []string{`p\a.exe`}, | |
| PATH: []string{"."}, | |
| arg0: `p\a.exe`, | |
| want: `p\a.exe`, | |
| }, | |
| { | |
| name: "with explicit dot", | |
| files: []string{`p\a.exe`}, | |
| PATH: []string{"."}, | |
| arg0: `.\p\a.exe`, | |
| want: `p\a.exe`, | |
| }, | |
| { | |
| name: "with irrelevant PATH", | |
| files: []string{`p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2"}, | |
| arg0: `p\a.exe`, | |
| want: `p\a.exe`, | |
| }, | |
| { | |
| name: "with slash and no extension", | |
| files: []string{`p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2"}, | |
| arg0: `p\a`, | |
| want: `p\a.exe`, | |
| }, | |
| // tests commands, like `a.exe`, with c.Dir set | |
| { | |
| // should not find a.exe in p, because LookPath(`a.exe`) will fail when | |
| // called by Command (before Dir is set), and that error is sticky. | |
| name: "not found before Dir", | |
| files: []string{`p\a.exe`}, | |
| PATH: []string{"."}, | |
| dir: `p`, | |
| arg0: `a.exe`, | |
| want: `p\a.exe`, | |
| wantRunErr: exec.ErrNotFound, | |
| }, | |
| { | |
| // LookPath(`a.exe`) will resolve to `.\a.exe`, but prefixing that with | |
| // dir `p\a.exe` will refer to a non-existent file | |
| name: "resolved before Dir", | |
| files: []string{`a.exe`, `p\not_important_file`}, | |
| PATH: []string{"."}, | |
| dir: `p`, | |
| arg0: `a.exe`, | |
| want: `a.exe`, | |
| wantErrDot: true, | |
| wantRunErr: fs.ErrNotExist, | |
| }, | |
| { | |
| // like above, but making test succeed by installing file | |
| // in referred destination (so LookPath(`a.exe`) will still | |
| // find `.\a.exe`, but we successfully execute `p\a.exe`) | |
| name: "relative to Dir", | |
| files: []string{`a.exe`, `p\a.exe`}, | |
| PATH: []string{"."}, | |
| dir: `p`, | |
| arg0: `a.exe`, | |
| want: `p\a.exe`, | |
| wantErrDot: true, | |
| }, | |
| { | |
| // like above, but add PATH in attempt to break the test | |
| name: "relative to Dir with extra PATH", | |
| files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2", "p"}, | |
| dir: `p`, | |
| arg0: `a.exe`, | |
| want: `p\a.exe`, | |
| wantErrDot: true, | |
| }, | |
| { | |
| // like above, but use "a" instead of "a.exe" for command | |
| name: "relative to Dir with extra PATH and no extension", | |
| files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2", "p"}, | |
| dir: `p`, | |
| arg0: `a`, | |
| want: `p\a.exe`, | |
| wantErrDot: true, | |
| }, | |
| { | |
| // finds `a.exe` in the PATH regardless of Dir because Command resolves the | |
| // full path (using LookPath) before Dir is set. | |
| name: "from PATH with no match in Dir", | |
| files: []string{`p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2", "p"}, | |
| dir: `p`, | |
| arg0: `a.exe`, | |
| want: `p2\a.exe`, | |
| }, | |
| // tests commands, like `.\a.exe`, with c.Dir set | |
| { | |
| // should use dir when command is path, like ".\a.exe" | |
| name: "relative to Dir with explicit dot", | |
| files: []string{`p\a.exe`}, | |
| PATH: []string{"."}, | |
| dir: `p`, | |
| arg0: `.\a.exe`, | |
| want: `p\a.exe`, | |
| }, | |
| { | |
| // like above, but with PATH added in attempt to break it | |
| name: "relative to Dir with dot and extra PATH", | |
| files: []string{`p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2"}, | |
| dir: `p`, | |
| arg0: `.\a.exe`, | |
| want: `p\a.exe`, | |
| }, | |
| { | |
| // LookPath(".\a") will fail before Dir is set, and that error is sticky. | |
| name: "relative to Dir with dot and extra PATH and no extension", | |
| files: []string{`p\a.exe`, `p2\a.exe`}, | |
| PATH: []string{".", "p2"}, | |
| dir: `p`, | |
| arg0: `.\a`, | |
| want: `p\a.exe`, | |
| }, | |
| { | |
| // LookPath(".\a") will fail before Dir is set, and that error is sticky. | |
| name: "relative to Dir with different extension", | |
| files: []string{`a.exe`, `p\a.bat`}, | |
| PATH: []string{"."}, | |
| dir: `p`, | |
| arg0: `.\a`, | |
| want: `p\a.bat`, | |
| }, | |
| } | |
| func TestCommand(t *testing.T) { | |
| // Not parallel: uses Chdir and Setenv. | |
| // We are using the "printpath" command mode to test exec.Command here, | |
| // so we won't be calling helperCommand to resolve it. | |
| // That may cause it to appear to be unused. | |
| maySkipHelperCommand("printpath") | |
| for _, tt := range commandTests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| if tt.PATH == nil { | |
| t.Fatalf("test must specify PATH") | |
| } | |
| root := t.TempDir() | |
| installProgs(t, root, tt.files) | |
| pathVar := makePATH(root, tt.PATH) | |
| t.Setenv("PATH", pathVar) | |
| t.Logf("set PATH=%s", pathVar) | |
| t.Chdir(root) | |
| cmd := exec.Command(tt.arg0, "printpath") | |
| cmd.Dir = filepath.Join(root, tt.dir) | |
| if tt.wantErrDot { | |
| if errors.Is(cmd.Err, exec.ErrDot) { | |
| cmd.Err = nil | |
| } else { | |
| t.Fatalf("cmd.Err = %v; want ErrDot", cmd.Err) | |
| } | |
| } | |
| out, err := cmd.Output() | |
| if err != nil { | |
| if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { | |
| t.Logf("%v: %v\n%s", cmd, err, ee.Stderr) | |
| } else { | |
| t.Logf("%v: %v", cmd, err) | |
| } | |
| if !errors.Is(err, tt.wantRunErr) { | |
| t.Errorf("want %v", tt.wantRunErr) | |
| } | |
| return | |
| } | |
| got := strings.TrimSpace(string(out)) | |
| if filepath.IsAbs(got) { | |
| got, err = filepath.Rel(root, got) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| if got != tt.want { | |
| t.Errorf("\nran %#q\nwant %#q", got, tt.want) | |
| } | |
| gotPath := cmd.Path | |
| wantPath := tt.wantPath | |
| if wantPath == "" { | |
| if strings.Contains(tt.arg0, `\`) { | |
| wantPath = tt.arg0 | |
| } else if tt.wantErrDot { | |
| wantPath = strings.TrimPrefix(tt.want, tt.dir+`\`) | |
| } else { | |
| wantPath = filepath.Join(root, tt.want) | |
| } | |
| } | |
| if gotPath != wantPath { | |
| t.Errorf("\ncmd.Path = %#q\nwant %#q", gotPath, wantPath) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAbsCommandWithDoubledExtension(t *testing.T) { | |
| t.Parallel() | |
| // We expect that ".com" is always included in PATHEXT, but it may also be | |
| // found in the import path of a Go package. If it is at the root of the | |
| // import path, the resulting executable may be named like "example.com.exe". | |
| // | |
| // Since "example.com" looks like a proper executable name, it is probably ok | |
| // for exec.Command to try to run it directly without re-resolving it. | |
| // However, exec.LookPath should try a little harder to figure it out. | |
| comPath := filepath.Join(t.TempDir(), "example.com") | |
| batPath := comPath + ".bat" | |
| installBat(t, batPath) | |
| cmd := exec.Command(comPath) | |
| out, err := cmd.CombinedOutput() | |
| t.Logf("%v: %v\n%s", cmd, err, out) | |
| if !errors.Is(err, fs.ErrNotExist) { | |
| t.Errorf("Command(%#q).Run: %v\nwant fs.ErrNotExist", comPath, err) | |
| } | |
| resolved, err := exec.LookPath(comPath) | |
| if err != nil || resolved != batPath { | |
| t.Fatalf("LookPath(%#q) = %v, %v; want %#q, <nil>", comPath, resolved, err, batPath) | |
| } | |
| } |