]> Cypherpunks repositories - gostls13.git/commitdiff
text/template: allow grouping of pipelines using parentheses
authorRob Pike <r@golang.org>
Fri, 24 Aug 2012 19:37:23 +0000 (12:37 -0700)
committerRob Pike <r@golang.org>
Fri, 24 Aug 2012 19:37:23 +0000 (12:37 -0700)
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

src/pkg/text/template/doc.go
src/pkg/text/template/exec.go
src/pkg/text/template/exec_test.go
src/pkg/text/template/parse/lex.go
src/pkg/text/template/parse/lex_test.go
src/pkg/text/template/parse/node.go
src/pkg/text/template/parse/parse.go
src/pkg/text/template/parse/parse_test.go

index 3e4c66a276159dcef1d53b5ce121cd4c3f1d5569..224775c46c126c16a117ea0667598ee4f91cf4cc 100644 (file)
@@ -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
          types and values behave as in methods. Functions and function
          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
 automatically indirects to the base type when required.
@@ -228,6 +230,8 @@ All produce the quoted word "output":
        {{"output" | printf "%q"}}
                A function call whose final argument comes from the previous
                command.
+       {{printf "%q" (print "out" "put")}}
+               A parenthesized argument.
        {{"put" | printf "%s%s" "out" | printf "%q"}}
                A more elaborate call.
        {{"output" | printf "%s" | printf "%q"}}
index a0413514487da0dea8901d96fd3d9bd8b3846f89..1739a86179d8a808106c36af96eafa0fb192d701 100644 (file)
@@ -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)
        case *parse.VariableNode:
                return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ)
+       case *parse.PipeNode:
+               return s.validateType(s.evalPipeline(dot, arg), typ)
        }
        switch typ.Kind() {
        case reflect.Bool:
@@ -666,6 +668,8 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu
                return reflect.ValueOf(n.Text)
        case *parse.VariableNode:
                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)
        panic("not reached")
index 95e0592df8e1847e2dbf560814c2a4af1a9e81dc..7f60dcafa519b6034405ad788c3a93a3fd1bdc4e 100644 (file)
@@ -337,6 +337,9 @@ var execTests = []execTest{
        {"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", 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 true", "{{if true}}TRUE{{end}}", "TRUE", tVal, true},
        {"if false", "{{if false}}TRUE{{else}}FALSE{{end}}", "FALSE", tVal, true},
@@ -524,6 +527,14 @@ func vfunc(V, *V) string {
        return "vfunc"
 }
 
+func add(args ...int) int {
+       sum := 0
+       for _, x := range args {
+               sum += x
+       }
+       return sum
+}
+
 func stringer(s fmt.Stringer) string {
        return s.String()
 }
@@ -531,6 +542,7 @@ func stringer(s fmt.Stringer) string {
 func testExecute(execTests []execTest, template *Template, t *testing.T) {
        b := new(bytes.Buffer)
        funcs := FuncMap{
+               "add":      add,
                "count":    count,
                "dddArg":   dddArg,
                "oneArg":   oneArg,
index 98f12a821fbff868ab3d4d67ac589fdd6615cd34..c73f533d19125b1c0d6c063d00fe94c581b39279 100644 (file)
@@ -46,10 +46,12 @@ const (
        itemField      // alphanumeric identifier, starting with '.', possibly chained ('.x.y')
        itemIdentifier // alphanumeric identifier
        itemLeftDelim  // left action delimiter
+       itemLeftParen  // '(' inside action
        itemNumber     // simple number, including imaginary
        itemPipe       // pipe symbol
        itemRawString  // raw quoted string (includes quotes)
        itemRightDelim // right action delimiter
+       itemRightParen // ')' inside action
        itemString     // quoted string (includes quotes)
        itemText       // plain text
        itemVariable   // variable starting with '$', such as '$' or  '$1' or '$hello'.
@@ -78,12 +80,15 @@ var itemName = map[itemType]string{
        itemField:        "field",
        itemIdentifier:   "identifier",
        itemLeftDelim:    "left delim",
+       itemLeftParen:    "(",
        itemNumber:       "number",
        itemPipe:         "pipe",
        itemRawString:    "raw string",
        itemRightDelim:   "right delim",
+       itemRightParen:   ")",
        itemString:       "string",
        itemVariable:     "variable",
+
        // keywords
        itemDot:      ".",
        itemDefine:   "define",
@@ -133,6 +138,7 @@ type lexer struct {
        width      int       // width of last rune read from input.
        lastPos    int       // position of most recent item returned by nextItem
        items      chan item // channel of scanned items.
+       parenDepth int       // nesting depth of ( ) exprs
 }
 
 // next returns the next rune in the input.
@@ -269,6 +275,7 @@ func lexLeftDelim(l *lexer) stateFn {
                return lexComment
        }
        l.emit(itemLeftDelim)
+       l.parenDepth = 0
        return lexInsideAction
 }
 
@@ -297,7 +304,10 @@ func lexInsideAction(l *lexer) stateFn {
        // Spaces separate and are ignored.
        // Pipe symbols separate and are emitted.
        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(); {
        case r == eof || r == '\n':
@@ -334,6 +344,17 @@ func lexInsideAction(l *lexer) stateFn {
        case isAlphaNumeric(r):
                l.backup()
                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):
                l.emit(itemChar)
                return lexInsideAction
@@ -386,7 +407,7 @@ func (l *lexer) atTerminator() bool {
                return true
        }
        switch r {
-       case eof, ',', '|', ':':
+       case eof, ',', '|', ':', ')', '(':
                return true
        }
        // Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
index f38057d8c367a8968789a0e5d5dc2efb66855036..5a4e8b658d8a01b3727771742d254ae7972ccdad 100644 (file)
@@ -43,6 +43,16 @@ var lexTests = []lexTest{
                tRight,
                tEOF,
        }},
+       {"parens", "{{((3))}}", []item{
+               tLeft,
+               {itemLeftParen, 0, "("},
+               {itemLeftParen, 0, "("},
+               {itemNumber, 0, "3"},
+               {itemRightParen, 0, ")"},
+               {itemRightParen, 0, ")"},
+               tRight,
+               tEOF,
+       }},
        {"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
        {"for", `{{for }}`, []item{tLeft, tFor, tRight, tEOF}},
        {"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
@@ -189,6 +199,18 @@ var lexTests = []lexTest{
                tLeft,
                {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
        // Many elements in an action blew the lookahead until
index 8a779ce1a9082fb0e5a7f6e6a9ece349d59b27ab..e6d106f3b77f941d06f2b8b38c2fa160d8cc70f8 100644 (file)
@@ -209,6 +209,10 @@ func (c *CommandNode) String() string {
                if i > 0 {
                        s += " "
                }
+               if arg, ok := arg.(*PipeNode); ok {
+                       s += "(" + arg.String() + ")"
+                       continue
+               }
                s += arg.String()
        }
        return s
index 7ddb6fff1e534e7d0b997a435225efa0f8ec3e31..6dc2f0fb783ff44ac8cf071de489ee29c962026f 100644 (file)
@@ -349,10 +349,13 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
        pipe = newPipeline(t.lex.lineNumber(), decl)
        for {
                switch token := t.next(); token.typ {
-               case itemRightDelim:
+               case itemRightDelim, itemRightParen:
                        if len(pipe.Cmds) == 0 {
                                t.errorf("missing value for %s", context)
                        }
+                       if token.typ == itemRightParen {
+                               t.backup()
+                       }
                        return
                case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
                        itemNumber, itemNil, itemRawString, itemString, itemVariable:
@@ -456,11 +459,17 @@ func (t *Tree) command() *CommandNode {
 Loop:
        for {
                switch token := t.next(); token.typ {
-               case itemRightDelim:
+               case itemRightDelim, itemRightParen:
                        t.backup()
                        break Loop
                case itemPipe:
                        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:
                        t.errorf("%s", token.val)
                case itemIdentifier:
index d7bfc78c058fdb741708a5d1ba04716f3f966256..da0df20950fd54f82909b9c70305d2a59823250e 100644 (file)
@@ -185,6 +185,8 @@ var parseTests = []parseTest{
                `{{.X | .Y}}`},
        {"pipeline with decl", "{{$x := .X|.Y}}", noError,
                `{{$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,
                `{{if .X}}"hello"{{end}}`},
        {"if with else", "{{if .X}}true{{else}}false{{end}}", noError,