2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-07-01 16:52:03 +00:00
2025-06-15 12:27:36 -04:00

224 lines
5.0 KiB
Go

// 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/moonlight"
"hilbish/util"
"github.com/arnodel/golua/lib/iolib"
rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
var snailMetaKey = rt.StringValue("hshsnail")
func Loader(mlr *moonlight.Runtime) moonlight.Value {
snailMeta := moonlight.NewTable()
snailMethods := moonlight.NewTable()
snailFuncs := map[string]moonlight.Export{
"run": {snailrun, 3, false},
"dir": {snaildir, 2, false},
}
mlr.SetExports(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)))
mlr.SetRegistry(snailMetaKey, moonlight.TableValue(snailMeta))
exports := map[string]moonlight.Export{
"new": {snailnew, 0, false},
}
mod := moonlight.NewTable()
mlr.SetExports(mod, exports)
return moonlight.TableValue(mod)
}
// new() -> @Snail
// Creates a new Snail instance.
func snailnew(mlr *moonlight.Runtime) error {
s := New(mlr)
mlr.PushNext1(moonlight.UserDataValue(snailUserData(s)))
return 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(mlr *moonlight.Runtime) error {
if err := mlr.CheckNArgs(2); err != nil {
return err
}
s, err := snailArg(mlr, 0)
if err != nil {
return err
}
cmd, err := mlr.StringArg(1)
if err != nil {
return err
}
streams := &util.Streams{}
thirdArg := mlr.Arg(2)
switch thirdArg.Type() {
case moonlight.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 moonlight.NilType: // noop
default:
return errors.New("expected 3rd arg to be a table")
}
var newline bool
var cont bool
var luaErr moonlight.Value = moonlight.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 = moonlight.StringValue(err.Error())
}
}
}
runnerRet := moonlight.NewTable()
runnerRet.SetField("input", moonlight.StringValue(cmd))
runnerRet.SetField("exitCode", moonlight.IntValue(int64(exitCode)))
runnerRet.SetField("continue", moonlight.BoolValue(cont))
runnerRet.SetField("newline", moonlight.BoolValue(newline))
runnerRet.SetField("err", luaErr)
runnerRet.SetField("bg", moonlight.BoolValue(bg))
mlr.PushNext1(moonlight.TableValue(runnerRet))
return 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(mlr *moonlight.Runtime) error {
if err := mlr.CheckNArgs(2); err != nil {
return err
}
s, err := snailArg(mlr, 0)
if err != nil {
return err
}
dir, err := mlr.StringArg(1)
if err != nil {
return err
}
interp.Dir(dir)(s.runner)
return 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(mlr *moonlight.Runtime, arg int) (*Snail, error) {
s, ok := valueToSnail(mlr.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) *moonlight.UserData {
snailMeta := s.runtime.Registry(snailMetaKey)
return moonlight.NewUserData(s, moonlight.ToTable(s.runtime, snailMeta))
}