src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/external_cmd.go (about)

     1  package eval
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strings"
    10  	"syscall"
    11  
    12  	"src.elv.sh/pkg/eval/errs"
    13  	"src.elv.sh/pkg/eval/vals"
    14  	"src.elv.sh/pkg/fsutil"
    15  	"src.elv.sh/pkg/parse"
    16  	"src.elv.sh/pkg/persistent/hash"
    17  )
    18  
    19  var (
    20  	// ErrExternalCmdOpts is thrown when an external command is passed Elvish
    21  	// options.
    22  	//
    23  	// TODO: Catch this kind of errors at compilation time.
    24  	ErrExternalCmdOpts = errors.New("external commands don't accept elvish options")
    25  	// ErrImplicitCdNoArg is thrown when an implicit cd form is passed arguments.
    26  	ErrImplicitCdNoArg = errors.New("implicit cd accepts no arguments")
    27  )
    28  
    29  // externalCmd is an external command.
    30  type externalCmd struct {
    31  	Name string
    32  }
    33  
    34  // NewExternalCmd returns a callable that executes the named external command.
    35  //
    36  // An external command converts all arguments to strings, and does not accept
    37  // any option.
    38  func NewExternalCmd(name string) Callable {
    39  	return externalCmd{name}
    40  }
    41  
    42  func (e externalCmd) Kind() string {
    43  	return "fn"
    44  }
    45  
    46  func (e externalCmd) Equal(a any) bool {
    47  	return e == a
    48  }
    49  
    50  func (e externalCmd) Hash() uint32 {
    51  	return hash.String(e.Name)
    52  }
    53  
    54  func (e externalCmd) Repr(int) string {
    55  	return "<external " + parse.Quote(e.Name) + ">"
    56  }
    57  
    58  // Call calls an external command.
    59  func (e externalCmd) Call(fm *Frame, argVals []any, opts map[string]any) error {
    60  	if len(opts) > 0 {
    61  		return ErrExternalCmdOpts
    62  	}
    63  	if fsutil.DontSearch(e.Name) {
    64  		stat, err := os.Stat(e.Name)
    65  		if err == nil && stat.IsDir() {
    66  			// implicit cd
    67  			if len(argVals) > 0 {
    68  				return ErrImplicitCdNoArg
    69  			}
    70  			fm.Deprecate("implicit cd is deprecated; use cd or location mode instead", fm.traceback.Head, 21)
    71  			return fm.Evaler.Chdir(e.Name)
    72  		}
    73  	}
    74  
    75  	files := make([]*os.File, len(fm.ports))
    76  	for i, port := range fm.ports {
    77  		if port != nil {
    78  			files[i] = port.File
    79  		}
    80  	}
    81  
    82  	args := make([]string, len(argVals)+1)
    83  	for i, a := range argVals {
    84  		// TODO: Maybe we should enforce string arguments instead of coercing
    85  		// all args to strings.
    86  		args[i+1] = vals.ToString(a)
    87  	}
    88  
    89  	path, err := exec.LookPath(e.Name)
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	if runtime.GOOS == "windows" && !filepath.IsAbs(path) {
    95  		// For some reason, Windows's CreateProcess API doesn't like forward
    96  		// slashes in relative paths: ".\foo.bat" works but "./foo.bat" results
    97  		// in an error message that "'.' is not recognized as an internal or
    98  		// external command, operable program or batch file."
    99  		//
   100  		// There seems to be no good reason for this behavior, so we work around
   101  		// it by replacing forward slashes with backslashes. PowerShell seems to
   102  		// be something similar to support "./foo.bat".
   103  		path = strings.ReplaceAll(path, "/", "\\")
   104  	}
   105  
   106  	args[0] = path
   107  
   108  	sys := makeSysProcAttr(fm.background)
   109  	proc, err := os.StartProcess(path, args, &os.ProcAttr{Files: files, Sys: sys})
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	state, err := proc.Wait()
   115  	if err != nil {
   116  		// This should be a can't happen situation. Nonetheless, treat it as a
   117  		// soft error rather than panicking since the Go documentation is not
   118  		// explicit that this can only happen if we make a mistake. Such as
   119  		// calling `Wait` twice on a particular process object.
   120  		return err
   121  	}
   122  	ws := state.Sys().(syscall.WaitStatus)
   123  	if ws.Signaled() && isSIGPIPE(ws.Signal()) {
   124  		readerGone := fm.ports[1].readerGone
   125  		if readerGone != nil && readerGone.Load() {
   126  			return errs.ReaderGone{}
   127  		}
   128  	}
   129  	return NewExternalCmdExit(e.Name, state.Sys().(syscall.WaitStatus), proc.Pid)
   130  }