diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index af1d2b0a4..7c4d76619 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "errors" "fmt" "io/ioutil" "regexp" @@ -17,13 +18,14 @@ var Funcs map[string]ast.Function func init() { Funcs = map[string]ast.Function{ - "file": interpolationFuncFile(), - "format": interpolationFuncFormat(), - "join": interpolationFuncJoin(), - "element": interpolationFuncElement(), - "replace": interpolationFuncReplace(), - "split": interpolationFuncSplit(), - "length": interpolationFuncLength(), + "file": interpolationFuncFile(), + "format": interpolationFuncFormat(), + "formatlist": interpolationFuncFormatList(), + "join": interpolationFuncJoin(), + "element": interpolationFuncElement(), + "replace": interpolationFuncReplace(), + "split": interpolationFuncSplit(), + "length": interpolationFuncLength(), // Concat is a little useless now since we supported embeddded // interpolations but we keep it around for backwards compat reasons. @@ -73,8 +75,8 @@ func interpolationFuncFile() ast.Function { } } -// interpolationFuncFormat implements the "replace" function that does -// string replacement. +// interpolationFuncFormat implements the "format" function that does +// string formatting. func interpolationFuncFormat() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, @@ -88,6 +90,69 @@ func interpolationFuncFormat() ast.Function { } } +// interpolationFuncFormatList implements the "formatlist" function that does +// string formatting on lists. +func interpolationFuncFormatList() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString}, + Variadic: true, + 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)) + } + } + + if n == 0 { + return nil, errors.New("no lists in arguments to formatlist") + } + + // Do the formatting. + format := args[0].(string) + + // 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 + }, + } +} + // interpolationFuncJoin implements the "join" function that allows // multi-variable values to be joined by some character. func interpolationFuncJoin() ast.Function { diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 044d4f843..5432cdbd7 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -106,6 +106,59 @@ func TestInterpolateFuncFormat(t *testing.T) { }) } +func TestInterpolateFuncFormatList(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + // formatlist requires at least one list + { + `${formatlist("hello")}`, + nil, + true, + }, + { + `${formatlist("hello %s", "world")}`, + nil, + true, + }, + // formatlist applies to each list element in turn + { + `${formatlist("<%s>", split(",", "A,B"))}`, + "" + InterpSplitDelim + "", + false, + }, + // formatlist repeats scalar elements + { + `${join(", ", formatlist("%s=%s", "x", split(",", "A,B,C")))}`, + "x=A, x=B, x=C", + false, + }, + // Multiple lists are walked in parallel + { + `${join(", ", formatlist("%s=%s", split(",", "A,B,C"), split(",", "1,2,3")))}`, + "A=1, B=2, C=3", + false, + }, + // formatlist of lists of length zero/one are repeated, just as scalars are + { + `${join(", ", formatlist("%s=%s", split(",", ""), split(",", "1,2,3")))}`, + "=1, =2, =3", + false, + }, + { + `${join(", ", formatlist("%s=%s", split(",", "A"), split(",", "1,2,3")))}`, + "A=1, A=2, A=3", + false, + }, + // Mismatched list lengths generate an error + { + `${formatlist("%s=%2s", split(",", "A,B,C,D"), split(",", "1,2,3"))}`, + nil, + true, + }, + }, + }) +} + func TestInterpolateFuncJoin(t *testing.T) { testFunction(t, testFunctionConfig{ Cases: []testFunctionCase{ diff --git a/config/interpolate_walk.go b/config/interpolate_walk.go index 17329e5a8..faacb5726 100644 --- a/config/interpolate_walk.go +++ b/config/interpolate_walk.go @@ -11,8 +11,7 @@ import ( ) // InterpSplitDelim is the delimeter that is looked for to split when -// it is returned. This is a comma right now but should eventually become -// a value that a user is very unlikely to use (such as UUID). +// it is returned. const InterpSplitDelim = `B780FFEC-B661-4EB8-9236-A01737AD98B6` // interpolationWalker implements interfaces for the reflectwalk package diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index d1e16bfba..fa2e75680 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -89,6 +89,16 @@ The supported built-in functions are: Example to zero-prefix a count, used commonly for naming servers: `format("web-%03d", count.index+1)`. + * `formatlist(format, args...)` - Formats each element of a list + according to the given format, similarly to `format`, and 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: + `formatlist("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 formatting is applied to the elements of the lists in parallel. + Example: + `formatlist("instance %v has private ip %v", aws_instance.foo.*.id, aws_instance.foo.*.private_ip)`. + Passing lists with different lengths to formatlist 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 greater than one. Example: `join(",", aws_instance.foo.*.id)`