From 0b0821f410ebc9eda9c71a052a570ea8c55b3185 Mon Sep 17 00:00:00 2001 From: sammyette Date: Sat, 28 Dec 2024 21:43:33 -0400 Subject: [PATCH 01/12] refactor: make initial changes for snail lib (shell interp) --- api.go | 4 +- exec.go | 45 +-------------------- golibs/fs/fs.go | 2 - golibs/terminal/snail.go | 0 job.go | 4 +- job_unix.go | 4 ++ job_windows.go | 5 +++ main.go | 2 - nature/commands/cd.lua | 8 ++-- nature/dirs.lua | 6 +++ util/util.go | 25 ++++++++++++ execfile_unix.go => util/util_unix.go | 9 +---- execfile_windows.go => util/util_windows.go | 9 +---- 13 files changed, 52 insertions(+), 71 deletions(-) create mode 100644 golibs/terminal/snail.go rename execfile_unix.go => util/util_unix.go (61%) rename execfile_windows.go => util/util_windows.go (74%) diff --git a/api.go b/api.go index 43e361a..8c2e6e4 100644 --- a/api.go +++ b/api.go @@ -508,7 +508,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } cmdArgs, _ := splitInput(cmd) if runtime.GOOS != "windows" { - cmdPath, err := exec.LookPath(cmdArgs[0]) + cmdPath, err := util.LookPath(cmdArgs[0]) if err != nil { fmt.Println(err) // if we get here, cmdPath will be nothing @@ -706,7 +706,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } - path, err := exec.LookPath(cmd) + path, err := util.LookPath(cmd) if err != nil { return c.Next(), nil } diff --git a/exec.go b/exec.go index 7f8e37b..01a5dfa 100644 --- a/exec.go +++ b/exec.go @@ -411,7 +411,7 @@ func execHandle(bg bool) interp.ExecHandlerFunc { return interp.NewExitStatus(exitcode) } - path, err := lookpath(args[0]) + path, err := util.LookPath(args[0]) if err == errNotExec { return execError{ typ: "not-executable", @@ -524,41 +524,14 @@ func handleExecErr(err error) (exit uint8) { return } -func lookpath(file string) (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 file, 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 path, 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 == '"' { @@ -574,22 +547,6 @@ func splitInput(input string) ([]string, string) { // 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) } diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index 002be90..1c0589e 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -110,12 +110,10 @@ func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } path = util.ExpandHome(strings.TrimSpace(path)) - abspath, _ := filepath.Abs(path) err = os.Chdir(path) if err != nil { return nil, err } - interp.Dir(abspath)(f.runner) return c.Next(), err } diff --git a/golibs/terminal/snail.go b/golibs/terminal/snail.go new file mode 100644 index 0000000..e69de29 diff --git a/job.go b/job.go index f5bd6f2..bb16e92 100644 --- a/job.go +++ b/job.go @@ -56,8 +56,8 @@ func (j *job) start() error { } j.setHandle(&cmd) } - // bgProcAttr is defined in execfile_.go, it holds a procattr struct - // in a simple explanation, it makes signals from hilbish (sigint) + // bgProcAttr is defined in job_.go, it holds a procattr struct + // in a simple explanation, it makes signals from hilbish (like sigint) // not go to it (child process) j.handle.SysProcAttr = bgProcAttr // reset output buffers diff --git a/job_unix.go b/job_unix.go index 0a038b1..2caa4ae 100644 --- a/job_unix.go +++ b/job_unix.go @@ -10,6 +10,10 @@ import ( "golang.org/x/sys/unix" ) +var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, +} + func (j *job) foreground() error { if jobs.foreground { return errors.New("(another) job already foregrounded") diff --git a/job_windows.go b/job_windows.go index 26818b5..1ac4646 100644 --- a/job_windows.go +++ b/job_windows.go @@ -4,8 +4,13 @@ package main import ( "errors" + "syscall" ) +var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, +} + func (j *job) foreground() error { return errors.New("not supported on windows") } diff --git a/main.go b/main.go index 1bddfc4..af7a22a 100644 --- a/main.go +++ b/main.go @@ -38,11 +38,9 @@ var ( cmds *commander.Commander defaultConfPath string defaultHistPath string - runner *interp.Runner ) func main() { - runner, _ = interp.New() curuser, _ = user.Current() homedir := curuser.HomeDir confDir, _ = os.UserConfigDir() diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua index 7cfe4a2..0efbb3f 100644 --- a/nature/commands/cd.lua +++ b/nature/commands/cd.lua @@ -3,8 +3,9 @@ local commander = require 'commander' local fs = require 'fs' local dirs = require 'nature.dirs' -dirs.old = hilbish.cwd() commander.register('cd', function (args, sinks) + local oldPath = hilbish.cwd() + if #args > 1 then sinks.out:writeln("cd: too many arguments") return 1 @@ -16,13 +17,10 @@ commander.register('cd', function (args, sinks) sinks.out:writeln(path) end - dirs.setOld(hilbish.cwd()) - dirs.push(path) - local ok, err = pcall(function() fs.cd(path) end) if not ok then sinks.out:writeln(err) return 1 end - bait.throw('cd', path) + bait.throw('hilbish.cd', fs.abs(path), oldPath) end) diff --git a/nature/dirs.lua b/nature/dirs.lua index 328b4b7..2efad63 100644 --- a/nature/dirs.lua +++ b/nature/dirs.lua @@ -1,4 +1,5 @@ -- @module dirs +local bait = require 'bait' local fs = require 'fs' local dirs = {} @@ -73,4 +74,9 @@ function dirs.setOld(d) dirs.old = d end +bait.catch('hilbish.cd', function(path, oldPath) + dirs.setOld(oldPath) + dirs.push(path) +end) + return dirs diff --git a/util/util.go b/util/util.go index 0fcd4b0..e60f66e 100644 --- a/util/util.go +++ b/util/util.go @@ -141,3 +141,28 @@ func AbbrevHome(path string) string { return path } + +func LookPath(file string) (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 file, 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 path, nil + } + } + + return "", os.ErrNotExist +} diff --git a/execfile_unix.go b/util/util_unix.go similarity index 61% rename from execfile_unix.go rename to util/util_unix.go index 82c738b..9fa6a6c 100644 --- a/execfile_unix.go +++ b/util/util_unix.go @@ -1,17 +1,12 @@ //go:build unix -package main +package util import ( "os" - "syscall" ) -var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, -} - -func findExecutable(path string, inPath, dirs bool) error { +func FindExecutable(path string, inPath, dirs bool) error { f, err := os.Stat(path) if err != nil { return err diff --git a/execfile_windows.go b/util/util_windows.go similarity index 74% rename from execfile_windows.go rename to util/util_windows.go index 3d6ef61..4eb50c8 100644 --- a/execfile_windows.go +++ b/util/util_windows.go @@ -1,18 +1,13 @@ //go:build windows -package main +package util import ( "path/filepath" "os" - "syscall" ) -var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ - CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, -} - -func findExecutable(path string, inPath, dirs bool) error { +func FindExecutable(path string, inPath, dirs bool) error { nameExt := filepath.Ext(path) pathExts := filepath.SplitList(os.Getenv("PATHEXT")) if inPath { From 571fcc3e9ee5f40a5648a9b4d00dad2f863ac85a Mon Sep 17 00:00:00 2001 From: sammyette Date: Sat, 28 Dec 2024 22:59:41 -0400 Subject: [PATCH 02/12] fix: remove sh runner dep from fs --- golibs/fs/fs.go | 61 +++++++++++++++++++------------------------------ lua.go | 3 +-- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index 1c0589e..9e03325 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -19,38 +19,25 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" "github.com/arnodel/golua/lib/iolib" - "mvdan.cc/sh/v3/interp" ) -type fs struct{ - runner *interp.Runner - Loader packagelib.Loader +var Loader = packagelib.Loader{ + Load: loaderFunc, + Name: "fs", } -func New(runner *interp.Runner) *fs { - f := &fs{ - runner: runner, - } - f.Loader = packagelib.Loader{ - Load: f.loaderFunc, - Name: "fs", - } - - return f -} - -func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { +func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { exports := map[string]util.LuaExport{ - "cd": util.LuaExport{f.fcd, 1, false}, - "mkdir": util.LuaExport{f.fmkdir, 2, false}, - "stat": util.LuaExport{f.fstat, 1, false}, - "readdir": util.LuaExport{f.freaddir, 1, false}, - "abs": util.LuaExport{f.fabs, 1, false}, - "basename": util.LuaExport{f.fbasename, 1, false}, - "dir": util.LuaExport{f.fdir, 1, false}, - "glob": util.LuaExport{f.fglob, 1, false}, - "join": util.LuaExport{f.fjoin, 0, true}, - "pipe": util.LuaExport{f.fpipe, 0, false}, + "cd": util.LuaExport{fcd, 1, false}, + "mkdir": util.LuaExport{fmkdir, 2, false}, + "stat": util.LuaExport{fstat, 1, false}, + "readdir": util.LuaExport{freaddir, 1, false}, + "abs": util.LuaExport{fabs, 1, false}, + "basename": util.LuaExport{fbasename, 1, false}, + "dir": util.LuaExport{fdir, 1, false}, + "glob": util.LuaExport{fglob, 1, false}, + "join": util.LuaExport{fjoin, 0, true}, + "pipe": util.LuaExport{fpipe, 0, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) @@ -65,7 +52,7 @@ func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { // This can be used to resolve short paths like `..` to `/home/user`. // #param path string // #returns string -func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { path, err := c.StringArg(0) if err != nil { return nil, err @@ -85,7 +72,7 @@ func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // `.` will be returned. // #param path string Path to get the base name of. // #returns string -func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -100,7 +87,7 @@ func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // cd(dir) // Changes Hilbish's directory to `dir`. // #param dir string Path to change directory to. -func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -123,7 +110,7 @@ func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // `~/Documents/doc.txt` then this function will return `~/Documents`. // #param path string Path to get the directory for. // #returns string -func (f *fs) fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -154,7 +141,7 @@ print(matches) -- -> {'init.lua', 'code.lua'} #example */ -func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -188,7 +175,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish')) -- -> '/home/user/.config/hilbish' on Linux #example */ -func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { strs := make([]string, len(c.Etc())) for i, v := range c.Etc() { if v.Type() != rt.StringType { @@ -215,7 +202,7 @@ func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { fs.mkdir('./foo/bar', true) #example */ -func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err } @@ -246,7 +233,7 @@ func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // The type returned is a Lua file, same as returned from `io` functions. // #returns File // #returns File -func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { rf, wf, err := os.Pipe() if err != nil { return nil, err @@ -261,7 +248,7 @@ func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // Returns a list of all files and directories in the provided path. // #param dir string // #returns table -func (f *fs) freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -309,7 +296,7 @@ Would print the following: ]]-- #example */ -func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } diff --git a/lua.go b/lua.go index 88fedf8..94b7910 100644 --- a/lua.go +++ b/lua.go @@ -30,8 +30,7 @@ func luaInit() { util.DoString(l, "hilbish = require 'hilbish'") // Add fs and terminal module module to Lua - f := fs.New(runner) - lib.LoadLibs(l, f.Loader) + lib.LoadLibs(l, fs.Loader) lib.LoadLibs(l, terminal.Loader) cmds = commander.New(l) From 52618191494ae7eeb3c373b5ed09b6f210e3f2cd Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:12:06 -0400 Subject: [PATCH 03/12] feat: implement snail library --- api.go | 16 +- complete.go | 4 +- exec.go | 386 ++------------------------------------- golibs/snail/lua.go | 125 +++++++++++++ golibs/snail/snail.go | 302 ++++++++++++++++++++++++++++++ golibs/terminal/snail.go | 0 job.go | 2 +- lua.go | 6 +- main.go | 10 - nature/runner.lua | 6 +- runnermode.go | 8 +- sink.go => sink/sink.go | 34 ++-- util/streams.go | 11 ++ util/util.go | 118 +++++++++++- util/util_unix.go | 2 +- 15 files changed, 614 insertions(+), 416 deletions(-) create mode 100644 golibs/snail/lua.go create mode 100644 golibs/snail/snail.go delete mode 100644 golibs/terminal/snail.go rename sink.go => sink/sink.go (87%) create mode 100644 util/streams.go diff --git a/api.go b/api.go index 8c2e6e4..d556589 100644 --- a/api.go +++ b/api.go @@ -13,10 +13,9 @@ package main import ( - "bytes" + //"bytes" "errors" "fmt" - "io" "os" "os/exec" "runtime" @@ -28,9 +27,9 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" - "github.com/arnodel/golua/lib/iolib" + //"github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" - "mvdan.cc/sh/v3/interp" + //"mvdan.cc/sh/v3/interp" ) var exports = map[string]util.LuaExport{ @@ -49,7 +48,7 @@ var exports = map[string]util.LuaExport{ "inputMode": {hlinputMode, 1, false}, "interval": {hlinterval, 2, false}, "read": {hlread, 1, false}, - "run": {hlrun, 1, true}, + //"run": {hlrun, 1, true}, "timeout": {hltimeout, 2, false}, "which": {hlwhich, 1, false}, } @@ -154,6 +153,7 @@ func unsetVimMode() { util.SetField(l, hshMod, "vimMode", rt.NilValue) } +/* func handleStream(v rt.Value, strms *streams, errStream bool) error { ud, ok := v.TryUserData() if !ok { @@ -182,6 +182,7 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error { return nil } +*/ // run(cmd, streams) -> exitCode (number), stdout (string), stderr (string) // Runs `cmd` in Hilbish's shell script interpreter. @@ -210,6 +211,7 @@ hilbish.run('wc -l', { }) */ // #example +/* func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN. if err := c.Check1Arg(); err != nil { @@ -288,6 +290,7 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil } +*/ // cwd() -> string // Returns the current directory of the shell. @@ -743,6 +746,8 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // runnerMode(mode) +// **NOTE: This function is deprecated and will be removed in 3.0** +// Use `hilbish.runner.setCurrent` instead. // 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. @@ -752,6 +757,7 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // Read [about runner mode](../features/runner-mode) for more information. // #param mode string|function func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + // TODO: Reimplement in Lua if err := c.Check1Arg(); err != nil { return nil, err } diff --git a/complete.go b/complete.go index 86938cb..e2f0812 100644 --- a/complete.go +++ b/complete.go @@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { if len(fileCompletions) != 0 { for _, f := range fileCompletions { fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) - if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { + if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { continue } completions = append(completions, f) @@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { // get basename from matches for _, match := range matches { // check if we have execute permissions for our match - err := findExecutable(match, true, false) + err := util.FindExecutable(match, true, false) if err != nil { continue } diff --git a/exec.go b/exec.go index 01a5dfa..19a5d2f 100644 --- a/exec.go +++ b/exec.go @@ -1,141 +1,45 @@ package main import ( - "bytes" - "context" "errors" - "os/exec" "fmt" "io" "os" - "os/signal" - "path/filepath" - "runtime" "strings" - "syscall" - "time" "hilbish/util" + //herror "hilbish/errors" 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 streams struct { - stdout io.Writer - stderr io.Writer - stdin io.Reader -} - -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) + currentRunner := runnerMode + rerun: var exitCode uint8 - var err error var cont bool var newline bool // save incase it changes while prompting (For some reason) - currentRunner := runnerMode - if currentRunner.Type() == rt.StringType { - switch currentRunner.AsString() { - case "hybrid": - _, _, err = handleLua(input) - if err == nil { - cmdFinish(0, input, priv) - return - } - input, exitCode, cont, newline, err = handleSh(input) - case "hybridRev": - _, _, _, _, err = handleSh(input) - if err == nil { - cmdFinish(0, input, priv) - return - } - input, exitCode, err = handleLua(input) - case "lua": - input, exitCode, err = handleLua(input) - case "sh": - input, exitCode, cont, newline, err = handleSh(input) - } - } else { - // can only be a string or function so - var runnerErr error - input, exitCode, cont, newline, 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 + input, exitCode, cont, newline, runnerErr, err := runLuaRunner(currentRunner, input) + if err != nil { + fmt.Fprintln(os.Stderr, err) + cmdFinish(124, input, priv) + return } + // 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 = continuePrompt(input, newline) @@ -147,8 +51,8 @@ func runInput(input string, priv bool) { } if err != nil && err != io.EOF { - if exErr, ok := isExecError(err); ok { - hooks.Emit("command." + exErr.typ, exErr.cmd) + if exErr, ok := util.IsExecError(err); ok { + hooks.Emit("command." + exErr.Typ, exErr.Cmd) } else { fmt.Fprintln(os.Stderr, err) } @@ -239,16 +143,7 @@ func handleLua(input string) (string, uint8, error) { return cmdString, 125, err } -func handleSh(cmdString string) (input string, exitCode uint8, cont bool, newline bool, runErr error) { - shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh")) - var err error - input, exitCode, cont, newline, runErr, err = runLuaRunner(shRunner, cmdString) - if err != nil { - runErr = err - } - return -} - +/* func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline bool, e error) { _, _, err := execCommand(cmdString, nil) if err != nil { @@ -274,256 +169,7 @@ func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline return cmdString, 0, false, false, nil } - -// Run command in sh interpreter -func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) { - file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") - if err != nil { - return nil, nil, err - } - - if strms == nil { - strms = &streams{} - } - - if strms.stdout == nil { - strms.stdout = os.Stdout - } - - if strms.stderr == nil { - strms.stderr = os.Stderr - } - - if strms.stdin == nil { - strms.stdin = os.Stdin - } - - interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner) - interp.Env(nil)(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 strms.stdout, strms.stderr, err - } - } - - return strms.stdout, strms.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)) - } - - hc := interp.HandlerCtx(ctx) - if cmd := cmds.Commands[args[0]]; cmd != nil { - stdin := newSinkInput(hc.Stdin) - stdout := newSinkOutput(hc.Stdout) - stderr := newSinkOutput(hc.Stderr) - - sinks := rt.NewTable() - sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud)) - sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud)) - sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud)) - sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud)) - - t := rt.NewThread(l) - sig := make(chan os.Signal) - exit := make(chan bool) - - luaexitcode := rt.IntValue(63) - var err error - go func() { - defer func() { - if r := recover(); r != nil { - exit <- true - } - }() - - signal.Notify(sig, os.Interrupt) - select { - case <-sig: - t.KillContext() - return - } - - }() - - go func() { - luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) - exit <- true - }() - - <-exit - 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(cmds.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) - } - - path, err := util.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 - - env := hc.Env - envList := os.Environ() - env.Each(func(name string, vr expand.Variable) bool { - 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 splitInput(input string) ([]string, string) { // end my suffering diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go new file mode 100644 index 0000000..40fad70 --- /dev/null +++ b/golibs/snail/lua.go @@ -0,0 +1,125 @@ +package snail + +import ( + "fmt" + "strings" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +var snailMetaKey = rt.StringValue("hshsnail") +var Loader = packagelib.Loader{ + Load: loaderFunc, + Name: "fs", +} + +func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + snailMeta := rt.NewTable() + snailMethods := rt.NewTable() + snailFuncs := map[string]util.LuaExport{ + "run": {srun, 1, false}, + } + util.SetExports(rtm, snailMethods, snailFuncs) + + snailIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + arg := c.Arg(1) + val := snailMethods.Get(arg) + + return c.PushingNext1(t.Runtime, val), nil + } + snailMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(snailIndex, "__index", 2, false))) + rtm.SetRegistry(snailMetaKey, rt.TableValue(snailMeta)) + + exports := map[string]util.LuaExport{ + "new": util.LuaExport{snew, 0, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return rt.TableValue(mod), nil +} + +func snew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + s := New(t.Runtime) + return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil +} + +func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := snailArg(c, 0) + if err != nil { + return nil, err + } + + cmd, err := c.StringArg(1) + if err != nil { + return nil, err + } + + var newline bool + var cont bool + var luaErr rt.Value = rt.NilValue + exitCode := 0 + bg, _, _, err := s.Run(cmd, nil) + if err != nil { + if syntax.IsIncomplete(err) { + /* + if !interactive { + return cmdString, 126, false, false, err + } + */ + if strings.Contains(err.Error(), "unclosed here-document") { + newline = true + } + cont = true + } else { + if code, ok := interp.IsExitStatus(err); ok { + exitCode = int(code) + } else { + luaErr = rt.StringValue(err.Error()) + } + } + } + runnerRet := rt.NewTable() + runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd)) + runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode))) + runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont)) + runnerRet.Set(rt.StringValue("newline"), rt.BoolValue(newline)) + runnerRet.Set(rt.StringValue("err"), luaErr) + + runnerRet.Set(rt.StringValue("bg"), rt.BoolValue(bg)) + return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil +} + +func snailArg(c *rt.GoCont, arg int) (*snail, error) { + s, ok := valueToSnail(c.Arg(arg)) + if !ok { + return nil, fmt.Errorf("#%d must be a snail", arg + 1) + } + + return s, nil +} + +func valueToSnail(val rt.Value) (*snail, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + s, ok := u.Value().(*snail) + return s, ok +} + +func snailUserData(s *snail) *rt.UserData { + snailMeta := s.runtime.Registry(snailMetaKey) + return rt.NewUserData(s, snailMeta.AsTable()) +} diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go new file mode 100644 index 0000000..4ef92ea --- /dev/null +++ b/golibs/snail/snail.go @@ -0,0 +1,302 @@ +// shell script interpreter library +package snail + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "time" + + "hilbish/sink" + "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" +) + +type snail struct{ + runner *interp.Runner + runtime *rt.Runtime +} + +func New(rtm *rt.Runtime) *snail { + runner, _ := interp.New() + + return &snail{ + runner: runner, + runtime: rtm, + } +} + +func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer, error){ + file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") + if err != nil { + return false, nil, nil, err + } + + if strms == nil { + strms = &util.Streams{} + } + + if strms.Stdout == nil { + strms.Stdout = os.Stdout + } + + if strms.Stderr == nil { + strms.Stderr = os.Stderr + } + + if strms.Stdin == nil { + strms.Stdin = os.Stdin + } + + interp.StdIO(strms.Stdin, strms.Stdout, strms.Stderr)(s.runner) + interp.Env(nil)(s.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(func(ctx context.Context, args []string) error { + _, argstring := splitInput(strings.Join(args, " ")) + // i dont really like this but it works + aliases := make(map[string]string) + aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.all()") + util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) { + aliases[k.AsString()] = v.AsString() + }) + if aliases[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 = util.MustDoString(s.runtime, fmt.Sprintf(`return hilbish.aliases.resolve("%s")`, argstring)).AsString() + + 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)) + } + + hc := interp.HandlerCtx(ctx) + + cmds := make(map[string]*rt.Closure) + luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable() + util.ForEach(luaCmds, func(k, v rt.Value) { + cmds[k.AsString()] = k.AsTable().Get(rt.StringValue("exec")).AsClosure() + }) + if cmd := cmds[args[0]]; cmd != nil { + stdin := sink.NewSinkInput(s.runtime, hc.Stdin) + stdout := sink.NewSinkOutput(s.runtime, hc.Stdout) + stderr := sink.NewSinkOutput(s.runtime, hc.Stderr) + + sinks := rt.NewTable() + sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.UserData)) + sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.UserData)) + sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.UserData)) + sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.UserData)) + + t := rt.NewThread(s.runtime) + sig := make(chan os.Signal) + exit := make(chan bool) + + luaexitcode := rt.IntValue(63) + var err error + go func() { + defer func() { + if r := recover(); r != nil { + exit <- true + } + }() + + signal.Notify(sig, os.Interrupt) + select { + case <-sig: + t.KillContext() + return + } + + }() + + go func() { + luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) + exit <- true + }() + + <-exit + 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(cmds, 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) + } + + path, err := util.LookPath(args[0]) + if err == util.ErrNotExec { + return util.ExecError{ + Typ: "not-executable", + Cmd: args[0], + Code: 126, + Colon: true, + Err: util.ErrNotExec, + } + } else if err != nil { + return util.ExecError{ + Typ: "not-found", + Cmd: args[0], + Code: 127, + Err: util.ErrNotFound, + } + } + + killTimeout := 2 * time.Second + // from here is basically copy-paste of the default exec handler from + // sh/interp but with our job handling + + env := hc.Env + envList := os.Environ() + env.Each(func(name string, vr expand.Variable) bool { + 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 := util.HandleExecErr(err) + + if bg { + //j.exitCode = int(exit) + //j.finish() + } + return interp.NewExitStatus(exit) + })(s.runner) + err = s.runner.Run(context.TODO(), stmt) + if err != nil { + return bg, strms.Stdout, strms.Stderr, err + } + } + + return bg, strms.Stdout, strms.Stderr, nil +} + +func splitInput(input string) ([]string, string) { + // end my suffering + // TODO: refactor this garbage + quoted := false + cmdArgs := []string{} + sb := &strings.Builder{} + cmdstr := &strings.Builder{} + + 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 { + sb.WriteRune(r) + } + cmdstr.WriteRune(r) + } + if sb.Len() > 0 { + cmdArgs = append(cmdArgs, sb.String()) + } + + return cmdArgs, cmdstr.String() +} diff --git a/golibs/terminal/snail.go b/golibs/terminal/snail.go deleted file mode 100644 index e69de29..0000000 diff --git a/job.go b/job.go index bb16e92..fcb1c2c 100644 --- a/job.go +++ b/job.go @@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if !j.running { err := j.start() - exit := handleExecErr(err) + exit := util.HandleExecErr(err) j.exitCode = int(exit) j.finish() } diff --git a/lua.go b/lua.go index 94b7910..9cefada 100644 --- a/lua.go +++ b/lua.go @@ -4,10 +4,12 @@ import ( "fmt" "os" + "hilbish/sink" "hilbish/util" "hilbish/golibs/bait" "hilbish/golibs/commander" "hilbish/golibs/fs" + "hilbish/golibs/snail" "hilbish/golibs/terminal" rt "github.com/arnodel/golua/runtime" @@ -23,15 +25,15 @@ func luaInit() { MessageHandler: debuglib.Traceback, }) lib.LoadAll(l) - setupSinkType(l) + sink.SetupSinkType(l) lib.LoadLibs(l, hilbishLoader) // yes this is stupid, i know util.DoString(l, "hilbish = require 'hilbish'") - // Add fs and terminal module module to Lua lib.LoadLibs(l, fs.Loader) lib.LoadLibs(l, terminal.Loader) + lib.LoadLibs(l, snail.Loader) cmds = commander.New(l) lib.LoadLibs(l, cmds.Loader) diff --git a/main.go b/main.go index af7a22a..c26a55e 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,6 @@ import ( "github.com/pborman/getopt" "github.com/maxlandon/readline" "golang.org/x/term" - "mvdan.cc/sh/v3/interp" ) var ( @@ -311,15 +310,6 @@ func removeDupes(slice []string) []string { return newSlice } -func contains(s []string, e string) bool { - for _, a := range s { - if strings.ToLower(a) == strings.ToLower(e) { - return true - } - } - return false -} - func exit(code int) { jobs.stopAll() diff --git a/nature/runner.lua b/nature/runner.lua index 235ab77..3d2bb61 100644 --- a/nature/runner.lua +++ b/nature/runner.lua @@ -1,4 +1,6 @@ --- hilbish.runner +local snail = require 'snail' + local currentRunner = 'hybrid' local runners = {} @@ -107,7 +109,5 @@ hilbish.runner.add('lua', function(input) return hilbish.runner.lua(cmdStr) end) -hilbish.runner.add('sh', function(input) - return hilbish.runner.sh(input) -end) +hilbish.runner.add('sh', snail.new()) diff --git a/runnermode.go b/runnermode.go index fb8bcf4..f1e2bf0 100644 --- a/runnermode.go +++ b/runnermode.go @@ -53,7 +53,7 @@ end) */ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ - "sh": {shRunner, 1, false}, + //"sh": {shRunner, 1, false}, "lua": {luaRunner, 1, false}, "setMode": {hlrunnerMode, 1, false}, } @@ -66,10 +66,12 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { // #interface runner // setMode(cb) +// **NOTE: This function is deprecated and will be removed in 3.0** +// Use `hilbish.runner.setCurrent` instead. // This is the same as the `hilbish.runnerMode` function. // It takes a callback, which will be used to execute all interactive input. // In normal cases, neither callbacks should be overrided by the user, -// as the higher level functions listed below this will handle it. +// as the higher level functions (setCurrent) this will handle it. // #param cb function func _runnerMode() {} @@ -78,6 +80,7 @@ func _runnerMode() {} // Runs a command in Hilbish's shell script interpreter. // This is the equivalent of using `source`. // #param cmd string +/* func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err @@ -101,6 +104,7 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil } +*/ // #interface runner // lua(cmd) diff --git a/sink.go b/sink/sink.go similarity index 87% rename from sink.go rename to sink/sink.go index 3aa5507..be8b7d1 100644 --- a/sink.go +++ b/sink/sink.go @@ -1,4 +1,4 @@ -package main +package sink import ( "bufio" @@ -17,15 +17,15 @@ var sinkMetaKey = rt.StringValue("hshsink") // #type // A sink is a structure that has input and/or output to/from // a desination. -type sink struct{ +type Sink struct{ writer *bufio.Writer reader *bufio.Reader file *os.File - ud *rt.UserData + UserData *rt.UserData autoFlush bool } -func setupSinkType(rtm *rt.Runtime) { +func SetupSinkType(rtm *rt.Runtime) { sinkMeta := rt.NewTable() sinkMethods := rt.NewTable() @@ -37,7 +37,7 @@ func setupSinkType(rtm *rt.Runtime) { "write": {luaSinkWrite, 2, false}, "writeln": {luaSinkWriteln, 2, false}, } - util.SetExports(l, sinkMethods, sinkFuncs) + util.SetExports(rtm, sinkMethods, sinkFuncs) sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { s, _ := sinkArg(c, 0) @@ -64,7 +64,7 @@ func setupSinkType(rtm *rt.Runtime) { } sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false))) - l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) + rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) } @@ -212,11 +212,11 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func newSinkInput(r io.Reader) *sink { - s := &sink{ +func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink { + s := &Sink{ reader: bufio.NewReader(r), } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) if f, ok := r.(*os.File); ok { s.file = f @@ -225,17 +225,17 @@ func newSinkInput(r io.Reader) *sink { return s } -func newSinkOutput(w io.Writer) *sink { - s := &sink{ +func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink { + s := &Sink{ writer: bufio.NewWriter(w), autoFlush: true, } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) return s } -func sinkArg(c *rt.GoCont, arg int) (*sink, error) { +func sinkArg(c *rt.GoCont, arg int) (*Sink, error) { s, ok := valueToSink(c.Arg(arg)) if !ok { return nil, fmt.Errorf("#%d must be a sink", arg + 1) @@ -244,17 +244,17 @@ func sinkArg(c *rt.GoCont, arg int) (*sink, error) { return s, nil } -func valueToSink(val rt.Value) (*sink, bool) { +func valueToSink(val rt.Value) (*Sink, bool) { u, ok := val.TryUserData() if !ok { return nil, false } - s, ok := u.Value().(*sink) + s, ok := u.Value().(*Sink) return s, ok } -func sinkUserData(s *sink) *rt.UserData { - sinkMeta := l.Registry(sinkMetaKey) +func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData { + sinkMeta := rtm.Registry(sinkMetaKey) return rt.NewUserData(s, sinkMeta.AsTable()) } diff --git a/util/streams.go b/util/streams.go new file mode 100644 index 0000000..11f9308 --- /dev/null +++ b/util/streams.go @@ -0,0 +1,11 @@ +package util + +import ( + "io" +) + +type Streams struct { + Stdout io.Writer + Stderr io.Writer + Stdin io.Reader +} diff --git a/util/util.go b/util/util.go index e60f66e..b32d865 100644 --- a/util/util.go +++ b/util/util.go @@ -2,14 +2,78 @@ package util import ( "bufio" + "context" + "errors" + "fmt" "io" + "path/filepath" "strings" "os" + "os/exec" "os/user" + "runtime" + "syscall" rt "github.com/arnodel/golua/runtime" ) +var ErrNotExec = errors.New("not executable") +var ErrNotFound = errors.New("not found") + +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 +} + // SetField sets a field in a table, adding docs for it. // It is accessible via the __docProp metatable. It is a table of the names of the fields. func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) { @@ -36,6 +100,15 @@ func DoString(rtm *rt.Runtime, code string) (rt.Value, error) { return ret, err } +func MustDoString(rtm *rt.Runtime, code string) rt.Value { + val, err := DoString(rtm, code) + if err != nil { + panic(err) + } + + return val +} + // DoFile runs the contents of the file in the Lua runtime. func DoFile(rtm *rt.Runtime, path string) error { f, err := os.Open(path) @@ -151,13 +224,13 @@ func LookPath(file string) (string, error) { // custom lookpath function so we k } for _, s := range skip { if strings.HasPrefix(file, s) { - return file, findExecutable(file, false, false) + return file, 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 { + err := FindExecutable(path, true, false) + if err == ErrNotExec { return "", err } else if err == nil { return path, nil @@ -166,3 +239,42 @@ func LookPath(file string) (string, error) { // custom lookpath function so we k return "", os.ErrNotExist } + +func Contains(s []string, e string) bool { + for _, a := range s { + if strings.ToLower(a) == strings.ToLower(e) { + return true + } + } + return false +} + +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 +} diff --git a/util/util_unix.go b/util/util_unix.go index 9fa6a6c..92813c8 100644 --- a/util/util_unix.go +++ b/util/util_unix.go @@ -20,5 +20,5 @@ func FindExecutable(path string, inPath, dirs bool) error { return nil } } - return errNotExec + return ErrNotExec } From 1379df7c2403ea92c8909e79a5f36a6ab2ea214b Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:18:45 -0400 Subject: [PATCH 04/12] fix: dont override fs lib on snail --- golibs/snail/lua.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go index 40fad70..ca4e3e9 100644 --- a/golibs/snail/lua.go +++ b/golibs/snail/lua.go @@ -15,7 +15,7 @@ import ( var snailMetaKey = rt.StringValue("hshsnail") var Loader = packagelib.Loader{ Load: loaderFunc, - Name: "fs", + Name: "snail", } func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { From 68fc973bfb5bf0644c51e1c013c7586eee5f67fd Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:35:01 -0400 Subject: [PATCH 05/12] fix: make runnerMode nil --- exec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exec.go b/exec.go index 19a5d2f..9819d15 100644 --- a/exec.go +++ b/exec.go @@ -16,7 +16,7 @@ import ( var errNotExec = errors.New("not executable") var errNotFound = errors.New("not found") -var runnerMode rt.Value = rt.StringValue("hybrid") +var runnerMode rt.Value = rt.NilValue func runInput(input string, priv bool) { running = true From 16ddb85b4f5edfdd6019ce18fd2c1ecca0acca43 Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:35:26 -0400 Subject: [PATCH 06/12] fix: make snail.run accept correct number of args --- golibs/snail/lua.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go index ca4e3e9..db2ff07 100644 --- a/golibs/snail/lua.go +++ b/golibs/snail/lua.go @@ -22,7 +22,7 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { snailMeta := rt.NewTable() snailMethods := rt.NewTable() snailFuncs := map[string]util.LuaExport{ - "run": {srun, 1, false}, + "run": {srun, 2, false}, } util.SetExports(rtm, snailMethods, snailFuncs) From 55e879bd028f4004d031955b1c59230afcd43e93 Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:36:33 -0400 Subject: [PATCH 07/12] fix: typos, set default runner to lua implemented hybrid, add back hilbish.runner.sh --- golibs/snail/snail.go | 4 ++-- nature/runner.lua | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go index 4ef92ea..ddfef49 100644 --- a/golibs/snail/snail.go +++ b/golibs/snail/snail.go @@ -82,7 +82,7 @@ func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer _, argstring := splitInput(strings.Join(args, " ")) // i dont really like this but it works aliases := make(map[string]string) - aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.all()") + aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.list()") util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) { aliases[k.AsString()] = v.AsString() }) @@ -115,7 +115,7 @@ func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer cmds := make(map[string]*rt.Closure) luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable() util.ForEach(luaCmds, func(k, v rt.Value) { - cmds[k.AsString()] = k.AsTable().Get(rt.StringValue("exec")).AsClosure() + cmds[k.AsString()] = v.AsTable().Get(rt.StringValue("exec")).AsClosure() }) if cmd := cmds[args[0]]; cmd != nil { stdin := sink.NewSinkInput(s.runtime, hc.Stdin) diff --git a/nature/runner.lua b/nature/runner.lua index 3d2bb61..9ece224 100644 --- a/nature/runner.lua +++ b/nature/runner.lua @@ -83,6 +83,11 @@ function hilbish.runner.getCurrent() return currentRunner end +local snaili = snail.new() +function hilbish.runner.sh(input) + return snaili:run(input) +end + hilbish.runner.add('hybrid', function(input) local cmdStr = hilbish.aliases.resolve(input) @@ -109,5 +114,5 @@ hilbish.runner.add('lua', function(input) return hilbish.runner.lua(cmdStr) end) -hilbish.runner.add('sh', snail.new()) - +hilbish.runner.add('sh', hilbish.runner.sh) +hilbish.runner.setCurrent 'hybrid' From a8435b649dc0b6bfd9151d3c8d823c6de2f2461c Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:44:24 -0400 Subject: [PATCH 08/12] fix: use error code if err is ExecError --- golibs/snail/lua.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go index db2ff07..61ca254 100644 --- a/golibs/snail/lua.go +++ b/golibs/snail/lua.go @@ -85,6 +85,9 @@ func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if code, ok := interp.IsExitStatus(err); ok { exitCode = int(code) } else { + if exErr, ok := util.IsExecError(err); ok { + exitCode = exErr.Code + } luaErr = rt.StringValue(err.Error()) } } From 7174f40b5f2cdce398d7979bfabdecac60d29cd5 Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:48:11 -0400 Subject: [PATCH 09/12] fix: undefined vars on windows --- util/util_windows.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/util/util_windows.go b/util/util_windows.go index 4eb50c8..ab490ad 100644 --- a/util/util_windows.go +++ b/util/util_windows.go @@ -5,6 +5,8 @@ package util import ( "path/filepath" "os" + + "hilbish/util" ) func FindExecutable(path string, inPath, dirs bool) error { @@ -21,15 +23,15 @@ func FindExecutable(path string, inPath, dirs bool) error { } else { _, err := os.Stat(path) if err == nil { - if contains(pathExts, nameExt) { return nil } - return errNotExec + if util.Contains(pathExts, nameExt) { return nil } + return util.ErrNotExec } } } else { _, err := os.Stat(path) if err == nil { - if contains(pathExts, nameExt) { return nil } - return errNotExec + if util.Contains(pathExts, nameExt) { return nil } + return util.ErrNotExec } } From d1934452bb985119369d077c89cb899d6fa2eeb6 Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 01:52:00 -0400 Subject: [PATCH 10/12] fix: dont import util in the util package (bruh) --- util/util_windows.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/util/util_windows.go b/util/util_windows.go index ab490ad..3321033 100644 --- a/util/util_windows.go +++ b/util/util_windows.go @@ -5,8 +5,6 @@ package util import ( "path/filepath" "os" - - "hilbish/util" ) func FindExecutable(path string, inPath, dirs bool) error { @@ -23,15 +21,15 @@ func FindExecutable(path string, inPath, dirs bool) error { } else { _, err := os.Stat(path) if err == nil { - if util.Contains(pathExts, nameExt) { return nil } - return util.ErrNotExec + if Contains(pathExts, nameExt) { return nil } + return ErrNotExec } } } else { _, err := os.Stat(path) if err == nil { - if util.Contains(pathExts, nameExt) { return nil } - return util.ErrNotExec + if Contains(pathExts, nameExt) { return nil } + return ErrNotExec } } From b84d985ce6d59149689a1239eee62607007b396f Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 11:28:14 -0400 Subject: [PATCH 11/12] fix: throw old cd hook --- nature/commands/cd.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua index 0efbb3f..284d420 100644 --- a/nature/commands/cd.lua +++ b/nature/commands/cd.lua @@ -22,5 +22,6 @@ commander.register('cd', function (args, sinks) sinks.out:writeln(err) return 1 end + bait.throw('cd', path, oldPath) bait.throw('hilbish.cd', fs.abs(path), oldPath) end) From 56ba00e213fca2bf91f057a5c77b896f76f2537d Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 29 Dec 2024 15:25:34 -0400 Subject: [PATCH 12/12] docs: fix hilbish.vimAction doc --- docs/hooks/hilbish.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/hooks/hilbish.md b/docs/hooks/hilbish.md index d5d8a48..038b721 100644 --- a/docs/hooks/hilbish.md +++ b/docs/hooks/hilbish.md @@ -43,5 +43,29 @@ The notification. The properties are defined in the link above.
-+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something -like yanking or pasting text. See `doc vim-mode actions` for more info. +## hilbish.cd +Sent when the current directory of the shell is changed (via interactive means.) +If you are implementing a custom command that changes the directory of the shell, +you must throw this hook manually for correctness. + +#### Variables +`string` **`path`** +Absolute path of the directory that was changed to. + +`string` **`oldPath`** +Absolute path of the directory Hilbish *was* in. + +
+ +## hilbish.vimAction +Sent when the user does a "vim action," being something like yanking or pasting text. +See `doc vim-mode actions` for more info. + +#### Variables +`string` **`actionName`** +Absolute path of the directory that was changed to. + +`table` **`args`** +Table of args relating to the Vim action. + +