diff --git a/cmdlang/ast.go b/cmdlang/ast.go index 0a29001..dc10311 100644 --- a/cmdlang/ast.go +++ b/cmdlang/ast.go @@ -11,16 +11,35 @@ type astLiteral struct { Str *string `parser:"@String"` } +type astHashKey struct { + Literal *astLiteral `parser:"@@"` + Ident *string `parser:"| @Ident"` + Var *string `parser:"| DOLLAR @Ident"` + Sub *astPipeline `parser:"| LP @@ RP"` +} + +type astElementPair struct { + Left astCmdArg `parser:"@@"` + Right *astCmdArg `parser:"( COLON @@ )? NL?"` +} + +type astListOrHash struct { + EmptyList bool `parser:"@(LS RS)"` + EmptyHash bool `parser:"| @(LS COLON RS)"` + Elements []*astElementPair `parser:"| LS NL? @@+ @@* RS"` +} + type astBlock struct { Statements []*astStatements `parser:"LC NL? @@ NL? RC"` } type astCmdArg struct { - Literal *astLiteral `parser:"@@"` - Ident *string `parser:"| @Ident"` - Var *string `parser:"| DOLLAR @Ident"` - Sub *astPipeline `parser:"| LP @@ RP"` - Block *astBlock `parser:"| @@"` + Literal *astLiteral `parser:"@@"` + Ident *string `parser:"| @Ident"` + Var *string `parser:"| DOLLAR @Ident"` + Sub *astPipeline `parser:"| LP @@ RP"` + ListOrHash *astListOrHash `parser:"| @@"` + Block *astBlock `parser:"| @@"` } type astCmd struct { @@ -47,8 +66,11 @@ var scanner = lexer.MustStateful(lexer.Rules{ {"Whitespace", `[ \t]+`, nil}, {"String", `"(\\"|[^"])*"`, nil}, {"DOLLAR", `\$`, nil}, + {"COLON", `\:`, nil}, {"LP", `\(`, nil}, {"RP", `\)`, nil}, + {"LS", `\[`, nil}, + {"RS", `\]`, nil}, {"LC", `\{`, nil}, {"RC", `\}`, nil}, {"NL", `[;\n][; \n\t]*`, nil}, diff --git a/cmdlang/eval.go b/cmdlang/eval.go index 77cd445..38eb13b 100644 --- a/cmdlang/eval.go +++ b/cmdlang/eval.go @@ -130,12 +130,57 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (objec return nil, nil case n.Sub != nil: return e.evalSub(ctx, ec, n.Sub) + case n.ListOrHash != nil: + return e.evalListOrHash(ctx, ec, n.ListOrHash) case n.Block != nil: return blockObject{block: n.Block}, nil } return nil, errors.New("unhandled arg type") } +func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (object, error) { + if loh.EmptyList { + return listObject{}, nil + } else if loh.EmptyHash { + return hashObject{}, nil + } + + if firstIsHash := loh.Elements[0].Right != nil; firstIsHash { + h := hashObject{} + for _, el := range loh.Elements { + if el.Right == nil { + return nil, errors.New("miss-match of lists and hash") + } + + n, err := e.evalArg(ctx, ec, el.Left) + if err != nil { + return nil, err + } + + v, err := e.evalArg(ctx, ec, *el.Right) + if err != nil { + return nil, err + } + + h[n.String()] = v + } + return h, nil + } + + l := listObject{} + for _, el := range loh.Elements { + if el.Right != nil { + return nil, errors.New("miss-match of lists and hash") + } + v, err := e.evalArg(ctx, ec, el.Left) + if err != nil { + return nil, err + } + l = append(l, v) + } + return l, nil +} + func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n *astLiteral) (object, error) { switch { case n.Str != nil: diff --git a/cmdlang/inst_test.go b/cmdlang/inst_test.go index beec7a8..bc5619b 100644 --- a/cmdlang/inst_test.go +++ b/cmdlang/inst_test.go @@ -13,7 +13,7 @@ func TestInst_Eval(t *testing.T) { tests := []struct { desc string expr string - want string + want any }{ {desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, {desc: "simple ident", expr: `firstarg a-test`, want: "a-test"}, @@ -39,6 +39,23 @@ 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"}, + + // Lists + {desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}}, + {desc: "list 2", expr: `set one "one" ; firstarg [$one (pipe "two" | toUpper) "three"]`, want: []any{"one", "TWO", "three"}}, + {desc: "list 3", expr: `firstarg []`, want: []any{}}, + + // Maps + {desc: "map 1", expr: `firstarg [one:"1" two:"2" three:"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}}, + {desc: "map 2", expr: `firstarg ["one":"1" "two":"2" "three":"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}}, + {desc: "map 3", expr: ` + set one "one" ; set n1 "1" + firstarg [ + $one:$n1 + (firstarg "two" | toUpper):(firstarg "2" | toUpper) + three:"3" + ]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}}, + {desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}}, } for _, tt := range tests { diff --git a/cmdlang/objs.go b/cmdlang/objs.go index 49cb1f5..9bba180 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -12,6 +12,26 @@ type object interface { Truthy() bool } +type listObject []object + +func (s listObject) String() string { + return fmt.Sprintf("%v", []object(s)) +} + +func (s listObject) Truthy() bool { + return len(s) > 0 +} + +type hashObject map[string]object + +func (s hashObject) String() string { + return fmt.Sprintf("%v", map[string]object(s)) +} + +func (s hashObject) Truthy() bool { + return len(s) > 0 +} + type strObject string func (s strObject) String() string { @@ -28,6 +48,26 @@ func toGoValue(obj object) (interface{}, bool) { return nil, true case strObject: return string(v), true + case listObject: + xs := make([]interface{}, 0, len(v)) + for _, va := range v { + x, ok := toGoValue(va) + if !ok { + continue + } + xs = append(xs, x) + } + return xs, true + case hashObject: + xs := make(map[string]interface{}) + for k, va := range v { + x, ok := toGoValue(va) + if !ok { + continue + } + xs[k] = x + } + return xs, true case proxyObject: return v.p, true }