diff --git a/api.go b/api.go
index 43e361a..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.
@@ -508,7 +511,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 +709,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
}
@@ -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/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.
+
+
diff --git a/exec.go b/exec.go
index 7f8e37b..9819d15 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
-}
+var runnerMode rt.Value = rt.NilValue
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,291 +169,15 @@ 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 := 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 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 +193,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..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
}
@@ -110,12 +97,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
}
@@ -125,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
}
@@ -156,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
}
@@ -190,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 {
@@ -217,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
}
@@ -248,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
@@ -263,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
}
@@ -311,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/golibs/snail/lua.go b/golibs/snail/lua.go
new file mode 100644
index 0000000..61ca254
--- /dev/null
+++ b/golibs/snail/lua.go
@@ -0,0 +1,128 @@
+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: "snail",
+}
+
+func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
+ snailMeta := rt.NewTable()
+ snailMethods := rt.NewTable()
+ snailFuncs := map[string]util.LuaExport{
+ "run": {srun, 2, 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 {
+ if exErr, ok := util.IsExecError(err); ok {
+ exitCode = exErr.Code
+ }
+ 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..ddfef49
--- /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.list()")
+ 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()] = v.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/job.go b/job.go
index f5bd6f2..fcb1c2c 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
@@ -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/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/lua.go b/lua.go
index 88fedf8..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,16 +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
- f := fs.New(runner)
- lib.LoadLibs(l, f.Loader)
+ 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 1bddfc4..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 (
@@ -38,11 +37,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()
@@ -313,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/commands/cd.lua b/nature/commands/cd.lua
index 7cfe4a2..284d420 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,11 @@ 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('cd', path, oldPath)
+ 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/nature/runner.lua b/nature/runner.lua
index 235ab77..9ece224 100644
--- a/nature/runner.lua
+++ b/nature/runner.lua
@@ -1,4 +1,6 @@
--- hilbish.runner
+local snail = require 'snail'
+
local currentRunner = 'hybrid'
local runners = {}
@@ -81,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)
@@ -107,7 +114,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', hilbish.runner.sh)
+hilbish.runner.setCurrent 'hybrid'
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 0fcd4b0..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)
@@ -141,3 +214,67 @@ 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
+}
+
+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/execfile_unix.go b/util/util_unix.go
similarity index 57%
rename from execfile_unix.go
rename to util/util_unix.go
index 82c738b..92813c8 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
@@ -25,5 +20,5 @@ func findExecutable(path string, inPath, dirs bool) error {
return nil
}
}
- return errNotExec
+ return ErrNotExec
}
diff --git a/execfile_windows.go b/util/util_windows.go
similarity index 56%
rename from execfile_windows.go
rename to util/util_windows.go
index 3d6ef61..3321033 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 {
@@ -26,15 +21,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 Contains(pathExts, nameExt) { return nil }
+ return ErrNotExec
}
}
} else {
_, err := os.Stat(path)
if err == nil {
- if contains(pathExts, nameExt) { return nil }
- return errNotExec
+ if Contains(pathExts, nameExt) { return nil }
+ return ErrNotExec
}
}