| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. |
| 4 |
| 5 // Package stringtemplate implements Python string.Template-like substitution. |
| 6 package stringtemplate |
| 7 |
| 8 import ( |
| 9 "regexp" |
| 10 "strings" |
| 11 |
| 12 "github.com/luci/luci-go/common/errors" |
| 13 ) |
| 14 |
| 15 const idPattern = "[_a-z][_a-z0-9]*" |
| 16 |
| 17 // We're looking for: |
| 18 // |
| 19 // Submatch indices: |
| 20 // [0:1] Full match |
| 21 // [2:3] $$ (escaped) |
| 22 // [4:5] $key (without braces) |
| 23 // [6:7] ${key} (with braces) |
| 24 // [8:9] $... (Invalid) |
| 25 var namedFormatMatcher = regexp.MustCompile( |
| 26 `\$(?:` + |
| 27 `(\$)|` + // Escaped ($$) |
| 28 `(` + idPattern + `)|` + // Without braces: $key |
| 29 `(?:\{(` + idPattern + `)\})|` + // With braces: ${key} |
| 30 `(.*)` + // Invalid |
| 31 `)`) |
| 32 |
| 33 // Resolve resolves substitutions in v using the supplied substitution map, |
| 34 // subst. |
| 35 // |
| 36 // A substitution can have the form: |
| 37 // |
| 38 // $key |
| 39 // ${key} |
| 40 // |
| 41 // The substitution can also be escaped using a second "$", such as "$$". |
| 42 // |
| 43 // If the string includes an erroneous substitution, or if a referenced |
| 44 // template variable isn't included in the "substitutions" map, Resolve will |
| 45 // return an error. |
| 46 func Resolve(v string, subst map[string]string) (string, error) { |
| 47 smi := namedFormatMatcher.FindAllStringSubmatchIndex(v, -1) |
| 48 if len(smi) == 0 { |
| 49 // No substitutions. |
| 50 return v, nil |
| 51 } |
| 52 |
| 53 var ( |
| 54 parts = make([]string, 0, (len(smi)*2)+1) |
| 55 pos = 0 |
| 56 ) |
| 57 |
| 58 for _, match := range smi { |
| 59 key := "" |
| 60 switch { |
| 61 case match[8] >= 0: |
| 62 // Invalid. |
| 63 return "", errors.Reason("invalid template: %(template)q
"). |
| 64 D("template", v). |
| 65 Err() |
| 66 |
| 67 case match[2] >= 0: |
| 68 // Escaped. |
| 69 parts = append(parts, v[pos:match[2]]) |
| 70 pos = match[3] |
| 71 continue |
| 72 |
| 73 case match[4] >= 0: |
| 74 // Key (without braces) |
| 75 key = v[match[4]:match[5]] |
| 76 case match[6] >= 0: |
| 77 // Key (with braces) |
| 78 key = v[match[6]:match[7]] |
| 79 |
| 80 default: |
| 81 panic("impossible") |
| 82 } |
| 83 |
| 84 // Add anything in between the previous match and the current. I
f our match |
| 85 // includes a non-escape character, add that too. |
| 86 if pos < match[0] { |
| 87 parts = append(parts, v[pos:match[0]]) |
| 88 } |
| 89 pos = match[1] |
| 90 |
| 91 subst, ok := subst[key] |
| 92 if !ok { |
| 93 return "", errors.Reason("no substitution for %(key)q"). |
| 94 D("key", key). |
| 95 Err() |
| 96 } |
| 97 parts = append(parts, subst) |
| 98 } |
| 99 |
| 100 // Append any trailing string. |
| 101 parts = append(parts, v[pos:]) |
| 102 |
| 103 // Join the parts. |
| 104 return strings.Join(parts, ""), nil |
| 105 } |
| OLD | NEW |