mirror of
https://github.com/golang/go
synced 2024-11-18 09:04:49 -07:00
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
This commit is contained in:
parent
3bd8684fac
commit
cc842c738e
@ -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"}}
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user