From f2fb9e7a8a3f0a1dfea6ff4a1e2f8815e0e7b6bf Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Tue, 5 May 2015 20:34:40 -0700 Subject: [PATCH] config: teach format to handle lists 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.) --- config/interpolate_funcs.go | 50 ++++++++++++++++++- config/interpolate_funcs_test.go | 35 +++++++++++++ .../docs/configuration/interpolation.html.md | 7 +++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index ee39c5a895f2..693ae29a8668 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -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 }, } } diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 044d4f843aa4..bd3d7070cd3b 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -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"))}`, + "" + InterpSplitDelim + "", + 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, + }, }, }) } diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index b24c02ce8795..9d168dd2d76e 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -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