diff --git a/api.go b/api.go index 8c2e6e4..d556589 100644 --- a/api.go +++ b/api.go @@ -13,10 +13,9 @@ package main import ( - "bytes" + //"bytes" "errors" "fmt" - "io" "os" "os/exec" "runtime" @@ -28,9 +27,9 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" - "github.com/arnodel/golua/lib/iolib" + //"github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" - "mvdan.cc/sh/v3/interp" + //"mvdan.cc/sh/v3/interp" ) var exports = map[string]util.LuaExport{ @@ -49,7 +48,7 @@ var exports = map[string]util.LuaExport{ "inputMode": {hlinputMode, 1, false}, "interval": {hlinterval, 2, false}, "read": {hlread, 1, false}, - "run": {hlrun, 1, true}, + //"run": {hlrun, 1, true}, "timeout": {hltimeout, 2, false}, "which": {hlwhich, 1, false}, } @@ -154,6 +153,7 @@ func unsetVimMode() { util.SetField(l, hshMod, "vimMode", rt.NilValue) } +/* func handleStream(v rt.Value, strms *streams, errStream bool) error { ud, ok := v.TryUserData() if !ok { @@ -182,6 +182,7 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error { return nil } +*/ // run(cmd, streams) -> exitCode (number), stdout (string), stderr (string) // Runs `cmd` in Hilbish's shell script interpreter. @@ -210,6 +211,7 @@ hilbish.run('wc -l', { }) */ // #example +/* func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN. if err := c.Check1Arg(); err != nil { @@ -288,6 +290,7 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil } +*/ // cwd() -> string // Returns the current directory of the shell. @@ -743,6 +746,8 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // runnerMode(mode) +// **NOTE: This function is deprecated and will be removed in 3.0** +// Use `hilbish.runner.setCurrent` instead. // Sets the execution/runner mode for interactive Hilbish. // This determines whether Hilbish wll try to run input as Lua // and/or sh or only do one of either. @@ -752,6 +757,7 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // Read [about runner mode](../features/runner-mode) for more information. // #param mode string|function func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + // TODO: Reimplement in Lua if err := c.Check1Arg(); err != nil { return nil, err } diff --git a/complete.go b/complete.go index 86938cb..e2f0812 100644 --- a/complete.go +++ b/complete.go @@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { if len(fileCompletions) != 0 { for _, f := range fileCompletions { fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) - if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { + if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { continue } completions = append(completions, f) @@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { // get basename from matches for _, match := range matches { // check if we have execute permissions for our match - err := findExecutable(match, true, false) + err := util.FindExecutable(match, true, false) if err != nil { continue } diff --git a/exec.go b/exec.go index 01a5dfa..19a5d2f 100644 --- a/exec.go +++ b/exec.go @@ -1,141 +1,45 @@ package main import ( - "bytes" - "context" "errors" - "os/exec" "fmt" "io" "os" - "os/signal" - "path/filepath" - "runtime" "strings" - "syscall" - "time" "hilbish/util" + //herror "hilbish/errors" rt "github.com/arnodel/golua/runtime" - "mvdan.cc/sh/v3/shell" //"github.com/yuin/gopher-lua/parse" - "mvdan.cc/sh/v3/interp" - "mvdan.cc/sh/v3/syntax" - "mvdan.cc/sh/v3/expand" ) var errNotExec = errors.New("not executable") var errNotFound = errors.New("not found") var runnerMode rt.Value = rt.StringValue("hybrid") -type streams struct { - stdout io.Writer - stderr io.Writer - stdin io.Reader -} - -type execError struct{ - typ string - cmd string - code int - colon bool - err error -} - -func (e execError) Error() string { - return fmt.Sprintf("%s: %s", e.cmd, e.typ) -} - -func (e execError) sprint() error { - sep := " " - if e.colon { - sep = ": " - } - - return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error()) -} - -func isExecError(err error) (execError, bool) { - if exErr, ok := err.(execError); ok { - return exErr, true - } - - fields := strings.Split(err.Error(), ": ") - knownTypes := []string{ - "not-found", - "not-executable", - } - - if len(fields) > 1 && contains(knownTypes, fields[1]) { - var colon bool - var e error - switch fields[1] { - case "not-found": - e = errNotFound - case "not-executable": - colon = true - e = errNotExec - } - - return execError{ - cmd: fields[0], - typ: fields[1], - colon: colon, - err: e, - }, true - } - - return execError{}, false -} - func runInput(input string, priv bool) { running = true cmdString := aliases.Resolve(input) hooks.Emit("command.preexec", input, cmdString) + currentRunner := runnerMode + rerun: var exitCode uint8 - var err error var cont bool var newline bool // save incase it changes while prompting (For some reason) - currentRunner := runnerMode - if currentRunner.Type() == rt.StringType { - switch currentRunner.AsString() { - case "hybrid": - _, _, err = handleLua(input) - if err == nil { - cmdFinish(0, input, priv) - return - } - input, exitCode, cont, newline, err = handleSh(input) - case "hybridRev": - _, _, _, _, err = handleSh(input) - if err == nil { - cmdFinish(0, input, priv) - return - } - input, exitCode, err = handleLua(input) - case "lua": - input, exitCode, err = handleLua(input) - case "sh": - input, exitCode, cont, newline, err = handleSh(input) - } - } else { - // can only be a string or function so - var runnerErr error - input, exitCode, cont, newline, runnerErr, err = runLuaRunner(currentRunner, input) - if err != nil { - fmt.Fprintln(os.Stderr, err) - cmdFinish(124, input, priv) - return - } - // yep, we only use `err` to check for lua eval error - // our actual error should only be a runner provided error at this point - // command not found type, etc - err = runnerErr + input, exitCode, cont, newline, runnerErr, err := runLuaRunner(currentRunner, input) + if err != nil { + fmt.Fprintln(os.Stderr, err) + cmdFinish(124, input, priv) + return } + // we only use `err` to check for lua eval error + // our actual error should only be a runner provided error at this point + // command not found type, etc + err = runnerErr if cont { input, err = continuePrompt(input, newline) @@ -147,8 +51,8 @@ func runInput(input string, priv bool) { } if err != nil && err != io.EOF { - if exErr, ok := isExecError(err); ok { - hooks.Emit("command." + exErr.typ, exErr.cmd) + if exErr, ok := util.IsExecError(err); ok { + hooks.Emit("command." + exErr.Typ, exErr.Cmd) } else { fmt.Fprintln(os.Stderr, err) } @@ -239,16 +143,7 @@ func handleLua(input string) (string, uint8, error) { return cmdString, 125, err } -func handleSh(cmdString string) (input string, exitCode uint8, cont bool, newline bool, runErr error) { - shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh")) - var err error - input, exitCode, cont, newline, runErr, err = runLuaRunner(shRunner, cmdString) - if err != nil { - runErr = err - } - return -} - +/* func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline bool, e error) { _, _, err := execCommand(cmdString, nil) if err != nil { @@ -274,256 +169,7 @@ func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline return cmdString, 0, false, false, nil } - -// Run command in sh interpreter -func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) { - file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") - if err != nil { - return nil, nil, err - } - - if strms == nil { - strms = &streams{} - } - - if strms.stdout == nil { - strms.stdout = os.Stdout - } - - if strms.stderr == nil { - strms.stderr = os.Stderr - } - - if strms.stdin == nil { - strms.stdin = os.Stdin - } - - interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner) - interp.Env(nil)(runner) - - buf := new(bytes.Buffer) - printer := syntax.NewPrinter() - - var bg bool - for _, stmt := range file.Stmts { - bg = false - if stmt.Background { - bg = true - printer.Print(buf, stmt.Cmd) - - stmtStr := buf.String() - buf.Reset() - jobs.add(stmtStr, []string{}, "") - } - - interp.ExecHandler(execHandle(bg))(runner) - err = runner.Run(context.TODO(), stmt) - if err != nil { - return strms.stdout, strms.stderr, err - } - } - - return strms.stdout, strms.stderr, nil -} - -func execHandle(bg bool) interp.ExecHandlerFunc { - return func(ctx context.Context, args []string) error { - _, argstring := splitInput(strings.Join(args, " ")) - // i dont really like this but it works - if aliases.All()[args[0]] != "" { - for i, arg := range args { - if strings.Contains(arg, " ") { - args[i] = fmt.Sprintf("\"%s\"", arg) - } - } - _, argstring = splitInput(strings.Join(args, " ")) - - // If alias was found, use command alias - argstring = aliases.Resolve(argstring) - var err error - args, err = shell.Fields(argstring, nil) - if err != nil { - return err - } - } - - // If command is defined in Lua then run it - luacmdArgs := rt.NewTable() - for i, str := range args[1:] { - luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) - } - - hc := interp.HandlerCtx(ctx) - if cmd := cmds.Commands[args[0]]; cmd != nil { - stdin := newSinkInput(hc.Stdin) - stdout := newSinkOutput(hc.Stdout) - stderr := newSinkOutput(hc.Stderr) - - sinks := rt.NewTable() - sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud)) - sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud)) - sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud)) - sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud)) - - t := rt.NewThread(l) - sig := make(chan os.Signal) - exit := make(chan bool) - - luaexitcode := rt.IntValue(63) - var err error - go func() { - defer func() { - if r := recover(); r != nil { - exit <- true - } - }() - - signal.Notify(sig, os.Interrupt) - select { - case <-sig: - t.KillContext() - return - } - - }() - - go func() { - luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) - exit <- true - }() - - <-exit - if err != nil { - fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) - return interp.NewExitStatus(1) - } - - var exitcode uint8 - - if code, ok := luaexitcode.TryInt(); ok { - exitcode = uint8(code) - } else if luaexitcode != rt.NilValue { - // deregister commander - delete(cmds.Commands, args[0]) - fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0]) - } - - return interp.NewExitStatus(exitcode) - } - - path, err := util.LookPath(args[0]) - if err == errNotExec { - return execError{ - typ: "not-executable", - cmd: args[0], - code: 126, - colon: true, - err: errNotExec, - } - } else if err != nil { - return execError{ - typ: "not-found", - cmd: args[0], - code: 127, - err: errNotFound, - } - } - - killTimeout := 2 * time.Second - // from here is basically copy-paste of the default exec handler from - // sh/interp but with our job handling - - env := hc.Env - envList := os.Environ() - env.Each(func(name string, vr expand.Variable) bool { - if vr.Exported && vr.Kind == expand.String { - envList = append(envList, name+"="+vr.String()) - } - return true - }) - - cmd := exec.Cmd{ - Path: path, - Args: args, - Env: envList, - Dir: hc.Dir, - Stdin: hc.Stdin, - Stdout: hc.Stdout, - Stderr: hc.Stderr, - } - - var j *job - if bg { - j = jobs.getLatest() - j.setHandle(&cmd) - err = j.start() - } else { - err = cmd.Start() - } - - if err == nil { - if done := ctx.Done(); done != nil { - go func() { - <-done - - if killTimeout <= 0 || runtime.GOOS == "windows" { - cmd.Process.Signal(os.Kill) - return - } - - // TODO: don't temporarily leak this goroutine - // if the program stops itself with the - // interrupt. - go func() { - time.Sleep(killTimeout) - cmd.Process.Signal(os.Kill) - }() - cmd.Process.Signal(os.Interrupt) - }() - } - - err = cmd.Wait() - } - - exit := handleExecErr(err) - - if bg { - j.exitCode = int(exit) - j.finish() - } - return interp.NewExitStatus(exit) - } -} - -func handleExecErr(err error) (exit uint8) { - ctx := context.TODO() - - switch x := err.(type) { - case *exec.ExitError: - // started, but errored - default to 1 if OS - // doesn't have exit statuses - if status, ok := x.Sys().(syscall.WaitStatus); ok { - if status.Signaled() { - if ctx.Err() != nil { - return - } - exit = uint8(128 + status.Signal()) - return - } - exit = uint8(status.ExitStatus()) - return - } - exit = 1 - return - case *exec.Error: - // did not start - //fmt.Fprintf(hc.Stderr, "%v\n", err) - exit = 127 - default: return - } - - return -} +*/ func splitInput(input string) ([]string, string) { // end my suffering diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go new file mode 100644 index 0000000..40fad70 --- /dev/null +++ b/golibs/snail/lua.go @@ -0,0 +1,125 @@ +package snail + +import ( + "fmt" + "strings" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +var snailMetaKey = rt.StringValue("hshsnail") +var Loader = packagelib.Loader{ + Load: loaderFunc, + Name: "fs", +} + +func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + snailMeta := rt.NewTable() + snailMethods := rt.NewTable() + snailFuncs := map[string]util.LuaExport{ + "run": {srun, 1, false}, + } + util.SetExports(rtm, snailMethods, snailFuncs) + + snailIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + arg := c.Arg(1) + val := snailMethods.Get(arg) + + return c.PushingNext1(t.Runtime, val), nil + } + snailMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(snailIndex, "__index", 2, false))) + rtm.SetRegistry(snailMetaKey, rt.TableValue(snailMeta)) + + exports := map[string]util.LuaExport{ + "new": util.LuaExport{snew, 0, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return rt.TableValue(mod), nil +} + +func snew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + s := New(t.Runtime) + return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil +} + +func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := snailArg(c, 0) + if err != nil { + return nil, err + } + + cmd, err := c.StringArg(1) + if err != nil { + return nil, err + } + + var newline bool + var cont bool + var luaErr rt.Value = rt.NilValue + exitCode := 0 + bg, _, _, err := s.Run(cmd, nil) + if err != nil { + if syntax.IsIncomplete(err) { + /* + if !interactive { + return cmdString, 126, false, false, err + } + */ + if strings.Contains(err.Error(), "unclosed here-document") { + newline = true + } + cont = true + } else { + if code, ok := interp.IsExitStatus(err); ok { + exitCode = int(code) + } else { + luaErr = rt.StringValue(err.Error()) + } + } + } + runnerRet := rt.NewTable() + runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd)) + runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode))) + runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont)) + runnerRet.Set(rt.StringValue("newline"), rt.BoolValue(newline)) + runnerRet.Set(rt.StringValue("err"), luaErr) + + runnerRet.Set(rt.StringValue("bg"), rt.BoolValue(bg)) + return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil +} + +func snailArg(c *rt.GoCont, arg int) (*snail, error) { + s, ok := valueToSnail(c.Arg(arg)) + if !ok { + return nil, fmt.Errorf("#%d must be a snail", arg + 1) + } + + return s, nil +} + +func valueToSnail(val rt.Value) (*snail, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + s, ok := u.Value().(*snail) + return s, ok +} + +func snailUserData(s *snail) *rt.UserData { + snailMeta := s.runtime.Registry(snailMetaKey) + return rt.NewUserData(s, snailMeta.AsTable()) +} diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go new file mode 100644 index 0000000..4ef92ea --- /dev/null +++ b/golibs/snail/snail.go @@ -0,0 +1,302 @@ +// shell script interpreter library +package snail + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "time" + + "hilbish/sink" + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "mvdan.cc/sh/v3/shell" + //"github.com/yuin/gopher-lua/parse" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" + "mvdan.cc/sh/v3/expand" +) + +type snail struct{ + runner *interp.Runner + runtime *rt.Runtime +} + +func New(rtm *rt.Runtime) *snail { + runner, _ := interp.New() + + return &snail{ + runner: runner, + runtime: rtm, + } +} + +func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer, error){ + file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") + if err != nil { + return false, nil, nil, err + } + + if strms == nil { + strms = &util.Streams{} + } + + if strms.Stdout == nil { + strms.Stdout = os.Stdout + } + + if strms.Stderr == nil { + strms.Stderr = os.Stderr + } + + if strms.Stdin == nil { + strms.Stdin = os.Stdin + } + + interp.StdIO(strms.Stdin, strms.Stdout, strms.Stderr)(s.runner) + interp.Env(nil)(s.runner) + + buf := new(bytes.Buffer) + //printer := syntax.NewPrinter() + + var bg bool + for _, stmt := range file.Stmts { + bg = false + if stmt.Background { + bg = true + //printer.Print(buf, stmt.Cmd) + + //stmtStr := buf.String() + buf.Reset() + //jobs.add(stmtStr, []string{}, "") + } + + interp.ExecHandler(func(ctx context.Context, args []string) error { + _, argstring := splitInput(strings.Join(args, " ")) + // i dont really like this but it works + aliases := make(map[string]string) + aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.all()") + util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) { + aliases[k.AsString()] = v.AsString() + }) + if aliases[args[0]] != "" { + for i, arg := range args { + if strings.Contains(arg, " ") { + args[i] = fmt.Sprintf("\"%s\"", arg) + } + } + _, argstring = splitInput(strings.Join(args, " ")) + + // If alias was found, use command alias + argstring = util.MustDoString(s.runtime, fmt.Sprintf(`return hilbish.aliases.resolve("%s")`, argstring)).AsString() + + var err error + args, err = shell.Fields(argstring, nil) + if err != nil { + return err + } + } + + // If command is defined in Lua then run it + luacmdArgs := rt.NewTable() + for i, str := range args[1:] { + luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) + } + + hc := interp.HandlerCtx(ctx) + + cmds := make(map[string]*rt.Closure) + luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable() + util.ForEach(luaCmds, func(k, v rt.Value) { + cmds[k.AsString()] = k.AsTable().Get(rt.StringValue("exec")).AsClosure() + }) + if cmd := cmds[args[0]]; cmd != nil { + stdin := sink.NewSinkInput(s.runtime, hc.Stdin) + stdout := sink.NewSinkOutput(s.runtime, hc.Stdout) + stderr := sink.NewSinkOutput(s.runtime, hc.Stderr) + + sinks := rt.NewTable() + sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.UserData)) + sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.UserData)) + sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.UserData)) + sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.UserData)) + + t := rt.NewThread(s.runtime) + sig := make(chan os.Signal) + exit := make(chan bool) + + luaexitcode := rt.IntValue(63) + var err error + go func() { + defer func() { + if r := recover(); r != nil { + exit <- true + } + }() + + signal.Notify(sig, os.Interrupt) + select { + case <-sig: + t.KillContext() + return + } + + }() + + go func() { + luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) + exit <- true + }() + + <-exit + if err != nil { + fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) + return interp.NewExitStatus(1) + } + + var exitcode uint8 + + if code, ok := luaexitcode.TryInt(); ok { + exitcode = uint8(code) + } else if luaexitcode != rt.NilValue { + // deregister commander + delete(cmds, args[0]) + fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0]) + } + + return interp.NewExitStatus(exitcode) + } + + path, err := util.LookPath(args[0]) + if err == util.ErrNotExec { + return util.ExecError{ + Typ: "not-executable", + Cmd: args[0], + Code: 126, + Colon: true, + Err: util.ErrNotExec, + } + } else if err != nil { + return util.ExecError{ + Typ: "not-found", + Cmd: args[0], + Code: 127, + Err: util.ErrNotFound, + } + } + + killTimeout := 2 * time.Second + // from here is basically copy-paste of the default exec handler from + // sh/interp but with our job handling + + env := hc.Env + envList := os.Environ() + env.Each(func(name string, vr expand.Variable) bool { + if vr.Exported && vr.Kind == expand.String { + envList = append(envList, name+"="+vr.String()) + } + return true + }) + + cmd := exec.Cmd{ + Path: path, + Args: args, + Env: envList, + Dir: hc.Dir, + Stdin: hc.Stdin, + Stdout: hc.Stdout, + Stderr: hc.Stderr, + } + + //var j *job + if bg { + /* + j = jobs.getLatest() + j.setHandle(&cmd) + err = j.start() + */ + } else { + err = cmd.Start() + } + + if err == nil { + if done := ctx.Done(); done != nil { + go func() { + <-done + + if killTimeout <= 0 || runtime.GOOS == "windows" { + cmd.Process.Signal(os.Kill) + return + } + + // TODO: don't temporarily leak this goroutine + // if the program stops itself with the + // interrupt. + go func() { + time.Sleep(killTimeout) + cmd.Process.Signal(os.Kill) + }() + cmd.Process.Signal(os.Interrupt) + }() + } + + err = cmd.Wait() + } + + exit := util.HandleExecErr(err) + + if bg { + //j.exitCode = int(exit) + //j.finish() + } + return interp.NewExitStatus(exit) + })(s.runner) + err = s.runner.Run(context.TODO(), stmt) + if err != nil { + return bg, strms.Stdout, strms.Stderr, err + } + } + + return bg, strms.Stdout, strms.Stderr, nil +} + +func splitInput(input string) ([]string, string) { + // end my suffering + // TODO: refactor this garbage + quoted := false + cmdArgs := []string{} + sb := &strings.Builder{} + cmdstr := &strings.Builder{} + + for _, r := range input { + if r == '"' { + // start quoted input + // this determines if other runes are replaced + quoted = !quoted + // dont add back quotes + //sb.WriteRune(r) + } else if !quoted && r == '~' { + // if not in quotes and ~ is found then make it $HOME + sb.WriteString(os.Getenv("HOME")) + } else if !quoted && r == ' ' { + // if not quoted and there's a space then add to cmdargs + cmdArgs = append(cmdArgs, sb.String()) + sb.Reset() + } else { + sb.WriteRune(r) + } + cmdstr.WriteRune(r) + } + if sb.Len() > 0 { + cmdArgs = append(cmdArgs, sb.String()) + } + + return cmdArgs, cmdstr.String() +} diff --git a/golibs/terminal/snail.go b/golibs/terminal/snail.go deleted file mode 100644 index e69de29..0000000 diff --git a/job.go b/job.go index bb16e92..fcb1c2c 100644 --- a/job.go +++ b/job.go @@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if !j.running { err := j.start() - exit := handleExecErr(err) + exit := util.HandleExecErr(err) j.exitCode = int(exit) j.finish() } diff --git a/lua.go b/lua.go index 94b7910..9cefada 100644 --- a/lua.go +++ b/lua.go @@ -4,10 +4,12 @@ import ( "fmt" "os" + "hilbish/sink" "hilbish/util" "hilbish/golibs/bait" "hilbish/golibs/commander" "hilbish/golibs/fs" + "hilbish/golibs/snail" "hilbish/golibs/terminal" rt "github.com/arnodel/golua/runtime" @@ -23,15 +25,15 @@ func luaInit() { MessageHandler: debuglib.Traceback, }) lib.LoadAll(l) - setupSinkType(l) + sink.SetupSinkType(l) lib.LoadLibs(l, hilbishLoader) // yes this is stupid, i know util.DoString(l, "hilbish = require 'hilbish'") - // Add fs and terminal module module to Lua lib.LoadLibs(l, fs.Loader) lib.LoadLibs(l, terminal.Loader) + lib.LoadLibs(l, snail.Loader) cmds = commander.New(l) lib.LoadLibs(l, cmds.Loader) diff --git a/main.go b/main.go index af7a22a..c26a55e 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,6 @@ import ( "github.com/pborman/getopt" "github.com/maxlandon/readline" "golang.org/x/term" - "mvdan.cc/sh/v3/interp" ) var ( @@ -311,15 +310,6 @@ func removeDupes(slice []string) []string { return newSlice } -func contains(s []string, e string) bool { - for _, a := range s { - if strings.ToLower(a) == strings.ToLower(e) { - return true - } - } - return false -} - func exit(code int) { jobs.stopAll() diff --git a/nature/runner.lua b/nature/runner.lua index 235ab77..3d2bb61 100644 --- a/nature/runner.lua +++ b/nature/runner.lua @@ -1,4 +1,6 @@ --- hilbish.runner +local snail = require 'snail' + local currentRunner = 'hybrid' local runners = {} @@ -107,7 +109,5 @@ hilbish.runner.add('lua', function(input) return hilbish.runner.lua(cmdStr) end) -hilbish.runner.add('sh', function(input) - return hilbish.runner.sh(input) -end) +hilbish.runner.add('sh', snail.new()) diff --git a/runnermode.go b/runnermode.go index fb8bcf4..f1e2bf0 100644 --- a/runnermode.go +++ b/runnermode.go @@ -53,7 +53,7 @@ end) */ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ - "sh": {shRunner, 1, false}, + //"sh": {shRunner, 1, false}, "lua": {luaRunner, 1, false}, "setMode": {hlrunnerMode, 1, false}, } @@ -66,10 +66,12 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { // #interface runner // setMode(cb) +// **NOTE: This function is deprecated and will be removed in 3.0** +// Use `hilbish.runner.setCurrent` instead. // This is the same as the `hilbish.runnerMode` function. // It takes a callback, which will be used to execute all interactive input. // In normal cases, neither callbacks should be overrided by the user, -// as the higher level functions listed below this will handle it. +// as the higher level functions (setCurrent) this will handle it. // #param cb function func _runnerMode() {} @@ -78,6 +80,7 @@ func _runnerMode() {} // Runs a command in Hilbish's shell script interpreter. // This is the equivalent of using `source`. // #param cmd string +/* func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -101,6 +104,7 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil } +*/ // #interface runner // lua(cmd) diff --git a/sink.go b/sink/sink.go similarity index 87% rename from sink.go rename to sink/sink.go index 3aa5507..be8b7d1 100644 --- a/sink.go +++ b/sink/sink.go @@ -1,4 +1,4 @@ -package main +package sink import ( "bufio" @@ -17,15 +17,15 @@ var sinkMetaKey = rt.StringValue("hshsink") // #type // A sink is a structure that has input and/or output to/from // a desination. -type sink struct{ +type Sink struct{ writer *bufio.Writer reader *bufio.Reader file *os.File - ud *rt.UserData + UserData *rt.UserData autoFlush bool } -func setupSinkType(rtm *rt.Runtime) { +func SetupSinkType(rtm *rt.Runtime) { sinkMeta := rt.NewTable() sinkMethods := rt.NewTable() @@ -37,7 +37,7 @@ func setupSinkType(rtm *rt.Runtime) { "write": {luaSinkWrite, 2, false}, "writeln": {luaSinkWriteln, 2, false}, } - util.SetExports(l, sinkMethods, sinkFuncs) + util.SetExports(rtm, sinkMethods, sinkFuncs) sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { s, _ := sinkArg(c, 0) @@ -64,7 +64,7 @@ func setupSinkType(rtm *rt.Runtime) { } sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false))) - l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) + rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) } @@ -212,11 +212,11 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func newSinkInput(r io.Reader) *sink { - s := &sink{ +func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink { + s := &Sink{ reader: bufio.NewReader(r), } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) if f, ok := r.(*os.File); ok { s.file = f @@ -225,17 +225,17 @@ func newSinkInput(r io.Reader) *sink { return s } -func newSinkOutput(w io.Writer) *sink { - s := &sink{ +func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink { + s := &Sink{ writer: bufio.NewWriter(w), autoFlush: true, } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) return s } -func sinkArg(c *rt.GoCont, arg int) (*sink, error) { +func sinkArg(c *rt.GoCont, arg int) (*Sink, error) { s, ok := valueToSink(c.Arg(arg)) if !ok { return nil, fmt.Errorf("#%d must be a sink", arg + 1) @@ -244,17 +244,17 @@ func sinkArg(c *rt.GoCont, arg int) (*sink, error) { return s, nil } -func valueToSink(val rt.Value) (*sink, bool) { +func valueToSink(val rt.Value) (*Sink, bool) { u, ok := val.TryUserData() if !ok { return nil, false } - s, ok := u.Value().(*sink) + s, ok := u.Value().(*Sink) return s, ok } -func sinkUserData(s *sink) *rt.UserData { - sinkMeta := l.Registry(sinkMetaKey) +func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData { + sinkMeta := rtm.Registry(sinkMetaKey) return rt.NewUserData(s, sinkMeta.AsTable()) } diff --git a/util/streams.go b/util/streams.go new file mode 100644 index 0000000..11f9308 --- /dev/null +++ b/util/streams.go @@ -0,0 +1,11 @@ +package util + +import ( + "io" +) + +type Streams struct { + Stdout io.Writer + Stderr io.Writer + Stdin io.Reader +} diff --git a/util/util.go b/util/util.go index e60f66e..b32d865 100644 --- a/util/util.go +++ b/util/util.go @@ -2,14 +2,78 @@ package util import ( "bufio" + "context" + "errors" + "fmt" "io" + "path/filepath" "strings" "os" + "os/exec" "os/user" + "runtime" + "syscall" rt "github.com/arnodel/golua/runtime" ) +var ErrNotExec = errors.New("not executable") +var ErrNotFound = errors.New("not found") + +type ExecError struct{ + Typ string + Cmd string + Code int + Colon bool + Err error +} + +func (e ExecError) Error() string { + return fmt.Sprintf("%s: %s", e.Cmd, e.Typ) +} + +func (e ExecError) sprint() error { + sep := " " + if e.Colon { + sep = ": " + } + + return fmt.Errorf("hilbish: %s%s%s", e.Cmd, sep, e.Err.Error()) +} + +func IsExecError(err error) (ExecError, bool) { + if exErr, ok := err.(ExecError); ok { + return exErr, true + } + + fields := strings.Split(err.Error(), ": ") + knownTypes := []string{ + "not-found", + "not-executable", + } + + if len(fields) > 1 && Contains(knownTypes, fields[1]) { + var colon bool + var e error + switch fields[1] { + case "not-found": + e = ErrNotFound + case "not-executable": + colon = true + e = ErrNotExec + } + + return ExecError{ + Cmd: fields[0], + Typ: fields[1], + Colon: colon, + Err: e, + }, true + } + + return ExecError{}, false +} + // SetField sets a field in a table, adding docs for it. // It is accessible via the __docProp metatable. It is a table of the names of the fields. func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) { @@ -36,6 +100,15 @@ func DoString(rtm *rt.Runtime, code string) (rt.Value, error) { return ret, err } +func MustDoString(rtm *rt.Runtime, code string) rt.Value { + val, err := DoString(rtm, code) + if err != nil { + panic(err) + } + + return val +} + // DoFile runs the contents of the file in the Lua runtime. func DoFile(rtm *rt.Runtime, path string) error { f, err := os.Open(path) @@ -151,13 +224,13 @@ func LookPath(file string) (string, error) { // custom lookpath function so we k } for _, s := range skip { if strings.HasPrefix(file, s) { - return file, findExecutable(file, false, false) + return file, FindExecutable(file, false, false) } } for _, dir := range filepath.SplitList(os.Getenv("PATH")) { path := filepath.Join(dir, file) - err := findExecutable(path, true, false) - if err == errNotExec { + err := FindExecutable(path, true, false) + if err == ErrNotExec { return "", err } else if err == nil { return path, nil @@ -166,3 +239,42 @@ func LookPath(file string) (string, error) { // custom lookpath function so we k return "", os.ErrNotExist } + +func Contains(s []string, e string) bool { + for _, a := range s { + if strings.ToLower(a) == strings.ToLower(e) { + return true + } + } + return false +} + +func HandleExecErr(err error) (exit uint8) { + ctx := context.TODO() + + switch x := err.(type) { + case *exec.ExitError: + // started, but errored - default to 1 if OS + // doesn't have exit statuses + if status, ok := x.Sys().(syscall.WaitStatus); ok { + if status.Signaled() { + if ctx.Err() != nil { + return + } + exit = uint8(128 + status.Signal()) + return + } + exit = uint8(status.ExitStatus()) + return + } + exit = 1 + return + case *exec.Error: + // did not start + //fmt.Fprintf(hc.Stderr, "%v\n", err) + exit = 127 + default: return + } + + return +} diff --git a/util/util_unix.go b/util/util_unix.go index 9fa6a6c..92813c8 100644 --- a/util/util_unix.go +++ b/util/util_unix.go @@ -20,5 +20,5 @@ func FindExecutable(path string, inPath, dirs bool) error { return nil } } - return errNotExec + return ErrNotExec }