diff --git a/api.go b/api.go index ef1e51c..54fbca4 100644 --- a/api.go +++ b/api.go @@ -26,6 +26,7 @@ var exports = map[string]lua.LGFunction { "complete": hlcomplete, "cwd": hlcwd, "exec": hlexec, + "runnerMode": hlrunnerMode, "goro": hlgoro, "multiprompt": hlmlprompt, "prependPath": hlprependPath, @@ -106,6 +107,10 @@ Check out the {blue}{bold}guide{reset} command to get started. util.Document(L, hshcomp, "Completions interface for Hilbish.") L.SetField(mod, "completion", hshcomp) + runnerModule := runnerModeLoader(L) + util.Document(L, runnerModule, "Runner/exec interface for Hilbish.") + L.SetField(mod, "runner", runnerModule) + jobs = newJobHandler() L.Push(mod) @@ -172,7 +177,7 @@ func unsetVimMode() { func hlrun(L *lua.LState) int { var exitcode uint8 cmd := L.CheckString(1) - err := execCommand(cmd, true) + err := execCommand(cmd) if code, ok := interp.IsExitStatus(err); ok { exitcode = code @@ -466,3 +471,29 @@ func hlinputMode(L *lua.LState) int { } return 0 } + +// runnerMode(mode) +// 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. +// Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +// sh, and lua. It also accepts a function, to which if it is passed one +// will call it to execute user input instead. +func hlrunnerMode(L *lua.LState) int { + mode := L.CheckAny(1) + switch mode.Type() { + case lua.LTString: + switch mode.String() { + // no fallthrough doesnt work so eh + case "hybrid": fallthrough + case "hybridRev": fallthrough + case "lua": fallthrough + case "sh": + runnerMode = mode + default: L.RaiseError("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received %v", mode) + } + case lua.LTFunction: runnerMode = mode + default: L.RaiseError("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received %v", mode) + } + + return 0 +} diff --git a/docs/runner-mode.txt b/docs/runner-mode.txt new file mode 100644 index 0000000..69d0e17 --- /dev/null +++ b/docs/runner-mode.txt @@ -0,0 +1,33 @@ +Hilbish is *unique,* when interactive it first attempts to run input as +Lua and then tries shell script. But if you're normal, you wouldn't +really be using Hilbish anyway but you'd also not want this +(or maybe want Lua only in some cases.) + +The "runner mode" of Hilbish is customizable via `hilbish.runnerMode`, +which determines how Hilbish will run user input. By default, this is +set to `hybrid` which is the previously mentioned behaviour of running Lua +first then going to shell script. If you want the reverse order, you can +set it to `hybridRev` and for isolated modes there is `sh` and `lua` +respectively. + +You can also set it to a function, which will be called everytime Hilbish +needs to run interactive input. For example, you can set this to a simple +function to compile and evaluate Fennel, and now you can run Fennel. +You can even mix it with sh to make a hybrid mode with Lua replaced by +Fennel. + +An example: +hilbish.runnerMode(function(input) + local ok = pcall(fennel.eval, input) + if ok then + return 0, nil + end + + return hilbish.runner.sh(input) +end) + +The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode` +and also provides the sh and Lua runner functions that Hilbish itself uses. +A runner function is expected to return 2 values: the exit code, and an error. +The exit code has to be a number, it will be 0 otherwise and the error can be +`nil` to indicate no error. diff --git a/exec.go b/exec.go index feda0e4..0a0ab74 100644 --- a/exec.go +++ b/exec.go @@ -24,13 +24,80 @@ import ( ) var errNotExec = errors.New("not executable") +var runnerMode lua.LValue = lua.LString("hybrid") func runInput(input string, priv bool) { running = true cmdString := aliases.Resolve(input) - hooks.Em.Emit("command.preexec", input, cmdString) + if runnerMode.Type() == lua.LTString { + switch runnerMode.String() { + case "hybrid": + _, err := handleLua(cmdString) + if err == nil { + cmdFinish(0, cmdString, priv) + return + } + exitCode, err := handleSh(cmdString) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + cmdFinish(exitCode, cmdString, priv) + case "hybridRev": + _, err := handleSh(cmdString) + if err == nil { + cmdFinish(0, cmdString, priv) + return + } + exitCode, err := handleLua(cmdString) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + cmdFinish(exitCode, cmdString, priv) + case "lua": + exitCode, err := handleLua(cmdString) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + cmdFinish(exitCode, cmdString, priv) + case "sh": + exitCode, err := handleSh(cmdString) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + cmdFinish(exitCode, cmdString, priv) + } + } else { + // can only be a string or function so + err := l.CallByParam(lua.P{ + Fn: runnerMode, + NRet: 2, + Protect: true, + }, lua.LString(cmdString)) + if err != nil { + fmt.Fprintln(os.Stderr, err) + cmdFinish(124, cmdString, priv) + return + } + + luaexitcode := l.Get(-2) // first return value (makes sense right i love stacks) + runErr := l.Get(-1) + l.Pop(2) + + var exitCode uint8 + if code, ok := luaexitcode.(lua.LNumber); luaexitcode != lua.LNil && ok { + exitCode = uint8(code) + } + + if runErr != lua.LNil { + fmt.Fprintln(os.Stderr, runErr) + } + cmdFinish(exitCode, cmdString, priv) + } +} + +func handleLua(cmdString string) (uint8, error) { // First try to load input, essentially compiling to bytecode fn, err := l.LoadString(cmdString) if err != nil && noexecute { @@ -41,7 +108,7 @@ func runInput(input string, priv bool) { } } */ - return + return 125, err } // And if there's no syntax errors and -n isnt provided, run if !noexecute { @@ -49,12 +116,14 @@ func runInput(input string, priv bool) { err = l.PCall(0, lua.MultRet, nil) } if err == nil { - cmdFinish(0, cmdString, priv) - return + return 0, nil } - // Last option: use sh interpreter - err = execCommand(cmdString, priv) + return 125, err +} + +func handleSh(cmdString string) (uint8, error) { + err := execCommand(cmdString) if err != nil { // If input is incomplete, start multiline prompting if syntax.IsIncomplete(err) { @@ -63,34 +132,31 @@ func runInput(input string, priv bool) { if err != nil { break } - err = execCommand(cmdString, priv) - if syntax.IsIncomplete(err) || strings.HasSuffix(input, "\\") { + err = execCommand(cmdString) + if syntax.IsIncomplete(err) || strings.HasSuffix(cmdString, "\\") { continue } else if code, ok := interp.IsExitStatus(err); ok { - cmdFinish(code, cmdString, priv) + return code, nil } else if err != nil { - cmdFinish(1, cmdString, priv) - fmt.Fprintln(os.Stderr, err) + return 126, err } else { - cmdFinish(0, cmdString, priv) + return 0, nil } - break } } else { if code, ok := interp.IsExitStatus(err); ok { - cmdFinish(code, cmdString, priv) + return code, nil } else { - cmdFinish(126, cmdString, priv) - fmt.Fprintln(os.Stderr, err) + return 126, err } } - } else { - cmdFinish(0, cmdString, priv) } + + return 0, nil } // Run command in sh interpreter -func execCommand(cmd string, priv bool) error { +func execCommand(cmd string) error { file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") if err != nil { return err @@ -141,7 +207,6 @@ func execCommand(cmd string, priv bool) error { exitcode = uint8(code) } - cmdFinish(exitcode, argstring, priv) return interp.NewExitStatus(exitcode) } diff --git a/runnermode.go b/runnermode.go new file mode 100644 index 0000000..6d632ec --- /dev/null +++ b/runnermode.go @@ -0,0 +1,46 @@ +package main + +import ( + "github.com/yuin/gopher-lua" +) + +func runnerModeLoader(L *lua.LState) *lua.LTable { + exports := map[string]lua.LGFunction{ + "sh": shRunner, + "lua": luaRunner, + "setMode": hlrunnerMode, + } + + mod := L.SetFuncs(L.NewTable(), exports) + L.SetField(mod, "mode", runnerMode) + + return mod +} + +func shRunner(L *lua.LState) int { + cmd := L.CheckString(1) + exitCode, err := handleSh(cmd) + var luaErr lua.LValue = lua.LNil + if err != nil { + luaErr = lua.LString(err.Error()) + } + + L.Push(lua.LNumber(exitCode)) + L.Push(luaErr) + + return 2 +} + +func luaRunner(L *lua.LState) int { + cmd := L.CheckString(1) + exitCode, err := handleLua(cmd) + var luaErr lua.LValue = lua.LNil + if err != nil { + luaErr = lua.LString(err.Error()) + } + + L.Push(lua.LNumber(exitCode)) + L.Push(luaErr) + + return 2 +}