Working on multi-line statements
This commit is contained in:
parent
82a6eac872
commit
730dc46095
|
@ -2,6 +2,7 @@ package cmdlang
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/alecthomas/participle/v2"
|
"github.com/alecthomas/participle/v2"
|
||||||
|
"github.com/alecthomas/participle/v2/lexer"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,8 +13,8 @@ type astLiteral struct {
|
||||||
|
|
||||||
type astCmdArg struct {
|
type astCmdArg struct {
|
||||||
Literal *astLiteral `parser:"@@"`
|
Literal *astLiteral `parser:"@@"`
|
||||||
Var *string `parser:"| '$' @Ident"`
|
Var *string `parser:"| DOLLAR @Ident"`
|
||||||
Sub *astPipeline `parser:"| '(' @@ ')'"`
|
Sub *astPipeline `parser:"| LP @@ RP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type astCmd struct {
|
type astCmd struct {
|
||||||
|
@ -23,15 +24,33 @@ type astCmd struct {
|
||||||
|
|
||||||
type astPipeline struct {
|
type astPipeline struct {
|
||||||
First *astCmd `parser:"@@"`
|
First *astCmd `parser:"@@"`
|
||||||
Rest []*astCmd `parser:"( '|' @@ )*"`
|
Rest []*astCmd `parser:"( PIPE @@ )*"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type astStatements struct {
|
type astStatements struct {
|
||||||
First *astPipeline `parser:"@@"`
|
First *astPipeline `parser:"@@"`
|
||||||
Rest []*astPipeline `parser:"( ';' @@ )*"` // TODO: also add support for newlines
|
Rest []*astPipeline `parser:"( (SEMICL | NL)+ @@ )*"` // TODO: also add support for newlines
|
||||||
}
|
}
|
||||||
|
|
||||||
var parser = participle.MustBuild[astStatements]()
|
type astBlock struct {
|
||||||
|
Statements *astStatements `parser:"'{' "`
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanner = lexer.MustStateful(lexer.Rules{
|
||||||
|
"Root": {
|
||||||
|
{"Whitespace", `[ ]`, nil},
|
||||||
|
{"NL", `\n\s*`, nil},
|
||||||
|
{"String", `"(\\"|[^"])*"`, nil},
|
||||||
|
{"DOLLAR", `\$`, nil},
|
||||||
|
{"LP", `\(`, nil},
|
||||||
|
{"RP", `\)`, nil},
|
||||||
|
{"SEMICL", `;`, nil},
|
||||||
|
{"PIPE", `\|`, nil},
|
||||||
|
{"Ident", `\w+`, nil},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
var parser = participle.MustBuild[astStatements](participle.Lexer(scanner),
|
||||||
|
participle.Elide("Whitespace"))
|
||||||
|
|
||||||
func parse(r io.Reader) (*astStatements, error) {
|
func parse(r io.Reader) (*astStatements, error) {
|
||||||
return parser.Parse("test", r)
|
return parser.Parse("test", r)
|
||||||
|
|
|
@ -11,7 +11,9 @@ import (
|
||||||
|
|
||||||
func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) {
|
func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) {
|
||||||
if len(args.args) == 0 {
|
if len(args.args) == 0 {
|
||||||
return strObject(""), nil
|
if _, err := fmt.Fprintln(args.inst.Out()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var line strings.Builder
|
var line strings.Builder
|
||||||
|
@ -21,7 +23,10 @@ func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strObject(line.String()), nil
|
if _, err := fmt.Fprintln(args.inst.Out(), line.String()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setBuiltin(ctx context.Context, args invocationArgs) (object, error) {
|
func setBuiltin(ctx context.Context, args invocationArgs) (object, error) {
|
||||||
|
|
|
@ -5,17 +5,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type evalCtx struct {
|
type evalCtx struct {
|
||||||
parent *evalCtx
|
parent *evalCtx
|
||||||
currentStream stream
|
commands map[string]invokable
|
||||||
commands map[string]invokable
|
vars map[string]object
|
||||||
vars map[string]object
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *evalCtx) withCurrentStream(s stream) *evalCtx {
|
|
||||||
return &evalCtx{
|
|
||||||
parent: ec,
|
|
||||||
currentStream: s,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *evalCtx) addCmd(name string, inv invokable) {
|
func (ec *evalCtx) addCmd(name string, inv invokable) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type evaluator struct {
|
type evaluator struct {
|
||||||
|
inst *Inst
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (object, error) {
|
func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (object, error) {
|
||||||
|
@ -39,7 +40,7 @@ func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStateme
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline) (object, error) {
|
func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline) (object, error) {
|
||||||
res, err := e.evalCmd(ctx, ec, n.First)
|
res, err := e.evalCmd(ctx, ec, nil, n.First)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline
|
||||||
|
|
||||||
// Command is a pipeline, so build it out
|
// Command is a pipeline, so build it out
|
||||||
for _, rest := range n.Rest {
|
for _, rest := range n.Rest {
|
||||||
out, err := e.evalCmd(ctx, ec.withCurrentStream(asStream(res)), rest)
|
out, err := e.evalCmd(ctx, ec, asStream(res), rest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -58,7 +59,7 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, ast *astCmd) (object, error) {
|
func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream stream, ast *astCmd) (object, error) {
|
||||||
cmd, err := ec.lookupCmd(ast.Name)
|
cmd, err := ec.lookupCmd(ast.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -71,13 +72,13 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, ast *astCmd) (objec
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
invArgs := invocationArgs{ec: ec, args: args}
|
invArgs := invocationArgs{ec: ec, inst: e.inst, args: args, currentStream: currentStream}
|
||||||
|
|
||||||
if ec.currentStream != nil {
|
if currentStream != nil {
|
||||||
if si, ok := cmd.(streamInvokable); ok {
|
if si, ok := cmd.(streamInvokable); ok {
|
||||||
return si.invokeWithStream(ctx, ec.currentStream, invArgs)
|
return si.invokeWithStream(ctx, currentStream, invArgs)
|
||||||
} else {
|
} else {
|
||||||
if err := ec.currentStream.close(); err != nil {
|
if err := currentStream.close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,13 @@ func New(opts ...InstOption) *Inst {
|
||||||
return inst
|
return inst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (inst *Inst) Out() io.Writer {
|
||||||
|
if inst.out == nil {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
return inst.out
|
||||||
|
}
|
||||||
|
|
||||||
func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) {
|
func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) {
|
||||||
res, err := inst.eval(ctx, expr)
|
res, err := inst.eval(ctx, expr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -67,7 +74,7 @@ func (inst *Inst) eval(ctx context.Context, expr string) (object, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
eval := evaluator{}
|
eval := evaluator{inst: inst}
|
||||||
|
|
||||||
return eval.evalStatement(ctx, inst.rootEC, ast)
|
return eval.evalStatement(ctx, inst.rootEC, ast)
|
||||||
}
|
}
|
||||||
|
@ -83,6 +90,10 @@ func (inst *Inst) EvalAndDisplay(ctx context.Context, expr string) error {
|
||||||
|
|
||||||
func (inst *Inst) display(ctx context.Context, res object) (err error) {
|
func (inst *Inst) display(ctx context.Context, res object) (err error) {
|
||||||
switch v := res.(type) {
|
switch v := res.(type) {
|
||||||
|
case nil:
|
||||||
|
if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case stream:
|
case stream:
|
||||||
return forEach(v, func(o object, _ int) error { return inst.display(ctx, o) })
|
return forEach(v, func(o object, _ int) error { return inst.display(ctx, o) })
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/lmika/cmdlang-proto/cmdlang"
|
"github.com/lmika/cmdlang-proto/cmdlang"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,14 +18,14 @@ func TestInst_Eval(t *testing.T) {
|
||||||
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
|
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
|
||||||
|
|
||||||
// Sub-expressions
|
// Sub-expressions
|
||||||
{desc: "sub expression 1", expr: `firstarg (echo "hello")`, want: "hello"},
|
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},
|
||||||
{desc: "sub expression 2", expr: `firstarg (echo "hello " "world")`, want: "hello world"},
|
{desc: "sub expression 2", expr: `firstarg (sjoin "hello " "world")`, want: "hello world"},
|
||||||
{desc: "sub expression 3", expr: `firstarg (echo "hello" (echo " ") (echo "world"))`, want: "hello world"},
|
{desc: "sub expression 3", expr: `firstarg (sjoin "hello" (sjoin " ") (sjoin "world"))`, want: "hello world"},
|
||||||
|
|
||||||
// Variables
|
// Variables
|
||||||
{desc: "var 1", expr: `firstarg $a`, want: "alpha"},
|
{desc: "var 1", expr: `firstarg $a`, want: "alpha"},
|
||||||
{desc: "var 2", expr: `firstarg $bee`, want: "buzz"},
|
{desc: "var 2", expr: `firstarg $bee`, want: "buzz"},
|
||||||
{desc: "var 3", expr: `firstarg (echo $bee " " $bee " " $bee)`, want: "buzz buzz buzz"},
|
{desc: "var 3", expr: `firstarg (sjoin $bee " " $bee " " $bee)`, want: "buzz buzz buzz"},
|
||||||
|
|
||||||
// Pipeline
|
// Pipeline
|
||||||
{desc: "pipe 1", expr: `pipe "aye" "bee" "see" | joinpipe`, want: "aye,bee,see"},
|
{desc: "pipe 1", expr: `pipe "aye" "bee" "see" | joinpipe`, want: "aye,bee,see"},
|
||||||
|
@ -37,6 +38,8 @@ func TestInst_Eval(t *testing.T) {
|
||||||
{desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"},
|
{desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"},
|
||||||
{desc: "multi 2", expr: `pipe "hello" | toUpper ; firstarg "world"`, want: "world"}, // TODO: assert for leaks
|
{desc: "multi 2", expr: `pipe "hello" | toUpper ; firstarg "world"`, want: "world"}, // TODO: assert for leaks
|
||||||
{desc: "multi 3", expr: `set new "this is new" ; firstarg $new`, want: "this is new"},
|
{desc: "multi 3", expr: `set new "this is new" ; firstarg $new`, want: "this is new"},
|
||||||
|
|
||||||
|
{desc: "multi-line 1", expr: "echo \"Hello\" \n echo \"world\"", want: "world"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -53,30 +56,33 @@ func TestInst_Eval(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInst_Builtins(t *testing.T) {
|
func TestInst_Builtins_Echo(t *testing.T) {
|
||||||
t.Run("echo", func(t *testing.T) {
|
tests := []struct {
|
||||||
tests := []struct {
|
desc string
|
||||||
desc string
|
expr string
|
||||||
expr string
|
want string
|
||||||
want string
|
}{
|
||||||
}{
|
{desc: "no args", expr: `echo`, want: "\n"},
|
||||||
{desc: "no args", expr: `echo`, want: "\n"},
|
{desc: "single arg", expr: `echo "hello"`, want: "hello\n"},
|
||||||
{desc: "single arg", expr: `echo "hello"`, want: "hello\n"},
|
{desc: "dual args", expr: `echo "hello " "world"`, want: "hello world\n"},
|
||||||
{desc: "dual args", expr: `echo "hello " "world"`, want: "hello world\n"},
|
{desc: "args to singleton stream", expr: `echo "aye" "bee" "see" | toUpper`, want: "AYEBEESEE\n"},
|
||||||
{desc: "args to singleton stream", expr: `echo "aye" "bee" "see" | toUpper`, want: "AYEBEESEE\n"},
|
{desc: "multi-line 1", expr: joinLines(`echo "Hello"`, `echo "world"`), want: "Hello\nworld"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
outW := bytes.NewBuffer(nil)
|
outW := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin())
|
inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin())
|
||||||
err := inst.EvalAndDisplay(ctx, tt.expr)
|
err := inst.EvalAndDisplay(ctx, tt.expr)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, tt.want, outW.String())
|
assert.Equal(t, tt.want, outW.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
func joinLines(ls ...string) string {
|
||||||
|
return strings.Join(ls, "\n")
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,10 @@ func toGoValue(obj object) (interface{}, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type invocationArgs struct {
|
type invocationArgs struct {
|
||||||
inst *Inst
|
inst *Inst
|
||||||
ec *evalCtx
|
ec *evalCtx
|
||||||
args []object
|
currentStream stream
|
||||||
|
args []object
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ia invocationArgs) expectArgn(x int) error {
|
func (ia invocationArgs) expectArgn(x int) error {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cmdlang
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,6 +13,21 @@ func WithTestBuiltin() InstOption {
|
||||||
return args.args[0], nil
|
return args.args[0], nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
i.rootEC.addCmd("sjoin", invokableFunc(func(ctx context.Context, args invocationArgs) (object, error) {
|
||||||
|
if len(args.args) == 0 {
|
||||||
|
return strObject(""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var line strings.Builder
|
||||||
|
for _, arg := range args.args {
|
||||||
|
if s, ok := arg.(fmt.Stringer); ok {
|
||||||
|
line.WriteString(s.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strObject(line.String()), nil
|
||||||
|
}))
|
||||||
|
|
||||||
i.rootEC.addCmd("pipe", invokableFunc(func(ctx context.Context, args invocationArgs) (object, error) {
|
i.rootEC.addCmd("pipe", invokableFunc(func(ctx context.Context, args invocationArgs) (object, error) {
|
||||||
return &listIterStream{
|
return &listIterStream{
|
||||||
list: args.args,
|
list: args.args,
|
||||||
|
|
Loading…
Reference in a new issue