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  	"internal/godebug"
    10  	"io/fs"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"syscall"
    15  )
    16  
    17  // ErrNotFound is the error resulting if a path search failed to find an executable file.
    18  var ErrNotFound = errors.New("executable file not found in %PATH%")
    19  
    20  func chkStat(file string) error {
    21  	d, err := os.Stat(file)
    22  	if err != nil {
    23  		return err
    24  	}
    25  	if d.IsDir() {
    26  		return fs.ErrPermission
    27  	}
    28  	return nil
    29  }
    30  
    31  func hasExt(file string) bool {
    32  	i := strings.LastIndex(file, ".")
    33  	if i < 0 {
    34  		return false
    35  	}
    36  	return strings.LastIndexAny(file, `:\/`) < i
    37  }
    38  
    39  func findExecutable(file string, exts []string) (string, error) {
    40  	if len(exts) == 0 {
    41  		return file, chkStat(file)
    42  	}
    43  	if hasExt(file) {
    44  		if chkStat(file) == nil {
    45  			return file, nil
    46  		}
    47  	}
    48  	for _, e := range exts {
    49  		if f := file + e; chkStat(f) == nil {
    50  			return f, nil
    51  		}
    52  	}
    53  	return "", fs.ErrNotExist
    54  }
    55  
    56  // LookPath searches for an executable named file in the
    57  // directories named by the PATH environment variable.
    58  // LookPath also uses PATHEXT environment variable to match
    59  // a suitable candidate.
    60  // If file contains a slash, it is tried directly and the PATH is not consulted.
    61  // Otherwise, on success, the result is an absolute path.
    62  //
    63  // In older versions of Go, LookPath could return a path relative to the current directory.
    64  // As of Go 1.19, LookPath will instead return that path along with an error satisfying
    65  // errors.Is(err, ErrDot). See the package documentation for more details.
    66  func LookPath(file string) (string, error) {
    67  	var exts []string
    68  	x := os.Getenv(`PATHEXT`)
    69  	if x != "" {
    70  		for _, e := range strings.Split(strings.ToLower(x), `;`) {
    71  			if e == "" {
    72  				continue
    73  			}
    74  			if e[0] != '.' {
    75  				e = "." + e
    76  			}
    77  			exts = append(exts, e)
    78  		}
    79  	} else {
    80  		exts = []string{".com", ".exe", ".bat", ".cmd"}
    81  	}
    82  
    83  	if strings.ContainsAny(file, `:\/`) {
    84  		f, err := findExecutable(file, exts)
    85  		if err == nil {
    86  			return f, nil
    87  		}
    88  		return "", &Error{file, err}
    89  	}
    90  
    91  	// On Windows, creating the NoDefaultCurrentDirectoryInExePath
    92  	// environment variable (with any value or no value!) signals that
    93  	// path lookups should skip the current directory.
    94  	// In theory we are supposed to call NeedCurrentDirectoryForExePathW
    95  	// "as the registry location of this environment variable can change"
    96  	// but that seems exceedingly unlikely: it would break all users who
    97  	// have configured their environment this way!
    98  	// https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw
    99  	// See also go.dev/issue/43947.
   100  	var (
   101  		dotf   string
   102  		dotErr error
   103  	)
   104  	if _, found := syscall.Getenv("NoDefaultCurrentDirectoryInExePath"); !found {
   105  		if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
   106  			if godebug.Get("execerrdot") == "0" {
   107  				return f, nil
   108  			}
   109  			dotf, dotErr = f, &Error{file, ErrDot}
   110  		}
   111  	}
   112  
   113  	path := os.Getenv("path")
   114  	for _, dir := range filepath.SplitList(path) {
   115  		if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
   116  			if dotErr != nil {
   117  				// https://go.dev/issue/53536: if we resolved a relative path implicitly,
   118  				// and it is the same executable that would be resolved from the explicit %PATH%,
   119  				// prefer the explicit name for the executable (and, likely, no error) instead
   120  				// of the equivalent implicit name with ErrDot.
   121  				//
   122  				// Otherwise, return the ErrDot for the implicit path as soon as we find
   123  				// out that the explicit one doesn't match.
   124  				dotfi, dotfiErr := os.Lstat(dotf)
   125  				fi, fiErr := os.Lstat(f)
   126  				if dotfiErr != nil || fiErr != nil || !os.SameFile(dotfi, fi) {
   127  					return dotf, dotErr
   128  				}
   129  			}
   130  
   131  			if !filepath.IsAbs(f) && godebug.Get("execerrdot") != "0" {
   132  				return f, &Error{file, ErrDot}
   133  			}
   134  			return f, nil
   135  		}
   136  	}
   137  
   138  	if dotErr != nil {
   139  		return dotf, dotErr
   140  	}
   141  	return "", &Error{file, ErrNotFound}
   142  }
   143  

View as plain text