Commit 9050550c authored by Rob Pike's avatar Rob Pike

text/template: allow .Field access to parenthesized expressions

Change the grammar so that field access is a proper operator.
This introduces a new node, ChainNode, into the public (but
actually internal) API of text/template/parse. For
compatibility, we only use the new node type for the specific
construct, which was not parseable before. Therefore this
should be backward-compatible.

Before, .X.Y was a token in the lexer; this CL breaks it out
into .Y applied to .X. But for compatibility we mush them
back together before delivering. One day we might remove
that hack; it's the simple TODO in parse.go/operand.

This change also provides grammatical distinction between
        f
and
        (f)
which might permit function values later, but not now.

Fixes #3999.

R=golang-dev, dsymonds, gri, rsc, mikesamuel
CC=golang-dev
https://golang.org/cl/6494119
parent edce6349
...@@ -1539,6 +1539,11 @@ func TestEnsurePipelineContains(t *testing.T) { ...@@ -1539,6 +1539,11 @@ func TestEnsurePipelineContains(t *testing.T) {
".X | urlquery | html | print", ".X | urlquery | html | print",
[]string{"urlquery", "html"}, []string{"urlquery", "html"},
}, },
{
"{{($).X | html | print}}",
"($).X | urlquery | html | print",
[]string{"urlquery", "html"},
},
} }
for i, test := range tests { for i, test := range tests {
tmpl := template.Must(template.New("test").Parse(test.input)) tmpl := template.Must(template.New("test").Parse(test.input))
......
...@@ -148,8 +148,10 @@ An argument is a simple value, denoted by one of the following. ...@@ -148,8 +148,10 @@ An argument is a simple value, denoted by one of the following.
The result is the value of invoking the function, fun(). The return The result is the value of invoking the function, fun(). The return
types and values behave as in methods. Functions and function types and values behave as in methods. Functions and function
names are described below. names are described below.
- Parentheses may be used for grouping, as in - A parenthesized instance of one the above, for grouping. The result
may be accessed by a field or map key invocation.
print (.F1 arg1) (.F2 arg2) print (.F1 arg1) (.F2 arg2)
(.StructValuedMethod "arg").Field
Arguments may evaluate to any type; if they are pointers the implementation Arguments may evaluate to any type; if they are pointers the implementation
automatically indirects to the base type when required. automatically indirects to the base type when required.
......
...@@ -315,9 +315,15 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref ...@@ -315,9 +315,15 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
switch n := firstWord.(type) { switch n := firstWord.(type) {
case *parse.FieldNode: case *parse.FieldNode:
return s.evalFieldNode(dot, n, cmd.Args, final) return s.evalFieldNode(dot, n, cmd.Args, final)
case *parse.ChainNode:
return s.evalChainNode(dot, n, cmd.Args, final)
case *parse.IdentifierNode: case *parse.IdentifierNode:
// Must be a function. // Must be a function.
return s.evalFunction(dot, n.Ident, cmd.Args, final) return s.evalFunction(dot, n.Ident, cmd.Args, final)
case *parse.PipeNode:
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
// TODO: is this right?
return s.evalPipeline(dot, n)
case *parse.VariableNode: case *parse.VariableNode:
return s.evalVariableNode(dot, n, cmd.Args, final) return s.evalVariableNode(dot, n, cmd.Args, final)
} }
...@@ -367,6 +373,15 @@ func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args [] ...@@ -367,6 +373,15 @@ func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []
return s.evalFieldChain(dot, dot, field.Ident, args, final) return s.evalFieldChain(dot, dot, field.Ident, args, final)
} }
func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value {
// (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields.
pipe := s.evalArg(dot, nil, chain.Node)
if len(chain.Field) == 0 {
s.errorf("internal error: no fields in evalChainNode")
}
return s.evalFieldChain(dot, pipe, chain.Field, args, final)
}
func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value {
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields. // $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
value := s.varValue(v.Ident[0]) value := s.varValue(v.Ident[0])
...@@ -521,13 +536,13 @@ func canBeNil(typ reflect.Type) bool { ...@@ -521,13 +536,13 @@ func canBeNil(typ reflect.Type) bool {
// validateType guarantees that the value is valid and assignable to the type. // validateType guarantees that the value is valid and assignable to the type.
func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value { func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value {
if !value.IsValid() { if !value.IsValid() {
if canBeNil(typ) { if typ == nil || canBeNil(typ) {
// An untyped nil interface{}. Accept as a proper nil value. // An untyped nil interface{}. Accept as a proper nil value.
return reflect.Zero(typ) return reflect.Zero(typ)
} }
s.errorf("invalid value; expected %s", typ) s.errorf("invalid value; expected %s", typ)
} }
if !value.Type().AssignableTo(typ) { if typ != nil && !value.Type().AssignableTo(typ) {
if value.Kind() == reflect.Interface && !value.IsNil() { if value.Kind() == reflect.Interface && !value.IsNil() {
value = value.Elem() value = value.Elem()
if value.Type().AssignableTo(typ) { if value.Type().AssignableTo(typ) {
......
...@@ -340,6 +340,12 @@ var execTests = []execTest{ ...@@ -340,6 +340,12 @@ var execTests = []execTest{
// Parenthesized expressions // Parenthesized expressions
{"parens in pipeline", "{{printf `%d %d %d` (1) (2 | add 3) (add 4 (add 5 6))}}", "1 5 15", tVal, true}, {"parens in pipeline", "{{printf `%d %d %d` (1) (2 | add 3) (add 4 (add 5 6))}}", "1 5 15", tVal, true},
// Parenthesized expressions with field accesses
{"parens: $ in paren", "{{($).X}}", "x", tVal, true},
{"parens: $.GetU in paren", "{{($.GetU).V}}", "v", tVal, true},
{"parens: $ in paren in pipe", "{{($ | echo).X}}", "x", tVal, true},
{"parens: spaces and args", `{{(makemap "up" "down" "left" "right").left}}`, "right", tVal, true},
// If. // If.
{"if true", "{{if true}}TRUE{{end}}", "TRUE", tVal, true}, {"if true", "{{if true}}TRUE{{end}}", "TRUE", tVal, true},
{"if false", "{{if false}}TRUE{{else}}FALSE{{end}}", "FALSE", tVal, true}, {"if false", "{{if false}}TRUE{{else}}FALSE{{end}}", "FALSE", tVal, true},
...@@ -535,6 +541,21 @@ func add(args ...int) int { ...@@ -535,6 +541,21 @@ func add(args ...int) int {
return sum return sum
} }
func echo(arg interface{}) interface{} {
return arg
}
func makemap(arg ...string) map[string]string {
if len(arg)%2 != 0 {
panic("bad makemap")
}
m := make(map[string]string)
for i := 0; i < len(arg); i += 2 {
m[arg[i]] = arg[i+1]
}
return m
}
func stringer(s fmt.Stringer) string { func stringer(s fmt.Stringer) string {
return s.String() return s.String()
} }
...@@ -545,6 +566,8 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) { ...@@ -545,6 +566,8 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) {
"add": add, "add": add,
"count": count, "count": count,
"dddArg": dddArg, "dddArg": dddArg,
"echo": echo,
"makemap": makemap,
"oneArg": oneArg, "oneArg": oneArg,
"typeOf": typeOf, "typeOf": typeOf,
"vfunc": vfunc, "vfunc": vfunc,
......
...@@ -43,8 +43,8 @@ const ( ...@@ -43,8 +43,8 @@ const (
itemComplex // complex constant (1+2i); imaginary is just a number itemComplex // complex constant (1+2i); imaginary is just a number
itemColonEquals // colon-equals (':=') introducing a declaration itemColonEquals // colon-equals (':=') introducing a declaration
itemEOF itemEOF
itemField // alphanumeric identifier, starting with '.', possibly chained ('.x.y') itemField // alphanumeric identifier starting with '.'
itemIdentifier // alphanumeric identifier itemIdentifier // alphanumeric identifier not starting with '.'
itemLeftDelim // left action delimiter itemLeftDelim // left action delimiter
itemLeftParen // '(' inside action itemLeftParen // '(' inside action
itemNumber // simple number, including imaginary itemNumber // simple number, including imaginary
...@@ -286,7 +286,7 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -286,7 +286,7 @@ func lexInsideAction(l *lexer) stateFn {
case r == '`': case r == '`':
return lexRawQuote return lexRawQuote
case r == '$': case r == '$':
return lexIdentifier return lexVariable
case r == '\'': case r == '\'':
return lexChar return lexChar
case r == '.': case r == '.':
...@@ -294,7 +294,7 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -294,7 +294,7 @@ func lexInsideAction(l *lexer) stateFn {
if l.pos < len(l.input) { if l.pos < len(l.input) {
r := l.input[l.pos] r := l.input[l.pos]
if r < '0' || '9' < r { if r < '0' || '9' < r {
return lexIdentifier // itemDot comes from the keyword table. return lexField
} }
} }
fallthrough // '.' can start a number. fallthrough // '.' can start a number.
...@@ -334,15 +334,13 @@ func lexSpace(l *lexer) stateFn { ...@@ -334,15 +334,13 @@ func lexSpace(l *lexer) stateFn {
return lexInsideAction return lexInsideAction
} }
// lexIdentifier scans an alphanumeric or field. // lexIdentifier scans an alphanumeric.
func lexIdentifier(l *lexer) stateFn { func lexIdentifier(l *lexer) stateFn {
Loop: Loop:
for { for {
switch r := l.next(); { switch r := l.next(); {
case isAlphaNumeric(r): case isAlphaNumeric(r):
// absorb. // absorb.
case r == '.' && (l.input[l.start] == '.' || l.input[l.start] == '$'):
// field chaining; absorb into one token.
default: default:
l.backup() l.backup()
word := l.input[l.start:l.pos] word := l.input[l.start:l.pos]
...@@ -354,8 +352,6 @@ Loop: ...@@ -354,8 +352,6 @@ Loop:
l.emit(key[word]) l.emit(key[word])
case word[0] == '.': case word[0] == '.':
l.emit(itemField) l.emit(itemField)
case word[0] == '$':
l.emit(itemVariable)
case word == "true", word == "false": case word == "true", word == "false":
l.emit(itemBool) l.emit(itemBool)
default: default:
...@@ -367,17 +363,59 @@ Loop: ...@@ -367,17 +363,59 @@ Loop:
return lexInsideAction return lexInsideAction
} }
// lexField scans a field: .Alphanumeric.
// The . has been scanned.
func lexField(l *lexer) stateFn {
return lexFieldOrVariable(l, itemField)
}
// lexVariable scans a Variable: $Alphanumeric.
// The $ has been scanned.
func lexVariable(l *lexer) stateFn {
if l.atTerminator() { // Nothing interesting follows -> "$".
l.emit(itemVariable)
return lexInsideAction
}
return lexFieldOrVariable(l, itemVariable)
}
// lexVariable scans a field or variable: [.$]Alphanumeric.
// The . or $ has been scanned.
func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
if l.atTerminator() { // Nothing interesting follows -> "." or "$".
if typ == itemVariable {
l.emit(itemVariable)
} else {
l.emit(itemDot)
}
return lexInsideAction
}
var r rune
for {
r = l.next()
if !isAlphaNumeric(r) {
l.backup()
break
}
}
if !l.atTerminator() {
return l.errorf("bad character %#U", r)
}
l.emit(typ)
return lexInsideAction
}
// atTerminator reports whether the input is at valid termination character to // atTerminator reports whether the input is at valid termination character to
// appear after an identifier. Mostly to catch cases like "$x+2" not being // appear after an identifier. Breaks .X.Y into two pieces. Also catches cases
// acceptable without a space, in case we decide one day to implement // like "$x+2" not being acceptable without a space, in case we decide one
// arithmetic. // day to implement arithmetic.
func (l *lexer) atTerminator() bool { func (l *lexer) atTerminator() bool {
r := l.peek() r := l.peek()
if isSpace(r) || isEndOfLine(r) { if isSpace(r) || isEndOfLine(r) {
return true return true
} }
switch r { switch r {
case eof, ',', '|', ':', ')', '(': case eof, '.', ',', '|', ':', ')', '(':
return true return true
} }
// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will // Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
......
...@@ -61,10 +61,12 @@ var ( ...@@ -61,10 +61,12 @@ var (
tEOF = item{itemEOF, 0, ""} tEOF = item{itemEOF, 0, ""}
tFor = item{itemIdentifier, 0, "for"} tFor = item{itemIdentifier, 0, "for"}
tLeft = item{itemLeftDelim, 0, "{{"} tLeft = item{itemLeftDelim, 0, "{{"}
tLpar = item{itemLeftParen, 0, "("}
tPipe = item{itemPipe, 0, "|"} tPipe = item{itemPipe, 0, "|"}
tQuote = item{itemString, 0, `"abc \n\t\" "`} tQuote = item{itemString, 0, `"abc \n\t\" "`}
tRange = item{itemRange, 0, "range"} tRange = item{itemRange, 0, "range"}
tRight = item{itemRightDelim, 0, "}}"} tRight = item{itemRightDelim, 0, "}}"}
tRpar = item{itemRightParen, 0, ")"}
tSpace = item{itemSpace, 0, " "} tSpace = item{itemSpace, 0, " "}
raw = "`" + `abc\n\t\" ` + "`" raw = "`" + `abc\n\t\" ` + "`"
tRawQuote = item{itemRawString, 0, raw} tRawQuote = item{itemRawString, 0, raw}
...@@ -90,11 +92,11 @@ var lexTests = []lexTest{ ...@@ -90,11 +92,11 @@ var lexTests = []lexTest{
}}, }},
{"parens", "{{((3))}}", []item{ {"parens", "{{((3))}}", []item{
tLeft, tLeft,
{itemLeftParen, 0, "("}, tLpar,
{itemLeftParen, 0, "("}, tLpar,
{itemNumber, 0, "3"}, {itemNumber, 0, "3"},
{itemRightParen, 0, ")"}, tRpar,
{itemRightParen, 0, ")"}, tRpar,
tRight, tRight,
tEOF, tEOF,
}}, }},
...@@ -160,7 +162,7 @@ var lexTests = []lexTest{ ...@@ -160,7 +162,7 @@ var lexTests = []lexTest{
tRight, tRight,
tEOF, tEOF,
}}, }},
{"dots", "{{.x . .2 .x.y}}", []item{ {"dots", "{{.x . .2 .x.y.z}}", []item{
tLeft, tLeft,
{itemField, 0, ".x"}, {itemField, 0, ".x"},
tSpace, tSpace,
...@@ -168,7 +170,9 @@ var lexTests = []lexTest{ ...@@ -168,7 +170,9 @@ var lexTests = []lexTest{
tSpace, tSpace,
{itemNumber, 0, ".2"}, {itemNumber, 0, ".2"},
tSpace, tSpace,
{itemField, 0, ".x.y"}, {itemField, 0, ".x"},
{itemField, 0, ".y"},
{itemField, 0, ".z"},
tRight, tRight,
tEOF, tEOF,
}}, }},
...@@ -202,13 +206,14 @@ var lexTests = []lexTest{ ...@@ -202,13 +206,14 @@ var lexTests = []lexTest{
tSpace, tSpace,
{itemVariable, 0, "$"}, {itemVariable, 0, "$"},
tSpace, tSpace,
{itemVariable, 0, "$var.Field"}, {itemVariable, 0, "$var"},
{itemField, 0, ".Field"},
tSpace, tSpace,
{itemField, 0, ".Method"}, {itemField, 0, ".Method"},
tRight, tRight,
tEOF, tEOF,
}}, }},
{"variable invocation ", "{{$x 23}}", []item{ {"variable invocation", "{{$x 23}}", []item{
tLeft, tLeft,
{itemVariable, 0, "$x"}, {itemVariable, 0, "$x"},
tSpace, tSpace,
...@@ -261,6 +266,15 @@ var lexTests = []lexTest{ ...@@ -261,6 +266,15 @@ var lexTests = []lexTest{
tRight, tRight,
tEOF, tEOF,
}}, }},
{"field of parenthesized expression", "{{(.X).Y}}", []item{
tLeft,
tLpar,
{itemField, 0, ".X"},
tRpar,
{itemField, 0, ".Y"},
tRight,
tEOF,
}},
// errors // errors
{"badchar", "#{{\x01}}", []item{ {"badchar", "#{{\x01}}", []item{
{itemText, 0, "#"}, {itemText, 0, "#"},
...@@ -294,14 +308,14 @@ var lexTests = []lexTest{ ...@@ -294,14 +308,14 @@ var lexTests = []lexTest{
}}, }},
{"unclosed paren", "{{(3}}", []item{ {"unclosed paren", "{{(3}}", []item{
tLeft, tLeft,
{itemLeftParen, 0, "("}, tLpar,
{itemNumber, 0, "3"}, {itemNumber, 0, "3"},
{itemError, 0, `unclosed left paren`}, {itemError, 0, `unclosed left paren`},
}}, }},
{"extra right paren", "{{3)}}", []item{ {"extra right paren", "{{3)}}", []item{
tLeft, tLeft,
{itemNumber, 0, "3"}, {itemNumber, 0, "3"},
{itemRightParen, 0, ")"}, tRpar,
{itemError, 0, `unexpected right paren U+0029 ')'`}, {itemError, 0, `unexpected right paren U+0029 ')'`},
}}, }},
......
...@@ -34,8 +34,9 @@ func (t NodeType) Type() NodeType { ...@@ -34,8 +34,9 @@ func (t NodeType) Type() NodeType {
const ( const (
NodeText NodeType = iota // Plain text. NodeText NodeType = iota // Plain text.
NodeAction // A simple action such as field evaluation. NodeAction // A non-control action such as a field evaluation.
NodeBool // A boolean constant. NodeBool // A boolean constant.
NodeChain // A sequence of field accesses.
NodeCommand // An element of a pipeline. NodeCommand // An element of a pipeline.
NodeDot // The cursor, dot. NodeDot // The cursor, dot.
nodeElse // An else action. Not added to tree. nodeElse // An else action. Not added to tree.
...@@ -168,7 +169,7 @@ func (p *PipeNode) Copy() Node { ...@@ -168,7 +169,7 @@ func (p *PipeNode) Copy() Node {
// ActionNode holds an action (something bounded by delimiters). // ActionNode holds an action (something bounded by delimiters).
// Control actions have their own nodes; ActionNode represents simple // Control actions have their own nodes; ActionNode represents simple
// ones such as field evaluations. // ones such as field evaluations and parenthesized pipelines.
type ActionNode struct { type ActionNode struct {
NodeType NodeType
Line int // The line number in the input. Line int // The line number in the input.
...@@ -248,11 +249,11 @@ func (i *IdentifierNode) Copy() Node { ...@@ -248,11 +249,11 @@ func (i *IdentifierNode) Copy() Node {
return NewIdentifier(i.Ident) return NewIdentifier(i.Ident)
} }
// VariableNode holds a list of variable names. The dollar sign is // VariableNode holds a list of variable names, possibly with chained field
// part of the name. // accesses. The dollar sign is part of the (first) name.
type VariableNode struct { type VariableNode struct {
NodeType NodeType
Ident []string // Variable names in lexical order. Ident []string // Variable name and fields in lexical order.
} }
func newVariable(ident string) *VariableNode { func newVariable(ident string) *VariableNode {
...@@ -337,6 +338,46 @@ func (f *FieldNode) Copy() Node { ...@@ -337,6 +338,46 @@ func (f *FieldNode) Copy() Node {
return &FieldNode{NodeType: NodeField, Ident: append([]string{}, f.Ident...)} return &FieldNode{NodeType: NodeField, Ident: append([]string{}, f.Ident...)}
} }
// ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
// The names may be chained ('.x.y').
// The periods are dropped from each ident.
type ChainNode struct {
NodeType
Node Node
Field []string // The identifiers in lexical order.
}
func newChain(node Node) *ChainNode {
return &ChainNode{NodeType: NodeChain, Node: node}
}
// Add adds the named field (which should start with a period) to the end of the chain.
func (c *ChainNode) Add(field string) {
if len(field) == 0 || field[0] != '.' {
panic("no dot in field")
}
field = field[1:] // Remove leading dot.
if field == "" {
panic("empty field")
}
c.Field = append(c.Field, field)
}
func (c *ChainNode) String() string {
s := c.Node.String()
if _, ok := c.Node.(*PipeNode); ok {
s = "(" + s + ")"
}
for _, field := range c.Field {
s += "." + field
}
return s
}
func (c *ChainNode) Copy() Node {
return &ChainNode{NodeType: NodeChain, Node: c.Node, Field: append([]string{}, c.Field...)}
}
// BoolNode holds a boolean constant. // BoolNode holds a boolean constant.
type BoolNode struct { type BoolNode struct {
NodeType NodeType
......
...@@ -353,8 +353,7 @@ func (t *Tree) action() (n Node) { ...@@ -353,8 +353,7 @@ func (t *Tree) action() (n Node) {
} }
// Pipeline: // Pipeline:
// field or command // declarations? command ('|' command)*
// pipeline "|" pipeline
func (t *Tree) pipeline(context string) (pipe *PipeNode) { func (t *Tree) pipeline(context string) (pipe *PipeNode) {
var decl []*VariableNode var decl []*VariableNode
// Are there declarations? // Are there declarations?
...@@ -369,9 +368,6 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -369,9 +368,6 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") { if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") {
t.nextNonSpace() t.nextNonSpace()
variable := newVariable(v.val) variable := newVariable(v.val)
if len(variable.Ident) != 1 {
t.errorf("illegal variable in declaration: %s", v.val)
}
decl = append(decl, variable) decl = append(decl, variable)
t.vars = append(t.vars, v.val) t.vars = append(t.vars, v.val)
if next.typ == itemChar && next.val == "," { if next.typ == itemChar && next.val == "," {
...@@ -400,7 +396,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -400,7 +396,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
} }
return return
case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier, case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
itemNumber, itemNil, itemRawString, itemString, itemVariable: itemNumber, itemNil, itemRawString, itemString, itemVariable, itemLeftParen:
t.backup() t.backup()
pipe.append(t.command()) pipe.append(t.command())
default: default:
...@@ -494,57 +490,29 @@ func (t *Tree) templateControl() Node { ...@@ -494,57 +490,29 @@ func (t *Tree) templateControl() Node {
} }
// command: // command:
// operand (space operand)*
// space-separated arguments up to a pipeline character or right delimiter. // space-separated arguments up to a pipeline character or right delimiter.
// we consume the pipe character but leave the right delim to terminate the action. // we consume the pipe character but leave the right delim to terminate the action.
func (t *Tree) command() *CommandNode { func (t *Tree) command() *CommandNode {
cmd := newCommand() cmd := newCommand()
Loop:
for { for {
switch token := t.nextNonSpace(); token.typ { t.peekNonSpace() // skip leading spaces.
operand := t.operand()
if operand != nil {
cmd.append(operand)
}
switch token := t.next(); token.typ {
case itemSpace:
continue
case itemError:
t.errorf("%s", token.val)
case itemRightDelim, itemRightParen: case itemRightDelim, itemRightParen:
t.backup() t.backup()
break Loop
case itemPipe: case itemPipe:
break Loop
case itemLeftParen:
p := t.pipeline("parenthesized expression")
if t.nextNonSpace().typ != itemRightParen {
t.errorf("missing right paren in parenthesized expression")
}
cmd.append(p)
case itemError:
t.errorf("%s", token.val)
case itemIdentifier:
if !t.hasFunction(token.val) {
t.errorf("function %q not defined", token.val)
}
cmd.append(NewIdentifier(token.val))
case itemDot:
cmd.append(newDot())
case itemNil:
cmd.append(newNil())
case itemVariable:
cmd.append(t.useVar(token.val))
case itemField:
cmd.append(newField(token.val))
case itemBool:
cmd.append(newBool(token.val == "true"))
case itemCharConstant, itemComplex, itemNumber:
number, err := newNumber(token.val, token.typ)
if err != nil {
t.error(err)
}
cmd.append(number)
case itemString, itemRawString:
s, err := strconv.Unquote(token.val)
if err != nil {
t.error(err)
}
cmd.append(newString(token.val, s))
default: default:
t.unexpected(token, "command") t.errorf("unexpected %s in operand; missing space?", token)
} }
t.terminate() break
} }
if len(cmd.Args) == 0 { if len(cmd.Args) == 0 {
t.errorf("empty command") t.errorf("empty command")
...@@ -552,15 +520,86 @@ Loop: ...@@ -552,15 +520,86 @@ Loop:
return cmd return cmd
} }
// terminate checks that the next token terminates an argument. This guarantees // operand:
// that arguments are space-separated, for example that (2)3 does not parse. // term .Field*
func (t *Tree) terminate() { // An operand is a space-separated component of a command,
token := t.peek() // a term possibly followed by field accesses.
switch token.typ { // A nil return means the next item is not an operand.
case itemChar, itemPipe, itemRightDelim, itemRightParen, itemSpace: func (t *Tree) operand() Node {
return node := t.term()
if node == nil {
return nil
}
if t.peek().typ == itemField {
chain := newChain(node)
for t.peek().typ == itemField {
chain.Add(t.next().val)
}
// Compatibility with original API: If the term is of type NodeField
// or NodeVariable, just put more fields on the original.
// Otherwise, keep the Chain node.
// TODO: Switch to Chains always when we can.
switch node.Type() {
case NodeField:
node = newField(chain.String())
case NodeVariable:
node = newVariable(chain.String())
default:
node = chain
}
}
return node
}
// term:
// literal (number, string, nil, boolean)
// function (identifier)
// .
// .Field
// $
// '(' pipeline ')'
// A term is a simple "expression".
// A nil return means the next item is not a term.
func (t *Tree) term() Node {
switch token := t.nextNonSpace(); token.typ {
case itemError:
t.errorf("%s", token.val)
case itemIdentifier:
if !t.hasFunction(token.val) {
t.errorf("function %q not defined", token.val)
}
return NewIdentifier(token.val)
case itemDot:
return newDot()
case itemNil:
return newNil()
case itemVariable:
return t.useVar(token.val)
case itemField:
return newField(token.val)
case itemBool:
return newBool(token.val == "true")
case itemCharConstant, itemComplex, itemNumber:
number, err := newNumber(token.val, token.typ)
if err != nil {
t.error(err)
}
return number
case itemLeftParen:
pipe := t.pipeline("parenthesized pipeline")
if token := t.next(); token.typ != itemRightParen {
t.errorf("unclosed right paren: unexpected %s", token)
}
return pipe
case itemString, itemRawString:
s, err := strconv.Unquote(token.val)
if err != nil {
t.error(err)
}
return newString(token.val, s)
} }
t.unexpected(token, "argument list (missing space?)") t.backup()
return nil
} }
// hasFunction reports if a function name exists in the Tree's maps. // hasFunction reports if a function name exists in the Tree's maps.
......
...@@ -188,6 +188,8 @@ var parseTests = []parseTest{ ...@@ -188,6 +188,8 @@ var parseTests = []parseTest{
`{{$x := .X | .Y}}`}, `{{$x := .X | .Y}}`},
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError, {"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`}, `{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
`{{(.Y .Z).Field}}`},
{"simple if", "{{if .X}}hello{{end}}", noError, {"simple if", "{{if .X}}hello{{end}}", noError,
`{{if .X}}"hello"{{end}}`}, `{{if .X}}"hello"{{end}}`},
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError, {"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
...@@ -370,8 +372,9 @@ var errorTests = []parseTest{ ...@@ -370,8 +372,9 @@ var errorTests = []parseTest{
"{{range .X}}", "{{range .X}}",
hasError, `unexpected EOF`}, hasError, `unexpected EOF`},
{"variable", {"variable",
"{{$a.b := 23}}", // Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
hasError, `illegal variable in declaration`}, "{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
hasError, `unexpected ":="`},
{"multidecl", {"multidecl",
"{{$a,$b,$c := 23}}", "{{$a,$b,$c := 23}}",
hasError, `too many declarations`}, hasError, `too many declarations`},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment