From 781a761ead31eb340b53dcb344026869ca21d441 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 10 Apr 2024 20:45:58 +1000 Subject: [PATCH] Initial version of prototype cmdlang --- .gitignore | 1 + cmd/cmsh/main.go | 30 +++++++++++++++++++++++++++++ cmdlang/ast.go | 26 +++++++++++++++++++++++++ cmdlang/builtins.go | 21 ++++++++++++++++++++ cmdlang/env.go | 32 ++++++++++++++++++++++++++++++ cmdlang/eval.go | 47 +++++++++++++++++++++++++++++++++++++++++++++ cmdlang/inst.go | 30 +++++++++++++++++++++++++++++ cmdlang/objs.go | 17 ++++++++++++++++ go.mod | 10 ++++++++++ go.sum | 10 ++++++++++ 10 files changed, 224 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/cmsh/main.go create mode 100644 cmdlang/ast.go create mode 100644 cmdlang/builtins.go create mode 100644 cmdlang/env.go create mode 100644 cmdlang/eval.go create mode 100644 cmdlang/inst.go create mode 100644 cmdlang/objs.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go new file mode 100644 index 0000000..235a6e2 --- /dev/null +++ b/cmd/cmsh/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "github.com/chzyer/readline" + "github.com/lmika/cmdlang-proto/cmdlang" + "log" +) + +func main() { + rl, err := readline.New("> ") + if err != nil { + panic(err) + } + defer rl.Close() + + inst := cmdlang.New() + ctx := context.Background() + + for { + line, err := rl.Readline() + if err != nil { // io.EOF + break + } + + if err := inst.Eval(ctx, line); err != nil { + log.Printf("%T: %v", err, err) + } + } +} diff --git a/cmdlang/ast.go b/cmdlang/ast.go new file mode 100644 index 0000000..f1446b8 --- /dev/null +++ b/cmdlang/ast.go @@ -0,0 +1,26 @@ +package cmdlang + +import ( + "github.com/alecthomas/participle/v2" + "io" +) + +type astLiteral struct { + Str *string `parser:"@String"` + Ident *string `parser:" | @Ident"` +} + +type astCmdArg struct { + Literal astLiteral `parser:"@@"` +} + +type astCmd struct { + Name string `parser:"@Ident"` + Args []astCmdArg `parser:"@@*"` +} + +var parser = participle.MustBuild[astCmd]() + +func parse(r io.Reader) (*astCmd, error) { + return parser.Parse("test", r) +} diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go new file mode 100644 index 0000000..094d1b1 --- /dev/null +++ b/cmdlang/builtins.go @@ -0,0 +1,21 @@ +package cmdlang + +import ( + "context" + "log" + "strings" +) + +func echoBuiltin(ctx context.Context, args invocationArgs) error { + if len(args.args) == 0 { + log.Print() + return nil + } + + var line strings.Builder + for _, arg := range args.args { + line.WriteString(arg) + } + log.Print(line.String()) + return nil +} diff --git a/cmdlang/env.go b/cmdlang/env.go new file mode 100644 index 0000000..1bf7fbc --- /dev/null +++ b/cmdlang/env.go @@ -0,0 +1,32 @@ +package cmdlang + +import ( + "errors" +) + +type evalCtx struct { + parent *evalCtx + commands map[string]invokable +} + +func (ec *evalCtx) addCmd(name string, inv invokable) { + if ec.commands == nil { + ec.commands = make(map[string]invokable) + } + + ec.commands[name] = inv +} + +func (ec *evalCtx) lookupCmd(name string) (invokable, error) { + for e := ec; e != nil; e = e.parent { + if ec.commands == nil { + continue + } + + if cmd, ok := ec.commands[name]; ok { + return cmd, nil + } + + } + return nil, errors.New("name " + name + " not found") +} diff --git a/cmdlang/eval.go b/cmdlang/eval.go new file mode 100644 index 0000000..bd62356 --- /dev/null +++ b/cmdlang/eval.go @@ -0,0 +1,47 @@ +package cmdlang + +import ( + "context" + "errors" + "github.com/lmika/gopkgs/fp/slices" + "strconv" +) + +type evaluator struct { +} + +func (e evaluator) evaluate(ctx context.Context, ec *evalCtx, ast *astCmd) error { + cmd, err := ec.lookupCmd(ast.Name) + if err != nil { + return err + } + + args, err := slices.MapWithError(ast.Args, func(a astCmdArg) (string, error) { + return e.evalArg(ctx, ec, a) + }) + if err != nil { + return err + } + + return cmd.invoke(ctx, invocationArgs{ + args: args, + }) +} + +func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (string, error) { + return e.evalLiteral(ctx, ec, n.Literal) +} + +func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n astLiteral) (string, error) { + switch { + case n.Str != nil: + uq, err := strconv.Unquote(*n.Str) + if err != nil { + return "", err + } + return uq, nil + case n.Ident != nil: + return *n.Ident, nil + } + return "", errors.New("unhandled literal type") +} diff --git a/cmdlang/inst.go b/cmdlang/inst.go new file mode 100644 index 0000000..7551758 --- /dev/null +++ b/cmdlang/inst.go @@ -0,0 +1,30 @@ +package cmdlang + +import ( + "context" + "strings" +) + +type Inst struct { + rootEC *evalCtx +} + +func New() *Inst { + rootEC := evalCtx{} + rootEC.addCmd("echo", invokableFunc(echoBuiltin)) + + return &Inst{ + rootEC: &rootEC, + } +} + +// TODO: return value? +func (inst *Inst) Eval(ctx context.Context, expr string) error { + ast, err := parse(strings.NewReader(expr)) + if err != nil { + return err + } + + eval := evaluator{} + return eval.evaluate(ctx, inst.rootEC, ast) +} diff --git a/cmdlang/objs.go b/cmdlang/objs.go new file mode 100644 index 0000000..5033bbd --- /dev/null +++ b/cmdlang/objs.go @@ -0,0 +1,17 @@ +package cmdlang + +import "context" + +type invocationArgs struct { + args []string +} + +type invokable interface { + invoke(ctx context.Context, args invocationArgs) error +} + +type invokableFunc func(ctx context.Context, args invocationArgs) error + +func (i invokableFunc) invoke(ctx context.Context, args invocationArgs) error { + return i(ctx, args) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..70190e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/lmika/cmdlang-proto + +go 1.21.1 + +require ( + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect + golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f982ff2 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=