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