454 lines
11 KiB
Go
454 lines
11 KiB
Go
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.evalAssignOrPipeline(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.evalAssignOrPipeline(ctx, ec, rest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = out
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (e evaluator) evalAssignOrPipeline(ctx context.Context, ec *evalCtx, n *astAssignOrPipeline) (Object, error) {
|
|
switch {
|
|
case n.Assign != nil:
|
|
// Assignment
|
|
assignVal, err := e.evalPipeline(ctx, ec, n.Assign)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return e.assignCmd(ctx, ec, n.First, assignVal)
|
|
case len(n.Pipeline) > 0:
|
|
res, err := e.evalCmd(ctx, ec, nil, n.First)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, rest := range n.Pipeline {
|
|
out, err := e.evalCmd(ctx, ec, res, rest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = out
|
|
}
|
|
return res, nil
|
|
|
|
}
|
|
|
|
return e.evalCmd(ctx, ec, nil, n.First)
|
|
}
|
|
|
|
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.InvokeArgs) > 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) assignCmd(ctx context.Context, ec *evalCtx, ast *astCmd, toVal Object) (Object, error) {
|
|
if len(ast.InvokeArgs) != 0 {
|
|
return nil, errors.New("cannot assign to multiple values")
|
|
}
|
|
return e.assignDot(ctx, ec, ast.Name, toVal)
|
|
}
|
|
|
|
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.InvokeArgs {
|
|
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) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal Object) (Object, error) {
|
|
if len(n.DotSuffix) == 0 {
|
|
return e.assignArg(ctx, ec, n.Arg, toVal)
|
|
}
|
|
|
|
return nil, errors.New("TODO")
|
|
}
|
|
|
|
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.PseudoVar != nil:
|
|
if v, ok := ec.getPseudoVar(*n.PseudoVar); ok {
|
|
return v.get(ctx, *n.PseudoVar)
|
|
}
|
|
|
|
if mph := e.inst.missingPseudoVarHandler; mph != nil {
|
|
return mph.get(ctx, *n.PseudoVar)
|
|
}
|
|
|
|
return nil, errors.New("unknown pseudo-variable: " + *n.PseudoVar)
|
|
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) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
|
|
switch {
|
|
case n.Literal != nil:
|
|
// We may use this for variable setting?
|
|
return nil, errors.New("cannot assign to a literal")
|
|
case n.Var != nil:
|
|
ec.setOrDefineVar(*n.Var, toVal)
|
|
return toVal, nil
|
|
case n.PseudoVar != nil:
|
|
pvar, ok := ec.getPseudoVar(*n.PseudoVar)
|
|
if ok {
|
|
if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
|
|
return nil, err
|
|
}
|
|
return toVal, nil
|
|
}
|
|
|
|
if pvar := e.inst.missingPseudoVarHandler; pvar != nil {
|
|
if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
|
|
return nil, err
|
|
}
|
|
return toVal, nil
|
|
}
|
|
return nil, errors.New("unknown pseudo-variable: " + *n.Var)
|
|
case n.MaybeSub != nil:
|
|
return nil, errors.New("cannot assign to a subexpression")
|
|
case n.ListOrHash != nil:
|
|
return nil, errors.New("cannot assign to a list or hash")
|
|
case n.Block != nil:
|
|
return nil, errors.New("cannot assign to a block")
|
|
}
|
|
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
|
|
}
|
|
|
|
type pseudoVar interface {
|
|
get(ctx context.Context, name string) (Object, error)
|
|
set(ctx context.Context, name string, v Object) error
|
|
}
|