go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/text/stringtemplate/template.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package stringtemplate implements Python string.Template-like substitution. 16 package stringtemplate 17 18 import ( 19 "regexp" 20 "strings" 21 22 "go.chromium.org/luci/common/errors" 23 ) 24 25 const idPattern = "[_a-z][_a-z0-9]*" 26 27 // We're looking for: 28 // 29 // Submatch indices: 30 // [0:1] Full match 31 // [2:3] $$ (escaped) 32 // [4:5] $key (without braces) 33 // [6:7] ${key} (with braces) 34 // [8:9] $... (Invalid) 35 var namedFormatMatcher = regexp.MustCompile( 36 `\$(?:` + 37 `(\$)|` + // Escaped ($$) 38 `(` + idPattern + `)|` + // Without braces: $key 39 `(?:\{(` + idPattern + `)\})|` + // With braces: ${key} 40 `(.*)` + // Invalid 41 `)`) 42 43 // Resolve resolves substitutions in v using the supplied substitution map, 44 // subst. 45 // 46 // A substitution can have the form: 47 // 48 // $key 49 // ${key} 50 // 51 // The substitution can also be escaped using a second "$", such as "$$". 52 // 53 // If the string includes an erroneous substitution, or if a referenced 54 // template variable isn't included in the "substitutions" map, Resolve will 55 // return an error. 56 func Resolve(v string, subst map[string]string) (string, error) { 57 smi := namedFormatMatcher.FindAllStringSubmatchIndex(v, -1) 58 if len(smi) == 0 { 59 // No substitutions. 60 return v, nil 61 } 62 63 var ( 64 parts = make([]string, 0, (len(smi)*2)+1) 65 pos = 0 66 ) 67 68 for _, match := range smi { 69 key := "" 70 switch { 71 case match[8] >= 0: 72 // Invalid. 73 return "", errors.Reason("invalid template: %q", v).Err() 74 75 case match[2] >= 0: 76 // Escaped. 77 parts = append(parts, v[pos:match[2]]) 78 pos = match[3] 79 continue 80 81 case match[4] >= 0: 82 // Key (without braces) 83 key = v[match[4]:match[5]] 84 case match[6] >= 0: 85 // Key (with braces) 86 key = v[match[6]:match[7]] 87 88 default: 89 panic("impossible") 90 } 91 92 // Add anything in between the previous match and the current. If our match 93 // includes a non-escape character, add that too. 94 if pos < match[0] { 95 parts = append(parts, v[pos:match[0]]) 96 } 97 pos = match[1] 98 99 subst, ok := subst[key] 100 if !ok { 101 return "", errors.Reason("no substitution for %q", key).Err() 102 } 103 parts = append(parts, subst) 104 } 105 106 // Append any trailing string. 107 parts = append(parts, v[pos:]) 108 109 // Join the parts. 110 return strings.Join(parts, ""), nil 111 }