--- a/state/state.go Mon Oct 02 09:25:54 2017 -0500
+++ b/state/state.go Mon Oct 02 11:21:03 2017 -0500
@@ -21,6 +21,7 @@
@@ -66,23 +67,77 @@
// MapSlice calls SliceMapper on the given environment, but also checks to
-// see if the variable could be expanded into a list.
+// see if the variable in the env parameter can be expanded inot a list. func (st *State) MapSlice(env, fullEnv []string) ([]string, error) {
- // If we only have one thing and it looks like $VAR or ${VAR}, then
- // see if we have it in the list of things we can expand on the stack.
- if delimiter, ok := st.getDelimiter(getName(env[0])); ok {
- mapped, err := environment.Mapper(env[0], fullEnv)
+ // TODO - make iterative + for _, data := range env { + expanded, err := st.expand(data, fullEnv) + clean = append(clean, expanded...) +// expand will attempt to expand any variables stored on the state expand +// stack. If no expandable variables are found, then the standard mapper +// is used. If multiple expandable variables are around, the mapper will +// be applied to the cartesian product of the variables. +func (st *State) expand(data string, fullEnv []string) ([]string, error) { + expansions := map[string][]string{} + // First, extract all of the expandable names and get their values + // in the full environment and split them by the proper delimiter. + for _, name := range getNames(data) { + if delimiter, ok := st.getDelimiter(name); ok { + val, err := environment.Mapper("$"+name, fullEnv)
- return strings.SplitN(mapped, delimiter, -1), nil
+ expansions[name] = strings.SplitN(val, delimiter, -1) - return environment.SliceMapper(env, fullEnv)
+ // If we don't have any expandable variables, just use the standard + // mapper. If we don't do this here we won't return anything useful + // as product will return a nil map. + if len(expansions) == 0 { + mapped, err := environment.Mapper(data, fullEnv) + return []string{mapped}, nil + // Construct the actual values. Product gives us a map of all possible + // combinations of expanded values. For each one, overwrite the vars + // in the full env and apply the map. + for _, m := range product(expansions) { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + val, err := environment.Mapper(data, environment.Merge(fullEnv, env)) + values = append(values, val) // GetDelimiter returns the highest (outermost extend task) delimiter registered
@@ -130,14 +185,94 @@
-// getName returns the name of the variable `var` if the string is of the
-func getName(env string) string {
- env = strings.TrimSpace(env)
+var re1 = regexp.MustCompile("\\$[A-Za-z0-9_]+") +var re2 = regexp.MustCompile("\\${[^}]+}") +// getNames extracts all variables with the format $var or ${var} from +func getNames(env string) []string { + for _, match := range re1.FindAllString(env, -1) { + names = append(names, match[1:]) + for _, match := range re2.FindAllString(env, -1) { + // Remove leading ${ and trailing } + names = append(names, match[2:len(match)-1]) - if strings.HasPrefix(env, "$") {
- return strings.Trim(env[1:], "{}")
+// product will return the cartesian product of the input map. For +// example, if the input map is A => 1, 2, 3; B => 4, 5, 6, then +// the list of outputs will contain the following: +func product(expansions map[string][]string) []map[string]string { + indices = make([]int, len(expansions)) + maps = []map[string]string{} + if len(expansions) == 0 {
+ // Create a stable order of keys - this will determine our + // iteration order (in the above [A B] is the list as the + // index for B will increase more frequently than A). + for key := range expansions { + order = append(order, key) + // The index map starts out as [0 0 ... 0] and will increase + // in the following order: + // Once the first index (which increases the most slowly) + // exceeds the length of that slice, we've seen everything. + for indices[0] < len(expansions[order[0]]) { + // Create a single element from the current state + snapshot := map[string]string{} + for i, j := range indices { + snapshot[order[i]] = expansions[order[i]][j] + maps = append(maps, snapshot) + // Advance the index map. Start from the end of the + // index map and increment by one. If we end up rolling + // over from n - 1 to 0 (where n is the length of the + // associated slice), then we also flip the index + // immediately to the left. + for j := len(indices) - 1; j >= 0; j-- { + if j == 0 || indices[j] < len(expansions[order[j]]) { --- a/state/state_test.go Mon Oct 02 09:25:54 2017 -0500
+++ b/state/state_test.go Mon Oct 02 11:21:03 2017 -0500
@@ -37,7 +37,7 @@
-func (e *stateSuite) TestMap(t sweet.T) {
+func (e *stateSuite) TestMapSlice(t sweet.T) { st.Environment = []string{"FOO=BAR"}
@@ -65,6 +65,39 @@
Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+func (e *stateSuite) TestMapSliceComplex(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + mapEnv := func(val string) []string { + mapped, err := st.MapSlice([]string{val}, st.Environment) + Expect(err).To(BeNil()) + st.PushExtendFrame([]string{"X=A;B;C"}, []string{"X"}, ";") + st.PushExtendFrame([]string{"Y=D;E;F"}, []string{"Y"}, ";") + Expect(mapEnv("BONK")).To(Equal([]string{"BONK"})) + Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"})) + Expect(mapEnv("$X")).To(Equal([]string{"A", "B", "C"})) + Expect(mapEnv("$Y")).To(Equal([]string{"D", "E", "F"})) + Expect(mapEnv("x${X}x")).To(Equal([]string{"xAx", "xBx", "xCx"})) + Expect(mapEnv("x${Y}x")).To(Equal([]string{"xDx", "xEx", "xFx"})) + Expect(mapEnv("$X/$Y")).To(Equal([]string{ func (e *stateSuite) TestPopExtendFrameSize(t sweet.T) {
st.PushExtendFrame(nil, nil, "")
@@ -93,8 +126,31 @@
Expect(st.PopExtendFrame()).To(Equal(errEmptyExtendStack))
-func (e *stateSuite) TestGetName(t sweet.T) {
- Expect(getName("foo")).To(Equal(""))
- Expect(getName("$FOO")).To(Equal("FOO"))
- Expect(getName("${FOO}")).To(Equal("FOO"))
+func (e *stateSuite) TestGetNames(t sweet.T) { + names := getNames("foo $bar ${baz} ${bar} $bonk_quux honk") + Expect(names).To(HaveLen(4)) + Expect(names).To(ConsistOf([]string{"bar", "baz", "bar", "bonk_quux"})) +func (e *stateSuite) TestProduct(t sweet.T) { + p := product(map[string][]string{ + "a": []string{"1", "2", "3"}, + "b": []string{"4", "5"}, + "c": []string{"6", "7"}, + Expect(p).To(ConsistOf([]map[string]string{ + map[string]string{"a": "1", "b": "4", "c": "6"}, + map[string]string{"a": "1", "b": "4", "c": "7"}, + map[string]string{"a": "1", "b": "5", "c": "6"}, + map[string]string{"a": "1", "b": "5", "c": "7"}, + map[string]string{"a": "2", "b": "4", "c": "6"}, + map[string]string{"a": "2", "b": "4", "c": "7"}, + map[string]string{"a": "2", "b": "5", "c": "6"}, + map[string]string{"a": "2", "b": "5", "c": "7"}, + map[string]string{"a": "3", "b": "4", "c": "6"}, + map[string]string{"a": "3", "b": "4", "c": "7"}, + map[string]string{"a": "3", "b": "5", "c": "6"}, + map[string]string{"a": "3", "b": "5", "c": "7"},