github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/compose/template/template.go (about) 1 package template 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 ) 8 9 var delimiter = "\\$" 10 var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?" 11 12 var patternString = fmt.Sprintf( 13 "%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))", 14 delimiter, delimiter, substitution, substitution, 15 ) 16 17 var defaultPattern = regexp.MustCompile(patternString) 18 19 // DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli 20 var DefaultSubstituteFuncs = []SubstituteFunc{ 21 softDefault, 22 hardDefault, 23 requiredNonEmpty, 24 required, 25 } 26 27 // InvalidTemplateError is returned when a variable template is not in a valid 28 // format 29 type InvalidTemplateError struct { 30 Template string 31 } 32 33 func (e InvalidTemplateError) Error() string { 34 return fmt.Sprintf("Invalid template: %#v", e.Template) 35 } 36 37 // Mapping is a user-supplied function which maps from variable names to values. 38 // Returns the value as a string and a bool indicating whether 39 // the value is present, to distinguish between an empty string 40 // and the absence of a value. 41 type Mapping func(string) (string, bool) 42 43 // SubstituteFunc is a user-supplied function that apply substitution. 44 // Returns the value as a string, a bool indicating if the function could apply 45 // the substitution and an error. 46 type SubstituteFunc func(string, Mapping) (string, bool, error) 47 48 // SubstituteWith subsitute variables in the string with their values. 49 // It accepts additional substitute function. 50 func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) { 51 var err error 52 result := pattern.ReplaceAllStringFunc(template, func(substring string) string { 53 matches := pattern.FindStringSubmatch(substring) 54 groups := matchGroups(matches, pattern) 55 if escaped := groups["escaped"]; escaped != "" { 56 return escaped 57 } 58 59 substitution := groups["named"] 60 if substitution == "" { 61 substitution = groups["braced"] 62 } 63 64 if substitution == "" { 65 err = &InvalidTemplateError{Template: template} 66 return "" 67 } 68 69 for _, f := range subsFuncs { 70 var ( 71 value string 72 applied bool 73 ) 74 value, applied, err = f(substitution, mapping) 75 if err != nil { 76 return "" 77 } 78 if !applied { 79 continue 80 } 81 return value 82 } 83 84 value, _ := mapping(substitution) 85 return value 86 }) 87 88 return result, err 89 } 90 91 // Substitute variables in the string with their values 92 func Substitute(template string, mapping Mapping) (string, error) { 93 return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...) 94 } 95 96 // ExtractVariables returns a map of all the variables defined in the specified 97 // composefile (dict representation) and their default value if any. 98 func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]string { 99 if pattern == nil { 100 pattern = defaultPattern 101 } 102 return recurseExtract(configDict, pattern) 103 } 104 105 func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]string { 106 m := map[string]string{} 107 108 switch value := value.(type) { 109 case string: 110 if values, is := extractVariable(value, pattern); is { 111 for _, v := range values { 112 m[v.name] = v.value 113 } 114 } 115 case map[string]interface{}: 116 for _, elem := range value { 117 submap := recurseExtract(elem, pattern) 118 for key, value := range submap { 119 m[key] = value 120 } 121 } 122 123 case []interface{}: 124 for _, elem := range value { 125 if values, is := extractVariable(elem, pattern); is { 126 for _, v := range values { 127 m[v.name] = v.value 128 } 129 } 130 } 131 } 132 133 return m 134 } 135 136 type extractedValue struct { 137 name string 138 value string 139 } 140 141 func extractVariable(value interface{}, pattern *regexp.Regexp) ([]extractedValue, bool) { 142 sValue, ok := value.(string) 143 if !ok { 144 return []extractedValue{}, false 145 } 146 matches := pattern.FindAllStringSubmatch(sValue, -1) 147 if len(matches) == 0 { 148 return []extractedValue{}, false 149 } 150 values := []extractedValue{} 151 for _, match := range matches { 152 groups := matchGroups(match, pattern) 153 if escaped := groups["escaped"]; escaped != "" { 154 continue 155 } 156 val := groups["named"] 157 if val == "" { 158 val = groups["braced"] 159 } 160 name := val 161 var defaultValue string 162 switch { 163 case strings.Contains(val, ":?"): 164 name, _ = partition(val, ":?") 165 case strings.Contains(val, "?"): 166 name, _ = partition(val, "?") 167 case strings.Contains(val, ":-"): 168 name, defaultValue = partition(val, ":-") 169 case strings.Contains(val, "-"): 170 name, defaultValue = partition(val, "-") 171 } 172 values = append(values, extractedValue{name: name, value: defaultValue}) 173 } 174 return values, len(values) > 0 175 } 176 177 // Soft default (fall back if unset or empty) 178 func softDefault(substitution string, mapping Mapping) (string, bool, error) { 179 sep := ":-" 180 if !strings.Contains(substitution, sep) { 181 return "", false, nil 182 } 183 name, defaultValue := partition(substitution, sep) 184 value, ok := mapping(name) 185 if !ok || value == "" { 186 return defaultValue, true, nil 187 } 188 return value, true, nil 189 } 190 191 // Hard default (fall back if-and-only-if empty) 192 func hardDefault(substitution string, mapping Mapping) (string, bool, error) { 193 sep := "-" 194 if !strings.Contains(substitution, sep) { 195 return "", false, nil 196 } 197 name, defaultValue := partition(substitution, sep) 198 value, ok := mapping(name) 199 if !ok { 200 return defaultValue, true, nil 201 } 202 return value, true, nil 203 } 204 205 func requiredNonEmpty(substitution string, mapping Mapping) (string, bool, error) { 206 return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" }) 207 } 208 209 func required(substitution string, mapping Mapping) (string, bool, error) { 210 return withRequired(substitution, mapping, "?", func(_ string) bool { return true }) 211 } 212 213 func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) { 214 if !strings.Contains(substitution, sep) { 215 return "", false, nil 216 } 217 name, errorMessage := partition(substitution, sep) 218 value, ok := mapping(name) 219 if !ok || !valid(value) { 220 return "", true, &InvalidTemplateError{ 221 Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage), 222 } 223 } 224 return value, true, nil 225 } 226 227 func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string { 228 groups := make(map[string]string) 229 for i, name := range pattern.SubexpNames()[1:] { 230 groups[name] = matches[i+1] 231 } 232 return groups 233 } 234 235 // Split the string at the first occurrence of sep, and return the part before the separator, 236 // and the part after the separator. 237 // 238 // If the separator is not found, return the string itself, followed by an empty string. 239 func partition(s, sep string) (string, string) { 240 if strings.Contains(s, sep) { 241 parts := strings.SplitN(s, sep, 2) 242 return parts[0], parts[1] 243 } 244 return s, "" 245 }