github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/eval/external_cmd.go (about)

     1  package eval
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"os/exec"
     7  	"syscall"
     8  
     9  	"src.elv.sh/pkg/eval/vals"
    10  	"src.elv.sh/pkg/fsutil"
    11  	"src.elv.sh/pkg/parse"
    12  	"src.elv.sh/pkg/persistent/hash"
    13  )
    14  
    15  var (
    16  	// ErrExternalCmdOpts is thrown when an external command is passed Elvish
    17  	// options.
    18  	//
    19  	// TODO: Catch this kind of errors at compilation time.
    20  	ErrExternalCmdOpts = errors.New("external commands don't accept elvish options")
    21  	// ErrImplicitCdNoArg is thrown when an implicit cd form is passed arguments.
    22  	ErrImplicitCdNoArg = errors.New("implicit cd accepts no arguments")
    23  )
    24  
    25  // externalCmd is an external command.
    26  type externalCmd struct {
    27  	Name string
    28  }
    29  
    30  // NewExternalCmd returns a callable that executes the named external command.
    31  //
    32  // An external command converts all arguments to strings, and does not accept
    33  // any option.
    34  func NewExternalCmd(name string) Callable {
    35  	return externalCmd{name}
    36  }
    37  
    38  func (e externalCmd) Kind() string {
    39  	return "fn"
    40  }
    41  
    42  func (e externalCmd) Equal(a interface{}) bool {
    43  	return e == a
    44  }
    45  
    46  func (e externalCmd) Hash() uint32 {
    47  	return hash.String(e.Name)
    48  }
    49  
    50  func (e externalCmd) Repr(int) string {
    51  	return "<external " + parse.Quote(e.Name) + ">"
    52  }
    53  
    54  // Call calls an external command.
    55  func (e externalCmd) Call(fm *Frame, argVals []interface{}, opts map[string]interface{}) error {
    56  	if len(opts) > 0 {
    57  		return ErrExternalCmdOpts
    58  	}
    59  	if fsutil.DontSearch(e.Name) {
    60  		stat, err := os.Stat(e.Name)
    61  		if err == nil && stat.IsDir() {
    62  			// implicit cd
    63  			if len(argVals) > 0 {
    64  				return ErrImplicitCdNoArg
    65  			}
    66  			return fm.Evaler.Chdir(e.Name)
    67  		}
    68  	}
    69  
    70  	files := make([]*os.File, len(fm.ports))
    71  	for i, port := range fm.ports {
    72  		if port != nil {
    73  			files[i] = port.File
    74  		}
    75  	}
    76  
    77  	args := make([]string, len(argVals)+1)
    78  	for i, a := range argVals {
    79  		// TODO: Maybe we should enforce string arguments instead of coercing
    80  		// all args to strings.
    81  		args[i+1] = vals.ToString(a)
    82  	}
    83  
    84  	path, err := exec.LookPath(e.Name)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	args[0] = path
    90  
    91  	sys := makeSysProcAttr(fm.background)
    92  	proc, err := os.StartProcess(path, args, &os.ProcAttr{Files: files, Sys: sys})
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	state, err := proc.Wait()
    98  	if err != nil {
    99  		// This should be a can't happen situation. Nonetheless, treat it as a
   100  		// soft error rather than panicking since the Go documentation is not
   101  		// explicit that this can only happen if we make a mistake. Such as
   102  		// calling `Wait` twice on a particular process object.
   103  		return err
   104  	}
   105  	return NewExternalCmdExit(e.Name, state.Sys().(syscall.WaitStatus), proc.Pid)
   106  }