From 2f6ab5fd92e77d46ec953bdcc39a7f7bd99536e0 Mon Sep 17 00:00:00 2001 From: sammyette Date: Fri, 20 Jan 2023 19:07:42 -0400 Subject: [PATCH] feat: add sink for commanders to write output/read input (#232) to write output, you would usually just use the print builtin since commanders are just lua custom commands but this does not consider the fact of pipes or other shell operators being used to redirect or whatever. this adds readable/writable "sinks" which is a type for input or output and is currently only used for commanders but can be used for other hilbish things in the future --- CHANGELOG.md | 2 + cmd/docgen/docgen.go | 12 ++-- docs/api/commander.md | 34 ++++++++++ exec.go | 13 +++- golibs/commander/commander.go | 37 ++++++++++- lua.go | 1 + nature/commands/bg.lua | 6 +- nature/commands/cat.lua | 8 +-- nature/commands/cd.lua | 8 +-- nature/commands/cdr.lua | 12 ++-- nature/commands/disown.lua | 8 +-- nature/commands/doc.lua | 6 +- nature/commands/fg.lua | 6 +- sink.go | 121 ++++++++++++++++++++++++++++++++++ 14 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 sink.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db665b3..4c02f04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ ### Added - Documented custom userdata types (Job and Timer Objects) - Coming with fix is also adding the return types for some functions that were missing it +- Added a dedicated input and dedicated outputs for commanders. ### Fixed - `hilbish.which` not working correctly with aliases +- Commanders not being able to pipe with commands or any related operator. ## [2.0.1] - 2022-12-28 ### Fixed diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index faa78453..c176676d 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -414,7 +414,14 @@ func main() { f, _ := os.Create(docPath) f.WriteString(fmt.Sprintf(header, modOrIface, modname, modu.ShortDescription)) - f.WriteString(fmt.Sprintf("## Introduction\n%s\n\n", modu.Description)) + typeTag, _ := regexp.Compile(`@\w+`) + modDescription := typeTag.ReplaceAllStringFunc(strings.Replace(modu.Description, "<", `\<`, -1), func(typ string) string { + typName := typ[1:] + typLookup := typeTable[strings.ToLower(typName)] + linkedTyp := fmt.Sprintf("/Hilbish/docs/api/%s/%s/#%s", typLookup[0], typLookup[0] + "." + typLookup[1], strings.ToLower(typName)) + return fmt.Sprintf(`%s`, linkedTyp, typName) + }) + f.WriteString(fmt.Sprintf("## Introduction\n%s\n\n", modDescription)) if len(modu.Fields) != 0 { f.WriteString("## Interface fields\n") for _, dps := range modu.Fields { @@ -435,7 +442,6 @@ func main() { } if len(modu.Docs) != 0 { - typeTag, _ := regexp.Compile(`@\w+`) f.WriteString("## Functions\n") for _, dps := range modu.Docs { if dps.IsMember { @@ -475,8 +481,6 @@ func main() { } } f.WriteString("\n") - typeTag, _ := regexp.Compile(`@\w+`) - f.WriteString("### Methods\n") for _, dps := range modu.Docs { if !dps.IsMember { diff --git a/docs/api/commander.md b/docs/api/commander.md index e3e5320c..a23dcb18 100644 --- a/docs/api/commander.md +++ b/docs/api/commander.md @@ -8,7 +8,41 @@ menu: --- ## Introduction + Commander is a library for writing custom commands in Lua. +In order to make it easier to write commands for Hilbish, +not require separate scripts and to be able to use in a config, +the Commander library exists. This is like a very simple wrapper +that works with Hilbish for writing commands. Example: + +```lua +local commander = require 'commander' + +commander.register('hello', function(args, sinks) + sinks.out:writeln 'Hello world!' +end) +``` + +In this example, a command with the name of `hello` is created +that will print `Hello world!` to output. One question you may +have is: What is the `sinks` parameter? + +A sink is a writable/readable pipe, or you can imagine a Lua +file. It's used in this case to write to the proper output, +incase a user either pipes to another command or redirects somewhere else. + +So, the `sinks` parameter is a table containing 3 sinks: +`in`, `out`, and `err`. +- `in` is the standard input. You can read from this sink +to get user input. (**This is currently unimplemented.**) +- `out` is standard output. This is usually where text meant for +output should go. +- `err` is standard error. This sink is for writing errors, as the +name would suggest. + +A sink has 2 methods: +- `write(str)` will write to the sink. +- `writeln(str)` will write to the sink with a newline at the end. ## Functions ### deregister(name) diff --git a/exec.go b/exec.go index e18cdd61..726a9866 100644 --- a/exec.go +++ b/exec.go @@ -323,8 +323,18 @@ func execHandle(bg bool) interp.ExecHandlerFunc { luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) } + hc := interp.HandlerCtx(ctx) if commands[args[0]] != nil { - luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs)) + 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("out"), rt.UserDataValue(stdout.ud)) + sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud)) + + luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) if err != nil { fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) return interp.NewExitStatus(1) @@ -364,7 +374,6 @@ func execHandle(bg bool) interp.ExecHandlerFunc { killTimeout := 2 * time.Second // from here is basically copy-paste of the default exec handler from // sh/interp but with our job handling - hc := interp.HandlerCtx(ctx) path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) if err != nil { fmt.Fprintln(hc.Stderr, err) diff --git a/golibs/commander/commander.go b/golibs/commander/commander.go index e1e8a95c..f67e9b80 100644 --- a/golibs/commander/commander.go +++ b/golibs/commander/commander.go @@ -1,5 +1,40 @@ // library for custom commands -// Commander is a library for writing custom commands in Lua. +/* +Commander is a library for writing custom commands in Lua. +In order to make it easier to write commands for Hilbish, +not require separate scripts and to be able to use in a config, +the Commander library exists. This is like a very simple wrapper +that works with Hilbish for writing commands. Example: + +```lua +local commander = require 'commander' + +commander.register('hello', function(args, sinks) + sinks.out:writeln 'Hello world!' +end) +``` + +In this example, a command with the name of `hello` is created +that will print `Hello world!` to output. One question you may +have is: What is the `sinks` parameter? + +A sink is a writable/readable pipe, or you can imagine a Lua +file. It's used in this case to write to the proper output, +incase a user either pipes to another command or redirects somewhere else. + +So, the `sinks` parameter is a table containing 3 sinks: +`in`, `out`, and `err`. +- `in` is the standard input. You can read from this sink +to get user input. (**This is currently unimplemented.**) +- `out` is standard output. This is usually where text meant for +output should go. +- `err` is standard error. This sink is for writing errors, as the +name would suggest. + +A sink has 2 methods: +- `write(str)` will write to the sink. +- `writeln(str)` will write to the sink with a newline at the end. +*/ package commander import ( diff --git a/lua.go b/lua.go index 79eb1f7b..0a7c1157 100644 --- a/lua.go +++ b/lua.go @@ -23,6 +23,7 @@ func luaInit() { MessageHandler: debuglib.Traceback, }) lib.LoadAll(l) + setupSinkType(l) lib.LoadLibs(l, hilbishLoader) // yes this is stupid, i know diff --git a/nature/commands/bg.lua b/nature/commands/bg.lua index f0aa462b..a7f486a9 100644 --- a/nature/commands/bg.lua +++ b/nature/commands/bg.lua @@ -1,15 +1,15 @@ local commander = require 'commander' -commander.register('bg', function() +commander.register('bg', function(_, sinks) local job = hilbish.jobs.last() if not job then - print 'bg: no last job' + sinks.out:writeln 'bg: no last job' return 1 end local err = job.background() if err then - print('bg: ' .. err) + sinks.out:writeln('bg: ' .. err) return 2 end end) diff --git a/nature/commands/cat.lua b/nature/commands/cat.lua index 132db5f9..06df5071 100644 --- a/nature/commands/cat.lua +++ b/nature/commands/cat.lua @@ -1,11 +1,11 @@ local commander = require 'commander' local fs = require 'fs' -commander.register('cat', function(args) +commander.register('cat', function(args, sinks) local exit = 0 if #args == 0 then - print [[ + sinks.out:writeln [[ usage: cat [file]...]] end @@ -13,11 +13,11 @@ usage: cat [file]...]] local f = io.open(fName) if f == nil then exit = 1 - print(string.format('cat: %s: no such file or directory', fName)) + sinks.out:writeln(string.format('cat: %s: no such file or directory', fName)) goto continue end - io.write(f:read '*a') + sinks.out:writeln(f:read '*a') ::continue:: end io.flush() diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua index 8c8dbc83..7cfe4a2d 100644 --- a/nature/commands/cd.lua +++ b/nature/commands/cd.lua @@ -4,16 +4,16 @@ local fs = require 'fs' local dirs = require 'nature.dirs' dirs.old = hilbish.cwd() -commander.register('cd', function (args) +commander.register('cd', function (args, sinks) if #args > 1 then - print("cd: too many arguments") + sinks.out:writeln("cd: too many arguments") return 1 end local path = args[1] and args[1] or hilbish.home if path == '-' then path = dirs.old - print(path) + sinks.out:writeln(path) end dirs.setOld(hilbish.cwd()) @@ -21,7 +21,7 @@ commander.register('cd', function (args) local ok, err = pcall(function() fs.cd(path) end) if not ok then - print(err) + sinks.out:writeln(err) return 1 end bait.throw('cd', path) diff --git a/nature/commands/cdr.lua b/nature/commands/cdr.lua index 0438e6f2..288ae220 100644 --- a/nature/commands/cdr.lua +++ b/nature/commands/cdr.lua @@ -3,9 +3,9 @@ local fs = require 'fs' local lunacolors = require 'lunacolors' local dirs = require 'nature.dirs' -commander.register('cdr', function(args) +commander.register('cdr', function(args, sinks) if not args[1] then - print(lunacolors.format [[ + sinks.out:writeln(lunacolors.format [[ cdr: change directory to one which has been recently visied usage: cdr @@ -17,21 +17,21 @@ to get a list of recent directories, use {green}{underline}cdr list{reset}]]) if args[1] == 'list' then local recentDirs = dirs.recentDirs if #recentDirs == 0 then - print 'No directories have been visited.' + sinks.out:writeln 'No directories have been visited.' return 1 end - print(table.concat(recentDirs, '\n')) + sinks.out:writeln(table.concat(recentDirs, '\n')) return end local index = tonumber(args[1]) if not index then - print(string.format('Received %s as index, which isn\'t a number.', index)) + sinks.out:writeln(string.format('Received %s as index, which isn\'t a number.', index)) return 1 end if not dirs.recent(index) then - print(string.format('No recent directory found at index %s.', index)) + sinks.out:writeln(string.format('No recent directory found at index %s.', index)) return 1 end diff --git a/nature/commands/disown.lua b/nature/commands/disown.lua index f8f144fb..6645a0f5 100644 --- a/nature/commands/disown.lua +++ b/nature/commands/disown.lua @@ -1,8 +1,8 @@ local commander = require 'commander' -commander.register('disown', function(args) +commander.register('disown', function(args, sinks) if #hilbish.jobs.all() == 0 then - print 'disown: no current job' + sinks.out:writeln 'disown: no current job' return 1 end @@ -10,7 +10,7 @@ commander.register('disown', function(args) if #args < 0 then id = tonumber(args[1]) if not id then - print 'disown: invalid id for job' + sinks.out:writeln 'disown: invalid id for job' return 1 end else @@ -19,7 +19,7 @@ commander.register('disown', function(args) local ok = pcall(hilbish.jobs.disown, id) if not ok then - print 'disown: job does not exist' + sinks.out:writeln 'disown: job does not exist' return 2 end end) diff --git a/nature/commands/doc.lua b/nature/commands/doc.lua index 379b3648..5d03823d 100644 --- a/nature/commands/doc.lua +++ b/nature/commands/doc.lua @@ -2,7 +2,7 @@ local commander = require 'commander' local fs = require 'fs' local lunacolors = require 'lunacolors' -commander.register('doc', function(args) +commander.register('doc', function(args, sinks) local moddocPath = hilbish.dataDir .. '/docs/' local stat = fs.stat '.git/refs/heads/extended-job-api' if stat then @@ -48,7 +48,7 @@ Available sections: ]] .. table.concat(modules, ', ') f = io.open(moddocPath .. subdocName .. '.md', 'rb') end if not f then - print('No documentation found for ' .. mod .. '.') + sinks.out:writeln('No documentation found for ' .. mod .. '.') return 1 end end @@ -86,7 +86,7 @@ Available sections: ]] .. table.concat(modules, ', ') end local backtickOccurence = 0 - print(lunacolors.format(doc:gsub('`', function() + sinks.out:writeln(lunacolors.format(doc:gsub('`', function() backtickOccurence = backtickOccurence + 1 if backtickOccurence % 2 == 0 then return '{reset}' diff --git a/nature/commands/fg.lua b/nature/commands/fg.lua index a3f14516..64b145f4 100644 --- a/nature/commands/fg.lua +++ b/nature/commands/fg.lua @@ -1,15 +1,15 @@ local commander = require 'commander' -commander.register('fg', function() +commander.register('fg', function(_, sinks) local job = hilbish.jobs.last() if not job then - print 'fg: no last job' + sinks.out:writeln 'fg: no last job' return 1 end local err = job.foreground() -- waits for job; blocks if err then - print('fg: ' .. err) + sinks.out:writeln('fg: ' .. err) return 2 end end) diff --git a/sink.go b/sink.go new file mode 100644 index 00000000..54f5014e --- /dev/null +++ b/sink.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "io" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +var sinkMetaKey = rt.StringValue("hshsink") + +// a sink is a structure that has input and/or output +// it is like a lua file when used in popen, but specific to hilbish +type sink struct{ + writer io.Writer + reader io.Reader + ud *rt.UserData +} + +func setupSinkType(rtm *rt.Runtime) { + sinkMeta := rt.NewTable() + + sinkMethods := rt.NewTable() + sinkFuncs := map[string]util.LuaExport{ + "write": {luaSinkWrite, 2, false}, + "writeln": {luaSinkWriteln, 2, false}, + } + util.SetExports(l, sinkMethods, sinkFuncs) + + sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + arg := c.Arg(1) + val := sinkMethods.Get(arg) + + return c.PushingNext1(t.Runtime, val), nil + } + + sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false))) + l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) +} + +func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + data, err := c.StringArg(1) + if err != nil { + return nil, err + } + + s.writer.Write([]byte(data)) + + return c.Next(), nil +} + +func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + data, err := c.StringArg(1) + if err != nil { + return nil, err + } + + s.writer.Write([]byte(data + "\n")) + + return c.Next(), nil +} + +func newSinkInput(r io.Reader) *sink { + s := &sink{ + reader: r, + } + s.ud = sinkUserData(s) + + return s +} + +func newSinkOutput(w io.Writer) *sink { + s := &sink{ + writer: w, + } + s.ud = sinkUserData(s) + + return s +} + +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) + } + + return s, nil +} + +func valueToSink(val rt.Value) (*sink, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + s, ok := u.Value().(*sink) + return s, ok +} + +func sinkUserData(s *sink) *rt.UserData { + sinkMeta := l.Registry(sinkMetaKey) + return rt.NewUserData(s, sinkMeta.AsTable()) +}