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