diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db665b..4c02f04 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 faa7845..c176676 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 e3e5320..a23dcb1 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 e18cdd6..726a986 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 e1e8a95..f67e9b8 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 79eb1f7..0a7c115 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 f0aa462..a7f486a 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 132db5f..06df507 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 8c8dbc8..7cfe4a2 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 0438e6f..288ae22 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 f8f144f..6645a0f 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 379b364..5d03823 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 a3f1451..64b145f 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 0000000..54f5014 --- /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()) +}