package ucl import ( "context" "errors" "fmt" "strings" ) type evaluator struct { inst *Inst } func (e evaluator) evalBlock(ctx context.Context, ec *evalCtx, n *astBlock) (lastRes Object, err error) { // TODO: push scope? for _, s := range n.Statements { lastRes, err = e.evalStatement(ctx, ec, s) if err != nil { return nil, err } } return lastRes, nil } func (e evaluator) evalScript(ctx context.Context, ec *evalCtx, n *astScript) (lastRes Object, err error) { return e.evalStatement(ctx, ec, n.Statements) } func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (Object, error) { if n == nil { return nil, nil } res, err := e.evalPipeline(ctx, ec, n.First) if err != nil { return nil, err } if len(n.Rest) == 0 { return res, nil } for _, rest := range n.Rest { out, err := e.evalPipeline(ctx, ec, rest) if err != nil { return nil, err } res = out } return res, nil } func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) { res, err := e.evalCmd(ctx, ec, nil, n.First) if err != nil { return nil, err } if len(n.Rest) == 0 { return res, nil } // Command is a pipeline, so build it out for _, rest := range n.Rest { out, err := e.evalCmd(ctx, ec, res, rest) if err != nil { return nil, err } res = out } return res, nil } func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd) (Object, error) { switch { case (ast.Name.Arg.Ident != nil) && len(ast.Name.DotSuffix) == 0: name := ast.Name.Arg.Ident.String() // Regular command if cmd := ec.lookupInvokable(name); cmd != nil { return e.evalInvokable(ctx, ec, currentPipe, ast, cmd) } else if macro := ec.lookupMacro(name); macro != nil { return e.evalMacro(ctx, ec, currentPipe != nil, currentPipe, ast, macro) } else if missingHandler := e.inst.missingBuiltinHandler; missingHandler != nil { return e.evalInvokable(ctx, ec, currentPipe, ast, e.inst.missingHandlerInvokable(name)) } else { return nil, errors.New("unknown command: " + name) } case len(ast.Args) > 0: nameElem, err := e.evalDot(ctx, ec, ast.Name) if err != nil { return nil, err } inv, ok := nameElem.(invokable) if !ok { return nil, errors.New("command is not invokable") } return e.evalInvokable(ctx, ec, currentPipe, ast, inv) } nameElem, err := e.evalDot(ctx, ec, ast.Name) if err != nil { return nil, err } return nameElem, nil } func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd, cmd invokable) (Object, error) { var ( pargs listObject kwargs map[string]*listObject argsPtr *listObject ) argsPtr = &pargs if currentPipe != nil { argsPtr.Append(currentPipe) } for _, arg := range ast.Args { if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' { // Arg switch if kwargs == nil { kwargs = make(map[string]*listObject) } argsPtr = &listObject{} kwargs[ident.String()[1:]] = argsPtr } else { ae, err := e.evalDot(ctx, ec, arg) if err != nil { return nil, err } argsPtr.Append(ae) } } invArgs := invocationArgs{eval: e, ec: ec, inst: e.inst, args: pargs, kwargs: kwargs} return cmd.invoke(ctx, invArgs) } func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, hasPipe bool, pipeArg Object, ast *astCmd, cmd macroable) (Object, error) { return cmd.invokeMacro(ctx, macroArgs{ eval: e, ec: ec, hasPipe: hasPipe, pipeArg: pipeArg, ast: ast, }) } func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object, error) { res, err := e.evalArg(ctx, ec, n.Arg) if err != nil { return nil, err } else if len(n.DotSuffix) == 0 { return res, nil } for _, dot := range n.DotSuffix { var idx Object if dot.KeyIdent != nil { idx = StringObject(dot.KeyIdent.String()) } else { idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) if err != nil { return nil, err } } res, err = indexLookup(ctx, res, idx) if err != nil { return nil, err } } return res, nil } func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { switch { case n.Literal != nil: return e.evalLiteral(ctx, ec, n.Literal) case n.Ident != nil: return StringObject(n.Ident.String()), nil case n.Var != nil: if v, ok := ec.getVar(*n.Var); ok { return v, nil } return nil, nil case n.MaybeSub != nil: sub := n.MaybeSub.Sub if sub == nil { return nil, nil } return e.evalSub(ctx, ec, sub) case n.ListOrHash != nil: return e.evalListOrHash(ctx, ec, n.ListOrHash) case n.Block != nil: return blockObject{block: n.Block, closedEC: ec}, 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.evalDot(ctx, ec, el.Left) if err != nil { return nil, err } v, err := e.evalDot(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.evalDot(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.StrInter != nil: sval, err := e.interpolateDoubleQuotedString(ctx, ec, n.StrInter) if err != nil { return nil, err } return sval, nil case n.SingleStrInter != nil: sval, err := e.interpolateSingleQuotedString(ctx, ec, n.SingleStrInter) if err != nil { return nil, err } return sval, nil case n.Int != nil: return IntObject(*n.Int), nil } return nil, errors.New("unhandled literal type") } func (e evaluator) interpolateSingleQuotedString(ctx context.Context, ec *evalCtx, s *astSingleString) (Object, error) { var sb strings.Builder for _, n := range s.Spans { switch { case n.Chars != nil: sb.WriteString(*n.Chars) } } return StringObject(sb.String()), nil } func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCtx, s *astDoubleString) (Object, error) { var sb strings.Builder for _, n := range s.Spans { switch { case n.Chars != nil: sb.WriteString(*n.Chars) case n.Escaped != nil: switch (*n.Escaped)[1:] { case "\\": sb.WriteByte('\\') case "n": sb.WriteByte('\n') case "t": sb.WriteByte('\t') case "$": sb.WriteByte('$') default: return nil, errors.New("unrecognised escaped pattern: \\" + *n.Escaped) } case n.IdentRef != nil: identVal := (*n.IdentRef)[1:] if v, ok := ec.getVar(identVal); ok && v != nil { sb.WriteString(v.String()) } case n.LongIdentRef != nil: v, err := e.interpolateLongIdent(ctx, ec, n.LongIdentRef) if err != nil { return nil, err } sb.WriteString(v) case n.SubExpr != nil: res, err := e.evalPipeline(ctx, ec, n.SubExpr) if err != nil { return nil, err } if res != nil { sb.WriteString(res.String()) } } } return StringObject(sb.String()), nil } func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *astLongIdent) (_ string, err error) { res, ok := ec.getVar(n.VarName) if !ok { return "", nil } for _, dot := range n.DotSuffix { if res == nil { return "", errorWithPos{fmt.Errorf("attempt to get field from nil value '%v'", n.VarName), n.Pos} } var idx Object if dot.KeyName != nil { idx = StringObject(*dot.KeyName) } else { idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) if err != nil { return "", err } } res, err = indexLookup(ctx, res, idx) if err != nil { return "", err } } if res == nil { return "", nil } return res.String(), nil } func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) { pipelineRes, err := e.evalPipeline(ctx, ec, n) if err != nil { return nil, err } return pipelineRes, nil }