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 }