2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-06-30 16:22:03 +00:00
Hilbish/exec.go
TorchedSammy 016a3a2ec7
refactor: start work on lua 5.4
lots of commented out code

ive found a go lua library which implements lua 5.4
and found an opportunity to start working on it.
this commit basically removes everything and just leaves
enough for the shell to be "usable" and able to start.
there are no builtins or libraries (besides the `hilbish` global)
2022-03-27 22:17:59 -04:00

450 lines
10 KiB
Go

package main
import (
"bytes"
"context"
"errors"
"os/exec"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
// "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"
)
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()*/ "hybrid" {
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
chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv()))
if err != nil && noexecute {
fmt.Println(err)
/* if lerr, ok := err.(*lua.ApiError); ok {
if perr, ok := lerr.Cause.(*parse.Error); ok {
print(perr.Pos.Line == parse.EOF)
}
}
*/
return 125, err
}
// And if there's no syntax errors and -n isnt provided, run
if !noexecute {
_, err = rt.Call1(l.MainThread(), rt.FunctionValue(chunk))
}
if err == nil {
return 0, nil
}
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) {
for {
cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\"))
if err != nil {
break
}
err = execCommand(cmdString)
if syntax.IsIncomplete(err) || strings.HasSuffix(cmdString, "\\") {
continue
} else if code, ok := interp.IsExitStatus(err); ok {
return code, nil
} else if err != nil {
return 126, err
} else {
return 0, nil
}
}
} else {
if code, ok := interp.IsExitStatus(err); ok {
return code, nil
} else {
return 126, err
}
}
}
return 0, nil
}
// Run command in sh interpreter
func execCommand(cmd string) error {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return err
}
var bg bool
exechandle := 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)
args, _ = shell.Fields(argstring, nil)
}
// If command is defined in Lua then run it
/*
luacmdArgs := l.NewTable()
for _, str := range args[1:] {
luacmdArgs.Append(lua.LString(str))
}
if commands[args[0]] != nil {
err := l.CallByParam(lua.P{
Fn: commands[args[0]],
NRet: 1,
Protect: true,
}, luacmdArgs)
if err != nil {
fmt.Fprintln(os.Stderr,
"Error in command:\n\n" + err.Error())
return interp.NewExitStatus(1)
}
luaexitcode := l.Get(-1)
var exitcode uint8
l.Pop(1)
if code, ok := luaexitcode.(lua.LNumber); luaexitcode != lua.LNil && ok {
exitcode = uint8(code)
}
return interp.NewExitStatus(exitcode)
}
*/
err := lookpath(args[0])
if err == errNotExec {
hooks.Em.Emit("command.no-perm", args[0])
hooks.Em.Emit("command.not-executable", args[0])
return interp.NewExitStatus(126)
} else if err != nil {
hooks.Em.Emit("command.not-found", args[0])
return interp.NewExitStatus(127)
}
killTimeout := 2 * time.Second
// from here is basically copy-paste of the default exec handler from
// sh/interp but with our job handling
hc := interp.HandlerCtx(ctx)
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
if err != nil {
fmt.Fprintln(hc.Stderr, err)
return interp.NewExitStatus(127)
}
env := hc.Env
envList := make([]string, 0, 64)
env.Each(func(name string, vr expand.Variable) bool {
if !vr.IsSet() {
// If a variable is set globally but unset in the
// runner, we need to ensure it's not part of the final
// list. Seems like zeroing the element is enough.
// This is a linear search, but this scenario should be
// rare, and the number of variables shouldn't be large.
for i, kv := range envList {
if strings.HasPrefix(kv, name+"=") {
envList[i] = ""
}
}
}
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,
}
err = cmd.Start()
var j *job
if bg {
j = jobs.getLatest()
j.setHandle(cmd.Process)
}
if err == nil {
if bg {
j.start(cmd.Process.Pid)
}
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()
}
var exit uint8
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 ctx.Err()
}
exit = uint8(128 + status.Signal())
goto end
}
exit = uint8(status.ExitStatus())
goto end
}
exit = 1
goto end
case *exec.Error:
// did not start
fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
goto end
case nil:
goto end
default:
return err
}
end:
if bg {
j.exitCode = int(exit)
j.finish()
}
return interp.NewExitStatus(exit)
}
runner, _ := interp.New(
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(exechandle),
)
buf := new(bytes.Buffer)
printer := syntax.NewPrinter()
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)
}
err = runner.Run(context.TODO(), stmt)
if err != nil {
return err
}
}
return nil
}
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
var skip []string
if runtime.GOOS == "windows" {
skip = []string{"./", "../", "~/", "C:"}
} else {
skip = []string{"./", "/", "../", "~/"}
}
for _, s := range skip {
if strings.HasPrefix(file, s) {
return 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 {
return err
} else if err == nil {
return nil
}
}
return os.ErrNotExist
}
func splitInput(input string) ([]string, string) {
// end my suffering
// TODO: refactor this garbage
quoted := false
startlastcmd := false
lastcmddone := false
cmdArgs := []string{}
sb := &strings.Builder{}
cmdstr := &strings.Builder{}
lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
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 if !quoted && r == '^' && startlastcmd && !lastcmddone {
// if ^ is found, isnt in quotes and is
// the second occurence of the character and is
// the first time "^^" has been used
cmdstr.WriteString(lastcmd)
sb.WriteString(lastcmd)
startlastcmd = !startlastcmd
lastcmddone = !lastcmddone
continue
} else if !quoted && r == '^' && !lastcmddone {
// if ^ is found, isnt in quotes and is the
// first time of starting "^^"
startlastcmd = !startlastcmd
continue
} else {
sb.WriteRune(r)
}
cmdstr.WriteRune(r)
}
if sb.Len() > 0 {
cmdArgs = append(cmdArgs, sb.String())
}
return cmdArgs, cmdstr.String()
}
func cmdFinish(code uint8, cmdstr string, private bool) {
// if input has space at the beginning, dont put in history
if interactive && !private {
handleHistory(cmdstr)
}
// util.SetField(l, hshMod, "exitCode", lua.LNumber(code), "Exit code of last exected command")
hooks.Em.Emit("command.exit", code, cmdstr)
}