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())
+}