Skip to content

Commit

Permalink
config: teach format to handle lists
Browse files Browse the repository at this point in the history
Format now distributes over lists. See the
doc changes 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."

Teaching format about lists (combined with join)
makes it easy to satisfy those needs.

This will change the behavior of format if anyone
is currently using it with lists of length > 1,
but the current behavior in that case is so
unusual that I doubt anyone is using it, and if
they are, it is probably a bug. It is also not
documented as being supported.

(It currently prefixes to the first element of the
list whatever is in the first half of the
formatted string and appends to the last element
of the list whatever is in the second half of the
formatted string.)
  • Loading branch information
josharian committed May 6, 2015
1 parent 42bf1c9 commit f2fb9e7
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 1 deletion.
50 changes: 49 additions & 1 deletion config/interpolate_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,56 @@ func interpolationFuncFormat() ast.Function {
VariadicType: ast.TypeAny,
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
// Make a copy of the variadic part of args
// to avoid modifying the original.
varargs := make([]interface{}, len(args)-1)
copy(varargs, args[1:])

// Convert arguments that are lists into slices.
// Confirm along the way that all lists have the same length (n).
var n int
for i := 1; i < len(args); i++ {
s, ok := args[i].(string)
if !ok {
continue
}
parts := strings.Split(s, InterpSplitDelim)
if len(parts) == 1 {
continue
}
varargs[i-1] = parts
if n == 0 {
// first list we've seen
n = len(parts)
continue
}
if n != len(parts) {
return nil, fmt.Errorf("format: mismatched list lengths: %d != %d", n, len(parts))
}
}

// Do the formatting.
format := args[0].(string)
return fmt.Sprintf(format, args[1:]...), nil
if n == 0 {
// Easy case: No lists.
return fmt.Sprintf(format, varargs...), nil
}

// Generate a list of formatted strings.
list := make([]string, n)
fmtargs := make([]interface{}, len(varargs))
for i := 0; i < n; i++ {
for j, arg := range varargs {
switch arg := arg.(type) {
default:
fmtargs[j] = arg
case []string:
fmtargs[j] = arg[i]
}
}
list[i] = fmt.Sprintf(format, fmtargs...)
}
return strings.Join(list, InterpSplitDelim), nil
},
}
}
Expand Down
35 changes: 35 additions & 0 deletions config/interpolate_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,41 @@ func TestInterpolateFuncFormat(t *testing.T) {
"hello 12345",
false,
},
// Format applies to each list element in turn
{
`${format("<%s>", split(",", "A,B"))}`,
"<A>" + InterpSplitDelim + "<B>",
false,
},
// Format repeats scalar elements
{
`${join(", ", format("%s=%s", "x", split(",", "A,B,C")))}`,
"x=A, x=B, x=C",
false,
},
// Multiple lists are walked in parallel
{
`${join(", ", format("%s=%s", split(",", "A,B,C"), split(",", "1,2,3")))}`,
"A=1, B=2, C=3",
false,
},
// Lists of length zero/one are repeated, just as scalars are
{
`${join(", ", format("%s=%s", split(",", ""), split(",", "1,2,3")))}`,
"=1, =2, =3",
false,
},
{
`${join(", ", format("%s=%s", split(",", "A"), split(",", "1,2,3")))}`,
"A=1, A=2, A=3",
false,
},
// Mismatched list lengths generate an error
{
`${format("%s=%2s", split(",", "A,B,C,D"), split(",", "1,2,3"))}`,
nil,
true,
},
},
})
}
Expand Down
7 changes: 7 additions & 0 deletions website/source/docs/configuration/interpolation.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ The supported built-in functions are:
Good documentation for the syntax can be [found here](http://golang.org/pkg/fmt/).
Example to zero-prefix a count, used commonly for naming servers:
`format("web-%03d", count.index+1)`.
If one of args is a list, then the format is applied to each element of the list and format returns a list. Non-list arguments are repeated for each list element.
For example, to convert a list of DNS addresses to a list of URLs, you might use:
`format("https://%s:%s/", aws_instance.foo.*.public_dns, var.port)`.
If multiple args are lists, and they have the same number of elements, then the format is applied to the elements of the lists in parallel.
Example:
`format("instance %v has private ip %v", aws_instance.foo.*.id, aws_instance.foo.*.private_ip)`.
Passing lists with different lengths to format results in an error.

* `join(delim, list)` - Joins the list with the delimiter. A list is
only possible with splat variables from resources with a count
Expand Down

0 comments on commit f2fb9e7

Please sign in to comment.