github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/eval/external_cmd.go (about)

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