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