diff --git a/cmdlang/ast.go b/cmdlang/ast.go index dc10311..ed79476 100644 --- a/cmdlang/ast.go +++ b/cmdlang/ast.go @@ -30,7 +30,8 @@ type astListOrHash struct { } type astBlock struct { - Statements []*astStatements `parser:"LC NL? @@ NL? RC"` + Names []string `parser:"LC NL? (PIPE @Ident+ PIPE NL?)?"` + Statements []*astStatements `parser:"@@ NL? RC"` } type astCmdArg struct { diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index 2918dac..36baf44 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -123,7 +123,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { } if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) { - return args.evalBlock(ctx, 1) + return args.evalBlock(ctx, 1, nil, false) } else if err != nil { return nil, err } @@ -137,7 +137,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { } if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) { - return args.evalBlock(ctx, 1) + return args.evalBlock(ctx, 1, nil, false) } else if err != nil { return nil, err } @@ -146,7 +146,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { } if args.identIs(ctx, 0, "else") && args.nargs() > 1 { - return args.evalBlock(ctx, 1) + return args.evalBlock(ctx, 1, nil, false) } else if args.nargs() == 0 { // no elif or else return nil, nil @@ -154,3 +154,36 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { return nil, errors.New("malformed if-elif-else") } + +func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { + if args.nargs() < 2 { + return nil, errors.New("need at least 2 arguments") + } + + items, err := args.evalArg(ctx, 0) + if err != nil { + return nil, err + } + + var last object + + switch t := items.(type) { + case listObject: + for _, v := range t { + last, err = args.evalBlock(ctx, 1, []object{v}, true) // TO INCLUDE: the index + if err != nil { + return nil, err + } + } + case hashObject: + for k, v := range t { + last, err = args.evalBlock(ctx, 1, []object{strObject(k), v}, true) + if err != nil { + return nil, err + } + } + // TODO: streams + } + + return last, nil +} diff --git a/cmdlang/env.go b/cmdlang/env.go index 5e35fd5..165c15b 100644 --- a/cmdlang/env.go +++ b/cmdlang/env.go @@ -7,6 +7,10 @@ type evalCtx struct { vars map[string]object } +func (ec *evalCtx) fork() *evalCtx { + return &evalCtx{parent: ec} +} + func (ec *evalCtx) addCmd(name string, inv invokable) { if ec.commands == nil { ec.commands = make(map[string]invokable) diff --git a/cmdlang/inst.go b/cmdlang/inst.go index a1646c0..1703289 100644 --- a/cmdlang/inst.go +++ b/cmdlang/inst.go @@ -32,6 +32,7 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("cat", invokableFunc(catBuiltin)) rootEC.addMacro("if", macroFunc(ifBuiltin)) + rootEC.addMacro("foreach", macroFunc(foreachBuiltin)) //rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin)) diff --git a/cmdlang/objs.go b/cmdlang/objs.go index 9bba180..b08ed21 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -123,7 +123,7 @@ func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) { return ma.eval.evalArg(ctx, ma.ec, ma.ast.Args[ma.argShift+n]) } -func (ma macroArgs) evalBlock(ctx context.Context, n int) (object, error) { +func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushScope bool) (object, error) { obj, err := ma.evalArg(ctx, n) if err != nil { return nil, err @@ -134,7 +134,17 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int) (object, error) { return nil, errors.New("not a block object") } - return ma.eval.evalBlock(ctx, ma.ec, block.block) + ec := ma.ec + if pushScope { + ec = ec.fork() + } + for i, n := range block.block.Names { + if i < len(args) { + ec.setVar(n, args[i]) + } + } + + return ma.eval.evalBlock(ctx, ec, block.block) } type invocationArgs struct { diff --git a/cmdlang/testbuiltins_test.go b/cmdlang/testbuiltins_test.go index 8103f33..59c14e3 100644 --- a/cmdlang/testbuiltins_test.go +++ b/cmdlang/testbuiltins_test.go @@ -172,3 +172,31 @@ func TestBuiltins_If(t *testing.T) { }) } } + +func TestBuiltins_ForEach(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "iterate over list", expr: ` + foreach ["1" "2" "3"] { |v| + echo $v + }`, want: "1\n2\n3\n(nil)\n"}, + {desc: "iterate over map", expr: ` + foreach [a:"1"] { |k v| echo $k "=" $v }`, want: "a=1\n(nil)\n"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + err := inst.EvalAndDisplay(ctx, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +}