mirror of
https://github.com/Hilbis/Hilbish
synced 2025-06-23 12:52:03 +00:00
major rewrite which changes the library hilbish uses for it's lua vm this one implements lua 5.4, and since that's a major version bump, it's a breaking change. introduced here also is a fix for `hilbish.login` not being the right value * 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) * fix: call cont next in prompt function this continues execution of lua, very obvious fixes an issue with code stopping at the prompt function * fix: handle errors in user config * fix: handle panic in lua input if it is incorrect * feat: implement bait * refactor: use util funcs to run lua where possible * refactor: move arg handle function to util * feat: implement commander * feat: implement fs * feat: add hilbish module functions used by prelude * chore: use custom fork of golua * fix: make sure args to setenv are strings in prelude * feat: implement completions * chore: remove comment * feat: implement terminal * feat: implement hilbish.interval * chore: update lunacolors * chore: update golua * feat: implement aliases * feat: add input mode * feat: implement runner mode * style: use comma separated cases instead of fallthrough * feat: implement syntax highlight and hints * chore: add comments to document util functions * chore: fix dofile comment doc * refactor: make loader functions for go modules unexported * feat: implement job management * feat: add hilbish properties * feat: implement all hilbish module functions * feat: implement history interface * feat: add completion interface * feat: add module description docs * feat: implement os interface * refactor: use hlalias for add function in hilbish.alias interface * feat: make it so hilbish.run can return command output * fix: set hilbish.exitCode to last command exit code * fix(ansikit): flush on io.write * fix: deregister commander if return isnt number * feat: run script when provided path * fix: read file manually in DoFile to avoid shebang * chore: add comment for reason of unreading byte * fix: remove prelude error printing * fix: add names at chunk load for context in errors * fix: add newline at the beginning of file buffer when there is shebang this makes the line count in error messages line up properly * fix: remove extra newline after error
449 lines
11 KiB
Go
449 lines
11 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 runnerMode rt.Value = rt.StringValue("hybrid")
|
|
|
|
func runInput(input string, priv bool) {
|
|
running = true
|
|
cmdString := aliases.Resolve(input)
|
|
hooks.Em.Emit("command.preexec", input, cmdString)
|
|
|
|
if runnerMode.Type() == rt.StringType {
|
|
switch runnerMode.AsString() {
|
|
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
|
|
term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 2, false)
|
|
err := rt.Call(l.MainThread(), runnerMode, []rt.Value{rt.StringValue(cmdString)}, term)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
cmdFinish(124, cmdString, priv)
|
|
return
|
|
}
|
|
|
|
luaexitcode := term.Get(0) // first return value (makes sense right i love stacks)
|
|
runErr := term.Get(1)
|
|
|
|
var exitCode uint8
|
|
if code, ok := luaexitcode.TryInt(); ok {
|
|
exitCode = uint8(code)
|
|
}
|
|
|
|
if runErr != rt.NilValue {
|
|
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 {
|
|
if chunk != nil {
|
|
_, 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, true)
|
|
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, true)
|
|
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, terminalOut bool) (io.Writer, io.Writer, error) {
|
|
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var stdout io.Writer = os.Stdout
|
|
var stderr io.Writer = os.Stderr
|
|
if !terminalOut {
|
|
stdout = new(bytes.Buffer)
|
|
stderr = new(bytes.Buffer)
|
|
}
|
|
|
|
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 := 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 {
|
|
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, stdout, 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 stdout, stderr, err
|
|
}
|
|
}
|
|
|
|
return stdout, stderr, 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", 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.Em.Emit("command.exit", rt.IntValue(int64(code)), cmdstr)
|
|
}
|