feat: add configurable runner mode (closes #110)

pull/128/head
TorchedSammy 2022-03-20 15:15:44 -04:00
parent 96c1487bfa
commit 86a15e6363
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
4 changed files with 196 additions and 21 deletions

33
api.go
View File

@ -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
}

View File

@ -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.

105
exec.go
View File

@ -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)
}

46
runnermode.go 100644
View File

@ -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
}