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
multiline
sammyette 2023-01-20 19:07:42 -04:00 committed by GitHub
parent 088e326bd1
commit 2f6ab5fd92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 240 additions and 34 deletions

View File

@ -4,9 +4,11 @@
### Added ### Added
- Documented custom userdata types (Job and Timer Objects) - Documented custom userdata types (Job and Timer Objects)
- Coming with fix is also adding the return types for some functions that were missing it - 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 ### Fixed
- `hilbish.which` not working correctly with aliases - `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 ## [2.0.1] - 2022-12-28
### Fixed ### Fixed

View File

@ -414,7 +414,14 @@ func main() {
f, _ := os.Create(docPath) f, _ := os.Create(docPath)
f.WriteString(fmt.Sprintf(header, modOrIface, modname, modu.ShortDescription)) 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(`<a href="%s" style="text-decoration: none;">%s</a>`, linkedTyp, typName)
})
f.WriteString(fmt.Sprintf("## Introduction\n%s\n\n", modDescription))
if len(modu.Fields) != 0 { if len(modu.Fields) != 0 {
f.WriteString("## Interface fields\n") f.WriteString("## Interface fields\n")
for _, dps := range modu.Fields { for _, dps := range modu.Fields {
@ -435,7 +442,6 @@ func main() {
} }
if len(modu.Docs) != 0 { if len(modu.Docs) != 0 {
typeTag, _ := regexp.Compile(`@\w+`)
f.WriteString("## Functions\n") f.WriteString("## Functions\n")
for _, dps := range modu.Docs { for _, dps := range modu.Docs {
if dps.IsMember { if dps.IsMember {
@ -475,8 +481,6 @@ func main() {
} }
} }
f.WriteString("\n") f.WriteString("\n")
typeTag, _ := regexp.Compile(`@\w+`)
f.WriteString("### Methods\n") f.WriteString("### Methods\n")
for _, dps := range modu.Docs { for _, dps := range modu.Docs {
if !dps.IsMember { if !dps.IsMember {

View File

@ -8,7 +8,41 @@ menu:
--- ---
## Introduction ## Introduction
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.
## Functions ## Functions
### deregister(name) ### deregister(name)

13
exec.go
View File

@ -323,8 +323,18 @@ func execHandle(bg bool) interp.ExecHandlerFunc {
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
} }
hc := interp.HandlerCtx(ctx)
if commands[args[0]] != nil { 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 { if err != nil {
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
return interp.NewExitStatus(1) return interp.NewExitStatus(1)
@ -364,7 +374,6 @@ func execHandle(bg bool) interp.ExecHandlerFunc {
killTimeout := 2 * time.Second killTimeout := 2 * time.Second
// from here is basically copy-paste of the default exec handler from // from here is basically copy-paste of the default exec handler from
// sh/interp but with our job handling // sh/interp but with our job handling
hc := interp.HandlerCtx(ctx)
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
if err != nil { if err != nil {
fmt.Fprintln(hc.Stderr, err) fmt.Fprintln(hc.Stderr, err)

View File

@ -1,5 +1,40 @@
// library for custom commands // 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 package commander
import ( import (

1
lua.go
View File

@ -23,6 +23,7 @@ func luaInit() {
MessageHandler: debuglib.Traceback, MessageHandler: debuglib.Traceback,
}) })
lib.LoadAll(l) lib.LoadAll(l)
setupSinkType(l)
lib.LoadLibs(l, hilbishLoader) lib.LoadLibs(l, hilbishLoader)
// yes this is stupid, i know // yes this is stupid, i know

View File

@ -1,15 +1,15 @@
local commander = require 'commander' local commander = require 'commander'
commander.register('bg', function() commander.register('bg', function(_, sinks)
local job = hilbish.jobs.last() local job = hilbish.jobs.last()
if not job then if not job then
print 'bg: no last job' sinks.out:writeln 'bg: no last job'
return 1 return 1
end end
local err = job.background() local err = job.background()
if err then if err then
print('bg: ' .. err) sinks.out:writeln('bg: ' .. err)
return 2 return 2
end end
end) end)

View File

@ -1,11 +1,11 @@
local commander = require 'commander' local commander = require 'commander'
local fs = require 'fs' local fs = require 'fs'
commander.register('cat', function(args) commander.register('cat', function(args, sinks)
local exit = 0 local exit = 0
if #args == 0 then if #args == 0 then
print [[ sinks.out:writeln [[
usage: cat [file]...]] usage: cat [file]...]]
end end
@ -13,11 +13,11 @@ usage: cat [file]...]]
local f = io.open(fName) local f = io.open(fName)
if f == nil then if f == nil then
exit = 1 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 goto continue
end end
io.write(f:read '*a') sinks.out:writeln(f:read '*a')
::continue:: ::continue::
end end
io.flush() io.flush()

View File

@ -4,16 +4,16 @@ local fs = require 'fs'
local dirs = require 'nature.dirs' local dirs = require 'nature.dirs'
dirs.old = hilbish.cwd() dirs.old = hilbish.cwd()
commander.register('cd', function (args) commander.register('cd', function (args, sinks)
if #args > 1 then if #args > 1 then
print("cd: too many arguments") sinks.out:writeln("cd: too many arguments")
return 1 return 1
end end
local path = args[1] and args[1] or hilbish.home local path = args[1] and args[1] or hilbish.home
if path == '-' then if path == '-' then
path = dirs.old path = dirs.old
print(path) sinks.out:writeln(path)
end end
dirs.setOld(hilbish.cwd()) dirs.setOld(hilbish.cwd())
@ -21,7 +21,7 @@ commander.register('cd', function (args)
local ok, err = pcall(function() fs.cd(path) end) local ok, err = pcall(function() fs.cd(path) end)
if not ok then if not ok then
print(err) sinks.out:writeln(err)
return 1 return 1
end end
bait.throw('cd', path) bait.throw('cd', path)

View File

@ -3,9 +3,9 @@ local fs = require 'fs'
local lunacolors = require 'lunacolors' local lunacolors = require 'lunacolors'
local dirs = require 'nature.dirs' local dirs = require 'nature.dirs'
commander.register('cdr', function(args) commander.register('cdr', function(args, sinks)
if not args[1] then if not args[1] then
print(lunacolors.format [[ sinks.out:writeln(lunacolors.format [[
cdr: change directory to one which has been recently visied cdr: change directory to one which has been recently visied
usage: cdr <index> usage: cdr <index>
@ -17,21 +17,21 @@ to get a list of recent directories, use {green}{underline}cdr list{reset}]])
if args[1] == 'list' then if args[1] == 'list' then
local recentDirs = dirs.recentDirs local recentDirs = dirs.recentDirs
if #recentDirs == 0 then if #recentDirs == 0 then
print 'No directories have been visited.' sinks.out:writeln 'No directories have been visited.'
return 1 return 1
end end
print(table.concat(recentDirs, '\n')) sinks.out:writeln(table.concat(recentDirs, '\n'))
return return
end end
local index = tonumber(args[1]) local index = tonumber(args[1])
if not index then 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 return 1
end end
if not dirs.recent(index) then 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 return 1
end end

View File

@ -1,8 +1,8 @@
local commander = require 'commander' local commander = require 'commander'
commander.register('disown', function(args) commander.register('disown', function(args, sinks)
if #hilbish.jobs.all() == 0 then if #hilbish.jobs.all() == 0 then
print 'disown: no current job' sinks.out:writeln 'disown: no current job'
return 1 return 1
end end
@ -10,7 +10,7 @@ commander.register('disown', function(args)
if #args < 0 then if #args < 0 then
id = tonumber(args[1]) id = tonumber(args[1])
if not id then if not id then
print 'disown: invalid id for job' sinks.out:writeln 'disown: invalid id for job'
return 1 return 1
end end
else else
@ -19,7 +19,7 @@ commander.register('disown', function(args)
local ok = pcall(hilbish.jobs.disown, id) local ok = pcall(hilbish.jobs.disown, id)
if not ok then if not ok then
print 'disown: job does not exist' sinks.out:writeln 'disown: job does not exist'
return 2 return 2
end end
end) end)

View File

@ -2,7 +2,7 @@ local commander = require 'commander'
local fs = require 'fs' local fs = require 'fs'
local lunacolors = require 'lunacolors' local lunacolors = require 'lunacolors'
commander.register('doc', function(args) commander.register('doc', function(args, sinks)
local moddocPath = hilbish.dataDir .. '/docs/' local moddocPath = hilbish.dataDir .. '/docs/'
local stat = fs.stat '.git/refs/heads/extended-job-api' local stat = fs.stat '.git/refs/heads/extended-job-api'
if stat then if stat then
@ -48,7 +48,7 @@ Available sections: ]] .. table.concat(modules, ', ')
f = io.open(moddocPath .. subdocName .. '.md', 'rb') f = io.open(moddocPath .. subdocName .. '.md', 'rb')
end end
if not f then if not f then
print('No documentation found for ' .. mod .. '.') sinks.out:writeln('No documentation found for ' .. mod .. '.')
return 1 return 1
end end
end end
@ -86,7 +86,7 @@ Available sections: ]] .. table.concat(modules, ', ')
end end
local backtickOccurence = 0 local backtickOccurence = 0
print(lunacolors.format(doc:gsub('`', function() sinks.out:writeln(lunacolors.format(doc:gsub('`', function()
backtickOccurence = backtickOccurence + 1 backtickOccurence = backtickOccurence + 1
if backtickOccurence % 2 == 0 then if backtickOccurence % 2 == 0 then
return '{reset}' return '{reset}'

View File

@ -1,15 +1,15 @@
local commander = require 'commander' local commander = require 'commander'
commander.register('fg', function() commander.register('fg', function(_, sinks)
local job = hilbish.jobs.last() local job = hilbish.jobs.last()
if not job then if not job then
print 'fg: no last job' sinks.out:writeln 'fg: no last job'
return 1 return 1
end end
local err = job.foreground() -- waits for job; blocks local err = job.foreground() -- waits for job; blocks
if err then if err then
print('fg: ' .. err) sinks.out:writeln('fg: ' .. err)
return 2 return 2
end end
end) end)

121
sink.go 100644
View File

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