diff --git a/cmdlang/ast.go b/cmdlang/ast.go index c3a076d..0943a95 100644 --- a/cmdlang/ast.go +++ b/cmdlang/ast.go @@ -11,7 +11,8 @@ type astLiteral struct { } type astCmdArg struct { - Literal astLiteral `parser:"@@"` + Literal *astLiteral `parser:"@@"` + Sub *astPipeline `parser:"| '[' @@ ']'"` } type astCmd struct { diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index 76b76a2..46fd5ba 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "errors" + "fmt" "io" "os" "strings" @@ -16,7 +17,9 @@ func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) { var line strings.Builder for _, arg := range args.args { - line.WriteString(arg) + if s, ok := arg.(fmt.Stringer); ok { + line.WriteString(s.String()) + } } return asStream(line.String()), nil @@ -41,7 +44,12 @@ func catBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, err } - return &fileLinesStream{filename: args.args[0]}, nil + filename, err := args.stringArg(0) + if err != nil { + return nil, err + } + + return &fileLinesStream{filename: filename}, nil } type fileLinesStream struct { diff --git a/cmdlang/eval.go b/cmdlang/eval.go index 81b0f70..2785872 100644 --- a/cmdlang/eval.go +++ b/cmdlang/eval.go @@ -3,8 +3,10 @@ package cmdlang import ( "context" "errors" + "fmt" "github.com/lmika/gopkgs/fp/slices" "strconv" + "strings" ) type evaluator struct { @@ -36,7 +38,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, ast *astCmd) (objec return nil, err } - args, err := slices.MapWithError(ast.Args, func(a astCmdArg) (string, error) { + args, err := slices.MapWithError(ast.Args, func(a astCmdArg) (object, error) { return e.evalArg(ctx, ec, a) }) if err != nil { @@ -49,20 +51,49 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, ast *astCmd) (objec }) } -func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (string, error) { - return e.evalLiteral(ctx, ec, n.Literal) +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.Sub != nil: + return e.evalSub(ctx, ec, n.Sub) + } + return nil, errors.New("unhandled arg type") } -func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n astLiteral) (string, error) { +func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n *astLiteral) (object, error) { switch { case n.Str != nil: uq, err := strconv.Unquote(*n.Str) if err != nil { return "", err } - return uq, nil + return strObject(uq), nil case n.Ident != nil: - return *n.Ident, nil + return strObject(*n.Ident), nil } return "", errors.New("unhandled literal type") } + +func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (object, error) { + pipelineRes, err := e.evaluate(ctx, ec, n) + if err != nil { + return "", err + } + + switch v := pipelineRes.(type) { + case stream: + // TODO: use proper lists here, not a string join + sb := strings.Builder{} + if err := forEach(v, func(o object) error { + // TODO: use o.String() + sb.WriteString(fmt.Sprint(o)) + return nil + }); err != nil { + return "", err + } + + return strObject(sb.String()), nil + } + return pipelineRes, nil +} diff --git a/cmdlang/objs.go b/cmdlang/objs.go index dd53a3c..934da5a 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -3,13 +3,21 @@ package cmdlang import ( "context" "errors" + "fmt" "strconv" ) -type object = any +type object interface { +} + +type strObject string + +func (s strObject) String() string { + return string(s) +} type invocationArgs struct { - args []string + args []object inStream stream } @@ -20,6 +28,17 @@ func (ia invocationArgs) expectArgn(x int) error { return nil } +func (ia invocationArgs) stringArg(i int) (string, error) { + if len(ia.args) < i { + return "", errors.New("expected at least " + strconv.Itoa(i) + " args") + } + s, ok := ia.args[i].(fmt.Stringer) + if !ok { + return "", errors.New("expected a string arg") + } + return s.String(), nil +} + // invokable is an object that can be executed as a command type invokable interface { invoke(ctx context.Context, args invocationArgs) (object, error)