Added single and double quoted string
Also started working on string interpolation
This commit is contained in:
parent
53c8d5d936
commit
9fc4f50452
38
ucl/ast.go
38
ucl/ast.go
|
@ -8,9 +8,30 @@ import (
|
|||
"github.com/alecthomas/participle/v2/lexer"
|
||||
)
|
||||
|
||||
type astStringStringSpan struct {
|
||||
Pos lexer.Position
|
||||
Chars *string `parser:"@SingleChar"`
|
||||
}
|
||||
|
||||
type astDoubleStringSpan struct {
|
||||
Pos lexer.Position
|
||||
Chars *string `parser:"@Char"`
|
||||
Escaped *string `parser:"| @Escaped"`
|
||||
IdentRef *string `parser:"| @IdentRef"`
|
||||
}
|
||||
|
||||
type astDoubleString struct {
|
||||
Spans []astDoubleStringSpan `parser:"StringStart @@* StringEnd"`
|
||||
}
|
||||
|
||||
type astSingleString struct {
|
||||
Spans []astStringStringSpan `parser:"SingleStringStart @@* SingleStringEnd"`
|
||||
}
|
||||
|
||||
type astLiteral struct {
|
||||
Str *string `parser:"@String"`
|
||||
Int *int `parser:"| @Int"`
|
||||
StrInter *astDoubleString `parser:"@@"`
|
||||
SingleStrInter *astSingleString `parser:"| @@"`
|
||||
Int *int `parser:"| @Int"`
|
||||
}
|
||||
|
||||
type astIdentNames struct {
|
||||
|
@ -91,7 +112,8 @@ var scanner = lexer.MustStateful(lexer.Rules{
|
|||
"Root": {
|
||||
{"Whitespace", `[ \t]+`, nil},
|
||||
{"Comment", `[#].*`, nil},
|
||||
{"String", `"(\\"|[^"])*"`, nil},
|
||||
{"StringStart", `"`, lexer.Push("String")},
|
||||
{"SingleStringStart", `'`, lexer.Push("SingleString")},
|
||||
{"Int", `[-]?[0-9][0-9]*`, nil},
|
||||
{"DOLLAR", `\$`, nil},
|
||||
{"COLON", `\:`, nil},
|
||||
|
@ -106,6 +128,16 @@ var scanner = lexer.MustStateful(lexer.Rules{
|
|||
{"PIPE", `\|`, nil},
|
||||
{"Ident", `[-]*[a-zA-Z_][\w-]*`, nil},
|
||||
},
|
||||
"String": {
|
||||
{"Escaped", `\\.`, nil},
|
||||
{"StringEnd", `"`, lexer.Pop()},
|
||||
{"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil},
|
||||
{"Char", `[^$"\\]+`, nil},
|
||||
},
|
||||
"SingleString": {
|
||||
{"SingleStringEnd", `'`, lexer.Pop()},
|
||||
{"SingleChar", `[^']+`, nil},
|
||||
},
|
||||
})
|
||||
var parser = participle.MustBuild[astScript](participle.Lexer(scanner),
|
||||
participle.Elide("Whitespace", "Comment"))
|
||||
|
|
86
ucl/eval.go
86
ucl/eval.go
|
@ -3,14 +3,14 @@ package ucl
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type evaluator struct {
|
||||
inst *Inst
|
||||
}
|
||||
|
||||
func (e evaluator) evalBlock(ctx context.Context, ec *evalCtx, n *astBlock) (lastRes object, err error) {
|
||||
func (e evaluator) evalBlock(ctx context.Context, ec *evalCtx, n *astBlock) (lastRes Object, err error) {
|
||||
// TODO: push scope?
|
||||
|
||||
for _, s := range n.Statements {
|
||||
|
@ -22,11 +22,11 @@ func (e evaluator) evalBlock(ctx context.Context, ec *evalCtx, n *astBlock) (las
|
|||
return lastRes, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalScript(ctx context.Context, ec *evalCtx, n *astScript) (lastRes object, err error) {
|
||||
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) {
|
||||
func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStatements) (Object, error) {
|
||||
if n == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func (e evaluator) evalStatement(ctx context.Context, ec *evalCtx, n *astStateme
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline) (object, error) {
|
||||
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
|
||||
|
@ -69,7 +69,7 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe object, ast *astCmd) (object, error) {
|
||||
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()
|
||||
|
@ -105,7 +105,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe object,
|
|||
return nameElem, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe object, ast *astCmd, cmd invokable) (object, error) {
|
||||
func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd, cmd invokable) (Object, error) {
|
||||
var (
|
||||
pargs listObject
|
||||
kwargs map[string]*listObject
|
||||
|
@ -138,7 +138,7 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe o
|
|||
return cmd.invoke(ctx, invArgs)
|
||||
}
|
||||
|
||||
func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, hasPipe bool, pipeArg object, ast *astCmd, cmd macroable) (object, error) {
|
||||
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,
|
||||
|
@ -148,7 +148,7 @@ func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, hasPipe bool, pip
|
|||
})
|
||||
}
|
||||
|
||||
func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (object, error) {
|
||||
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
|
||||
|
@ -157,9 +157,9 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (object,
|
|||
}
|
||||
|
||||
for _, dot := range n.DotSuffix {
|
||||
var idx object
|
||||
var idx Object
|
||||
if dot.KeyIdent != nil {
|
||||
idx = strObject(dot.KeyIdent.String())
|
||||
idx = StringObject(dot.KeyIdent.String())
|
||||
} else {
|
||||
idx, err = e.evalPipeline(ctx, ec, dot.Pipeline)
|
||||
if err != nil {
|
||||
|
@ -175,12 +175,12 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (object,
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (object, error) {
|
||||
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 strObject(n.Ident.String()), nil
|
||||
return StringObject(n.Ident.String()), nil
|
||||
case n.Var != nil:
|
||||
if v, ok := ec.getVar(*n.Var); ok {
|
||||
return v, nil
|
||||
|
@ -195,12 +195,12 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (objec
|
|||
case n.ListOrHash != nil:
|
||||
return e.evalListOrHash(ctx, ec, n.ListOrHash)
|
||||
case n.Block != nil:
|
||||
return blockObject{block: 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) {
|
||||
func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (Object, error) {
|
||||
if loh.EmptyList {
|
||||
return listObject{}, nil
|
||||
} else if loh.EmptyHash {
|
||||
|
@ -243,21 +243,67 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList
|
|||
return l, nil
|
||||
}
|
||||
|
||||
func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n *astLiteral) (object, 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)
|
||||
case n.StrInter != nil:
|
||||
sval, err := e.interpolateDoubleQuotedString(ctx, ec, n.StrInter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strObject(uq), nil
|
||||
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) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (object, error) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
return StringObject(sb.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
|
||||
|
|
|
@ -102,6 +102,18 @@ func TestBuiltins_Echo(t *testing.T) {
|
|||
echo "world" # command after this
|
||||
;
|
||||
`, want: "Hello\nworld\n"},
|
||||
{desc: "interpolated string 1", expr: `set what "world" ; echo "Hello, $what"`, want: "Hello, world\n"},
|
||||
{desc: "interpolated string 2", expr: `set what "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"},
|
||||
{desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"},
|
||||
{desc: "interpolated string 4", expr: `set what "Hello" ; set where "world" ; echo "$what, $where"`, want: "Hello, world\n"},
|
||||
{desc: "interpolated string 5", expr: `
|
||||
foreach [123 "foo" true ()] { |x|
|
||||
echo "[[$x]]"
|
||||
}
|
||||
`, want: "[[123]]\n[[foo]]\n[[true]]\n[[]]\n"},
|
||||
{desc: "single quote string 1", expr: `echo 'Hello, world'`, want: "Hello, world\n"},
|
||||
{desc: "single quote string 2", expr: `echo 'No $vars here'`, want: "No $vars here\n"},
|
||||
{desc: "single quote string 3", expr: `echo 'No \escape \nhere'`, want: "No \\escape \\nhere\n"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
Loading…
Reference in a new issue