From 730dc460959ea825c570d05dabfa7b56149dad78 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 13 Apr 2024 09:25:16 +1000 Subject: [PATCH] Working on multi-line statements --- cmdlang/ast.go | 29 ++++++++++++++--- cmdlang/builtins.go | 9 ++++-- cmdlang/env.go | 14 ++------- cmdlang/eval.go | 15 ++++----- cmdlang/inst.go | 13 +++++++- cmdlang/inst_test.go | 60 ++++++++++++++++++++---------------- cmdlang/objs.go | 7 +++-- cmdlang/testbuiltins_test.go | 16 ++++++++++ 8 files changed, 107 insertions(+), 56 deletions(-) diff --git a/cmdlang/ast.go b/cmdlang/ast.go index 4adbc2c..48b70c0 100644 --- a/cmdlang/ast.go +++ b/cmdlang/ast.go @@ -2,6 +2,7 @@ package cmdlang import ( "github.com/alecthomas/participle/v2" + "github.com/alecthomas/participle/v2/lexer" "io" ) @@ -12,8 +13,8 @@ type astLiteral struct { type astCmdArg struct { Literal *astLiteral `parser:"@@"` - Var *string `parser:"| '$' @Ident"` - Sub *astPipeline `parser:"| '(' @@ ')'"` + Var *string `parser:"| DOLLAR @Ident"` + Sub *astPipeline `parser:"| LP @@ RP"` } type astCmd struct { @@ -23,15 +24,33 @@ type astCmd struct { type astPipeline struct { First *astCmd `parser:"@@"` - Rest []*astCmd `parser:"( '|' @@ )*"` + Rest []*astCmd `parser:"( PIPE @@ )*"` } type astStatements struct { 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) { return parser.Parse("test", r) diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index ab9ce24..546531f 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -11,7 +11,9 @@ import ( func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) { if len(args.args) == 0 { - return strObject(""), nil + if _, err := fmt.Fprintln(args.inst.Out()); err != nil { + return nil, err + } } 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) { diff --git a/cmdlang/env.go b/cmdlang/env.go index 9861e0a..947b931 100644 --- a/cmdlang/env.go +++ b/cmdlang/env.go @@ -5,17 +5,9 @@ import ( ) type evalCtx struct { - parent *evalCtx - currentStream stream - commands map[string]invokable - vars map[string]object -} - -func (ec *evalCtx) withCurrentStream(s stream) *evalCtx { - return &evalCtx{ - parent: ec, - currentStream: s, - } + parent *evalCtx + commands map[string]invokable + vars map[string]object } func (ec *evalCtx) addCmd(name string, inv invokable) { diff --git a/cmdlang/eval.go b/cmdlang/eval.go index 62514fc..587bb13 100644 --- a/cmdlang/eval.go +++ b/cmdlang/eval.go @@ -10,6 +10,7 @@ import ( ) type evaluator struct { + inst *Inst } 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) { - res, err := e.evalCmd(ctx, ec, n.First) + res, err := e.evalCmd(ctx, ec, nil, n.First) if err != nil { 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 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 { return nil, err } @@ -58,7 +59,7 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline 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) if err != nil { return nil, err @@ -71,13 +72,13 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, ast *astCmd) (objec 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 { - return si.invokeWithStream(ctx, ec.currentStream, invArgs) + return si.invokeWithStream(ctx, currentStream, invArgs) } else { - if err := ec.currentStream.close(); err != nil { + if err := currentStream.close(); err != nil { return nil, err } } diff --git a/cmdlang/inst.go b/cmdlang/inst.go index 05ea127..6b7ea5d 100644 --- a/cmdlang/inst.go +++ b/cmdlang/inst.go @@ -47,6 +47,13 @@ func New(opts ...InstOption) *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) { res, err := inst.eval(ctx, expr) if err != nil { @@ -67,7 +74,7 @@ func (inst *Inst) eval(ctx context.Context, expr string) (object, error) { return nil, err } - eval := evaluator{} + eval := evaluator{inst: inst} 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) { switch v := res.(type) { + case nil: + if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil { + return err + } case stream: return forEach(v, func(o object, _ int) error { return inst.display(ctx, o) }) default: diff --git a/cmdlang/inst_test.go b/cmdlang/inst_test.go index ca9bcb6..7156bcf 100644 --- a/cmdlang/inst_test.go +++ b/cmdlang/inst_test.go @@ -5,6 +5,7 @@ import ( "context" "github.com/lmika/cmdlang-proto/cmdlang" "github.com/stretchr/testify/assert" + "strings" "testing" ) @@ -17,14 +18,14 @@ func TestInst_Eval(t *testing.T) { {desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, // Sub-expressions - {desc: "sub expression 1", expr: `firstarg (echo "hello")`, want: "hello"}, - {desc: "sub expression 2", expr: `firstarg (echo "hello " "world")`, want: "hello world"}, - {desc: "sub expression 3", expr: `firstarg (echo "hello" (echo " ") (echo "world"))`, want: "hello world"}, + {desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"}, + {desc: "sub expression 2", expr: `firstarg (sjoin "hello " "world")`, want: "hello world"}, + {desc: "sub expression 3", expr: `firstarg (sjoin "hello" (sjoin " ") (sjoin "world"))`, want: "hello world"}, // Variables {desc: "var 1", expr: `firstarg $a`, want: "alpha"}, {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 {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 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-line 1", expr: "echo \"Hello\" \n echo \"world\"", want: "world"}, } for _, tt := range tests { @@ -53,30 +56,33 @@ func TestInst_Eval(t *testing.T) { } } -func TestInst_Builtins(t *testing.T) { - t.Run("echo", func(t *testing.T) { - tests := []struct { - desc string - expr string - want string - }{ - {desc: "no args", expr: `echo`, want: "\n"}, - {desc: "single arg", expr: `echo "hello"`, want: "hello\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"}, - } +func TestInst_Builtins_Echo(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "no args", expr: `echo`, want: "\n"}, + {desc: "single arg", expr: `echo "hello"`, want: "hello\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: "multi-line 1", expr: joinLines(`echo "Hello"`, `echo "world"`), want: "Hello\nworld"}, + } - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - ctx := context.Background() - outW := bytes.NewBuffer(nil) + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) - inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin()) - err := inst.EvalAndDisplay(ctx, tt.expr) + inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin()) + err := inst.EvalAndDisplay(ctx, tt.expr) - assert.NoError(t, err) - assert.Equal(t, tt.want, outW.String()) - }) - } - }) + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} + +func joinLines(ls ...string) string { + return strings.Join(ls, "\n") } diff --git a/cmdlang/objs.go b/cmdlang/objs.go index a8696d0..77ea948 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -29,9 +29,10 @@ func toGoValue(obj object) (interface{}, bool) { } type invocationArgs struct { - inst *Inst - ec *evalCtx - args []object + inst *Inst + ec *evalCtx + currentStream stream + args []object } func (ia invocationArgs) expectArgn(x int) error { diff --git a/cmdlang/testbuiltins_test.go b/cmdlang/testbuiltins_test.go index e879a1e..2f32af1 100644 --- a/cmdlang/testbuiltins_test.go +++ b/cmdlang/testbuiltins_test.go @@ -2,6 +2,7 @@ package cmdlang import ( "context" + "fmt" "strings" ) @@ -12,6 +13,21 @@ func WithTestBuiltin() InstOption { 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) { return &listIterStream{ list: args.args,