From 02c89b99dd1c72c47e4f984e38f18f6566c787e4 Mon Sep 17 00:00:00 2001 From: sammyette Date: Thu, 3 Apr 2025 00:38:35 -0400 Subject: [PATCH] refactor: decouple sh use in core exec code (#337) --- api.go | 152 +----- cmd/docgen/docgen.go | 34 +- cmd/docgen/docgen.lua | 37 +- complete.go | 4 +- docs/api/hilbish/_index.md | 135 +++-- docs/api/hilbish/hilbish.messages.md | 135 +++++ docs/api/hilbish/hilbish.runner.md | 189 +++++-- docs/api/snail.md | 50 ++ docs/hooks/hilbish.md | 28 +- docs/nature/dirs.md | 68 +-- docs/nature/doc.md | 42 +- emmyLuaDocs/hilbish.lua | 72 +-- emmyLuaDocs/snail.lua | 16 + emmyLuaDocs/util.lua | 83 +++ exec.go | 534 +------------------- golibs/fs/fs.go | 63 +-- golibs/snail/lua.go | 221 ++++++++ golibs/snail/snail.go | 302 +++++++++++ job.go | 6 +- job_unix.go | 4 + job_windows.go | 5 + lua.go | 7 +- main.go | 12 - nature/commands/cd.lua | 11 +- nature/dirs.lua | 14 +- nature/hilbish.lua | 78 +++ nature/init.lua | 2 + nature/runner.lua | 86 +++- runnermode.go | 40 -- sink.go => util/sink.go | 91 ++-- util/streams.go | 11 + util/util.go | 137 +++++ execfile_unix.go => util/util_unix.go | 11 +- execfile_windows.go => util/util_windows.go | 17 +- 34 files changed, 1631 insertions(+), 1066 deletions(-) create mode 100644 docs/api/hilbish/hilbish.messages.md create mode 100644 docs/api/snail.md create mode 100644 emmyLuaDocs/snail.lua create mode 100644 emmyLuaDocs/util.lua create mode 100644 golibs/snail/lua.go create mode 100644 golibs/snail/snail.go create mode 100644 nature/hilbish.lua rename sink.go => util/sink.go (70%) create mode 100644 util/streams.go rename execfile_unix.go => util/util_unix.go (57%) rename execfile_windows.go => util/util_windows.go (56%) diff --git a/api.go b/api.go index 43e361a..315884c 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{ @@ -39,7 +38,6 @@ var exports = map[string]util.LuaExport{ "complete": {hlcomplete, 2, false}, "cwd": {hlcwd, 0, false}, "exec": {hlexec, 1, false}, - "runnerMode": {hlrunnerMode, 1, false}, "goro": {hlgoro, 1, true}, "highlighter": {hlhighlighter, 1, false}, "hinter": {hlhinter, 1, false}, @@ -49,7 +47,6 @@ var exports = map[string]util.LuaExport{ "inputMode": {hlinputMode, 1, false}, "interval": {hlinterval, 2, false}, "read": {hlread, 1, false}, - "run": {hlrun, 1, true}, "timeout": {hltimeout, 2, false}, "which": {hlwhich, 1, false}, } @@ -134,6 +131,9 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { pluginModule := moduleLoader(rtm) mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule)) + sinkModule := util.SinkLoader(l) + mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule)) + return rt.TableValue(mod), nil } @@ -154,6 +154,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,112 +183,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. -// The `streams` parameter specifies the output and input streams the command should use. -// For example, to write command output to a sink. -// As a table, the caller can directly specify the standard output, error, and input -// streams of the command with the table keys `out`, `err`, and `input` respectively. -// As a boolean, it specifies whether the command should use standard output or return its output streams. -// #param cmd string -// #param streams table|boolean -// #returns number, string, string -// #example -/* -// This code is the same as `ls -l | wc -l` -local fs = require 'fs' -local pr, pw = fs.pipe() -hilbish.run('ls -l', { - stdout = pw, - stderr = pw, -}) - -pw:close() - -hilbish.run('wc -l', { - stdin = pr -}) */ -// #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 { - return nil, err - } - cmd, err := c.StringArg(0) - if err != nil { - return nil, err - } - - strms := &streams{} - var terminalOut bool - if len(c.Etc()) != 0 { - tout := c.Etc()[0] - - var ok bool - terminalOut, ok = tout.TryBool() - if !ok { - luastreams, ok := tout.TryTable() - if !ok { - return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")") - } - - handleStream(luastreams.Get(rt.StringValue("out")), strms, false) - handleStream(luastreams.Get(rt.StringValue("err")), strms, true) - - stdinstrm := luastreams.Get(rt.StringValue("input")) - if !stdinstrm.IsNil() { - ud, ok := stdinstrm.TryUserData() - if !ok { - return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")") - } - - val := ud.Value() - var varstrm io.Reader - if f, ok := val.(*iolib.File); ok { - varstrm = f.Handle() - } - - if f, ok := val.(*sink); ok { - varstrm = f.reader - } - - if varstrm == nil { - return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)") - } - - strms.stdin = varstrm - } - } else { - if !terminalOut { - strms = &streams{ - stdout: new(bytes.Buffer), - stderr: new(bytes.Buffer), - } - } - } - } - - var exitcode uint8 - stdout, stderr, err := execCommand(cmd, strms) - - if code, ok := interp.IsExitStatus(err); ok { - exitcode = code - } else if err != nil { - exitcode = 1 - } - - var stdoutStr, stderrStr string - if stdoutBuf, ok := stdout.(*bytes.Buffer); ok { - stdoutStr = stdoutBuf.String() - } - if stderrBuf, ok := stderr.(*bytes.Buffer); ok { - stderrStr = stderrBuf.String() - } - - 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. @@ -404,7 +300,7 @@ hilbish.multiprompt '-->' */ func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { - return nil, err + return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil } prompt, err := c.StringArg(0) if err != nil { @@ -508,7 +404,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 +602,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 } @@ -742,34 +638,6 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -// runnerMode(mode) -// 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. -// Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), -// sh, and lua. It also accepts a function, to which if it is passed one -// will call it to execute user input instead. -// 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) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - mode := c.Arg(0) - - switch mode.Type() { - case rt.StringType: - switch mode.AsString() { - case "hybrid", "hybridRev", "lua", "sh": runnerMode = mode - default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.AsString()) - } - case rt.FunctionType: runnerMode = mode - default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.TypeName()) - } - - return c.Next(), nil -} - // hinter(line, pos) // The command line hint handler. It gets called on every key insert to // determine what text to use as an inline hint. It is passed the current diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index 4743dea..1521e0e 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -84,6 +84,7 @@ var prefix = map[string]string{ "commander": "c", "bait": "b", "terminal": "term", + "snail": "snail", } func getTagsAndDocs(docs string) (map[string][]tag, []string) { @@ -208,6 +209,10 @@ func setupDocType(mod string, typ *doc.Type) *docPiece { } func setupDoc(mod string, fun *doc.Func) *docPiece { + if fun.Doc == "" { + return nil + } + docs := strings.TrimSpace(fun.Doc) tags, parts := getTagsAndDocs(docs) @@ -320,7 +325,7 @@ provided by Hilbish. os.Mkdir("emmyLuaDocs", 0777) - dirs := []string{"./"} + dirs := []string{"./", "./util"} filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error { if !info.IsDir() { return nil @@ -347,7 +352,7 @@ provided by Hilbish. pieces := []docPiece{} typePieces := []docPiece{} mod := l - if mod == "main" { + if mod == "main" || mod == "util" { mod = "hilbish" } var hasInterfaces bool @@ -431,14 +436,23 @@ provided by Hilbish. interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece) } - docs[mod] = module{ - Types: filteredTypePieces, - Docs: filteredPieces, - ShortDescription: shortDesc, - Description: strings.Join(desc, "\n"), - HasInterfaces: hasInterfaces, - Properties: docPieceTag("property", tags), - Fields: docPieceTag("field", tags), + fmt.Println(filteredTypePieces) + if newDoc, ok := docs[mod]; ok { + oldMod := docs[mod] + newDoc.Types = append(filteredTypePieces, oldMod.Types...) + newDoc.Docs = append(filteredPieces, oldMod.Docs...) + + docs[mod] = newDoc + } else { + docs[mod] = module{ + Types: filteredTypePieces, + Docs: filteredPieces, + ShortDescription: shortDesc, + Description: strings.Join(desc, "\n"), + HasInterfaces: hasInterfaces, + Properties: docPieceTag("property", tags), + Fields: docPieceTag("field", tags), + } } } diff --git a/cmd/docgen/docgen.lua b/cmd/docgen/docgen.lua index e1e57d6..073456b 100644 --- a/cmd/docgen/docgen.lua +++ b/cmd/docgen/docgen.lua @@ -15,7 +15,6 @@ for _, fname in ipairs(files) do local mod = header:match(modpattern) if not mod then goto continue end - print(fname, mod) pieces[mod] = {} descriptions[mod] = {} @@ -42,10 +41,12 @@ for _, fname in ipairs(files) do local dps = { description = {}, + example = {}, params = {} } local offset = 1 + local doingExample = false while true do local prev = lines[lineno - offset] @@ -59,17 +60,25 @@ for _, fname in ipairs(files) do if emmy then if emmy == 'param' then - print('bruh', emmythings[1], emmythings[2]) table.insert(dps.params, 1, { name = emmythings[1], type = emmythings[2], -- the +1 accounts for space. description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1) }) - print(table.concat(emmythings, '/')) end else - table.insert(dps.description, 1, docline) + if docline:match '#example' then + doingExample = not doingExample + end + + if not docline:match '#example' then + if doingExample then + table.insert(dps.example, 1, docline) + else + table.insert(dps.description, 1, docline) + end + end end offset = offset + 1 else @@ -77,7 +86,7 @@ for _, fname in ipairs(files) do end end - pieces[mod][funcName] = dps + table.insert(pieces[mod], {funcName, dps}) end docPiece = {} goto continue2 @@ -109,11 +118,15 @@ for iface, dps in pairs(pieces) do docParent = "API" path = string.format('docs/api/%s/%s.md', mod, iface) end + if iface == 'hilbish' then + docParent = "API" + path = string.format('docs/api/hilbish/_index.md', mod, iface) + end fs.mkdir(fs.dir(path), true) local exists = pcall(fs.stat, path) - local newOrNotNature = exists and mod ~= 'nature' + local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish' local f = io.open(path, newOrNotNature and 'r+' or 'w+') local tocPos @@ -129,9 +142,6 @@ for iface, dps in pairs(pieces) do tocPos = f:seek() end end - print(f) - - print('mod and path:', mod, path) local tocSearch = false for line in f:lines() do @@ -144,7 +154,10 @@ for iface, dps in pairs(pieces) do end end - for func, docs in pairs(dps) do + table.sort(dps, function(a, b) return a[1] < b[1] end) + for _, piece in pairs(dps) do + local func = piece[1] + local docs = piece[2] local sig = string.format('%s.%s(', iface, func) local params = '' for idx, param in ipairs(docs.params) do @@ -186,6 +199,10 @@ for iface, dps in pairs(pieces) do f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type)) f:write(string.format('%s\n\n', param.description)) end + if #docs.example ~= 0 then + f:write '#### Example\n' + f:write(string.format('```lua\n%s\n```\n', table.concat(docs.example, '\n'))) + end --[[ local params = table.filter(docs, function(t) return t:match '^%-%-%- @param' 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/api/hilbish/_index.md b/docs/api/hilbish/_index.md index 5c7a0f0..5aa7045 100644 --- a/docs/api/hilbish/_index.md +++ b/docs/api/hilbish/_index.md @@ -28,10 +28,10 @@ interfaces and functions which directly relate to shell functionality. |prependPath(dir)|Prepends `dir` to $PATH.| |prompt(str, typ)|Changes the shell prompt to the provided string.| |read(prompt) -> input (string)|Read input from the user, using Hilbish's line editor/input reader.| -|run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)|Runs `cmd` in Hilbish's shell script interpreter.| -|runnerMode(mode)|Sets the execution/runner mode for interactive Hilbish.| |timeout(cb, time) -> @Timer|Executed the `cb` function after a period of `time`.| |which(name) -> string|Checks if `name` is a valid command.| +|runnerMode(mode)|Sets the execution/runner mode for interactive Hilbish.| +|run(cmd, streams)|Runs `cmd` in Hilbish's shell script interpreter.| ## Static module fields ||| @@ -408,72 +408,6 @@ Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. `string` **`prompt?`** Text to print before input, can be empty. - - -
-
-

-hilbish.run(cmd, streams) -> exitCode (number), stdout (string), stderr (string) - - - -

- -Runs `cmd` in Hilbish's shell script interpreter. -The `streams` parameter specifies the output and input streams the command should use. -For example, to write command output to a sink. -As a table, the caller can directly specify the standard output, error, and input -streams of the command with the table keys `out`, `err`, and `input` respectively. -As a boolean, it specifies whether the command should use standard output or return its output streams. - -#### Parameters -`string` **`cmd`** - - -`table|boolean` **`streams`** - - -#### Example -```lua - -// This code is the same as `ls -l | wc -l` -local fs = require 'fs' -local pr, pw = fs.pipe() -hilbish.run('ls -l', { - stdout = pw, - stderr = pw, -}) - -pw:close() - -hilbish.run('wc -l', { - stdin = pr -}) - -``` -
- -
-
-

-hilbish.runnerMode(mode) - - - -

- -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. -Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), -sh, and lua. It also accepts a function, to which if it is passed one -will call it to execute user input instead. -Read [about runner mode](../features/runner-mode) for more information. - -#### Parameters -`string|function` **`mode`** - -

@@ -519,8 +453,7 @@ Will return the path of the binary, or a basename if it's a commander.
## Sink -A sink is a structure that has input and/or output to/from -a desination. +A sink is a structure that has input and/or output to/from a desination. ### Methods #### autoFlush(auto) @@ -542,3 +475,65 @@ Writes data to a sink. #### writeln(str) Writes data to a sink with a newline at the end. +
+
+

+hilbish.run(cmd, streams) + + + +

+ +Runs `cmd` in Hilbish's shell script interpreter. +The `streams` parameter specifies the output and input streams the command should use. +For example, to write command output to a sink. +As a table, the caller can directly specify the standard output, error, and input +streams of the command with the table keys `out`, `err`, and `input` respectively. +As a boolean, it specifies whether the command should use standard output or return its output streams. +#### Parameters +`cmd` **`string`** + + +`streams` **`table|boolean`** + + +#### Example +```lua +-- This code is the same as `ls -l | wc -l` +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) +pw:close() +hilbish.run('wc -l', { + stdin = pr +}) +``` +
+ +
+
+

+hilbish.runnerMode(mode) + + + +

+ +Sets the execution/runner mode for interactive Hilbish. +**NOTE: This function is deprecated and will be removed in 3.0** +Use `hilbish.runner.setCurrent` instead. +This determines whether Hilbish wll try to run input as Lua +and/or sh or only do one of either. +Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +sh, and lua. It also accepts a function, to which if it is passed one +will call it to execute user input instead. +Read [about runner mode](../features/runner-mode) for more information. +#### Parameters +`mode` **`string|function`** + + +
+ diff --git a/docs/api/hilbish/hilbish.messages.md b/docs/api/hilbish/hilbish.messages.md new file mode 100644 index 0000000..705cfa2 --- /dev/null +++ b/docs/api/hilbish/hilbish.messages.md @@ -0,0 +1,135 @@ +--- +title: Module hilbish.messages +description: simplistic message passing +layout: doc +menu: + docs: + parent: "API" +--- + + +## Introduction +The messages interface defines a way for Hilbish-integrated commands, +user config and other tasks to send notifications to alert the user.z +The `hilbish.message` type is a table with the following keys: +`title` (string): A title for the message notification. +`text` (string): The contents of the message. +`channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks. +`summary` (string): A short summary of the `text`. +`icon` (string): Unicode (preferably standard emoji) icon for the message notification +`read` (boolean): Whether the full message has been read or not. + +## Functions +||| +|----|----| +|unreadCount()|Returns the amount of unread messages.| +|send(message)|Sends a message.| +|readAll()|Marks all messages as read.| +|read(idx)|Marks a message at `idx` as read.| +|delete(idx)|Deletes the message at `idx`.| +|clear()|Deletes all messages.| +|all()|Returns all messages.| +
+
+

+hilbish.messages.all() + + + +

+ +Returns all messages. +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.messages.clear() + + + +

+ +Deletes all messages. +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.messages.delete(idx) + + + +

+ +Deletes the message at `idx`. +#### Parameters +`idx` **`number`** + + +
+ +
+
+

+hilbish.messages.read(idx) + + + +

+ +Marks a message at `idx` as read. +#### Parameters +`idx` **`number`** + + +
+ +
+
+

+hilbish.messages.readAll() + + + +

+ +Marks all messages as read. +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.messages.send(message) + + + +

+ +Sends a message. +#### Parameters +`message` **`hilbish.message`** + + +
+ +
+
+

+hilbish.messages.unreadCount() + + + +

+ +Returns the amount of unread messages. +#### Parameters +This function has no parameters. +
+ diff --git a/docs/api/hilbish/hilbish.runner.md b/docs/api/hilbish/hilbish.runner.md index 8c89a52..4ba4999 100644 --- a/docs/api/hilbish/hilbish.runner.md +++ b/docs/api/hilbish/hilbish.runner.md @@ -54,29 +54,16 @@ end) ## Functions ||| |----|----| -|setMode(cb)|This is the same as the `hilbish.runnerMode` function.| |lua(cmd)|Evaluates `cmd` as Lua input. This is the same as using `dofile`| -|sh(cmd)|Runs a command in Hilbish's shell script interpreter.| - -
-
-

-hilbish.runner.setMode(cb) - - - -

- -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. - -#### Parameters -`function` **`cb`** - - -
+|sh()|nil| +|setMode(mode)|**NOTE: This function is deprecated and will be removed in 3.0**| +|setCurrent(name)|Sets Hilbish's runner mode by name.| +|set(name, runner)|*Sets* a runner by name. The difference between this function and| +|run(input, priv)|Runs `input` with the currently set Hilbish runner.| +|getCurrent()|Returns the current runner by name.| +|get(name)|Get a runner by name.| +|exec(cmd, runnerName)|Executes `cmd` with a runner.| +|add(name, runner)|Adds a runner to the table of available runners.|
@@ -97,20 +84,164 @@ or `load`, but is appropriated for the runner interface.

-
+

-hilbish.runner.sh(cmd) - +hilbish.runner.add(name, runner) +

-Runs a command in Hilbish's shell script interpreter. -This is the equivalent of using `source`. - +Adds a runner to the table of available runners. +If runner is a table, it must have the run function in it. #### Parameters -`string` **`cmd`** +`name` **`string`** + Name of the runner + +`runner` **`function|table`** + + +
+ +
+
+

+hilbish.runner.exec(cmd, runnerName) + + + +

+ +Executes `cmd` with a runner. +If `runnerName` is not specified, it uses the default Hilbish runner. +#### Parameters +`cmd` **`string`** + + +`runnerName` **`string?`**
+
+
+

+hilbish.runner.get(name) + + + +

+ +Get a runner by name. +#### Parameters +`name` **`string`** + Name of the runner to retrieve. + +
+ +
+
+

+hilbish.runner.getCurrent() + + + +

+ +Returns the current runner by name. +#### Parameters +This function has no parameters. +
+ +
+
+

+hilbish.runner.run(input, priv) + + + +

+ +Runs `input` with the currently set Hilbish runner. +This method is how Hilbish executes commands. +`priv` is an optional boolean used to state if the input should be saved to history. +#### Parameters +`input` **`string`** + + +`priv` **`bool`** + + +
+ +
+
+

+hilbish.runner.set(name, runner) + + + +

+ +*Sets* a runner by name. The difference between this function and +add, is set will *not* check if the named runner exists. +The runner table must have the run function in it. +#### Parameters +`name` **`string`** + + +`runner` **`table`** + + +
+ +
+
+

+hilbish.runner.setCurrent(name) + + + +

+ +Sets Hilbish's runner mode by name. +#### Parameters +`name` **`string`** + + +
+ +
+
+

+hilbish.runner.setMode(mode) + + + +

+ +**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. +Or a string which names the runner mode to use. +#### Parameters +`mode` **`string|function`** + + +
+ +
+
+

+hilbish.runner.sh() + + + +

+ + +#### Parameters +This function has no parameters. +
+ diff --git a/docs/api/snail.md b/docs/api/snail.md new file mode 100644 index 0000000..f183306 --- /dev/null +++ b/docs/api/snail.md @@ -0,0 +1,50 @@ +--- +title: Module snail +description: shell script interpreter library +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +The snail library houses Hilbish's Lua wrapper of its shell script interpreter. +It's not very useful other than running shell scripts, which can be done with other +Hilbish functions. + +## Functions +||| +|----|----| +|new() -> @Snail|Creates a new Snail instance.| + +
+
+

+snail.new() -> Snail + + + +

+ +Creates a new Snail instance. + +#### Parameters +This function has no parameters. +
+ +## Types +
+ +## Snail +A Snail is a shell script interpreter instance. + +### Methods +#### dir(path) +Changes the directory of the snail instance. +The interpreter keeps its set directory even when the Hilbish process changes +directory, so this should be called on the `hilbish.cd` hook. + +#### run(command, streams) +Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams. + 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/docs/nature/dirs.md b/docs/nature/dirs.md index e49247c..7f25706 100644 --- a/docs/nature/dirs.md +++ b/docs/nature/dirs.md @@ -15,43 +15,11 @@ directories. ## Functions ||| |----|----| +|setOld(d)|Sets the old directory string.| |recent(idx)|Get entry from recent directories list based on index.| +|push(dir)|Add `dir` to the recent directories list.| |pop(num)|Remove the specified amount of dirs from the recent directories list.| |peak(num)|Look at `num` amount of recent directories, starting from the latest.| -|push(dir)|Add `dir` to the recent directories list.| -|setOld(d)|Sets the old directory string.| -
-
-

-dirs.setOld(d) - - - -

- -Sets the old directory string. -#### Parameters -`d` **`string`** - - -
- -
-
-

-dirs.push(dir) - - - -

- -Add `dir` to the recent directories list. -#### Parameters -`dir` **`string`** - - -
-

@@ -83,6 +51,22 @@ Remove the specified amount of dirs from the recent directories list. `num` **`number`** +

+ +
+
+

+dirs.push(dir) + + + +

+ +Add `dir` to the recent directories list. +#### Parameters +`dir` **`string`** + +

@@ -101,3 +85,19 @@ Get entry from recent directories list based on index.
+
+
+

+dirs.setOld(d) + + + +

+ +Sets the old directory string. +#### Parameters +`d` **`string`** + + +
+ diff --git a/docs/nature/doc.md b/docs/nature/doc.md index 6a2ca48..f940c0d 100644 --- a/docs/nature/doc.md +++ b/docs/nature/doc.md @@ -17,29 +17,9 @@ is by the Greenhouse pager. ## Functions ||| |----|----| +|renderInfoBlock(type, text)|Renders an info block. An info block is a block of text with| |renderCodeBlock(text)|Assembles and renders a code block. This returns| |highlight(text)|Performs basic Lua code highlighting.| -|renderInfoBlock(type, text)|Renders an info block. An info block is a block of text with| -
-
-

-doc.renderInfoBlock(type, text) - - - -

- -Renders an info block. An info block is a block of text with -an icon and styled text block. -#### Parameters -`type` **`string`** - Type of info block. The only one specially styled is the `warning`. - -`text` **`string`** - - -
-

@@ -74,3 +54,23 @@ and styles it to resemble a code block.

+
+
+

+doc.renderInfoBlock(type, text) + + + +

+ +Renders an info block. An info block is a block of text with +an icon and styled text block. +#### Parameters +`type` **`string`** + Type of info block. The only one specially styled is the `warning`. + +`text` **`string`** + + +
+ diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index b80a660..4b4bd8f 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -7,12 +7,6 @@ local hilbish = {} --- @param cmd string function hilbish.aliases.add(alias, cmd) end ---- 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. -function hilbish.runner.setMode(cb) end - --- Returns the current input line. function hilbish.editor.getLine() end @@ -131,24 +125,6 @@ function hilbish.prompt(str, typ) end --- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. function hilbish.read(prompt) end ---- Runs `cmd` in Hilbish's shell script interpreter. ---- The `streams` parameter specifies the output and input streams the command should use. ---- For example, to write command output to a sink. ---- As a table, the caller can directly specify the standard output, error, and input ---- streams of the command with the table keys `out`, `err`, and `input` respectively. ---- As a boolean, it specifies whether the command should use standard output or return its output streams. ---- -function hilbish.run(cmd, streams) end - ---- 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. ---- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), ---- sh, and lua. It also accepts a function, to which if it is passed one ---- will call it to execute user input instead. ---- Read [about runner mode](../features/runner-mode) for more information. -function hilbish.runnerMode(mode) end - --- Executed the `cb` function after a period of `time`. --- This creates a Timer that starts ticking immediately. function hilbish.timeout(cb, time) end @@ -168,28 +144,6 @@ function hilbish.jobs:foreground() end --- or `load`, but is appropriated for the runner interface. function hilbish.runner.lua(cmd) end ---- Sets/toggles the option of automatically flushing output. ---- A call with no argument will toggle the value. ---- @param auto boolean|nil -function hilbish:autoFlush(auto) end - ---- Flush writes all buffered input to the sink. -function hilbish:flush() end - ---- Reads a liine of input from the sink. ---- @returns string -function hilbish:read() end - ---- Reads all input from the sink. ---- @returns string -function hilbish:readAll() end - ---- Writes data to a sink. -function hilbish:write(str) end - ---- Writes data to a sink with a newline at the end. -function hilbish:writeln(str) end - --- Starts running the job. function hilbish.jobs:start() end @@ -200,10 +154,6 @@ function hilbish.jobs:stop() end --- It will throw if any error occurs. function hilbish.module.load(path) end ---- Runs a command in Hilbish's shell script interpreter. ---- This is the equivalent of using `source`. -function hilbish.runner.sh(cmd) end - --- Starts a timer. function hilbish.timers:start() end @@ -262,4 +212,26 @@ function hilbish.timers.create(type, time, callback) end --- Retrieves a timer via its ID. function hilbish.timers.get(id) end +--- Sets/toggles the option of automatically flushing output. +--- A call with no argument will toggle the value. +--- @param auto boolean|nil +function hilbish:autoFlush(auto) end + +--- Flush writes all buffered input to the sink. +function hilbish:flush() end + +--- Reads a liine of input from the sink. +--- @returns string +function hilbish:read() end + +--- Reads all input from the sink. +--- @returns string +function hilbish:readAll() end + +--- Writes data to a sink. +function hilbish:write(str) end + +--- Writes data to a sink with a newline at the end. +function hilbish:writeln(str) end + return hilbish diff --git a/emmyLuaDocs/snail.lua b/emmyLuaDocs/snail.lua new file mode 100644 index 0000000..94c84df --- /dev/null +++ b/emmyLuaDocs/snail.lua @@ -0,0 +1,16 @@ +--- @meta + +local snail = {} + +--- Changes the directory of the snail instance. +--- The interpreter keeps its set directory even when the Hilbish process changes +--- directory, so this should be called on the `hilbish.cd` hook. +function snail:dir(path) end + +--- Creates a new Snail instance. +function snail.new() end + +--- Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams. +function snail:run(command, streams) end + +return snail diff --git a/emmyLuaDocs/util.lua b/emmyLuaDocs/util.lua new file mode 100644 index 0000000..9f8d634 --- /dev/null +++ b/emmyLuaDocs/util.lua @@ -0,0 +1,83 @@ +--- @meta + +local util = {} + +--- +function util.AbbrevHome changes the user's home directory in the path string to ~ (tilde) end + +--- +function util. end + +--- +function util.DoFile runs the contents of the file in the Lua runtime. end + +--- +function util.DoString runs the code string in the Lua runtime. end + +--- directory. +function util.ExpandHome expands ~ (tilde) in the path, changing it to the user home end + +--- +function util. end + +--- +function util.ForEach loops through a Lua table. end + +--- +function util. end + +--- a string and a closure. +function util.HandleStrCallback handles function parameters for Go functions which take end + +--- +function util. end + +--- +function util. end + +--- +function util.SetExports puts the Lua function exports in the table. end + +--- It is accessible via the __docProp metatable. It is a table of the names of the fields. +function util.SetField sets a field in a table, adding docs for it. end + +--- is one which has a metatable proxy to ensure no overrides happen to it. +--- It sets the field in the table and sets the __docProp metatable on the +--- user facing table. +function util.SetFieldProtected sets a field in a protected table. A protected table end + +--- Sets/toggles the option of automatically flushing output. +--- A call with no argument will toggle the value. +--- @param auto boolean|nil +function util:autoFlush(auto) end + +--- Flush writes all buffered input to the sink. +function util:flush() end + +--- +function util. end + +--- Reads a liine of input from the sink. +--- @returns string +function util:read() end + +--- Reads all input from the sink. +--- @returns string +function util:readAll() end + +--- Writes data to a sink. +function util:write(str) end + +--- Writes data to a sink with a newline at the end. +function util:writeln(str) end + +--- +function util. end + +--- +function util. end + +--- +function util. end + +return util diff --git a/exec.go b/exec.go index 7f8e37b..4ed53a0 100644 --- a/exec.go +++ b/exec.go @@ -1,215 +1,26 @@ package main import ( - "bytes" - "context" "errors" - "os/exec" "fmt" - "io" "os" - "os/signal" - "path/filepath" - "runtime" "strings" - "syscall" - "time" - - "hilbish/util" rt "github.com/arnodel/golua/runtime" - "mvdan.cc/sh/v3/shell" //"github.com/yuin/gopher-lua/parse" - "mvdan.cc/sh/v3/interp" - "mvdan.cc/sh/v3/syntax" - "mvdan.cc/sh/v3/expand" ) var errNotExec = errors.New("not executable") var errNotFound = errors.New("not found") -var runnerMode rt.Value = rt.StringValue("hybrid") - -type 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) - - 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 - } - - if cont { - input, err = continuePrompt(input, newline) - if err == nil { - goto rerun - } else if err == io.EOF { - lr.SetPrompt(fmtPrompt(prompt)) - } - } - - if err != nil && err != io.EOF { - if exErr, ok := isExecError(err); ok { - hooks.Emit("command." + exErr.typ, exErr.cmd) - } else { - fmt.Fprintln(os.Stderr, err) - } - } - cmdFinish(exitCode, input, priv) -} - -func reprompt(input string, newline bool) (string, error) { - for { - /* - if strings.HasSuffix(input, "\\") { - input = strings.TrimSuffix(input, "\\") + "\n" - } - */ - in, err := continuePrompt(input, newline) - if err != nil { - lr.SetPrompt(fmtPrompt(prompt)) - return input, err - } - - return in, nil - } -} - -func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, newline bool, runnerErr, err error) { - term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 3, false) - err = rt.Call(l.MainThread(), runr, []rt.Value{rt.StringValue(userInput)}, term) + runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run")) + _, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv)) if err != nil { - return "", 124, false, false, nil, err + fmt.Fprintln(os.Stderr, err) } - - var runner *rt.Table - var ok bool - runnerRet := term.Get(0) - if runner, ok = runnerRet.TryTable(); !ok { - fmt.Fprintln(os.Stderr, "runner did not return a table") - exitCode = 125 - input = userInput - return - } - - if code, ok := runner.Get(rt.StringValue("exitCode")).TryInt(); ok { - exitCode = uint8(code) - } - - if inp, ok := runner.Get(rt.StringValue("input")).TryString(); ok { - input = inp - } - - if errStr, ok := runner.Get(rt.StringValue("err")).TryString(); ok { - runnerErr = fmt.Errorf("%s", errStr) - } - - if c, ok := runner.Get(rt.StringValue("continue")).TryBool(); ok { - continued = c - } - - if nl, ok := runner.Get(rt.StringValue("newline")).TryBool(); ok { - newline = nl - } - return } func handleLua(input string) (string, uint8, error) { @@ -239,326 +50,13 @@ 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 { - // If input is incomplete, start multiline prompting - if syntax.IsIncomplete(err) { - if !interactive { - return cmdString, 126, false, false, err - } - - newline := false - if strings.Contains(err.Error(), "unclosed here-document") { - newline = true - } - return cmdString, 126, true, newline, err - } else { - if code, ok := interp.IsExitStatus(err); ok { - return cmdString, code, false, false, nil - } else { - return cmdString, 126, false, false, err - } - } - } - - 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 +72,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) } @@ -601,11 +83,3 @@ func splitInput(input string) ([]string, string) { return cmdArgs, cmdstr.String() } - -func cmdFinish(code uint8, cmdstr string, private bool) { - util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code))) - // using AsValue (to convert to lua type) on an interface which is an int - // results in it being unknown in lua .... ???? - // so we allow the hook handler to take lua runtime Values - hooks.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private) -} 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..5850f37 --- /dev/null +++ b/golibs/snail/lua.go @@ -0,0 +1,221 @@ +// shell script interpreter library +/* +The snail library houses Hilbish's Lua wrapper of its shell script interpreter. +It's not very useful other than running shell scripts, which can be done with other +Hilbish functions. +*/ +package snail + +import ( + "errors" + "fmt" + "io" + "strings" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" + "github.com/arnodel/golua/lib/iolib" + "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": {snailrun, 3, false}, + "dir": {snaildir, 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{snailnew, 0, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return rt.TableValue(mod), nil +} + +// new() -> @Snail +// Creates a new Snail instance. +func snailnew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + s := New(t.Runtime) + return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil +} + +// #member +// run(command, streams) +// Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams. +// #param command string +// #param streams table +func snailrun(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 + } + + streams := &util.Streams{} + thirdArg := c.Arg(2) + switch thirdArg.Type() { + case rt.TableType: + args := thirdArg.AsTable() + + if luastreams, ok := args.Get(rt.StringValue("sinks")).TryTable(); ok { + handleStream(luastreams.Get(rt.StringValue("out")), streams, false, false) + handleStream(luastreams.Get(rt.StringValue("err")), streams, true, false) + handleStream(luastreams.Get(rt.StringValue("input")), streams, false, true) + } + case rt.NilType: // noop + default: + return nil, errors.New("expected 3rd arg to be a table") + } + + var newline bool + var cont bool + var luaErr rt.Value = rt.NilValue + exitCode := 0 + bg, _, _, err := s.Run(cmd, streams) + 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 +} + +// #member +// dir(path) +// Changes the directory of the snail instance. +// The interpreter keeps its set directory even when the Hilbish process changes +// directory, so this should be called on the `hilbish.cd` hook. +// #param path string Has to be an absolute path. +func snaildir(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 + } + + dir, err := c.StringArg(1) + if err != nil { + return nil, err + } + + interp.Dir(dir)(s.runner) + return c.Next(), nil +} + +func handleStream(v rt.Value, strms *util.Streams, errStream, inStream bool) error { + if v == rt.NilValue { + return nil + } + + ud, ok := v.TryUserData() + if !ok { + return errors.New("expected metatable argument") + } + + val := ud.Value() + var varstrm io.ReadWriter + if f, ok := val.(*iolib.File); ok { + varstrm = f.Handle() + } + + if f, ok := val.(*util.Sink); ok { + varstrm = f.Rw + } + + if varstrm == nil { + return errors.New("expected either a sink or file") + } + + if errStream { + strms.Stderr = varstrm + } else if inStream { + strms.Stdin = varstrm + } else { + strms.Stdout = varstrm + } + + return 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..3ca1d12 --- /dev/null +++ b/golibs/snail/snail.go @@ -0,0 +1,302 @@ +package snail + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "time" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "mvdan.cc/sh/v3/shell" + //"github.com/yuin/gopher-lua/parse" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" + "mvdan.cc/sh/v3/expand" +) + +// #type +// A Snail is a shell script interpreter instance. +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 := util.NewSinkInput(s.runtime, hc.Stdin) + stdout := util.NewSinkOutput(s.runtime, hc.Stdout) + stderr := util.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 923fe91..859a39d 100644 --- a/lua.go +++ b/lua.go @@ -9,6 +9,7 @@ import ( "hilbish/golibs/bait" "hilbish/golibs/commander" "hilbish/golibs/fs" + "hilbish/golibs/snail" "hilbish/golibs/terminal" rt "github.com/arnodel/golua/runtime" @@ -24,16 +25,14 @@ func luaInit() { MessageHandler: debuglib.Traceback, }) lib.LoadAll(l) - 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 bd6f03d..77b1847 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,7 +37,6 @@ var ( cmds *commander.Commander defaultConfPath string defaultHistPath string - runner *interp.Runner ) func main() { @@ -58,7 +56,6 @@ func main() { } } - runner, _ = interp.New() curuser, _ = user.Current() confDir, _ = os.UserConfigDir() @@ -327,15 +324,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..9f532ca 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,13 @@ commander.register('cd', function (args, sinks) sinks.out:writeln(path) end - dirs.setOld(hilbish.cwd()) - dirs.push(path) - + local absPath = fs.abs(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', absPath, oldPath) end) diff --git a/nature/dirs.lua b/nature/dirs.lua index 6265c37..db55954 100644 --- a/nature/dirs.lua +++ b/nature/dirs.lua @@ -2,6 +2,7 @@ -- internal directory management -- The dirs module defines a small set of functions to store and manage -- directories. +local bait = require 'bait' local fs = require 'fs' local dirs = {} @@ -47,11 +48,11 @@ end --- @param dir string function dirs.push(dir) dirs.recentDirs[dirs.recentSize + 1] = nil - if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then - ok, d = pcall(fs.abs, d) - assert(ok, 'could not turn "' .. d .. '"into an absolute path') + if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then + local ok, dir = pcall(fs.abs, dir) + assert(ok, 'could not turn "' .. dir .. '"into an absolute path') - table.insert(dirs.recentDirs, 1, d) + table.insert(dirs.recentDirs, 1, dir) end end @@ -77,4 +78,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/hilbish.lua b/nature/hilbish.lua new file mode 100644 index 0000000..d6a869c --- /dev/null +++ b/nature/hilbish.lua @@ -0,0 +1,78 @@ +-- @module hilbish +local bait = require 'bait' +local snail = require 'snail' + +hilbish.snail = snail.new() +bait.catch('hilbish.cd', function(path) + hilbish.snail:dir(path) +end) +--- Runs `cmd` in Hilbish's shell script interpreter. +--- The `streams` parameter specifies the output and input streams the command should use. +--- For example, to write command output to a sink. +--- As a table, the caller can directly specify the standard output, error, and input +--- streams of the command with the table keys `out`, `err`, and `input` respectively. +--- As a boolean, it specifies whether the command should use standard output or return its output streams. +--- #example +--- -- This code is the same as `ls -l | wc -l` +--- local fs = require 'fs' +--- local pr, pw = fs.pipe() +--- hilbish.run('ls -l', { +--- stdout = pw, +--- stderr = pw, +--- }) +--- pw:close() +--- hilbish.run('wc -l', { +--- stdin = pr +--- }) +--- #example +-- @param cmd string +-- @param streams table|boolean +-- @returns number, string, string +function hilbish.run(cmd, streams) + local sinks = {} + + if type(streams) == 'boolean' then + if not streams then + sinks = { + out = hilbish.sink.new(), + err = hilbish.sink.new(), + input = io.stdin + } + end + elseif type(streams) == 'table' then + sinks = streams + end + + local out = hilbish.snail:run(cmd, {sinks = sinks}) + local returns = {out} + + if type(streams) == 'boolean' and not streams then + table.insert(returns, sinks.out:readAll()) + table.insert(returns, sinks.err:readAll()) + end + + return table.unpack(returns) +end + +--- Sets the execution/runner mode for interactive Hilbish. +--- **NOTE: This function is deprecated and will be removed in 3.0** +--- Use `hilbish.runner.setCurrent` instead. +--- This determines whether Hilbish wll try to run input as Lua +--- and/or sh or only do one of either. +--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +--- sh, and lua. It also accepts a function, to which if it is passed one +--- will call it to execute user input instead. +--- Read [about runner mode](../features/runner-mode) for more information. +-- @param mode string|function +function hilbish.runnerMode(mode) + if type(mode) == 'string' then + hilbish.runner.setCurrent(mode) + elseif type(mode) == 'function' then + hilbish.runner.set('_', { + run = mode + }) + hilbish.runner.setCurrent '_' + else + error('expected runner mode type to be either string or function, got', type(mode)) + end +end diff --git a/nature/init.lua b/nature/init.lua index a0579d7..4c47bfe 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -18,6 +18,8 @@ table.insert(package.searchers, function(module) return function() return hilbish.module.load(path) end, path end) +require 'nature.hilbish' + require 'nature.commands' require 'nature.completions' require 'nature.opts' diff --git a/nature/runner.lua b/nature/runner.lua index 86c2ad9..427fb7e 100644 --- a/nature/runner.lua +++ b/nature/runner.lua @@ -1,4 +1,5 @@ -- @module hilbish.runner +local snail = require 'snail' local currentRunner = 'hybrid' local runners = {} @@ -71,10 +72,8 @@ end --- Sets Hilbish's runner mode by name. --- @param name string function hilbish.runner.setCurrent(name) - local r = hilbish.runner.get(name) + hilbish.runner.get(name) -- throws if it doesnt exist. currentRunner = name - - hilbish.runner.setMode(r.run) end --- Returns the current runner by name. @@ -83,6 +82,81 @@ function hilbish.runner.getCurrent() return currentRunner end +--- **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. +--- Or a string which names the runner mode to use. +-- @param mode string|function +function hilbish.runner.setMode(mode) + hilbish.runnerMode(mode) +end + +local function finishExec(exitCode, input, priv) + hilbish.exitCode = exitCode + bait.throw('command.exit', exitCode, input, priv) +end + +local function continuePrompt(prev, newline) + local multilinePrompt = hilbish.multiprompt() + -- the return of hilbish.read is nil when error or ctrl-d + local cont = hilbish.read(multilinePrompt) + if not cont then + return + end + + if newline then + cont = '\n' .. cont + end + + if cont:match '\\$' then + cont = cont:gsub('\\$', '') .. '\n' + end + + return prev .. cont +end + +--- Runs `input` with the currently set Hilbish runner. +--- This method is how Hilbish executes commands. +--- `priv` is an optional boolean used to state if the input should be saved to history. +-- @param input string +-- @param priv bool +function hilbish.runner.run(input, priv) + local command = hilbish.aliases.resolve(input) + bait.throw('command.preexec', input, command) + + ::rerun:: + local runner = hilbish.runner.get(currentRunner) + local ok, out = pcall(runner.run, input) + if not ok then + io.stderr:write(out .. '\n') + finishExec(124, out.input, priv) + return + end + + if out.continue then + local contInput = continuePrompt(input, out.newline) + if contInput then + input = contInput + goto rerun + end + end + + if out.err then + local fields = string.split(out.err, ': ') + if fields[2] == 'not-found' or fields[2] == 'not-executable' then + bait.throw('command.' .. fields[2], fields[1]) + else + io.stderr:write(out.err .. '\n') + end + end + finishExec(out.exitCode, out.input, priv) +end + +function hilbish.runner.sh(input) + return hilbish.snail:run(input) +end + hilbish.runner.add('hybrid', function(input) local cmdStr = hilbish.aliases.resolve(input) @@ -109,7 +183,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..9e7a3ff 100644 --- a/runnermode.go +++ b/runnermode.go @@ -53,9 +53,7 @@ end) */ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ - "sh": {shRunner, 1, false}, "lua": {luaRunner, 1, false}, - "setMode": {hlrunnerMode, 1, false}, } mod := rt.NewTable() @@ -64,44 +62,6 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { return mod } -// #interface runner -// setMode(cb) -// 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. -// #param cb function -func _runnerMode() {} - -// #interface runner -// sh(cmd) -// 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 - } - cmd, err := c.StringArg(0) - if err != nil { - return nil, err - } - - _, exitCode, cont, newline, err := execSh(aliases.Resolve(cmd)) - var luaErr rt.Value = rt.NilValue - if err != nil { - 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) - - return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil -} - // #interface runner // lua(cmd) // Evaluates `cmd` as Lua input. This is the same as using `dofile` diff --git a/sink.go b/util/sink.go similarity index 70% rename from sink.go rename to util/sink.go index 3aa5507..8d1167e 100644 --- a/sink.go +++ b/util/sink.go @@ -1,35 +1,32 @@ -package main +package util import ( "bufio" + "bytes" "fmt" "io" "os" "strings" - "hilbish/util" - rt "github.com/arnodel/golua/runtime" ) var sinkMetaKey = rt.StringValue("hshsink") // #type -// A sink is a structure that has input and/or output to/from -// a desination. -type sink struct{ - writer *bufio.Writer - reader *bufio.Reader +// A sink is a structure that has input and/or output to/from a desination. +type Sink struct{ + Rw *bufio.ReadWriter file *os.File - ud *rt.UserData + UserData *rt.UserData autoFlush bool } -func setupSinkType(rtm *rt.Runtime) { +func SinkLoader(rtm *rt.Runtime) *rt.Table { sinkMeta := rt.NewTable() sinkMethods := rt.NewTable() - sinkFuncs := map[string]util.LuaExport{ + sinkFuncs := map[string]LuaExport{ "flush": {luaSinkFlush, 1, false}, "read": {luaSinkRead, 1, false}, "readAll": {luaSinkReadAll, 1, false}, @@ -37,7 +34,7 @@ func setupSinkType(rtm *rt.Runtime) { "write": {luaSinkWrite, 2, false}, "writeln": {luaSinkWriteln, 2, false}, } - util.SetExports(l, sinkMethods, sinkFuncs) + SetExports(rtm, sinkMethods, sinkFuncs) sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { s, _ := sinkArg(c, 0) @@ -64,10 +61,25 @@ 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)) + + exports := map[string]LuaExport{ + "new": {luaSinkNew, 0, false}, + } + + mod := rt.NewTable() + SetExports(rtm, mod, exports) + + return mod } +func luaSinkNew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + snk := NewSink(t.Runtime, new(bytes.Buffer)) + + return c.PushingNext1(t.Runtime, rt.UserDataValue(snk.UserData)), nil +} + // #member // readAll() -> string // --- @returns string @@ -84,7 +96,7 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { lines := []string{} for { - line, err := s.reader.ReadString('\n') + line, err := s.Rw.ReadString('\n') if err != nil { if err == io.EOF { break @@ -113,7 +125,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - str, _ := s.reader.ReadString('\n') + str, _ := s.Rw.ReadString('\n') return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil } @@ -135,9 +147,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - s.writer.Write([]byte(data)) + s.Rw.Write([]byte(data)) if s.autoFlush { - s.writer.Flush() + s.Rw.Flush() } return c.Next(), nil @@ -160,9 +172,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - s.writer.Write([]byte(data + "\n")) + s.Rw.Write([]byte(data + "\n")) if s.autoFlush { - s.writer.Flush() + s.Rw.Flush() } return c.Next(), nil @@ -181,7 +193,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - s.writer.Flush() + s.Rw.Flush() return c.Next(), nil } @@ -212,11 +224,24 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func newSinkInput(r io.Reader) *sink { - s := &sink{ - reader: bufio.NewReader(r), +func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink { + s := &Sink{ + Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)), } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) + + if f, ok := Rw.(*os.File); ok { + s.file = f + } + + return s +} + +func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink { + s := &Sink{ + Rw: bufio.NewReadWriter(bufio.NewReader(r), nil), + } + s.UserData = sinkUserData(rtm, s) if f, ok := r.(*os.File); ok { s.file = f @@ -225,17 +250,17 @@ func newSinkInput(r io.Reader) *sink { return s } -func newSinkOutput(w io.Writer) *sink { - s := &sink{ - writer: bufio.NewWriter(w), +func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink { + s := &Sink{ + Rw: bufio.NewReadWriter(nil, 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 +269,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 } }