Skip to content

Commit 02e751e

Browse files
committed
config: add formatlist
formatlist distributes formatting over lists. See the docs for details. As a colleague commented: "It happens all the time that we want a set of outputs, but in a slightly different way than just simple joining or concatting." formatlist (combined with join) makes it easy to satisfy those needs.
1 parent a3f79cd commit 02e751e

3 files changed

Lines changed: 135 additions & 7 deletions

File tree

config/interpolate_funcs.go

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"bytes"
5+
"errors"
56
"fmt"
67
"io/ioutil"
78
"regexp"
@@ -17,13 +18,14 @@ var Funcs map[string]ast.Function
1718

1819
func init() {
1920
Funcs = map[string]ast.Function{
20-
"file": interpolationFuncFile(),
21-
"format": interpolationFuncFormat(),
22-
"join": interpolationFuncJoin(),
23-
"element": interpolationFuncElement(),
24-
"replace": interpolationFuncReplace(),
25-
"split": interpolationFuncSplit(),
26-
"length": interpolationFuncLength(),
21+
"file": interpolationFuncFile(),
22+
"format": interpolationFuncFormat(),
23+
"formatlist": interpolationFuncFormatList(),
24+
"join": interpolationFuncJoin(),
25+
"element": interpolationFuncElement(),
26+
"replace": interpolationFuncReplace(),
27+
"split": interpolationFuncSplit(),
28+
"length": interpolationFuncLength(),
2729

2830
// Concat is a little useless now since we supported embeddded
2931
// interpolations but we keep it around for backwards compat reasons.
@@ -88,6 +90,69 @@ func interpolationFuncFormat() ast.Function {
8890
}
8991
}
9092

93+
// interpolationFuncFormatList implements the "formatlist" function that does
94+
// string formatting on lists.
95+
func interpolationFuncFormatList() ast.Function {
96+
return ast.Function{
97+
ArgTypes: []ast.Type{ast.TypeString},
98+
Variadic: true,
99+
VariadicType: ast.TypeAny,
100+
ReturnType: ast.TypeString,
101+
Callback: func(args []interface{}) (interface{}, error) {
102+
// Make a copy of the variadic part of args
103+
// to avoid modifying the original.
104+
varargs := make([]interface{}, len(args)-1)
105+
copy(varargs, args[1:])
106+
107+
// Convert arguments that are lists into slices.
108+
// Confirm along the way that all lists have the same length (n).
109+
var n int
110+
for i := 1; i < len(args); i++ {
111+
s, ok := args[i].(string)
112+
if !ok {
113+
continue
114+
}
115+
parts := strings.Split(s, InterpSplitDelim)
116+
if len(parts) == 1 {
117+
continue
118+
}
119+
varargs[i-1] = parts
120+
if n == 0 {
121+
// first list we've seen
122+
n = len(parts)
123+
continue
124+
}
125+
if n != len(parts) {
126+
return nil, fmt.Errorf("format: mismatched list lengths: %d != %d", n, len(parts))
127+
}
128+
}
129+
130+
if n == 0 {
131+
return nil, errors.New("no lists in arguments to formatlist")
132+
}
133+
134+
// Do the formatting.
135+
format := args[0].(string)
136+
137+
// Generate a list of formatted strings.
138+
list := make([]string, n)
139+
fmtargs := make([]interface{}, len(varargs))
140+
for i := 0; i < n; i++ {
141+
for j, arg := range varargs {
142+
switch arg := arg.(type) {
143+
default:
144+
fmtargs[j] = arg
145+
case []string:
146+
fmtargs[j] = arg[i]
147+
}
148+
}
149+
list[i] = fmt.Sprintf(format, fmtargs...)
150+
}
151+
return strings.Join(list, InterpSplitDelim), nil
152+
},
153+
}
154+
}
155+
91156
// interpolationFuncJoin implements the "join" function that allows
92157
// multi-variable values to be joined by some character.
93158
func interpolationFuncJoin() ast.Function {

config/interpolate_funcs_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,59 @@ func TestInterpolateFuncFormat(t *testing.T) {
106106
})
107107
}
108108

109+
func TestInterpolateFuncFormatList(t *testing.T) {
110+
testFunction(t, testFunctionConfig{
111+
Cases: []testFunctionCase{
112+
// formatlist requires at least one list
113+
{
114+
`${formatlist("hello")}`,
115+
nil,
116+
true,
117+
},
118+
{
119+
`${formatlist("hello %s", "world")}`,
120+
nil,
121+
true,
122+
},
123+
// formatlist applies to each list element in turn
124+
{
125+
`${formatlist("<%s>", split(",", "A,B"))}`,
126+
"<A>" + InterpSplitDelim + "<B>",
127+
false,
128+
},
129+
// formatlist repeats scalar elements
130+
{
131+
`${join(", ", formatlist("%s=%s", "x", split(",", "A,B,C")))}`,
132+
"x=A, x=B, x=C",
133+
false,
134+
},
135+
// Multiple lists are walked in parallel
136+
{
137+
`${join(", ", formatlist("%s=%s", split(",", "A,B,C"), split(",", "1,2,3")))}`,
138+
"A=1, B=2, C=3",
139+
false,
140+
},
141+
// formatlist of lists of length zero/one are repeated, just as scalars are
142+
{
143+
`${join(", ", formatlist("%s=%s", split(",", ""), split(",", "1,2,3")))}`,
144+
"=1, =2, =3",
145+
false,
146+
},
147+
{
148+
`${join(", ", formatlist("%s=%s", split(",", "A"), split(",", "1,2,3")))}`,
149+
"A=1, A=2, A=3",
150+
false,
151+
},
152+
// Mismatched list lengths generate an error
153+
{
154+
`${formatlist("%s=%2s", split(",", "A,B,C,D"), split(",", "1,2,3"))}`,
155+
nil,
156+
true,
157+
},
158+
},
159+
})
160+
}
161+
109162
func TestInterpolateFuncJoin(t *testing.T) {
110163
testFunction(t, testFunctionConfig{
111164
Cases: []testFunctionCase{

website/source/docs/configuration/interpolation.html.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ The supported built-in functions are:
8989
Example to zero-prefix a count, used commonly for naming servers:
9090
`format("web-%03d", count.index+1)`.
9191

92+
* `formatlist(format, args...)` - Formats each element of a list
93+
according to the given format, similarly to `format`, and returns a list.
94+
Non-list arguments are repeated for each list element.
95+
For example, to convert a list of DNS addresses to a list of URLs, you might use:
96+
`formatlist("https://%s:%s/", aws_instance.foo.*.public_dns, var.port)`.
97+
If multiple args are lists, and they have the same number of elements, then the formatting is applied to the elements of the lists in parallel.
98+
Example:
99+
`formatlist("instance %v has private ip %v", aws_instance.foo.*.id, aws_instance.foo.*.private_ip)`.
100+
Passing lists with different lengths to formatlist results in an error.
101+
92102
* `join(delim, list)` - Joins the list with the delimiter. A list is
93103
only possible with splat variables from resources with a count
94104
greater than one. Example: `join(",", aws_instance.foo.*.id)`

0 commit comments

Comments
 (0)