grim/convey

Parents 0652a28e5a71
Children 2c14c5d09ab5
Add initial draft to handle expanded variables in a more general way (allows string templates).
--- 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 @@
"errors"
"fmt"
"os"
+ "regexp"
"strings"
"time"
@@ -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 len(env) == 1 {
- // 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)
+ clean := []string{}
+
+ // TODO - make iterative
+ for _, data := range env {
+ expanded, err := st.expand(data, fullEnv)
+ if err != nil {
+ return nil, err
+ }
+
+ clean = append(clean, expanded...)
+ }
+
+ return clean, 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 {
+ val, err := environment.Mapper("$"+name, fullEnv)
if err != nil {
return nil, err
}
- // Split it!
- 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)
+ if err != nil {
+ return nil, err
+ }
+
+ 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.
+
+ values := []string{}
+ for _, m := range product(expansions) {
+ env := []string{}
+ for k, v := range m {
+ env = append(env, fmt.Sprintf("%s=%s", k, v))
+ }
+
+ val, err := environment.Mapper(data, environment.Merge(fullEnv, env))
+ if err != nil {
+ return nil, err
+ }
+
+ values = append(values, val)
+ }
+
+ return values, nil
}
// GetDelimiter returns the highest (outermost extend task) delimiter registered
@@ -130,14 +185,94 @@
return nil
}
-// getName returns the name of the variable `var` if the string is of the
-// form $var or ${var}.
-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
+// the given string.
+func getNames(env string) []string {
+ names := []string{}
+
+ for _, match := range re1.FindAllString(env, -1) {
+ // Remove leading $
+ names = append(names, match[1:])
+ }
+
+ for _, match := range re2.FindAllString(env, -1) {
+ // Remove leading ${ and trailing }
+ names = append(names, match[2:len(match)-1])
+ }
+
+ return names
+}
- 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:
+// - A => 1; B => 4
+// - A => 1; B => 5
+// - A => 1; B => 6
+// - A => 2; B => 4
+// - A => 2; B => 5
+// - A => 2; B => 6
+// - A => 3; B => 4
+// - A => 3; B => 5
+// - A => 3; B => 6
+func product(expansions map[string][]string) []map[string]string {
+ var (
+ order = []string{}
+ indices = make([]int, len(expansions))
+ maps = []map[string]string{}
+ )
+
+ if len(expansions) == 0 {
+ return nil
}
- return ""
+ // 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:
+ // - [0 0 ... 0 1]
+ // - [0 0 ... 0 2]
+ // - [0 0 ... 0 (n-1)]
+ // - ...
+ // - [0 0 ... 1 0]
+ //
+ // 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
+ // of the index map.
+
+ 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-- {
+ indices[j]++
+ if j == 0 || indices[j] < len(expansions[order[j]]) {
+ break
+ }
+
+ indices[j] = 0
+ }
+ }
+
+ return maps
}
--- 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 := &State{}
st.Environment = []string{"FOO=BAR"}
@@ -65,6 +65,39 @@
Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
}
+func (e *stateSuite) TestMapSliceComplex(t sweet.T) {
+ st := &State{}
+ st.Environment = []string{"FOO=BAR"}
+
+ mapEnv := func(val string) []string {
+ mapped, err := st.MapSlice([]string{val}, st.Environment)
+ Expect(err).To(BeNil())
+ return mapped
+ }
+
+ st.PushExtendFrame([]string{"X=A;B;C"}, []string{"X"}, ";")
+ st.PushExtendFrame([]string{"Y=D;E;F"}, []string{"Y"}, ";")
+
+ // No expansion
+ Expect(mapEnv("BONK")).To(Equal([]string{"BONK"}))
+ Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+
+ // Simple
+ Expect(mapEnv("$X")).To(Equal([]string{"A", "B", "C"}))
+ Expect(mapEnv("$Y")).To(Equal([]string{"D", "E", "F"}))
+
+ // Templated
+ Expect(mapEnv("x${X}x")).To(Equal([]string{"xAx", "xBx", "xCx"}))
+ Expect(mapEnv("x${Y}x")).To(Equal([]string{"xDx", "xEx", "xFx"}))
+
+ // Cartesian product
+ Expect(mapEnv("$X/$Y")).To(Equal([]string{
+ "A/D", "A/E", "A/F",
+ "B/D", "B/E", "B/F",
+ "C/D", "C/E", "C/F",
+ }))
+}
+
func (e *stateSuite) TestPopExtendFrameSize(t sweet.T) {
st := &State{}
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"},
+ }))
+}