Source file src/os/exec/lp_windows.go

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

View as plain text