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.abbr.md b/docs/api/hilbish/hilbish.abbr.md new file mode 100644 index 0000000..8e88c19 --- /dev/null +++ b/docs/api/hilbish/hilbish.abbr.md @@ -0,0 +1,67 @@ +--- +title: Module hilbish.abbr +description: command line abbreviations +layout: doc +menu: + docs: + parent: "API" +--- + + +## Introduction +The abbr module manages Hilbish abbreviations. These are words that can be replaced +with longer command line strings when entered. +As an example, `git push` can be abbreviated to `gp`. When the user types +`gp` into the command line, after hitting space or enter, it will expand to `git push`. +Abbreviations can be used as an alternative to aliases. They are saved entirely in the history +Instead of the aliased form of the same command. + +## Functions +||| +|----|----| +|remove(abbr)|Removes the named `abbr`.| +|add(abbr, expanded|function, opts)|Adds an abbreviation. The `abbr` is the abbreviation itself,| +
+
+

+hilbish.abbr.add(abbr, expanded|function, opts) + + + +

+ +Adds an abbreviation. The `abbr` is the abbreviation itself, +while `expanded` is what the abbreviation should expand to. +It can be either a function or a string. If it is a function, it will expand to what +the function returns. +`opts` is a table that accepts 1 key: `anywhere`. +`opts.anywhere` defines whether the abbr expands anywhere in the command line or not, +whereas the default behavior is only at the beginning of the line +#### Parameters +`abbr` **`string`** + + +`expanded|function` **`string`** + + +`opts` **`table`** + + +
+ +
+
+

+hilbish.abbr.remove(abbr) + + + +

+ +Removes the named `abbr`. +#### Parameters +`abbr` **`string`** + + +
+ diff --git a/docs/api/hilbish/hilbish.editor.md b/docs/api/hilbish/hilbish.editor.md index c70b605..6dac64b 100644 --- a/docs/api/hilbish/hilbish.editor.md +++ b/docs/api/hilbish/hilbish.editor.md @@ -14,12 +14,30 @@ directly interact with the line editor in use. ## Functions ||| |----|----| +|deleteByAmount(amount)|Deletes characters in the line by the given amount.| |getLine() -> string|Returns the current input line.| |getVimRegister(register) -> string|Returns the text that is at the register.| |insert(text)|Inserts text into the Hilbish command line.| |getChar() -> string|Reads a keystroke from the user. This is in a format of something like Ctrl-L.| |setVimRegister(register, text)|Sets the vim register at `register` to hold the passed text.| +
+
+

+hilbish.editor.deleteByAmount(amount) + + + +

+ +Deletes characters in the line by the given amount. + +#### Parameters +`number` **`amount`** + + +
+

@@ -96,6 +114,9 @@ hilbish.editor.setVimRegister(register, text) Sets the vim register at `register` to hold the passed text. #### Parameters +`string` **`register`** + + `string` **`text`** 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/editor.go b/editor.go index 9c49440..2c04f25 100644 --- a/editor.go +++ b/editor.go @@ -17,6 +17,7 @@ func editorLoader(rtm *rt.Runtime) *rt.Table { "getVimRegister": {editorGetRegister, 2, false}, "getLine": {editorGetLine, 0, false}, "readChar": {editorReadChar, 0, false}, + "deleteByAmount": {editorDeleteByAmount, 1, false}, } mod := rt.NewTable() @@ -47,7 +48,7 @@ func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // #interface editor // setVimRegister(register, text) // Sets the vim register at `register` to hold the passed text. -// #aram register string +// #param register string // #param text string func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { @@ -106,3 +107,22 @@ func editorReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil } + +// #interface editor +// deleteByAmount(amount) +// Deletes characters in the line by the given amount. +// #param amount number +func editorDeleteByAmount(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + amount, err := c.IntArg(0) + if err != nil { + return nil, err + } + + lr.rl.DeleteByAmount(int(amount)) + + return c.Next(), nil +} diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index b80a660..a2935bb 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -7,11 +7,8 @@ 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 +--- Deletes characters in the line by the given amount. +function hilbish.editor.deleteByAmount(amount) end --- Returns the current input line. function hilbish.editor.getLine() end @@ -131,24 +128,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 +147,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 +157,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 +215,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/abbr.lua b/nature/abbr.lua new file mode 100644 index 0000000..cbe89ff --- /dev/null +++ b/nature/abbr.lua @@ -0,0 +1,61 @@ +-- @module hilbish.abbr +-- command line abbreviations +-- The abbr module manages Hilbish abbreviations. These are words that can be replaced +-- with longer command line strings when entered. +-- As an example, `git push` can be abbreviated to `gp`. When the user types +-- `gp` into the command line, after hitting space or enter, it will expand to `git push`. +-- Abbreviations can be used as an alternative to aliases. They are saved entirely in the history +-- Instead of the aliased form of the same command. +local bait = require 'bait' +local hilbish = require 'hilbish' +hilbish.abbr = { + all = {} +} + +--- Adds an abbreviation. The `abbr` is the abbreviation itself, +--- while `expanded` is what the abbreviation should expand to. +--- It can be either a function or a string. If it is a function, it will expand to what +--- the function returns. +--- `opts` is a table that accepts 1 key: `anywhere`. +--- `opts.anywhere` defines whether the abbr expands anywhere in the command line or not, +--- whereas the default behavior is only at the beginning of the line +-- @param abbr string +-- @param expanded|function string +-- @param opts table +function hilbish.abbr.add(abbr, expanded, opts) + print(abbr, expanded, opts) + opts = opts or {} + opts.abbr = abbr + opts.expand = expanded + hilbish.abbr.all[abbr] = opts +end + +--- Removes the named `abbr`. +-- @param abbr string +function hilbish.abbr.remove(abbr) + hilbish.abbr.all[abbr] = nil +end + +bait.catch('hilbish.rawInput', function(c) + -- 0x0d == enter + if c == ' ' or c == string.char(0x0d) then + -- check if the last "word" was a valid abbreviation + local line = hilbish.editor.getLine() + local lineSplits = string.split(line, ' ') + local thisAbbr = hilbish.abbr.all[lineSplits[#lineSplits]] + + if thisAbbr and (#lineSplits == 1 or thisAbbr.anywhere == true) then + hilbish.editor.deleteByAmount(-lineSplits[#lineSplits]:len()) + if type(thisAbbr.expand) == 'string' then + hilbish.editor.insert(thisAbbr.expand) + elseif type(thisAbbr.expand) == 'function' then + local expandRet = thisAbbr.expand() + if type(expandRet) ~= 'string' then + print(string.format('abbr %s has an expand function that did not return a string. instead it returned: %s', thisAbbr.abbr, expandRet)) + return + end + hilbish.editor.insert(expandRet) + end + end + end +end) 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..cd4fd7a 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -18,12 +18,15 @@ 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' require 'nature.vim' require 'nature.runner' require 'nature.hummingbird' +require 'nature.abbr' local shlvl = tonumber(os.getenv 'SHLVL') if shlvl ~= nil then 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/readline/completers/command-arguments.go b/readline/completers/command-arguments.go deleted file mode 100644 index 912ac7e..0000000 --- a/readline/completers/command-arguments.go +++ /dev/null @@ -1,23 +0,0 @@ -package completers - -import ( - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// CompleteCommandArguments - Completes all values for arguments to a command. -// Arguments here are different from command options (--option). -// Many categories, from multiple sources in multiple contexts -func completeCommandArguments(cmd *flags.Command, arg string, lastWord string) (prefix string, completions []*readline.CompletionGroup) { - - // the prefix is the last word, by default - prefix = lastWord - - // SEE completeOptionArguments FOR A WAY TO ADD COMPLETIONS TO SPECIFIC ARGUMENTS ------------------------------ - - // found := argumentByName(cmd, arg) - // var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions - - return -} diff --git a/readline/completers/env.go b/readline/completers/env.go deleted file mode 100644 index ae77aab..0000000 --- a/readline/completers/env.go +++ /dev/null @@ -1,124 +0,0 @@ -package completers - -import ( - "os" - "strings" - - "github.com/maxlandon/readline" -) - -// completeEnvironmentVariables - Returns all environment variables as suggestions -func completeEnvironmentVariables(lastWord string) (last string, completions []*readline.CompletionGroup) { - - // Check if last input is made of several different variables - allVars := strings.Split(lastWord, "/") - lastVar := allVars[len(allVars)-1] - - var evaluated = map[string]string{} - - grp := &readline.CompletionGroup{ - Name: "console OS environment", - MaxLength: 5, // Should be plenty enough - DisplayType: readline.TabDisplayGrid, - TrimSlash: true, // Some variables can be paths - } - - for k, v := range clientEnv { - if strings.HasPrefix("$"+k, lastVar) { - grp.Suggestions = append(grp.Suggestions, "$"+k+"/") - evaluated[k] = v - } - } - - completions = append(completions, grp) - - return lastVar, completions -} - -// clientEnv - Contains all OS environment variables, client-side. -// This is used for things like downloading/uploading files from localhost, etc., -// therefore we need completion and parsing stuff, sometimes. -var clientEnv = map[string]string{} - -// ParseEnvironmentVariables - Parses a line of input and replace detected environment variables with their values. -func ParseEnvironmentVariables(args []string) (processed []string, err error) { - - for _, arg := range args { - - // Anywhere a $ is assigned means there is an env variable - if strings.Contains(arg, "$") || strings.Contains(arg, "~") { - - //Split in case env is embedded in path - envArgs := strings.Split(arg, "/") - - // If its not a path - if len(envArgs) == 1 { - processed = append(processed, handleCuratedVar(arg)) - } - - // If len of the env var split is > 1, its a path - if len(envArgs) > 1 { - processed = append(processed, handleEmbeddedVar(arg)) - } - } else if arg != "" && arg != " " { - // Else, if arg is not an environment variable, return it as is - processed = append(processed, arg) - } - - } - return -} - -// handleCuratedVar - Replace an environment variable alone and without any undesired characters attached -func handleCuratedVar(arg string) (value string) { - if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" { - envVar := strings.TrimPrefix(arg, "$") - val, ok := clientEnv[envVar] - if !ok { - return envVar - } - return val - } - if arg != "" && arg == "~" { - return clientEnv["HOME"] - } - - return arg -} - -// handleEmbeddedVar - Replace an environment variable that is in the middle of a path, or other one-string combination -func handleEmbeddedVar(arg string) (value string) { - - envArgs := strings.Split(arg, "/") - var path []string - - for _, arg := range envArgs { - if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" { - envVar := strings.TrimPrefix(arg, "$") - val, ok := clientEnv[envVar] - if !ok { - // Err will be caught when command is ran anyway, or completion will stop... - path = append(path, arg) - } - path = append(path, val) - } else if arg != "" && arg == "~" { - path = append(path, clientEnv["HOME"]) - } else if arg != " " && arg != "" { - path = append(path, arg) - } - } - - return strings.Join(path, "/") -} - -// loadClientEnv - Loads all user environment variables -func loadClientEnv() error { - env := os.Environ() - - for _, kv := range env { - key := strings.Split(kv, "=")[0] - value := strings.Split(kv, "=")[1] - clientEnv[key] = value - } - return nil -} diff --git a/readline/completers/hint-completer.go b/readline/completers/hint-completer.go deleted file mode 100644 index e838961..0000000 --- a/readline/completers/hint-completer.go +++ /dev/null @@ -1,180 +0,0 @@ -package completers - -import ( - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// HintCompleter - Entrypoint to all hints in the Wiregost console -func (c *CommandCompleter) HintCompleter(line []rune, pos int) (hint []rune) { - - // Format and sanitize input - // @args => All items of the input line - // @last => The last word detected in input line as []rune - // @lastWord => The last word detected in input as string - args, last, lastWord := formatInput(line) - - // Detect base command automatically - var command = c.detectedCommand(args) - - // Menu hints (command line is empty, or nothing recognized) - if noCommandOrEmpty(args, last, command) { - hint = MenuHint(args, last) - } - - // Check environment variables - if envVarAsked(args, lastWord) { - return envVarHint(args, last) - } - - // Command Hint - if commandFound(command) { - - // Command hint by default (no space between cursor and last command character) - hint = CommandHint(command) - - // Check environment variables - if envVarAsked(args, lastWord) { - return envVarHint(args, last) - } - - // If options are asked for root command, return commpletions. - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - hint = OptionArgumentHint(args, last, opt) - } - } - } - - // If command has args, hint for args - if arg, yes := commandArgumentRequired(lastWord, args, command); yes { - hint = []rune(CommandArgumentHints(args, last, command, arg)) - } - - // Brief subcommand hint - if lastIsSubCommand(lastWord, command) { - hint = []rune(commandHint + command.Find(string(last)).ShortDescription) - } - - // Handle subcommand if found - if sub, ok := subCommandFound(lastWord, args, command); ok { - return HandleSubcommandHints(args, last, sub) - } - - } - - // Handle system binaries, shell commands, etc... - if commandFoundInPath(args[0]) { - // hint = []rune(exeHint + util.ParseSummary(util.GetManPages(args[0]))) - } - - return -} - -// CommandHint - Yields the hint of a Wiregost command -func CommandHint(command *flags.Command) (hint []rune) { - return []rune(commandHint + command.ShortDescription) -} - -// HandleSubcommandHints - Handles hints for a subcommand and its arguments, options, etc. -func HandleSubcommandHints(args []string, last []rune, command *flags.Command) (hint []rune) { - - // If command has args, hint for args - if arg, yes := commandArgumentRequired(string(last), args, command); yes { - hint = []rune(CommandArgumentHints(args, last, command, arg)) - return - } - - // Environment variables - if envVarAsked(args, string(last)) { - hint = envVarHint(args, last) - } - - // If the last word in input is an option --name, yield argument hint if needed - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - hint = OptionArgumentHint(args, last, opt) - } - } - } - - // If user asks for completions with "-" or "--". - // (Note: This takes precedence on any argument hints, as it is evaluated after them) - if commandOptionsAsked(args, string(last), command) { - return OptionHints(args, last, command) - } - - return -} - -// CommandArgumentHints - Yields hints for arguments to commands if they have some -func CommandArgumentHints(args []string, last []rune, command *flags.Command, arg string) (hint []rune) { - - found := argumentByName(command, arg) - // Base Hint is just a description of the command argument - hint = []rune(argHint + found.Description) - - return -} - -// ModuleOptionHints - If the option being set has a description, show it -func ModuleOptionHints(opt string) (hint []rune) { - return -} - -// OptionHints - Yields hints for proposed options lists/groups -func OptionHints(args []string, last []rune, command *flags.Command) (hint []rune) { - return -} - -// OptionArgumentHint - Yields hints for arguments to an option (generally the last word in input) -func OptionArgumentHint(args []string, last []rune, opt *flags.Option) (hint []rune) { - return []rune(valueHint + opt.Description) -} - -// MenuHint - Returns the Hint for a given menu context -func MenuHint(args []string, current []rune) (hint []rune) { - return -} - -// SpecialCommandHint - Shows hints for Wiregost special commands -func SpecialCommandHint(args []string, current []rune) (hint []rune) { - return current -} - -// envVarHint - Yields hints for environment variables -func envVarHint(args []string, last []rune) (hint []rune) { - // Trim last in case its a path with multiple vars - allVars := strings.Split(string(last), "/") - lastVar := allVars[len(allVars)-1] - - // Base hint - hint = []rune(envHint + lastVar) - - envVar := strings.TrimPrefix(lastVar, "$") - - if v, ok := clientEnv[envVar]; ok { - if v != "" { - hintStr := string(hint) + " => " + clientEnv[envVar] - hint = []rune(hintStr) - } - } - return -} - -var ( - // Hint signs - menuHint = readline.RESET + readline.DIM + readline.BOLD + " menu " + readline.RESET // Dim - envHint = readline.RESET + readline.GREEN + readline.BOLD + " env " + readline.RESET + readline.DIM + readline.GREEN // Green - commandHint = readline.RESET + readline.DIM + readline.BOLD + " command " + readline.RESET + readline.DIM + "\033[38;5;244m" // Cream - exeHint = readline.RESET + readline.DIM + readline.BOLD + " shell " + readline.RESET + readline.DIM // Dim - optionHint = "\033[38;5;222m" + readline.BOLD + " options " + readline.RESET + readline.DIM + "\033[38;5;222m" // Cream-Yellow - valueHint = readline.RESET + readline.DIM + readline.BOLD + " value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream - // valueHint = "\033[38;5;217m" + readline.BOLD + " Value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream - argHint = readline.DIM + "\033[38;5;217m" + readline.BOLD + " arg " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream -) diff --git a/readline/completers/local-filesystem.go b/readline/completers/local-filesystem.go deleted file mode 100644 index fcec4c5..0000000 --- a/readline/completers/local-filesystem.go +++ /dev/null @@ -1,205 +0,0 @@ -package completers - -import ( - "io/ioutil" - "os" - "os/user" - "path/filepath" - "strings" - - "github.com/maxlandon/readline" -) - -func completeLocalPath(last string) (string, *readline.CompletionGroup) { - - // Completions - completion := &readline.CompletionGroup{ - Name: "(console) local path", - MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength - DisplayType: readline.TabDisplayGrid, - TrimSlash: true, - } - var suggestions []string - - // Any parsing error is silently ignored, for not messing the prompt - processedPath, _ := ParseEnvironmentVariables([]string{last}) - - // Check if processed input is empty - var inputPath string - if len(processedPath) == 1 { - inputPath = processedPath[0] - } - - // Add a slash if the raw input has one but not the processed input - if len(last) > 0 && last[len(last)-1] == '/' { - inputPath += "/" - } - - var linePath string // curated version of the inputPath - var absPath string // absolute path (excluding suffix) of the inputPath - var lastPath string // last directory in the input path - - if strings.HasSuffix(string(inputPath), "/") { - linePath = filepath.Dir(string(inputPath)) - absPath, _ = expand(string(linePath)) // Get absolute path - - } else if string(inputPath) == "" { - linePath = "." - absPath, _ = expand(string(linePath)) - } else { - linePath = filepath.Dir(string(inputPath)) - absPath, _ = expand(string(linePath)) // Get absolute path - lastPath = filepath.Base(string(inputPath)) // Save filter - } - - // 2) We take the absolute path we found, and get all dirs in it. - var dirs []string - files, _ := ioutil.ReadDir(absPath) - for _, file := range files { - if file.IsDir() { - dirs = append(dirs, file.Name()) - } - } - - switch lastPath { - case "": - for _, dir := range dirs { - if strings.HasPrefix(dir, lastPath) || lastPath == dir { - tokenized := addSpaceTokens(dir) - suggestions = append(suggestions, tokenized+"/") - } - } - default: - filtered := []string{} - for _, dir := range dirs { - if strings.HasPrefix(dir, lastPath) { - filtered = append(filtered, dir) - } - } - - for _, dir := range filtered { - if !hasPrefix([]rune(lastPath), []rune(dir)) || lastPath == dir { - tokenized := addSpaceTokens(dir) - suggestions = append(suggestions, tokenized+"/") - } - } - - } - - completion.Suggestions = suggestions - return string(lastPath), completion -} - -func addSpaceTokens(in string) (path string) { - items := strings.Split(in, " ") - for i := range items { - if len(items) == i+1 { // If last one, no char, add and return - path += items[i] - return - } - path += items[i] + "\\ " // By default add space char and roll - } - return -} - -func completeLocalPathAndFiles(last string) (string, *readline.CompletionGroup) { - - // Completions - completion := &readline.CompletionGroup{ - Name: "(console) local directory/files", - MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength - DisplayType: readline.TabDisplayGrid, - TrimSlash: true, - } - var suggestions []string - - // Any parsing error is silently ignored, for not messing the prompt - processedPath, _ := ParseEnvironmentVariables([]string{last}) - - // Check if processed input is empty - var inputPath string - if len(processedPath) == 1 { - inputPath = processedPath[0] - } - - // Add a slash if the raw input has one but not the processed input - if len(last) > 0 && last[len(last)-1] == '/' { - inputPath += "/" - } - - var linePath string // curated version of the inputPath - var absPath string // absolute path (excluding suffix) of the inputPath - var lastPath string // last directory in the input path - - if strings.HasSuffix(string(inputPath), "/") { - linePath = filepath.Dir(string(inputPath)) // Trim the non needed slash - absPath, _ = expand(string(linePath)) // Get absolute path - - } else if string(inputPath) == "" { - linePath = "." - absPath, _ = expand(string(linePath)) - } else { - linePath = filepath.Dir(string(inputPath)) - absPath, _ = expand(string(linePath)) // Get absolute path - lastPath = filepath.Base(string(inputPath)) // Save filter - } - - // 2) We take the absolute path we found, and get all dirs in it. - var dirs []string - files, _ := ioutil.ReadDir(absPath) - for _, file := range files { - if file.IsDir() { - dirs = append(dirs, file.Name()) - } - } - - switch lastPath { - case "": - for _, file := range files { - if strings.HasPrefix(file.Name(), lastPath) || lastPath == file.Name() { - if file.IsDir() { - suggestions = append(suggestions, file.Name()+"/") - } else { - suggestions = append(suggestions, file.Name()) - } - } - } - default: - filtered := []os.FileInfo{} - for _, file := range files { - if strings.HasPrefix(file.Name(), lastPath) { - filtered = append(filtered, file) - } - } - - for _, file := range filtered { - if !hasPrefix([]rune(lastPath), []rune(file.Name())) || lastPath == file.Name() { - if file.IsDir() { - suggestions = append(suggestions, file.Name()+"/") - } else { - suggestions = append(suggestions, file.Name()) - } - } - } - - } - - completion.Suggestions = suggestions - return string(lastPath), completion -} - -// expand will expand a path with ~ to the $HOME of the current user. -func expand(path string) (string, error) { - if path == "" { - return path, nil - } - home := os.Getenv("HOME") - if home == "" { - usr, err := user.Current() - if err != nil { - return "", err - } - home = usr.HomeDir - } - return filepath.Abs(strings.Replace(path, "~", home, 1)) -} diff --git a/readline/completers/option-arguments.go b/readline/completers/option-arguments.go deleted file mode 100644 index 472c480..0000000 --- a/readline/completers/option-arguments.go +++ /dev/null @@ -1,77 +0,0 @@ -package completers - -import ( - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// completeOptionArguments - Completes all values for arguments to a command. Arguments here are different from command options (--option). -// Many categories, from multiple sources in multiple contexts -func completeOptionArguments(cmd *flags.Command, opt *flags.Option, lastWord string) (prefix string, completions []*readline.CompletionGroup) { - - // By default the last word is the prefix - prefix = lastWord - - var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions - - // First of all: some options, no matter their contexts and subject, have default values. - // When we have such an option, we don't bother analyzing context, we just build completions and return. - if len(opt.Choices) > 0 { - comp = &readline.CompletionGroup{ - Name: opt.ValueName, // Value names are specified in struct metadata fields - DisplayType: readline.TabDisplayGrid, - } - for _, choice := range opt.Choices { - if strings.HasPrefix(choice, lastWord) { - comp.Suggestions = append(comp.Suggestions, choice) - } - } - completions = append(completions, comp) - return - } - - // EXAMPLE OF COMPLETING ARGUMENTS BASED ON THEIR NAMES ----------------------------------------------------------------------- - // We have 3 words, potentially different, with which we can filter: - // - // 1) '--option-name' is the string typed as input. - // 2) 'OptionName' is the name of the struct/type for this option. - // 3) 'ValueName' is the name of the value we expect. - // var match = func(name string) bool { - // if strings.Contains(opt.Field().Name, name) { - // return true - // } - // return false - // } - // - // // Sessions - // if match("ImplantID") || match("SessionID") { - // completions = append(completions, sessionIDs(lastWord)) - // } - // - // // Any arguments with a path name. Often we "save" files that need paths, certificates, etc - // if match("Path") || match("Save") || match("Certificate") || match("PrivateKey") { - // switch cmd.Name { - // case constants.WebContentTypeStr, constants.WebUpdateStr, constants.AddWebContentStr, constants.RmWebContentStr: - // // Make an exception for WebPath option in websites commands. - // default: - // switch opt.ValueName { - // case "local-path", "path": - // prefix, comp = completeLocalPath(lastWord) - // completions = append(completions, comp) - // case "local-file", "file": - // prefix, comp = completeLocalPathAndFiles(lastWord) - // completions = append(completions, comp) - // default: - // // We always have a default searching for files, locally - // prefix, comp = completeLocalPathAndFiles(lastWord) - // completions = append(completions, comp) - // } - // - // } - // } - // - return -} diff --git a/readline/completers/patterns.go b/readline/completers/patterns.go deleted file mode 100644 index 6de587a..0000000 --- a/readline/completers/patterns.go +++ /dev/null @@ -1,548 +0,0 @@ -package completers - -import ( - "os/exec" - "reflect" - "strings" - "unicode" - - "github.com/jessevdk/go-flags" -) - -// These functions are just shorthands for checking various conditions on the input line. -// They make the main function more readable, which might be useful, should a logic error pop somewhere. - -// [ Parser Commands & Options ] -------------------------------------------------------------------------- -// ArgumentByName Get the name of a detected command's argument -func argumentByName(command *flags.Command, name string) *flags.Arg { - args := command.Args() - for _, arg := range args { - if arg.Name == name { - return arg - } - } - return nil -} - -// optionByName - Returns an option for a command or a subcommand, identified by name -func optionByName(cmd *flags.Command, option string) *flags.Option { - - if cmd == nil { - return nil - } - // Get all (root) option groups. - groups := cmd.Groups() - - // For each group, build completions - for _, grp := range groups { - // Add each option to completion group - for _, opt := range grp.Options() { - if opt.LongName == option { - return opt - } - } - } - return nil -} - -// [ Menus ] -------------------------------------------------------------------------------------------- -// Is the input line is either empty, or without any detected command ? -func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool { - if len(args) == 0 || len(args) == 1 && command == nil { - return true - } - return false -} - -// [ Commands ] ------------------------------------------------------------------------------------- -// detectedCommand - Returns the base command from parser if detected, depending on context -func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) { - arg := strings.TrimSpace(args[0]) - command = c.parser.Find(arg) - return -} - -// is the command a special command, usually not handled by parser ? -func isSpecialCommand(args []string, command *flags.Command) bool { - - // If command is not nil, return - if command == nil { - // Shell - if args[0] == "!" { - return true - } - // Exit - if args[0] == "exit" { - return true - } - return false - } - return false -} - -// The commmand has been found -func commandFound(command *flags.Command) bool { - if command != nil { - return true - } - return false -} - -// Search for input in $PATH -func commandFoundInPath(input string) bool { - _, err := exec.LookPath(input) - if err != nil { - return false - } - return true -} - -// [ SubCommands ]------------------------------------------------------------------------------------- -// Does the command have subcommands ? -func hasSubCommands(command *flags.Command, args []string) bool { - if len(args) < 2 || command == nil { - return false - } - - if len(command.Commands()) != 0 { - return true - } - - return false -} - -// Does the input has a subcommand in it ? -func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) { - // First, filter redundant spaces. This does not modify the actual line - args := ignoreRedundantSpaces(raw) - - if len(args) <= 1 || command == nil { - return nil, false - } - - sub = command.Find(args[1]) - if sub != nil { - return sub, true - } - - return nil, false -} - -// Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand -func lastIsSubCommand(lastWord string, command *flags.Command) bool { - if sub := command.Find(lastWord); sub != nil { - return true - } - return false -} - -// [ Arguments ]------------------------------------------------------------------------------------- -// Does the command have arguments ? -func hasArgs(command *flags.Command) bool { - if len(command.Args()) != 0 { - return true - } - return false -} - -// commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for -func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) { - - // First, filter redundant spaces. This does not modify the actual line - args := ignoreRedundantSpaces(raw) - - // Trim command and subcommand args - var remain []string - if args[0] == command.Name { - remain = args[1:] - } - if len(args) > 1 && args[1] == command.Name { - remain = args[2:] - } - - // The remain may include a "" as a last element, - // which we don't consider as a real remain, so we move it away - switch lastWord { - case "": - case command.Name: - return "", false - } - - // Trim all --option flags and their arguments if they have - remain = filterOptions(remain, command) - - // For each argument, check if needs completion. If not continue, if yes return. - // The arguments remainder is popped according to the number of values expected. - for i, arg := range command.Args() { - - // If it's required and has one argument, check filled. - if arg.Required == 1 && arg.RequiredMaximum == 1 { - - // If last word is the argument, and we are - // last arg in: line keep completing. - if len(remain) < 1 { - return arg.Name, true - } - - // If the we are still writing the argument - if len(remain) == 1 { - if lastWord != "" { - return arg.Name, true - } - } - - // If filed and we are not last arg, continue - if len(remain) > 1 && i < (len(command.Args())-1) { - remain = remain[1:] - continue - } - - continue - } - - // If we need more than one value and we knwo the maximum, - // either return or pop the remain. - if arg.Required > 0 && arg.RequiredMaximum > 1 { - // Pop the corresponding amount of arguments. - var found int - for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ { - remain = remain[1:] - found++ - } - - // If we still need values: - if len(remain) == 0 && found <= arg.RequiredMaximum { - if lastWord == "" { // We are done, no more completions. - break - } else { - return arg.Name, true - } - } - // Else go on with the next argument - continue - } - - // If has required arguments, with no limit of needs, return true - if arg.Required > 0 && arg.RequiredMaximum == -1 { - return arg.Name, true - } - - // Else, if no requirements and the command has subcommands, - // return so that we complete subcommands - if arg.Required == -1 && len(command.Commands()) > 0 { - continue - } - - // Else, return this argument - // NOTE: This block is after because we always use []type arguments - // AFTER individual argument fields. Thus blocks any args that have - // not been processed. - if arg.Required == -1 { - return arg.Name, true - } - } - - // Once we exited the loop, it means that none of the arguments require completion: - // They are all either optional, or fullfiled according to their required numbers. - // Thus we return none - return "", false -} - -// getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args -func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) { - - var input []string - // Clean subcommand name - if args[0] == command.Name && len(args) >= 2 { - input = args[1:] - } else if len(args) == 1 { - input = args - } - - // For each each argument - for i := 0; i < len(input); i++ { - // Check option prefix - if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") { - // Clean it - cur := strings.TrimPrefix(input[i], "--") - cur = strings.TrimPrefix(cur, "-") - - // Check if option matches any command option - if opt := command.FindOptionByLongName(cur); opt != nil { - boolean := true - if opt.Field().Type == reflect.TypeOf(boolean) { - continue // If option is boolean, don't skip an argument - } - i++ // Else skip next arg in input - continue - } - } - - // Safety check - if input[i] == "" || input[i] == " " { - continue - } - - remain = append(remain, input[i]) - } - - return -} - -// [ Options ]------------------------------------------------------------------------------------- -// commandOptionsAsked - Does the user asks for options in a root command ? -func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool { - if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) { - return true - } - return false -} - -// commandOptionsAsked - Does the user asks for options in a subcommand ? -func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool { - if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) { - return true - } - return false -} - -// Is the last input argument is a dash ? -func isOptionDash(args []string, last []rune) bool { - if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) { - return true - } - return false -} - -// optionIsAlreadySet - Detects in input if an option is already set -func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool { - return false -} - -// Check if option type allows for repetition -func optionNotRepeatable(opt *flags.Option) bool { - return true -} - -// [ Option Values ]------------------------------------------------------------------------------------- -// Is the last input word an option name (--option) ? -func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) { - - var lastItem string - var lastOption string - var option *flags.Option - - // If there is argument required we must have 1) command 2) --option inputs at least. - if len(args) <= 2 { - return nil, false - } - - // Check for last two arguments in input - if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") { - - // Long opts - if strings.HasPrefix(args[len(args)-2], "--") { - lastOption = strings.TrimPrefix(args[len(args)-2], "--") - if opt := group.FindOptionByLongName(lastOption); opt != nil { - option = opt - } - - // Short opts - } else if strings.HasPrefix(args[len(args)-2], "-") { - lastOption = strings.TrimPrefix(args[len(args)-2], "-") - if len(lastOption) > 0 { - if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil { - option = opt - } - } - } - - } - - // If option is found, and we still are in writing the argument - if (lastItem == "" && option != nil) || option != nil { - // Check if option is a boolean, if yes return false - boolean := true - if option.Field().Type == reflect.TypeOf(boolean) { - return nil, false - } - - return option, true - } - - // Check for previous argument - if lastItem != "" && option == nil { - if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") { - - // Long opts - if strings.HasPrefix(args[len(args)-2], "--") { - lastOption = strings.TrimPrefix(args[len(args)-2], "--") - if opt := group.FindOptionByLongName(lastOption); opt != nil { - option = opt - return option, true - } - - // Short opts - } else if strings.HasPrefix(args[len(args)-2], "-") { - lastOption = strings.TrimPrefix(args[len(args)-2], "-") - if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil { - option = opt - return option, true - } - } - } - } - - return nil, false -} - -// [ Other ]------------------------------------------------------------------------------------- -// Does the user asks for Environment variables ? -func envVarAsked(args []string, lastWord string) bool { - - // Check if the current word is an environment variable, or if the last part of it is a variable - if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") { - if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") { - return true - } - return false - } - - // Check if env var is asked in a path or something - if len(lastWord) > 1 { - // If last is a path, it cannot be an env var anymore - if lastWord[len(lastWord)-1] == '/' { - return false - } - - if lastWord[len(lastWord)-1] == '$' { - return true - } - } - - // If we are at the beginning of an env var - if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' { - return true - } - - return false -} - -// filterOptions - Check various elements of an option and return a list -func filterOptions(args []string, command *flags.Command) (processed []string) { - - for i := 0; i < len(args); i++ { - arg := args[i] - // --long-name options - if strings.HasPrefix(arg, "--") { - name := strings.TrimPrefix(arg, "--") - if opt := optionByName(command, name); opt != nil { - var boolean = true - if opt.Field().Type == reflect.TypeOf(boolean) { - continue - } - // Else skip the option argument (next item) - i++ - } - continue - } - // -s short options - if strings.HasPrefix(arg, "-") { - name := strings.TrimPrefix(arg, "-") - if opt := optionByName(command, name); opt != nil { - var boolean = true - if opt.Field().Type == reflect.TypeOf(boolean) { - continue - } - // Else skip the option argument (next item) - i++ - } - continue - } - processed = append(processed, arg) - } - - return -} - -// Other Functions -------------------------------------------------------------------------------------------------------------// - -// formatInput - Formats & sanitize the command line input -func formatInput(line []rune) (args []string, last []rune, lastWord string) { - args = strings.Split(string(line), " ") // The readline input as a []string - last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input - lastWord = string(last) - return -} - -// FormatInput - Formats & sanitize the command line input -func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) { - args = strings.SplitN(string(line), " ", -1) - last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input - lastWord = string(last) - return -} - -// ignoreRedundantSpaces - We might have several spaces between each real arguments. -// However these indivual spaces are counted as args themselves. -// For each space arg found, verify that no space args follow, -// and if some are found, delete them. -func ignoreRedundantSpaces(raw []string) (args []string) { - - for i := 0; i < len(raw); i++ { - // Catch a space argument. - if raw[i] == "" { - // The arg evaulated is always kept, because we just adjusted - // the indexing to avoid the ones we don't need - // args = append(args, raw[i]) - - for y, next := range raw[i:] { - if next != "" { - i += y - 1 - break - } - // If we come to the end while not breaking - // we push the outer loop straight to the end. - if y == len(raw[i:])-1 { - i += y - } - } - } else { - // The arg evaulated is always kept, because we just adjusted - // the indexing to avoid the ones we don't need - args = append(args, raw[i]) - } - } - - return -} - -func trimSpaceLeft(in []rune) []rune { - firstIndex := len(in) - for i, r := range in { - if unicode.IsSpace(r) == false { - firstIndex = i - break - } - } - return in[firstIndex:] -} - -func equal(a, b []rune) bool { - if len(a) != len(b) { - return false - } - for i := 0; i < len(a); i++ { - if a[i] != b[i] { - return false - } - } - return true -} - -func hasPrefix(r, prefix []rune) bool { - if len(r) < len(prefix) { - return false - } - return equal(r[:len(prefix)], prefix) -} diff --git a/readline/completers/syntax-highlighter.go b/readline/completers/syntax-highlighter.go deleted file mode 100644 index 8bce99f..0000000 --- a/readline/completers/syntax-highlighter.go +++ /dev/null @@ -1,151 +0,0 @@ -package completers - -import ( - "fmt" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// SyntaxHighlighter - Entrypoint to all input syntax highlighting in the Wiregost console -func (c *CommandCompleter) SyntaxHighlighter(input []rune) (line string) { - - // Format and sanitize input - args, last, lastWord := formatInputHighlighter(input) - - // Remain is all arguments that have not been highlighted, we need it for completing long commands - var remain = args - - // Detect base command automatically - var command = c.detectedCommand(args) - - // Return input as is - if noCommandOrEmpty(remain, last, command) { - return string(input) - } - - // Base command - if commandFound(command) { - line, remain = highlightCommand(remain, command) - - // SubCommand - if sub, ok := subCommandFound(lastWord, args, command); ok { - line, remain = highlightSubCommand(line, remain, sub) - } - - } - - line = processRemain(line, remain) - - return -} - -func highlightCommand(args []string, command *flags.Command) (line string, remain []string) { - line = readline.BOLD + args[0] + readline.RESET + " " - remain = args[1:] - return -} - -func highlightSubCommand(input string, args []string, command *flags.Command) (line string, remain []string) { - line = input - line += readline.BOLD + args[0] + readline.RESET + " " - remain = args[1:] - return -} - -func processRemain(input string, remain []string) (line string) { - - // Check the last is not the last space in input - if len(remain) == 1 && remain[0] == " " { - return input - } - - line = input + strings.Join(remain, " ") - // line = processEnvVars(input, remain) - return -} - -// processEnvVars - Highlights environment variables. NOTE: Rewrite with logic from console/env.go -func processEnvVars(input string, remain []string) (line string) { - - var processed []string - - inputSlice := strings.Split(input, " ") - - // Check already processed input - for _, arg := range inputSlice { - if arg == "" || arg == " " { - continue - } - if strings.HasPrefix(arg, "$") { // It is an env var. - if args := strings.Split(arg, "/"); len(args) > 1 { - for _, a := range args { - fmt.Println(a) - if strings.HasPrefix(a, "$") && a != " " { // It is an env var. - processed = append(processed, "\033[38;5;108m"+readline.DIM+a+readline.RESET) - continue - } - } - } - processed = append(processed, "\033[38;5;108m"+readline.DIM+arg+readline.RESET) - continue - } - processed = append(processed, arg) - } - - // Check remaining args (non-processed) - for _, arg := range remain { - if arg == "" { - continue - } - if strings.HasPrefix(arg, "$") && arg != "$" { // It is an env var. - var full string - args := strings.Split(arg, "/") - if len(args) == 1 { - if strings.HasPrefix(args[0], "$") && args[0] != "" && args[0] != "$" { // It is an env var. - full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET - continue - } - } - if len(args) > 1 { - var counter int - for _, arg := range args { - // If var is an env var - if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" { - if counter < len(args)-1 { - full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET + "/" - counter++ - continue - } - if counter == len(args)-1 { - full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET - counter++ - continue - } - } - - // Else, if we are not at the end of array - if counter < len(args)-1 && arg != "" { - full += arg + "/" - counter++ - } - if counter == len(args)-1 { - full += arg - counter++ - } - } - } - // Else add first var - processed = append(processed, full) - } - } - - line = strings.Join(processed, " ") - - // Very important, keeps the line clear when erasing - // line += " " - - return -} diff --git a/readline/completers/tab-completer.go b/readline/completers/tab-completer.go deleted file mode 100644 index 1c9a942..0000000 --- a/readline/completers/tab-completer.go +++ /dev/null @@ -1,289 +0,0 @@ -package completers - -import ( - "errors" - "fmt" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order -// to build completions for commands, arguments, options and their arguments as well. -// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil. -type CommandCompleter struct { - parser *flags.Parser -} - -// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser. -func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) { - if parser == nil { - return nil, errors.New("command completer was instantiated with a nil parser") - } - return &CommandCompleter{parser: parser}, nil -} - -// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser. -func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) { - - // Format and sanitize input - // @args => All items of the input line - // @last => The last word detected in input line as []rune - // @lastWord => The last word detected in input as string - args, last, lastWord := formatInput(line) - - // Detect base command automatically - var command = c.detectedCommand(args) - - // Propose commands - if noCommandOrEmpty(args, last, command) { - return c.completeMenuCommands(lastWord, pos) - } - - // Check environment variables - if envVarAsked(args, lastWord) { - completeEnvironmentVariables(lastWord) - } - - // Base command has been identified - if commandFound(command) { - // Check environment variables again - if envVarAsked(args, lastWord) { - return completeEnvironmentVariables(lastWord) - } - - // If options are asked for root command, return commpletions. - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - return completeOptionArguments(command, opt, lastWord) - } - } - } - - // Then propose subcommands. We don't return from here, otherwise it always skips the next steps. - if hasSubCommands(command, args) { - completions = completeSubCommands(args, lastWord, command) - } - - // Handle subcommand if found (maybe we should rewrite this function and use it also for base command) - if sub, ok := subCommandFound(lastWord, args, command); ok { - return handleSubCommand(line, pos, sub) - } - - // If user asks for completions with "-" / "--", show command options. - // We ask this here, after having ensured there is no subcommand invoked. - // This prevails over command arguments, even if they are required. - if commandOptionsAsked(args, lastWord, command) { - return completeCommandOptions(args, lastWord, command) - } - - // Propose argument completion before anything, and if needed - if arg, yes := commandArgumentRequired(lastWord, args, command); yes { - return completeCommandArguments(command, arg, lastWord) - } - - } - - return -} - -// [ Main Completion Functions ] ----------------------------------------------------------------------------------------------------------------- - -// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions -// Many categories, all from command parsers. -func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) { - - prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions. - - // Check their namespace (which should be their "group" (like utils, core, Jobs, etc)) - for _, cmd := range c.parser.Commands() { - // If command matches readline input - if strings.HasPrefix(cmd.Name, lastWord) { - // Check command group: add to existing group if found - var found bool - for _, grp := range completions { - if grp.Name == cmd.Aliases[0] { - found = true - grp.Suggestions = append(grp.Suggestions, cmd.Name) - grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription) - } - } - // Add a new group if not found - if !found { - grp := &readline.CompletionGroup{ - Name: cmd.Aliases[0], - Suggestions: []string{cmd.Name}, - Descriptions: map[string]string{ - cmd.Name: readline.Dim(cmd.ShortDescription), - }, - } - completions = append(completions, grp) - } - } - } - - // Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc. - for _, grp := range completions { - // If the length of suggestions is too long and we have - // many groups, use grid display. - if len(completions) >= 10 && len(grp.Suggestions) >= 7 { - grp.DisplayType = readline.TabDisplayGrid - } else { - // By default, we use a map of command to descriptions - grp.DisplayType = readline.TabDisplayList - } - } - - return -} - -// completeSubCommands - Takes subcommands and gives them as suggestions -// One category, from one source (a parent command). -func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) { - - group := &readline.CompletionGroup{ - Name: command.Name, - Suggestions: []string{}, - Descriptions: map[string]string{}, - DisplayType: readline.TabDisplayList, - } - - for _, sub := range command.Commands() { - if strings.HasPrefix(sub.Name, lastWord) { - group.Suggestions = append(group.Suggestions, sub.Name) - group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET - } - } - - completions = append(completions, group) - - return -} - -// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion -// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command. -func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) { - - args, last, lastWord := formatInput(line) - - // Check environment variables - if envVarAsked(args, lastWord) { - completeEnvironmentVariables(lastWord) - } - - // Check argument options - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - return completeOptionArguments(command, opt, lastWord) - } - } - } - - // If user asks for completions with "-" or "--". This must take precedence on arguments. - if subCommandOptionsAsked(args, lastWord, command) { - return completeCommandOptions(args, lastWord, command) - } - - // If command has non-filled arguments, propose them first - if arg, yes := commandArgumentRequired(lastWord, args, command); yes { - return completeCommandArguments(command, arg, lastWord) - } - - return -} - -// completeCommandOptions - Yields completion for options of a command, with various decorators -// Many categories, from one source (a command) -func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) { - - prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions. - - // Get all (root) option groups. - groups := cmd.Groups() - - // Append command options not gathered in groups - groups = append(groups, cmd.Group) - - // For each group, build completions - for _, grp := range groups { - - _, comp := completeOptionGroup(lastWord, grp, "") - - // No need to add empty groups, will screw the completion system. - if len(comp.Suggestions) > 0 { - completions = append(completions, comp) - } - } - - // Do the same for global options, which are not part of any group "per-se" - _, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options") - if len(gcomp.Suggestions) > 0 { - completions = append(completions, gcomp) - } - - return -} - -// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty. -func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) { - - compGrp = &readline.CompletionGroup{ - Name: grp.ShortDescription, - Descriptions: map[string]string{}, - DisplayType: readline.TabDisplayList, - Aliases: map[string]string{}, - } - - // An optional title for this comp group. - // Used by global flag options, added to all commands. - if title != "" { - compGrp.Name = title - } - - // Add each option to completion group - for _, opt := range grp.Options() { - - // Check if option is already set, next option if yes - // if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) { - // continue - // } - - // Depending on the current last word, either build a group with option longs only, or with shorts - if strings.HasPrefix("--"+opt.LongName, lastWord) { - optName := "--" + opt.LongName - compGrp.Suggestions = append(compGrp.Suggestions, optName) - - // Add short if there is, and that the prefix is only one dash - if strings.HasPrefix("-", lastWord) { - if opt.ShortName != 0 { - compGrp.Aliases[optName] = "-" + string(opt.ShortName) - } - } - - // Option default value if any - var def string - if len(opt.Default) > 0 { - def = " (default:" - for _, d := range opt.Default { - def += " " + d + "," - } - def = strings.TrimSuffix(def, ",") - def += ")" - } - - desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET) - compGrp.Descriptions[optName] = desc - } - } - return -} - -// RecursiveGroupCompletion - Handles recursive completion for nested option groups -// Many categories, one source (a command's root option group). Called by the function just above. -func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) { - return -} diff --git a/readline/examples/arguments.go b/readline/examples/arguments.go deleted file mode 100644 index d976888..0000000 --- a/readline/examples/arguments.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -// This file defines a few argument choices for commands - -import ( - "github.com/jessevdk/go-flags" -) - -// Command/option argument choices -var ( - // Logs & components - logLevels = []string{"trace", "debug", "info", "warning", "error"} - loggers = []string{"client", "comm"} - - // Stages / Stagers - implantOS = []string{"windows", "linux", "darwin"} - implantArch = []string{"amd64", "x86"} - implantFmt = []string{"exe", "shared", "service", "shellcode"} - - stageListenerProtocols = []string{"tcp", "http", "https"} - - // MSF - msfStagerProtocols = []string{"tcp", "http", "https"} - msfTransformFormats = []string{ - "bash", - "c", - "csharp", - "dw", - "dword", - "hex", - "java", - "js_be", - "js_le", - "num", - "perl", - "pl", - "powershell", - "ps1", - "py", - "python", - "raw", - "rb", - "ruby", - "sh", - "vbapplication", - "vbscript", - } - - msfEncoders = []string{ - "x86/shikata_ga_nai", - "x64/xor_dynamic", - } - - msfPayloads = map[string][]string{ - "windows": windowsMsfPayloads, - "linux": linuxMsfPayloads, - "osx": osxMsfPayloads, - } - - // ValidPayloads - Valid payloads and OS combos - windowsMsfPayloads = []string{ - "meterpreter_reverse_http", - "meterpreter_reverse_https", - "meterpreter_reverse_tcp", - "meterpreter/reverse_tcp", - "meterpreter/reverse_http", - "meterpreter/reverse_https", - } - linuxMsfPayloads = []string{ - "meterpreter_reverse_http", - "meterpreter_reverse_https", - "meterpreter_reverse_tcp", - } - osxMsfPayloads = []string{ - "meterpreter_reverse_http", - "meterpreter_reverse_https", - "meterpreter_reverse_tcp", - } - - // Comm network protocols - portfwdProtocols = []string{"tcp", "udp"} - transportProtocols = []string{"tcp", "udp", "ip"} - applicationProtocols = []string{"http", "https", "mtls", "quic", "http3", "dns", "named_pipe"} -) - -// loadArgumentCompletions - Adds a bunch of choices for command arguments (and their completions.) -func loadArgumentCompletions(parser *flags.Parser) { - if parser == nil { - return - } - serverCompsAddtional(parser) -} - -// Additional completion mappings for command in the server context -func serverCompsAddtional(parser *flags.Parser) { - - // Stage options - g := parser.Find("generate") - g.FindOptionByLongName("os").Choices = implantOS - g.FindOptionByLongName("arch").Choices = implantArch - g.FindOptionByLongName("format").Choices = implantFmt - - // Stager options (mostly MSF) - gs := g.Find("stager") - gs.FindOptionByLongName("os").Choices = implantOS - gs.FindOptionByLongName("arch").Choices = implantArch - gs.FindOptionByLongName("protocol").Choices = msfStagerProtocols - gs.FindOptionByLongName("msf-format").Choices = msfTransformFormats -} diff --git a/readline/examples/commands.go b/readline/examples/commands.go deleted file mode 100644 index fcd9271..0000000 --- a/readline/examples/commands.go +++ /dev/null @@ -1,315 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// This file declares a go-flags parser and a few commands. - -var ( - // commandParser - The command parser used by the example console. - commandParser = flags.NewNamedParser("example", flags.IgnoreUnknown) -) - -func bindCommands() (err error) { - - // core console - // ---------------------------------------------------------------------------------------- - ex, err := commandParser.AddCommand("exit", // Command string - "Exit from the client/server console", // Description (completions, help usage) - "", // Long description - &Exit{}) // Command implementation - ex.Aliases = []string{"core"} - - cd, err := commandParser.AddCommand("cd", - "Change client working directory", - "", - &ChangeClientDirectory{}) - cd.Aliases = []string{"core"} - - ls, err := commandParser.AddCommand("ls", - "List directory contents", - "", - &ListClientDirectories{}) - ls.Aliases = []string{"core"} - - // Log - log, err := commandParser.AddCommand("log", - "Manage log levels of one or more components", - "", - &Log{}) - log.Aliases = []string{"core"} - - // Implant generation - // ---------------------------------------------------------------------------------------- - g, err := commandParser.AddCommand("generate", - "Configure and compile an implant (staged or stager)", - "", - &Generate{}) - g.Aliases = []string{"builds"} - g.SubcommandsOptional = true - - _, err = g.AddCommand("stager", - "Generate a stager shellcode payload using MSFVenom, (to file: --save, to stdout: --format", - "", - &GenerateStager{}) - - r, err := commandParser.AddCommand("regenerate", - "Recompile an implant by name, passed as argument (completed)", - "", - &Regenerate{}) - r.Aliases = []string{"builds"} - - // Add choices completions (and therefore completions) to some of these commands. - loadArgumentCompletions(commandParser) - - return -} - -// Exit - Kill the current client console -type Exit struct{} - -// Execute - Run -func (e *Exit) Execute(args []string) (err error) { - - reader := bufio.NewReader(os.Stdin) - fmt.Print("Confirm exit (Y/y): ") - text, _ := reader.ReadString('\n') - answer := strings.TrimSpace(text) - - if (answer == "Y") || (answer == "y") { - os.Exit(0) - } - - fmt.Println() - return -} - -// ChangeClientDirectory - Change the working directory of the client console -type ChangeClientDirectory struct { - Positional struct { - Path string `description:"local path" required:"1-1"` - } `positional-args:"yes" required:"yes"` -} - -// Execute - Handler for ChangeDirectory -func (cd *ChangeClientDirectory) Execute(args []string) (err error) { - - dir, err := expand(cd.Positional.Path) - - err = os.Chdir(dir) - if err != nil { - fmt.Printf(CommandError+"%s \n", err) - } else { - fmt.Printf(Info+"Changed directory to %s \n", dir) - } - - return -} - -// ListClientDirectories - List directory contents -type ListClientDirectories struct { - Positional struct { - Path []string `description:"local directory/file"` - } `positional-args:"yes"` -} - -// Execute - Command -func (ls *ListClientDirectories) Execute(args []string) error { - - base := []string{"ls", "--color", "-l"} - - if len(ls.Positional.Path) == 0 { - ls.Positional.Path = []string{"."} - } - - fullPaths := []string{} - for _, path := range ls.Positional.Path { - full, _ := expand(path) - fullPaths = append(fullPaths, full) - } - base = append(base, fullPaths...) - - // Print output - out, err := shellExec(base[0], base[1:]) - if err != nil { - fmt.Printf(CommandError+"%s \n", err.Error()) - return nil - } - - // Print output - fmt.Println(out) - - return nil -} - -// shellExec - Execute a program -func shellExec(executable string, args []string) (string, error) { - path, err := exec.LookPath(executable) - if err != nil { - return "", err - } - - cmd := exec.Command(path, args...) - - // Load OS environment - cmd.Env = os.Environ() - - out, err := cmd.CombinedOutput() - - if err != nil { - return "", err - } - return strings.Trim(string(out), "/"), nil -} - -// Generate - Configure and compile an implant -type Generate struct { - StageOptions // Command makes use of full stage options -} - -// StageOptions - All these options, regrouped by area, are used by any command that needs full -// configuration information for a stage Sliver implant. -type StageOptions struct { - // CoreOptions - All options about OS/arch, files to save, debugs, etc. - CoreOptions struct { - OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"` - Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"` - Format string `long:"format" short:"f" description:"output formats (exe, shared (DLL), service (see 'psexec' for info), shellcode (Windows only)" default:"exe" value-name:"stage formats"` - Profile string `long:"profile-name" description:"implant profile name to use (use with generate-profile)"` - Name string `long:"name" short:"N" description:"implant name to use (overrides random name generation)"` - Save string `long:"save" short:"s" description:"directory/file where to save binary"` - Debug bool `long:"debug" short:"d" description:"enable debug features (incompatible with obfuscation, and prevailing)"` - } `group:"core options"` - - // TransportOptions - All options pertaining to transport/RPC matters - TransportOptions struct { - MTLS []string `long:"mtls" short:"m" description:"mTLS C2 domain(s), comma-separated (ex: mtls://host:port)" env-delim:","` - DNS []string `long:"dns" short:"n" description:"DNS C2 domain(s), comma-separated (ex: dns://mydomain.com)" env-delim:","` - HTTP []string `long:"http" short:"h" description:"HTTP(S) C2 domain(s)" env-delim:","` - NamedPipe []string `long:"named-pipe" short:"p" description:"Named pipe transport strings, comma-separated" env-delim:","` - TCPPivot []string `long:"tcp-pivot" short:"i" description:"TCP pivot transport strings, comma-separated" env-delim:","` - Reconnect int `long:"reconnect" short:"j" description:"attempt to reconnect every n second(s)" default:"60"` - MaxErrors int `long:"max-errors" short:"k" description:"max number of transport errors" default:"10"` - } `group:"transport options"` - - // SecurityOptions - All security-oriented options like restrictions. - SecurityOptions struct { - LimitDatetime string `long:"limit-datetime" short:"w" description:"limit execution to before datetime"` - LimitDomain bool `long:"limit-domain-joined" short:"D" description:"limit execution to domain joined machines"` - LimitUsername string `long:"limit-username" short:"U" description:"limit execution to specified username"` - LimitHosname string `long:"limit-hostname" short:"H" description:"limit execution to specified hostname"` - LimitFileExits string `long:"limit-file-exists" short:"F" description:"limit execution to hosts with this file in the filesystem"` - } `group:"security options"` - - // EvasionOptions - All proactive security options (obfuscation, evasion, etc) - EvasionOptions struct { - Canary []string `long:"canary" short:"c" description:"DNS canary domain strings, comma-separated" env-delim:","` - SkipSymbols bool `long:"skip-obfuscation" short:"b" description:"skip binary/symbol obfuscation"` - Evasion bool `long:"evasion" short:"e" description:"enable evasion features"` - } `group:"evasion options"` -} - -// Execute - Configure and compile an implant -func (g *Generate) Execute(args []string) (err error) { - save := g.CoreOptions.Save - if save == "" { - save, _ = os.Getwd() - } - - fmt.Println("Executed 'generate' command. ") - return -} - -// Regenerate - Recompile an implant by name, passed as argument (completed) -type Regenerate struct { - Positional struct { - ImplantName string `description:"Name of Sliver implant to recompile" required:"1-1"` - } `positional-args:"yes" required:"yes"` - Save string `long:"save" short:"s" description:"Directory/file where to save binary"` -} - -// Execute - Recompile an implant with a given profile -func (r *Regenerate) Execute(args []string) (err error) { - fmt.Println("Executed 'regenerate' command. ") - return -} - -// GenerateStager - Generate a stager payload using MSFVenom -type GenerateStager struct { - PayloadOptions struct { - OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"` - Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"` - Format string `long:"msf-format" short:"f" description:"output format (MSF Venom formats). List is auto-completed" default:"raw" value-name:"MSF Venom transform formats"` - BadChars string `long:"badchars" short:"b" description:"bytes to exclude from stage shellcode"` - Save string `long:"save" short:"s" description:"directory to save the generated stager to"` - } `group:"payload options"` - TransportOptions struct { - LHost string `long:"lhost" short:"l" description:"listening host address" required:"true"` - LPort int `long:"lport" short:"p" description:"listening host port" default:"8443"` - Protocol string `long:"protocol" short:"P" description:"staging protocol (tcp/http/https)" default:"tcp" value-name:"stager protocol"` - } `group:"transport options"` -} - -// Execute - Generate a stager payload using MSFVenom -func (g *GenerateStager) Execute(args []string) (err error) { - fmt.Println("Executed 'generate stager' subcommand. ") - return -} - -// Log - Log management commands. Sets log level by default. -type Log struct { - Positional struct { - Level string `description:"log level to filter by" required:"1-1"` - Components []string `description:"components on which to apply log filter" required:"1"` - } `positional-args:"yes" required:"true"` -} - -// Execute - Set the log level of one or more components -func (l *Log) Execute(args []string) (err error) { - fmt.Println("Executed 'log' command. ") - return -} - -var ( - Info = fmt.Sprintf("%s[-]%s ", readline.BLUE, readline.RESET) - Warn = fmt.Sprintf("%s[!]%s ", readline.YELLOW, readline.RESET) - Error = fmt.Sprintf("%s[!]%s ", readline.RED, readline.RESET) - Success = fmt.Sprintf("%s[*]%s ", readline.GREEN, readline.RESET) - - Infof = fmt.Sprintf("%s[-] ", readline.BLUE) // Infof - formatted - Warnf = fmt.Sprintf("%s[!] ", readline.YELLOW) // Warnf - formatted - Errorf = fmt.Sprintf("%s[!] ", readline.RED) // Errorf - formatted - Sucessf = fmt.Sprintf("%s[*] ", readline.GREEN) // Sucessf - formatted - - RPCError = fmt.Sprintf("%s[RPC Error]%s ", readline.RED, readline.RESET) - CommandError = fmt.Sprintf("%s[Command Error]%s ", readline.RED, readline.RESET) - ParserError = fmt.Sprintf("%s[Parser Error]%s ", readline.RED, readline.RESET) - DBError = fmt.Sprintf("%s[DB Error]%s ", readline.RED, readline.RESET) -) - -// expand will expand a path with ~ to the $HOME of the current user. -func expand(path string) (string, error) { - if path == "" { - return path, nil - } - home := os.Getenv("HOME") - if home == "" { - usr, err := user.Current() - if err != nil { - return "", err - } - home = usr.HomeDir - } - return filepath.Abs(strings.Replace(path, "~", home, 1)) -} diff --git a/readline/examples/main.go b/readline/examples/main.go deleted file mode 100644 index 16fd4de..0000000 --- a/readline/examples/main.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" - "github.com/maxlandon/readline/completers" -) - -// This file shows a typical way of using readline in a loop. - -func main() { - // Instantiate a console object - console := newConsole() - - // Bind commands to the console - bindCommands() - - // Setup the console completers, prompts, and input modes - console.setup() - - // Start the readline loop (blocking) - console.Start() -} - -// newConsole - Instantiates a new console with some default behavior. -// We modify/add elements of behavior later in setup. -func newConsole() *console { - console := &console{ - shell: readline.NewInstance(), - parser: commandParser, - } - return console -} - -// console - A simple console example. -type console struct { - shell *readline.Instance - parser *flags.Parser -} - -// setup - The console sets up various elements such as the completion system, hints, -// syntax highlighting, prompt system, commands binding, and client environment loading. -func (c *console) setup() (err error) { - - // Input mode & defails - c.shell.InputMode = readline.Vim // Could be readline.Emacs for emacs input mode. - c.shell.ShowVimMode = true - c.shell.VimModeColorize = true - - // Prompt: we want a two-line prompt, with a custom indicator after the Vim status - c.shell.SetPrompt("readline ") - c.shell.Multiline = true - c.shell.MultilinePrompt = " > " - - // Instantiate a default completer associated with the parser - // declared in commands.go, and embedded into the console struct. - // The error is muted, because we don't pass an nil parser, therefore no problems. - defaultCompleter, _ := completers.NewCommandCompleter(c.parser) - - // Register the completer for command/option completions, hints and syntax highlighting. - // The completer can handle all of them. - c.shell.TabCompleter = defaultCompleter.TabCompleter - c.shell.HintText = defaultCompleter.HintCompleter - c.shell.SyntaxHighlighter = defaultCompleter.SyntaxHighlighter - - // History: by default the history is in-memory, use it with Ctrl-R - - return -} - -// Start - The console has a working RPC connection: we setup all -// things pertaining to the console itself, and start the input loop. -func (c *console) Start() (err error) { - - // Setup console elements - err = c.setup() - if err != nil { - return fmt.Errorf("Console setup failed: %s", err) - } - - // Start input loop - for { - // Read input line - line, _ := c.Readline() - - // Split and sanitize input - sanitized, empty := sanitizeInput(line) - if empty { - continue - } - - // Process various tokens on input (environment variables, paths, etc.) - // These tokens will be expaneded by completers anyway, so this is not absolutely required. - envParsed, _ := completers.ParseEnvironmentVariables(sanitized) - - // Other types of tokens, needed by commands who expect a certain type - // of arguments, such as paths with spaces. - tokenParsed := c.parseTokens(envParsed) - - // Execute the command and print any errors - if _, parserErr := c.parser.ParseArgs(tokenParsed); parserErr != nil { - fmt.Println(readline.RED + "[Error] " + readline.RESET + parserErr.Error() + "\n") - } - } -} - -// Readline - Add an empty line between input line and command output. -func (c *console) Readline() (line string, err error) { - line, err = c.shell.Readline() - fmt.Println() - return -} - -// sanitizeInput - Trims spaces and other unwished elements from the input line. -func sanitizeInput(line string) (sanitized []string, empty bool) { - - // Assume the input is not empty - empty = false - - // Trim border spaces - trimmed := strings.TrimSpace(line) - if len(line) < 1 { - empty = true - return - } - unfiltered := strings.Split(trimmed, " ") - - // Catch any eventual empty items - for _, arg := range unfiltered { - if arg != "" { - sanitized = append(sanitized, arg) - } - } - return -} - -// parseTokens - Parse and process any special tokens that are not treated by environment-like parsers. -func (c *console) parseTokens(sanitized []string) (parsed []string) { - - // PATH SPACE TOKENS - // Catch \ tokens, which have been introduced in paths where some directories have spaces in name. - // For each of these splits, we concatenate them with the next string. - // This will also inspect commands/options/arguments, but there is no reason why a backlash should be present in them. - var pathAdjusted []string - var roll bool - var arg string - for i := range sanitized { - if strings.HasSuffix(sanitized[i], "\\") { - // If we find a suffix, replace with a space. Go on with next input - arg += strings.TrimSuffix(sanitized[i], "\\") + " " - roll = true - } else if roll { - // No suffix but part of previous input. Add it and go on. - arg += sanitized[i] - pathAdjusted = append(pathAdjusted, arg) - arg = "" - roll = false - } else { - // Default, we add our path and go on. - pathAdjusted = append(pathAdjusted, sanitized[i]) - } - } - parsed = pathAdjusted - - // Add new function here, act on parsed []string from now on, not sanitized - return -} diff --git a/readline/vimdelete.go b/readline/vimdelete.go index 7a07259..f5c1806 100644 --- a/readline/vimdelete.go +++ b/readline/vimdelete.go @@ -142,6 +142,10 @@ func (rl *Instance) viDeleteByAdjust(adjust int) { rl.updateHelpers() } +func (rl *Instance) DeleteByAmount(adjust int) { + rl.viDeleteByAdjust(adjust) +} + func (rl *Instance) vimDeleteToken(r rune) bool { tokens, _, _ := tokeniseSplitSpaces(rl.line, 0) pos := int(r) - 48 // convert ASCII to integer 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/testplugin/testplugin.go b/testplugin/testplugin.go deleted file mode 100644 index 2d8a41b..0000000 --- a/testplugin/testplugin.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - rt "github.com/arnodel/golua/runtime" -) - -func Loader(rtm *rt.Runtime) rt.Value { - return rt.StringValue("hello world!") -} diff --git a/testplugin/testplugin.so b/testplugin/testplugin.so deleted file mode 100644 index 3c83992..0000000 Binary files a/testplugin/testplugin.so and /dev/null differ 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 } } diff --git a/vars.go b/vars.go index c93e2b6..86ed253 100644 --- a/vars.go +++ b/vars.go @@ -11,8 +11,8 @@ var ( // Version info var ( - ver = "v2.3.4" - releaseName = "Alyssum" + ver = "v2.4.0" + releaseName = "Moonflower" gitCommit string gitBranch string