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 5package exec 6 7import ( 8 "errors" 9 "io/fs" 10 "os" 11 "path/filepath" 12 "strings" 13 "syscall" 14) 15 16// ErrNotFound is the error resulting if a path search failed to find an executable file. 17var ErrNotFound = errors.New("executable file not found in %PATH%") 18 19func chkStat(file string) error { 20 d, err := os.Stat(file) 21 if err != nil { 22 return err 23 } 24 if d.IsDir() { 25 return fs.ErrPermission 26 } 27 return nil 28} 29 30func hasExt(file string) bool { 31 i := strings.LastIndex(file, ".") 32 if i < 0 { 33 return false 34 } 35 return strings.LastIndexAny(file, `:\/`) < i 36} 37 38func findExecutable(file string, exts []string) (string, error) { 39 if len(exts) == 0 { 40 return file, chkStat(file) 41 } 42 if hasExt(file) { 43 if chkStat(file) == nil { 44 return file, nil 45 } 46 // Keep checking exts below, so that programs with weird names 47 // like "foo.bat.exe" will resolve instead of failing. 48 } 49 for _, e := range exts { 50 if f := file + e; chkStat(f) == nil { 51 return f, nil 52 } 53 } 54 if hasExt(file) { 55 return "", fs.ErrNotExist 56 } 57 return "", ErrNotFound 58} 59 60// LookPath searches for an executable named file in the 61// directories named by the PATH environment variable. 62// LookPath also uses PATHEXT environment variable to match 63// a suitable candidate. 64// If file contains a slash, it is tried directly and the PATH is not consulted. 65// Otherwise, on success, the result is an absolute path. 66// 67// In older versions of Go, LookPath could return a path relative to the current directory. 68// As of Go 1.19, LookPath will instead return that path along with an error satisfying 69// [errors.Is](err, [ErrDot]). See the package documentation for more details. 70func LookPath(file string) (string, error) { 71 return lookPath(file, pathExt()) 72} 73 74// lookExtensions finds windows executable by its dir and path. 75// It uses LookPath to try appropriate extensions. 76// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`. 77// 78// If the path already has an extension found in PATHEXT, 79// lookExtensions returns it directly without searching 80// for additional extensions. For example, 81// "C:\foo\example.com" would be returned as-is even if the 82// program is actually "C:\foo\example.com.exe". 83func lookExtensions(path, dir string) (string, error) { 84 if filepath.Base(path) == path { 85 path = "." + string(filepath.Separator) + path 86 } 87 exts := pathExt() 88 if ext := filepath.Ext(path); ext != "" { 89 for _, e := range exts { 90 if strings.EqualFold(ext, e) { 91 // Assume that path has already been resolved. 92 return path, nil 93 } 94 } 95 } 96 if dir == "" { 97 return lookPath(path, exts) 98 } 99 if filepath.VolumeName(path) != "" { 100 return lookPath(path, exts) 101 } 102 if len(path) > 1 && os.IsPathSeparator(path[0]) { 103 return lookPath(path, exts) 104 } 105 dirandpath := filepath.Join(dir, path) 106 // We assume that LookPath will only add file extension. 107 lp, err := lookPath(dirandpath, exts) 108 if err != nil { 109 return "", err 110 } 111 ext := strings.TrimPrefix(lp, dirandpath) 112 return path + ext, nil 113} 114 115func pathExt() []string { 116 var exts []string 117 x := os.Getenv(`PATHEXT`) 118 if x != "" { 119 for _, e := range strings.Split(strings.ToLower(x), `;`) { 120 if e == "" { 121 continue 122 } 123 if e[0] != '.' { 124 e = "." + e 125 } 126 exts = append(exts, e) 127 } 128 } else { 129 exts = []string{".com", ".exe", ".bat", ".cmd"} 130 } 131 return exts 132} 133 134// lookPath implements LookPath for the given PATHEXT list. 135func lookPath(file string, exts []string) (string, error) { 136 if strings.ContainsAny(file, `:\/`) { 137 f, err := findExecutable(file, exts) 138 if err == nil { 139 return f, nil 140 } 141 return "", &Error{file, err} 142 } 143 144 // On Windows, creating the NoDefaultCurrentDirectoryInExePath 145 // environment variable (with any value or no value!) signals that 146 // path lookups should skip the current directory. 147 // In theory we are supposed to call NeedCurrentDirectoryForExePathW 148 // "as the registry location of this environment variable can change" 149 // but that seems exceedingly unlikely: it would break all users who 150 // have configured their environment this way! 151 // https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw 152 // See also go.dev/issue/43947. 153 var ( 154 dotf string 155 dotErr error 156 ) 157 if _, found := syscall.Getenv("NoDefaultCurrentDirectoryInExePath"); !found { 158 if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { 159 if execerrdot.Value() == "0" { 160 execerrdot.IncNonDefault() 161 return f, nil 162 } 163 dotf, dotErr = f, &Error{file, ErrDot} 164 } 165 } 166 167 path := os.Getenv("path") 168 for _, dir := range filepath.SplitList(path) { 169 if dir == "" { 170 // Skip empty entries, consistent with what PowerShell does. 171 // (See https://go.dev/issue/61493#issuecomment-1649724826.) 172 continue 173 } 174 175 if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { 176 if dotErr != nil { 177 // https://go.dev/issue/53536: if we resolved a relative path implicitly, 178 // and it is the same executable that would be resolved from the explicit %PATH%, 179 // prefer the explicit name for the executable (and, likely, no error) instead 180 // of the equivalent implicit name with ErrDot. 181 // 182 // Otherwise, return the ErrDot for the implicit path as soon as we find 183 // out that the explicit one doesn't match. 184 dotfi, dotfiErr := os.Lstat(dotf) 185 fi, fiErr := os.Lstat(f) 186 if dotfiErr != nil || fiErr != nil || !os.SameFile(dotfi, fi) { 187 return dotf, dotErr 188 } 189 } 190 191 if !filepath.IsAbs(f) { 192 if execerrdot.Value() != "0" { 193 // If this is the same relative path that we already found, 194 // dotErr is non-nil and we already checked it above. 195 // Otherwise, record this path as the one to which we must resolve, 196 // with or without a dotErr. 197 if dotErr == nil { 198 dotf, dotErr = f, &Error{file, ErrDot} 199 } 200 continue 201 } 202 execerrdot.IncNonDefault() 203 } 204 return f, nil 205 } 206 } 207 208 if dotErr != nil { 209 return dotf, dotErr 210 } 211 return "", &Error{file, ErrNotFound} 212} 213