mirror of https://github.com/Hilbis/Hilbish
560 lines
13 KiB
Go
560 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"os/exec"
|
|
"fmt"
|
|
"io"
|
|
"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 errNotFound = errors.New("not found")
|
|
var runnerMode rt.Value = rt.StringValue("hybrid")
|
|
|
|
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)
|
|
|
|
rerun:
|
|
var exitCode uint8
|
|
var err error
|
|
var cont bool
|
|
// save incase it changes while prompting (For some reason)
|
|
currentRunner := runnerMode
|
|
if currentRunner.Type() == rt.StringType {
|
|
switch currentRunner.AsString() {
|
|
case "hybrid":
|
|
_, _, err = handleLua(cmdString)
|
|
if err == nil {
|
|
cmdFinish(0, input, priv)
|
|
return
|
|
}
|
|
input, exitCode, cont, err = handleSh(cmdString)
|
|
case "hybridRev":
|
|
_, _, _, err = handleSh(input)
|
|
if err == nil {
|
|
cmdFinish(0, input, priv)
|
|
return
|
|
}
|
|
input, exitCode, err = handleLua(cmdString)
|
|
case "lua":
|
|
input, exitCode, err = handleLua(cmdString)
|
|
case "sh":
|
|
input, exitCode, cont, err = handleSh(cmdString)
|
|
}
|
|
} else {
|
|
// can only be a string or function so
|
|
var runnerErr error
|
|
input, exitCode, cont, 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
|
|
}
|
|
|
|
if cont {
|
|
input, err = reprompt(input)
|
|
if err == nil {
|
|
goto rerun
|
|
} else if err == io.EOF {
|
|
return
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
if exErr, ok := isExecError(err); ok {
|
|
hooks.Emit("command." + exErr.typ, exErr.cmd)
|
|
err = exErr.sprint()
|
|
}
|
|
fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
cmdFinish(exitCode, input, priv)
|
|
}
|
|
|
|
func reprompt(input string) (string, error) {
|
|
for {
|
|
in, err := continuePrompt(strings.TrimSuffix(input, "\\"))
|
|
if err != nil {
|
|
lr.SetPrompt(fmtPrompt(prompt))
|
|
return input, err
|
|
}
|
|
|
|
if strings.HasSuffix(in, "\\") {
|
|
continue
|
|
}
|
|
return in, nil
|
|
}
|
|
}
|
|
|
|
func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, runnerErr, err error) {
|
|
term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 3, false)
|
|
err = rt.Call(l.MainThread(), runr, []rt.Value{rt.StringValue(userInput)}, term)
|
|
if err != nil {
|
|
return "", 124, false, nil, err
|
|
}
|
|
|
|
var runner *rt.Table
|
|
var ok bool
|
|
runnerRet := term.Get(0)
|
|
if runner, ok = runnerRet.TryTable(); !ok {
|
|
fmt.Fprintln(os.Stderr, "runner did not return a table")
|
|
}
|
|
|
|
if code, ok := runner.Get(rt.StringValue("exitCode")).TryInt(); ok {
|
|
exitCode = uint8(code)
|
|
}
|
|
|
|
if inp, ok := runner.Get(rt.StringValue("input")).TryString(); ok {
|
|
input = inp
|
|
}
|
|
|
|
if errStr, ok := runner.Get(rt.StringValue("err")).TryString(); ok {
|
|
runnerErr = fmt.Errorf("%s", errStr)
|
|
}
|
|
|
|
if c, ok := runner.Get(rt.StringValue("continue")).TryBool(); ok {
|
|
continued = c
|
|
}
|
|
return
|
|
}
|
|
|
|
func handleLua(cmdString string) (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 cmdString, 125, err
|
|
}
|
|
// And if there's no syntax errors and -n isnt provided, run
|
|
if !noexecute {
|
|
if chunk != nil {
|
|
_, err = rt.Call1(l.MainThread(), rt.FunctionValue(chunk))
|
|
}
|
|
}
|
|
if err == nil {
|
|
return cmdString, 0, nil
|
|
}
|
|
|
|
return cmdString, 125, err
|
|
}
|
|
|
|
func handleSh(cmdString string) (input string, exitCode uint8, cont bool, runErr error) {
|
|
shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh"))
|
|
var err error
|
|
input, exitCode, cont, runErr, err = runLuaRunner(shRunner, cmdString)
|
|
if err != nil {
|
|
runErr = err
|
|
}
|
|
return
|
|
}
|
|
|
|
func execSh(cmdString string) (string, uint8, bool, error) {
|
|
_, _, err := execCommand(cmdString, true)
|
|
if err != nil {
|
|
// If input is incomplete, start multiline prompting
|
|
if syntax.IsIncomplete(err) {
|
|
if !interactive {
|
|
return cmdString, 126, false, err
|
|
}
|
|
return cmdString, 126, true, err
|
|
} else {
|
|
if code, ok := interp.IsExitStatus(err); ok {
|
|
return cmdString, code, false, nil
|
|
} else {
|
|
return cmdString, 126, false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return cmdString, 0, false, nil
|
|
}
|
|
|
|
// Run command in sh interpreter
|
|
func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) {
|
|
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
runner, _ := interp.New()
|
|
|
|
var stdout io.Writer
|
|
var stderr io.Writer
|
|
if terminalOut {
|
|
interp.StdIO(os.Stdin, os.Stdout, os.Stderr)(runner)
|
|
} else {
|
|
stdout = new(bytes.Buffer)
|
|
stderr = new(bytes.Buffer)
|
|
interp.StdIO(os.Stdin, stdout, stderr)(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 stdout, stderr, err
|
|
}
|
|
}
|
|
|
|
return stdout, 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))
|
|
}
|
|
|
|
if commands[args[0]] != nil {
|
|
luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs))
|
|
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(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)
|
|
}
|
|
|
|
err := 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
|
|
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,
|
|
}
|
|
|
|
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 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) {
|
|
util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)), "Exit code of last exected command")
|
|
// using AsValue (to convert to lua type) on an interface which is an int
|
|
// results in it being unknown in lua .... ????
|
|
// so we allow the hook handler to take lua runtime Values
|
|
hooks.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private)
|
|
}
|