package strutil import ( "encoding/base64" "encoding/json" "fmt" "sort" "strings" "github.com/hashicorp/errwrap" glob "github.com/ryanuber/go-glob" ) // StrListContainsGlob looks for a string in a list of strings and allows // globs. func StrListContainsGlob(haystack []string, needle string) bool { for _, item := range haystack { if glob.Glob(item, needle) { return true } } return false } // StrListContains looks for a string in a list of strings. func StrListContains(haystack []string, needle string) bool { for _, item := range haystack { if item == needle { return true } } return false } // StrListSubset checks if a given list is a subset // of another set func StrListSubset(super, sub []string) bool { for _, item := range sub { if !StrListContains(super, item) { return false } } return true } // ParseDedupAndSortStrings parses a comma separated list of strings // into a slice of strings. The return slice will be sorted and will // not contain duplicate or empty items. func ParseDedupAndSortStrings(input string, sep string) []string { input = strings.TrimSpace(input) parsed := []string{} if input == "" { // Don't return nil return parsed } return RemoveDuplicates(strings.Split(input, sep), false) } // ParseDedupLowercaseAndSortStrings parses a comma separated list of // strings into a slice of strings. The return slice will be sorted and // will not contain duplicate or empty items. The values will be converted // to lower case. func ParseDedupLowercaseAndSortStrings(input string, sep string) []string { input = strings.TrimSpace(input) parsed := []string{} if input == "" { // Don't return nil return parsed } return RemoveDuplicates(strings.Split(input, sep), true) } // ParseKeyValues parses a comma separated list of `=` tuples // into a map[string]string. func ParseKeyValues(input string, out map[string]string, sep string) error { if out == nil { return fmt.Errorf("'out is nil") } keyValues := ParseDedupLowercaseAndSortStrings(input, sep) if len(keyValues) == 0 { return nil } for _, keyValue := range keyValues { shards := strings.Split(keyValue, "=") if len(shards) != 2 { return fmt.Errorf("invalid format") } key := strings.TrimSpace(shards[0]) value := strings.TrimSpace(shards[1]) if key == "" || value == "" { return fmt.Errorf("invalid pair: key: %q value: %q", key, value) } out[key] = value } return nil } // ParseArbitraryKeyValues parses arbitrary tuples. The input // can be one of the following: // * JSON string // * Base64 encoded JSON string // * Comma separated list of `=` pairs // * Base64 encoded string containing comma separated list of // `=` pairs // // Input will be parsed into the output parameter, which should // be a non-nil map[string]string. func ParseArbitraryKeyValues(input string, out map[string]string, sep string) error { input = strings.TrimSpace(input) if input == "" { return nil } if out == nil { return fmt.Errorf("'out' is nil") } // Try to base64 decode the input. If successful, consider the decoded // value as input. inputBytes, err := base64.StdEncoding.DecodeString(input) if err == nil { input = string(inputBytes) } // Try to JSON unmarshal the input. If successful, consider that the // metadata was supplied as JSON input. err = json.Unmarshal([]byte(input), &out) if err != nil { // If JSON unmarshalling fails, consider that the input was // supplied as a comma separated string of 'key=value' pairs. if err = ParseKeyValues(input, out, sep); err != nil { return errwrap.Wrapf("failed to parse the input: {{err}}", err) } } // Validate the parsed input for key, value := range out { if key != "" && value == "" { return fmt.Errorf("invalid value for key %q", key) } } return nil } // ParseStringSlice parses a `sep`-separated list of strings into a // []string with surrounding whitespace removed. // // The output will always be a valid slice but may be of length zero. func ParseStringSlice(input string, sep string) []string { input = strings.TrimSpace(input) if input == "" { return []string{} } splitStr := strings.Split(input, sep) ret := make([]string, len(splitStr)) for i, val := range splitStr { ret[i] = strings.TrimSpace(val) } return ret } // ParseArbitraryStringSlice parses arbitrary string slice. The input // can be one of the following: // * JSON string // * Base64 encoded JSON string // * `sep` separated list of values // * Base64-encoded string containing a `sep` separated list of values // // Note that the separator is ignored if the input is found to already be in a // structured format (e.g., JSON) // // The output will always be a valid slice but may be of length zero. func ParseArbitraryStringSlice(input string, sep string) []string { input = strings.TrimSpace(input) if input == "" { return []string{} } // Try to base64 decode the input. If successful, consider the decoded // value as input. inputBytes, err := base64.StdEncoding.DecodeString(input) if err == nil { input = string(inputBytes) } ret := []string{} // Try to JSON unmarshal the input. If successful, consider that the // metadata was supplied as JSON input. err = json.Unmarshal([]byte(input), &ret) if err != nil { // If JSON unmarshalling fails, consider that the input was // supplied as a separated string of values. return ParseStringSlice(input, sep) } if ret == nil { return []string{} } return ret } // TrimStrings takes a slice of strings and returns a slice of strings // with trimmed spaces func TrimStrings(items []string) []string { ret := make([]string, len(items)) for i, item := range items { ret[i] = strings.TrimSpace(item) } return ret } // RemoveDuplicates removes duplicate and empty elements from a slice of // strings. This also may convert the items in the slice to lower case and // returns a sorted slice. func RemoveDuplicates(items []string, lowercase bool) []string { itemsMap := map[string]bool{} for _, item := range items { item = strings.TrimSpace(item) if lowercase { item = strings.ToLower(item) } if item == "" { continue } itemsMap[item] = true } items = make([]string, 0, len(itemsMap)) for item := range itemsMap { items = append(items, item) } sort.Strings(items) return items } // RemoveDuplicatesStable removes duplicate and empty elements from a slice of // strings, preserving order (and case) of the original slice. // In all cases, strings are compared after trimming whitespace // If caseInsensitive, strings will be compared after ToLower() func RemoveDuplicatesStable(items []string, caseInsensitive bool) []string { itemsMap := make(map[string]bool, len(items)) deduplicated := make([]string, 0, len(items)) for _, item := range items { key := strings.TrimSpace(item) if caseInsensitive { key = strings.ToLower(key) } if key == "" || itemsMap[key] { continue } itemsMap[key] = true deduplicated = append(deduplicated, item) } return deduplicated } // RemoveEmpty removes empty elements from a slice of // strings func RemoveEmpty(items []string) []string { if len(items) == 0 { return items } itemsSlice := make([]string, 0, len(items)) for _, item := range items { if item == "" { continue } itemsSlice = append(itemsSlice, item) } return itemsSlice } // EquivalentSlices checks whether the given string sets are equivalent, as in, // they contain the same values. func EquivalentSlices(a, b []string) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } // First we'll build maps to ensure unique values mapA := map[string]bool{} mapB := map[string]bool{} for _, keyA := range a { mapA[keyA] = true } for _, keyB := range b { mapB[keyB] = true } // Now we'll build our checking slices var sortedA, sortedB []string for keyA := range mapA { sortedA = append(sortedA, keyA) } for keyB := range mapB { sortedB = append(sortedB, keyB) } sort.Strings(sortedA) sort.Strings(sortedB) // Finally, compare if len(sortedA) != len(sortedB) { return false } for i := range sortedA { if sortedA[i] != sortedB[i] { return false } } return true } // EqualStringMaps tests whether two map[string]string objects are equal. // Equal means both maps have the same sets of keys and values. This function // is 6-10x faster than a call to reflect.DeepEqual(). func EqualStringMaps(a, b map[string]string) bool { if len(a) != len(b) { return false } for k := range a { v, ok := b[k] if !ok || a[k] != v { return false } } return true } // StrListDelete removes the first occurrence of the given item from the slice // of strings if the item exists. func StrListDelete(s []string, d string) []string { if s == nil { return s } for index, element := range s { if element == d { return append(s[:index], s[index+1:]...) } } return s } // GlobbedStringsMatch compares item to val with support for a leading and/or // trailing wildcard '*' in item. func GlobbedStringsMatch(item, val string) bool { if len(item) < 2 { return val == item } hasPrefix := strings.HasPrefix(item, "*") hasSuffix := strings.HasSuffix(item, "*") if hasPrefix && hasSuffix { return strings.Contains(val, item[1:len(item)-1]) } else if hasPrefix { return strings.HasSuffix(val, item[1:]) } else if hasSuffix { return strings.HasPrefix(val, item[:len(item)-1]) } return val == item } // AppendIfMissing adds a string to a slice if the given string is not present func AppendIfMissing(slice []string, i string) []string { if StrListContains(slice, i) { return slice } return append(slice, i) } // MergeSlices adds an arbitrary number of slices together, uniquely func MergeSlices(args ...[]string) []string { all := map[string]struct{}{} for _, slice := range args { for _, v := range slice { all[v] = struct{}{} } } result := make([]string, 0, len(all)) for k := range all { result = append(result, k) } sort.Strings(result) return result } // Difference returns the set difference (A - B) of the two given slices. The // result will also remove any duplicated values in set A regardless of whether // that matches any values in set B. func Difference(a, b []string, lowercase bool) []string { if len(a) == 0 { return a } if len(b) == 0 { if !lowercase { return a } newA := make([]string, len(a)) for i, v := range a { newA[i] = strings.ToLower(v) } return newA } a = RemoveDuplicates(a, lowercase) b = RemoveDuplicates(b, lowercase) itemsMap := map[string]bool{} for _, aVal := range a { itemsMap[aVal] = true } // Perform difference calculation for _, bVal := range b { if _, ok := itemsMap[bVal]; ok { itemsMap[bVal] = false } } items := []string{} for item, exists := range itemsMap { if exists { items = append(items, item) } } sort.Strings(items) return items } // GetString attempts to retrieve a value from the provided map and assert that it is a string. If the key does not // exist in the map, this will return an empty string. If the key exists, but the value is not a string type, this will // return an error. If no map or key is provied, this will return an error func GetString(m map[string]interface{}, key string) (string, error) { if m == nil { return "", fmt.Errorf("missing map") } if key == "" { return "", fmt.Errorf("missing key") } rawVal, ok := m[key] if !ok { return "", nil } str, ok := rawVal.(string) if !ok { return "", fmt.Errorf("invalid value at %s: is a %T", key, rawVal) } return str, nil }