--- a/environment/mapper.go Tue Oct 03 21:33:29 2017 -0500
+++ b/environment/mapper.go Wed Oct 04 03:27:13 2017 +0000
@@ -41,6 +41,12 @@
+// Map will return the value matching a KEY=VAL pair in the given environment. +func Map(key string, env []string) string { + mapper := envMapper{env} // Mapper will replace ${TEMPLATE} patterns in the string with the KEY=VAL pairs
// in the given environment. This function will replace patterns recursively, so
// if VAL has the form ${TEMPLATE}, it will be replaced again.
--- a/state/state.go Tue Oct 03 21:33:29 2017 -0500
+++ b/state/state.go Wed Oct 04 03:27:13 2017 +0000
@@ -29,6 +29,8 @@
"bitbucket.org/rw_grim/convey/workspace"
+const ExpansionLimit = 100 Workspace workspace.Workspace
@@ -84,23 +86,95 @@
// 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 into 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)
+ // Protect ourselves against a weird infinite expansion. This can + // happen if something occurs like X => A,$Y; Y => B,$X. This is + // never a useful case - a high expansion limit should catch this + // without ever hitting a practical edge. + for i := 0; i < ExpansionLimit; i++ { + next, err := st.expandSlice(prev, fullEnv) + // If we haven't made a change, return the final result. + if isSame(next, prev) { + return nil, fmt.Errorf("hit limit while expanding '%s'", env)
- return strings.SplitN(mapped, delimiter, -1), nil
+func (st *State) expandSlice(env, fullEnv []string) ([]string, error) { + for _, data := range env { + expanded, err := st.expand(data, fullEnv) + all = append(all, expanded...) + return removeDuplicates(all), nil +// 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 { + expansions[name] = strings.SplitN(environment.Map(name, fullEnv), 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, map using ONLY the + // expanded vars. Additional mapping will apply on the next iteration. + for _, m := range product(expansions) { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + val, err := environment.Mapper(data, env) + values = append(values, val) // GetDelimiter returns the highest (outermost extend task) delimiter registered
@@ -178,15 +252,3 @@
-// getName returns the name of the variable `var` if the string is of the
-func getName(env string) string {
- env = strings.TrimSpace(env)
- if strings.HasPrefix(env, "$") {
- return strings.Trim(env[1:], "{}")
--- a/state/state_test.go Tue Oct 03 21:33:29 2017 -0500
+++ b/state/state_test.go Wed Oct 04 03:27:13 2017 +0000
@@ -37,27 +37,101 @@
-func (s *stateSuite) TestMap(t sweet.T) {
- mapEnv := func(st *State, val string) []string {
- mapped, err := st.MapSlice([]string{val}, st.Environment)
- Expect(err).To(BeNil())
+func (s *stateSuite) TestMapSlice(t sweet.T) { st1.Environment = []string{"FOO=BAR"}
- Expect(mapEnv(st1, "$X")).To(Equal([]string{"$X"}))
- Expect(mapEnv(st1, "$FOO")).To(Equal([]string{"BAR"}))
+ Expect(mapEnv(st1, "$X")).To(ConsistOf([]string{"$X"})) + Expect(mapEnv(st1, "$FOO")).To(ConsistOf([]string{"BAR"})) st2 := st1.WrapWithExpandableEnv([]string{"X=A;B;C"}, []string{"X"}, ";")
- Expect(mapEnv(st2, "$X")).To(Equal([]string{"A", "B", "C"}))
- Expect(mapEnv(st2, "$FOO")).To(Equal([]string{"BAR"}))
+ Expect(mapEnv(st2, "$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv(st2, "$FOO")).To(ConsistOf([]string{"BAR"})) st3 := st2.WrapWithExpandableEnv([]string{"BAR=B;A;R::B;A;Z", "FOO=SAME"}, []string{"BAR"}, "::")
- Expect(mapEnv(st3, "$X")).To(Equal([]string{"A", "B", "C"}))
- Expect(mapEnv(st3, "$FOO")).To(Equal([]string{"BAR"}))
- Expect(mapEnv(st3, "$BAR")).To(Equal([]string{"B;A;R", "B;A;Z"}))
+ Expect(mapEnv(st3, "$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv(st3, "$FOO")).To(ConsistOf([]string{"BAR"})) + Expect(mapEnv(st3, "$BAR")).To(ConsistOf([]string{"B;A;R", "B;A;Z"})) +func (s *stateSuite) TestMapSliceComplex(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st = st.WrapWithExpandableEnv([]string{"X=A;B;C", "Y=D;E;F"}, []string{"X", "Y"}, ";") + Expect(mapEnv(st, "BONK")).To(ConsistOf([]string{"BONK"})) + Expect(mapEnv(st, "$FOO")).To(ConsistOf([]string{"BAR"})) + Expect(mapEnv(st, "$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv(st, "$Y")).To(ConsistOf([]string{"D", "E", "F"})) + Expect(mapEnv(st, "x${X}x")).To(ConsistOf([]string{"xAx", "xBx", "xCx"})) + Expect(mapEnv(st, "x${Y}x")).To(ConsistOf([]string{"xDx", "xEx", "xFx"})) + Expect(mapEnv(st, "$X/$Y")).To(ConsistOf([]string{ +func (s *stateSuite) TestMapSliceIterative(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st = st.WrapWithExpandableEnv([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;E"}, []string{"X", "Y", "Z"}, ";") + Expect(mapEnv(st, "$X")).To(ConsistOf([]string{"A", "xB", "xyC", "xyD", "xyE"})) +func (s *stateSuite) TestMapSliceIndirect(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st = st.WrapWithExpandableEnv([]string{"X=$Y", "Y=A;$Z;C", "Z=B;$W", "W=D"}, []string{"X", "Y", "Z"}, ";") + Expect(mapEnv(st, "$X")).To(ConsistOf([]string{"A", "B", "C", "D"})) +func (s *stateSuite) TestMapSliceRecursive(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st = st.WrapWithExpandableEnv([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;$X"}, []string{"X", "Y", "Z"}, ";") + _, err := st.MapSlice([]string{"$X"}, st.Environment) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("hit limit")) +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"}, func (s *stateSuite) TestWrapParent(t sweet.T) {
@@ -80,12 +154,6 @@
Expect(st2.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ", "BAZ=BONK"}))
-func (s *stateSuite) TestGetName(t sweet.T) {
- Expect(getName("foo")).To(Equal(""))
- Expect(getName("$FOO")).To(Equal("FOO"))
- Expect(getName("${FOO}")).To(Equal("FOO"))
func (s *stateSuite) TestDetached(t sweet.T) {
Expect(st1.GetDetached()).To(BeEmpty())
@@ -103,3 +171,9 @@
Expect(st1.GetDetached()).To(ConsistOf([]string{"foo", "bar"}))
+func mapEnv(st *State, val string) []string { + mapped, err := st.MapSlice([]string{val}, st.Environment) + Expect(err).To(BeNil())