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 }