2016-02-19 05:52:05 +08:00
|
|
|
package eval
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"os"
|
2017-12-05 07:48:38 +08:00
|
|
|
"os/exec"
|
2023-02-28 06:57:02 +08:00
|
|
|
"path/filepath"
|
|
|
|
"runtime"
|
|
|
|
"strings"
|
2021-05-20 07:10:17 +08:00
|
|
|
"sync/atomic"
|
2016-02-19 05:52:05 +08:00
|
|
|
"syscall"
|
2016-02-21 21:32:13 +08:00
|
|
|
|
2021-05-20 07:10:17 +08:00
|
|
|
"src.elv.sh/pkg/eval/errs"
|
2021-01-27 09:28:38 +08:00
|
|
|
"src.elv.sh/pkg/eval/vals"
|
|
|
|
"src.elv.sh/pkg/fsutil"
|
|
|
|
"src.elv.sh/pkg/parse"
|
2021-05-04 05:17:46 +08:00
|
|
|
"src.elv.sh/pkg/persistent/hash"
|
2016-02-19 05:52:05 +08:00
|
|
|
)
|
|
|
|
|
2016-09-15 05:34:10 +08:00
|
|
|
var (
|
2019-04-19 05:15:34 +08:00
|
|
|
// ErrExternalCmdOpts is thrown when an external command is passed Elvish
|
|
|
|
// options.
|
|
|
|
//
|
|
|
|
// TODO: Catch this kind of errors at compilation time.
|
2016-09-15 05:34:10 +08:00
|
|
|
ErrExternalCmdOpts = errors.New("external commands don't accept elvish options")
|
2019-04-19 05:15:34 +08:00
|
|
|
// ErrImplicitCdNoArg is thrown when an implicit cd form is passed arguments.
|
|
|
|
ErrImplicitCdNoArg = errors.New("implicit cd accepts no arguments")
|
2016-09-15 05:34:10 +08:00
|
|
|
)
|
2016-02-19 05:52:05 +08:00
|
|
|
|
2021-01-05 12:07:35 +08:00
|
|
|
// externalCmd is an external command.
|
|
|
|
type externalCmd struct {
|
2016-02-19 05:52:05 +08:00
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
2021-01-05 12:07:35 +08:00
|
|
|
// NewExternalCmd returns a callable that executes the named external command.
|
|
|
|
//
|
|
|
|
// An external command converts all arguments to strings, and does not accept
|
|
|
|
// any option.
|
|
|
|
func NewExternalCmd(name string) Callable {
|
|
|
|
return externalCmd{name}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e externalCmd) Kind() string {
|
2016-02-19 05:52:05 +08:00
|
|
|
return "fn"
|
|
|
|
}
|
|
|
|
|
2022-03-20 23:50:25 +08:00
|
|
|
func (e externalCmd) Equal(a any) bool {
|
2017-07-14 08:15:14 +08:00
|
|
|
return e == a
|
|
|
|
}
|
|
|
|
|
2021-01-05 12:07:35 +08:00
|
|
|
func (e externalCmd) Hash() uint32 {
|
2017-08-31 02:52:27 +08:00
|
|
|
return hash.String(e.Name)
|
|
|
|
}
|
|
|
|
|
2021-01-05 12:07:35 +08:00
|
|
|
func (e externalCmd) Repr(int) string {
|
2016-04-05 12:20:18 +08:00
|
|
|
return "<external " + parse.Quote(e.Name) + ">"
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Call calls an external command.
|
2022-03-20 23:50:25 +08:00
|
|
|
func (e externalCmd) Call(fm *Frame, argVals []any, opts map[string]any) error {
|
2016-09-15 05:34:10 +08:00
|
|
|
if len(opts) > 0 {
|
2018-01-21 19:05:36 +08:00
|
|
|
return ErrExternalCmdOpts
|
2016-09-15 05:34:10 +08:00
|
|
|
}
|
2020-09-03 12:27:18 +08:00
|
|
|
if fsutil.DontSearch(e.Name) {
|
2016-02-19 05:52:05 +08:00
|
|
|
stat, err := os.Stat(e.Name)
|
|
|
|
if err == nil && stat.IsDir() {
|
|
|
|
// implicit cd
|
|
|
|
if len(argVals) > 0 {
|
2019-04-19 05:15:34 +08:00
|
|
|
return ErrImplicitCdNoArg
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
2021-01-05 07:54:13 +08:00
|
|
|
return fm.Evaler.Chdir(e.Name)
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-01 10:17:56 +08:00
|
|
|
files := make([]*os.File, len(fm.ports))
|
|
|
|
for i, port := range fm.ports {
|
2020-01-12 20:32:25 +08:00
|
|
|
if port != nil {
|
|
|
|
files[i] = port.File
|
|
|
|
}
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
args := make([]string, len(argVals)+1)
|
|
|
|
for i, a := range argVals {
|
2020-08-13 12:09:38 +08:00
|
|
|
// TODO: Maybe we should enforce string arguments instead of coercing
|
|
|
|
// all args to strings.
|
2018-02-15 17:14:05 +08:00
|
|
|
args[i+1] = vals.ToString(a)
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
|
|
|
|
2017-12-05 07:48:38 +08:00
|
|
|
path, err := exec.LookPath(e.Name)
|
2016-02-19 05:52:05 +08:00
|
|
|
if err != nil {
|
2018-01-21 19:05:36 +08:00
|
|
|
return err
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
|
|
|
|
2023-02-28 06:57:02 +08:00
|
|
|
if runtime.GOOS == "windows" && !filepath.IsAbs(path) {
|
|
|
|
// For some reason, Windows's CreateProcess API doesn't like forward
|
|
|
|
// slashes in relative paths: ".\foo.bat" works but "./foo.bat" results
|
|
|
|
// in an error message that "'.' is not recognized as an internal or
|
|
|
|
// external command, operable program or batch file."
|
|
|
|
//
|
|
|
|
// There seems to be no good reason for this behavior, so we work around
|
|
|
|
// it by replacing forward slashes with backslashes. PowerShell seems to
|
|
|
|
// be something similar to support "./foo.bat".
|
|
|
|
path = strings.ReplaceAll(path, "/", "\\")
|
|
|
|
}
|
|
|
|
|
2016-02-19 05:52:05 +08:00
|
|
|
args[0] = path
|
2017-12-04 04:46:08 +08:00
|
|
|
|
2018-03-01 10:17:56 +08:00
|
|
|
sys := makeSysProcAttr(fm.background)
|
2017-12-04 04:46:08 +08:00
|
|
|
proc, err := os.StartProcess(path, args, &os.ProcAttr{Files: files, Sys: sys})
|
2016-02-19 05:52:05 +08:00
|
|
|
if err != nil {
|
2018-01-21 19:05:36 +08:00
|
|
|
return err
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
|
|
|
|
2017-12-04 04:46:08 +08:00
|
|
|
state, err := proc.Wait()
|
2016-02-19 05:52:05 +08:00
|
|
|
if err != nil {
|
2020-08-13 12:09:38 +08:00
|
|
|
// This should be a can't happen situation. Nonetheless, treat it as a
|
2020-10-06 02:35:52 +08:00
|
|
|
// soft error rather than panicking since the Go documentation is not
|
2020-08-13 12:09:38 +08:00
|
|
|
// explicit that this can only happen if we make a mistake. Such as
|
|
|
|
// calling `Wait` twice on a particular process object.
|
2018-01-21 19:05:36 +08:00
|
|
|
return err
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|
2021-05-20 07:10:17 +08:00
|
|
|
ws := state.Sys().(syscall.WaitStatus)
|
|
|
|
if ws.Signaled() && isSIGPIPE(ws.Signal()) {
|
|
|
|
readerGone := fm.ports[1].readerGone
|
|
|
|
if readerGone != nil && atomic.LoadInt32(readerGone) == 1 {
|
|
|
|
return errs.ReaderGone{}
|
|
|
|
}
|
|
|
|
}
|
2018-01-21 19:05:36 +08:00
|
|
|
return NewExternalCmdExit(e.Name, state.Sys().(syscall.WaitStatus), proc.Pid)
|
2016-02-19 05:52:05 +08:00
|
|
|
}
|