Commit cc842c73 authored by Rob Pike's avatar Rob Pike

text/template: allow grouping of pipelines using parentheses

Based on work by Russ Cox. From his CL:

        This is generally useful but especially helpful when trying
        to use the built-in boolean operators.  It lets you write:

        {{if not (f 1)}} foo {{end}}
        {{if and (f 1) (g 2)}} bar {{end}}
        {{if or (f 1) (g 2)}} quux {{end}}

        instead of

        {{if f 1 | not}} foo {{end}}
        {{if f 1}}{{if g 2}} bar {{end}}{{end}}
        {{$do := 0}}{{if f 1}}{{$do := 1}}{{else if g 2}}{{$do := 1}}{{end}}{{if $do}} quux {{end}}

The result can be a bit LISPy but the benefit in expressiveness and readability
for such a small change justifies it.

I believe no changes are required to html/template.

Fixes #3276.

R=golang-dev, adg, rogpeppe, minux.ma
CC=golang-dev
https://golang.org/cl/6482056
parent 3bd8684f
...@@ -148,6 +148,8 @@ An argument is a simple value, denoted by one of the following. ...@@ -148,6 +148,8 @@ 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
print (.F1 arg1) (.F2 arg2)
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.
...@@ -228,6 +230,8 @@ All produce the quoted word "output": ...@@ -228,6 +230,8 @@ All produce the quoted word "output":
{{"output" | printf "%q"}} {{"output" | printf "%q"}}
A function call whose final argument comes from the previous A function call whose final argument comes from the previous
command. command.
{{printf "%q" (print "out" "put")}}
A parenthesized argument.
{{"put" | printf "%s%s" "out" | printf "%q"}} {{"put" | printf "%s%s" "out" | printf "%q"}}
A more elaborate call. A more elaborate call.
{{"output" | printf "%s" | printf "%q"}} {{"output" | printf "%s" | printf "%q"}}
......
...@@ -564,6 +564,8 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle ...@@ -564,6 +564,8 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle
return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, zero), typ) return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, zero), typ)
case *parse.VariableNode: case *parse.VariableNode:
return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ) return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ)
case *parse.PipeNode:
return s.validateType(s.evalPipeline(dot, arg), typ)
} }
switch typ.Kind() { switch typ.Kind() {
case reflect.Bool: case reflect.Bool:
...@@ -666,6 +668,8 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu ...@@ -666,6 +668,8 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu
return reflect.ValueOf(n.Text) return reflect.ValueOf(n.Text)
case *parse.VariableNode: case *parse.VariableNode:
return s.evalVariableNode(dot, n, nil, zero) return s.evalVariableNode(dot, n, nil, zero)
case *parse.PipeNode:
return s.evalPipeline(dot, n)
} }
s.errorf("can't handle assignment of %s to empty interface argument", n) s.errorf("can't handle assignment of %s to empty interface argument", n)
panic("not reached") panic("not reached")
......
...@@ -337,6 +337,9 @@ var execTests = []execTest{ ...@@ -337,6 +337,9 @@ var execTests = []execTest{
{"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true}, {"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true},
{"pipeline func", "-{{call .VariadicFunc `llo` | call .VariadicFunc `he` }}-", "-<he+<llo>>-", tVal, true}, {"pipeline func", "-{{call .VariadicFunc `llo` | call .VariadicFunc `he` }}-", "-<he+<llo>>-", tVal, true},
// Parenthesized expressions
{"parens in pipeline", "{{printf `%d %d %d` (1) (2 | add 3) (add 4 (add 5 6))}}", "1 5 15", 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},
...@@ -524,6 +527,14 @@ func vfunc(V, *V) string { ...@@ -524,6 +527,14 @@ func vfunc(V, *V) string {
return "vfunc" return "vfunc"
} }
func add(args ...int) int {
sum := 0
for _, x := range args {
sum += x
}
return sum
}
func stringer(s fmt.Stringer) string { func stringer(s fmt.Stringer) string {
return s.String() return s.String()
} }
...@@ -531,6 +542,7 @@ func stringer(s fmt.Stringer) string { ...@@ -531,6 +542,7 @@ func stringer(s fmt.Stringer) string {
func testExecute(execTests []execTest, template *Template, t *testing.T) { func testExecute(execTests []execTest, template *Template, t *testing.T) {
b := new(bytes.Buffer) b := new(bytes.Buffer)
funcs := FuncMap{ funcs := FuncMap{
"add": add,
"count": count, "count": count,
"dddArg": dddArg, "dddArg": dddArg,
"oneArg": oneArg, "oneArg": oneArg,
......
...@@ -46,10 +46,12 @@ const ( ...@@ -46,10 +46,12 @@ const (
itemField // alphanumeric identifier, starting with '.', possibly chained ('.x.y') itemField // alphanumeric identifier, starting with '.', possibly chained ('.x.y')
itemIdentifier // alphanumeric identifier itemIdentifier // alphanumeric identifier
itemLeftDelim // left action delimiter itemLeftDelim // left action delimiter
itemLeftParen // '(' inside action
itemNumber // simple number, including imaginary itemNumber // simple number, including imaginary
itemPipe // pipe symbol itemPipe // pipe symbol
itemRawString // raw quoted string (includes quotes) itemRawString // raw quoted string (includes quotes)
itemRightDelim // right action delimiter itemRightDelim // right action delimiter
itemRightParen // ')' inside action
itemString // quoted string (includes quotes) itemString // quoted string (includes quotes)
itemText // plain text itemText // plain text
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'. itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'.
...@@ -78,12 +80,15 @@ var itemName = map[itemType]string{ ...@@ -78,12 +80,15 @@ var itemName = map[itemType]string{
itemField: "field", itemField: "field",
itemIdentifier: "identifier", itemIdentifier: "identifier",
itemLeftDelim: "left delim", itemLeftDelim: "left delim",
itemLeftParen: "(",
itemNumber: "number", itemNumber: "number",
itemPipe: "pipe", itemPipe: "pipe",
itemRawString: "raw string", itemRawString: "raw string",
itemRightDelim: "right delim", itemRightDelim: "right delim",
itemRightParen: ")",
itemString: "string", itemString: "string",
itemVariable: "variable", itemVariable: "variable",
// keywords // keywords
itemDot: ".", itemDot: ".",
itemDefine: "define", itemDefine: "define",
...@@ -133,6 +138,7 @@ type lexer struct { ...@@ -133,6 +138,7 @@ type lexer struct {
width int // width of last rune read from input. width int // width of last rune read from input.
lastPos int // position of most recent item returned by nextItem lastPos int // position of most recent item returned by nextItem
items chan item // channel of scanned items. items chan item // channel of scanned items.
parenDepth int // nesting depth of ( ) exprs
} }
// next returns the next rune in the input. // next returns the next rune in the input.
...@@ -269,6 +275,7 @@ func lexLeftDelim(l *lexer) stateFn { ...@@ -269,6 +275,7 @@ func lexLeftDelim(l *lexer) stateFn {
return lexComment return lexComment
} }
l.emit(itemLeftDelim) l.emit(itemLeftDelim)
l.parenDepth = 0
return lexInsideAction return lexInsideAction
} }
...@@ -297,7 +304,10 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -297,7 +304,10 @@ func lexInsideAction(l *lexer) stateFn {
// Spaces separate and are ignored. // Spaces separate and are ignored.
// Pipe symbols separate and are emitted. // Pipe symbols separate and are emitted.
if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
return lexRightDelim if l.parenDepth == 0 {
return lexRightDelim
}
return l.errorf("unclosed left paren")
} }
switch r := l.next(); { switch r := l.next(); {
case r == eof || r == '\n': case r == eof || r == '\n':
...@@ -334,6 +344,17 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -334,6 +344,17 @@ func lexInsideAction(l *lexer) stateFn {
case isAlphaNumeric(r): case isAlphaNumeric(r):
l.backup() l.backup()
return lexIdentifier return lexIdentifier
case r == '(':
l.emit(itemLeftParen)
l.parenDepth++
return lexInsideAction
case r == ')':
l.emit(itemRightParen)
l.parenDepth--
if l.parenDepth < 0 {
return l.errorf("unexpected right paren %#U", r)
}
return lexInsideAction
case r <= unicode.MaxASCII && unicode.IsPrint(r): case r <= unicode.MaxASCII && unicode.IsPrint(r):
l.emit(itemChar) l.emit(itemChar)
return lexInsideAction return lexInsideAction
...@@ -386,7 +407,7 @@ func (l *lexer) atTerminator() bool { ...@@ -386,7 +407,7 @@ func (l *lexer) atTerminator() bool {
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
......
...@@ -43,6 +43,16 @@ var lexTests = []lexTest{ ...@@ -43,6 +43,16 @@ var lexTests = []lexTest{
tRight, tRight,
tEOF, tEOF,
}}, }},
{"parens", "{{((3))}}", []item{
tLeft,
{itemLeftParen, 0, "("},
{itemLeftParen, 0, "("},
{itemNumber, 0, "3"},
{itemRightParen, 0, ")"},
{itemRightParen, 0, ")"},
tRight,
tEOF,
}},
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}}, {"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
{"for", `{{for }}`, []item{tLeft, tFor, tRight, tEOF}}, {"for", `{{for }}`, []item{tLeft, tFor, tRight, tEOF}},
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}}, {"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
...@@ -189,6 +199,18 @@ var lexTests = []lexTest{ ...@@ -189,6 +199,18 @@ var lexTests = []lexTest{
tLeft, tLeft,
{itemError, 0, `bad number syntax: "3k"`}, {itemError, 0, `bad number syntax: "3k"`},
}}, }},
{"unclosed paren", "{{(3}}", []item{
tLeft,
{itemLeftParen, 0, "("},
{itemNumber, 0, "3"},
{itemError, 0, `unclosed left paren`},
}},
{"extra right paren", "{{3)}}", []item{
tLeft,
{itemNumber, 0, "3"},
{itemRightParen, 0, ")"},
{itemError, 0, `unexpected right paren U+0029 ')'`},
}},
// Fixed bugs // Fixed bugs
// Many elements in an action blew the lookahead until // Many elements in an action blew the lookahead until
......
...@@ -209,6 +209,10 @@ func (c *CommandNode) String() string { ...@@ -209,6 +209,10 @@ func (c *CommandNode) String() string {
if i > 0 { if i > 0 {
s += " " s += " "
} }
if arg, ok := arg.(*PipeNode); ok {
s += "(" + arg.String() + ")"
continue
}
s += arg.String() s += arg.String()
} }
return s return s
......
...@@ -349,10 +349,13 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -349,10 +349,13 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
pipe = newPipeline(t.lex.lineNumber(), decl) pipe = newPipeline(t.lex.lineNumber(), decl)
for { for {
switch token := t.next(); token.typ { switch token := t.next(); token.typ {
case itemRightDelim: case itemRightDelim, itemRightParen:
if len(pipe.Cmds) == 0 { if len(pipe.Cmds) == 0 {
t.errorf("missing value for %s", context) t.errorf("missing value for %s", context)
} }
if token.typ == itemRightParen {
t.backup()
}
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:
...@@ -456,11 +459,17 @@ func (t *Tree) command() *CommandNode { ...@@ -456,11 +459,17 @@ func (t *Tree) command() *CommandNode {
Loop: Loop:
for { for {
switch token := t.next(); token.typ { switch token := t.next(); token.typ {
case itemRightDelim: case itemRightDelim, itemRightParen:
t.backup() t.backup()
break Loop break Loop
case itemPipe: case itemPipe:
break Loop break Loop
case itemLeftParen:
p := t.pipeline("parenthesized expression")
if t.next().typ != itemRightParen {
t.errorf("missing right paren in parenthesized expression")
}
cmd.append(p)
case itemError: case itemError:
t.errorf("%s", token.val) t.errorf("%s", token.val)
case itemIdentifier: case itemIdentifier:
......
...@@ -185,6 +185,8 @@ var parseTests = []parseTest{ ...@@ -185,6 +185,8 @@ var parseTests = []parseTest{
`{{.X | .Y}}`}, `{{.X | .Y}}`},
{"pipeline with decl", "{{$x := .X|.Y}}", noError, {"pipeline with decl", "{{$x := .X|.Y}}", noError,
`{{$x := .X | .Y}}`}, `{{$x := .X | .Y}}`},
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
{"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,
......
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