|
7 | 7 | "strings" |
8 | 8 | ) |
9 | 9 |
|
10 | | -// reVarSubstitution matches $VAR, ${VAR}, '$VAR', '${VAR}' patterns for variable substitution. |
11 | | -var reVarSubstitution = regexp.MustCompile(`[']{0,1}\$\{([^}]+)\}[']{0,1}|[']{0,1}\$([a-zA-Z0-9_][a-zA-Z0-9_]*)[']{0,1}`) |
| 10 | +// reVarSubstitution matches ${...} and $VAR patterns for variable substitution. |
| 11 | +// Quote handling is done by callers based on surrounding characters. |
| 12 | +var reVarSubstitution = regexp.MustCompile(`\$\{([^}]+)\}|\$([a-zA-Z0-9_][a-zA-Z0-9_]*)`) |
12 | 13 |
|
13 | 14 | // reQuotedJSONRef matches quoted JSON references like "${FOO.bar}" and simple variables like "${VAR}" |
14 | 15 | var reQuotedJSONRef = regexp.MustCompile(`"\$\{([A-Za-z0-9_]\w*(?:\.[^}]+)?)\}"`) |
@@ -110,34 +111,60 @@ func (r *resolver) resolveJSONSource(name string) (string, bool) { |
110 | 111 | return r.lookupScopeNonOS(name) |
111 | 112 | } |
112 | 113 |
|
113 | | -// extractVarKey extracts the variable key from a regex match. |
114 | | -// Returns the key and false if the match is single-quoted. |
115 | | -func extractVarKey(match string) (string, bool) { |
116 | | - if match[0] == '\'' && match[len(match)-1] == '\'' { |
117 | | - return "", false |
118 | | - } |
119 | | - if strings.HasPrefix(match, "${") { |
120 | | - return match[2 : len(match)-1], true |
121 | | - } |
122 | | - return match[1:], true |
| 114 | +// isSingleQuotedVar reports whether the matched variable token is enclosed |
| 115 | +// in single quotes in the original input (e.g., '${VAR}' or '$VAR'). |
| 116 | +// Note: this is a simple adjacent-character heuristic. It may misidentify |
| 117 | +// nested quote contexts such as 'foo'${BAR}'baz' where the quote before |
| 118 | +// ${BAR} actually closes a prior segment. This is acceptable for the |
| 119 | +// targeted use cases (nu shell $'...' syntax, simple quoting). |
| 120 | +func isSingleQuotedVar(input string, start, end int) bool { |
| 121 | + return start > 0 && end < len(input) && input[start-1] == '\'' && input[end] == '\'' |
123 | 122 | } |
124 | 123 |
|
125 | 124 | // replaceVars substitutes $VAR and ${VAR} patterns using all resolver sources. |
126 | 125 | // JSON path references (containing dots) are skipped; those are handled by expandReferences. |
127 | 126 | func (r *resolver) replaceVars(template string) string { |
128 | | - return reVarSubstitution.ReplaceAllStringFunc(template, func(match string) string { |
129 | | - key, ok := extractVarKey(match) |
130 | | - if !ok { |
131 | | - return match |
| 127 | + matches := reVarSubstitution.FindAllStringSubmatchIndex(template, -1) |
| 128 | + if len(matches) == 0 { |
| 129 | + return template |
| 130 | + } |
| 131 | + |
| 132 | + var b strings.Builder |
| 133 | + last := 0 |
| 134 | + for _, loc := range matches { |
| 135 | + b.WriteString(template[last:loc[0]]) |
| 136 | + last = loc[1] |
| 137 | + |
| 138 | + match := template[loc[0]:loc[1]] |
| 139 | + if isSingleQuotedVar(template, loc[0], loc[1]) { |
| 140 | + b.WriteString(match) |
| 141 | + continue |
| 142 | + } |
| 143 | + |
| 144 | + var key string |
| 145 | + if loc[2] >= 0 { // Group 1: ${...} |
| 146 | + key = template[loc[2]:loc[3]] |
| 147 | + } else if loc[4] >= 0 { // Group 2: $VAR |
| 148 | + key = template[loc[4]:loc[5]] |
| 149 | + } else { |
| 150 | + // Neither group captured — preserve original text. |
| 151 | + b.WriteString(match) |
| 152 | + continue |
132 | 153 | } |
| 154 | + |
133 | 155 | if strings.Contains(key, ".") { |
134 | | - return match |
| 156 | + b.WriteString(match) |
| 157 | + continue |
135 | 158 | } |
136 | 159 | if val, found := r.resolve(key); found { |
137 | | - return val |
| 160 | + b.WriteString(val) |
| 161 | + continue |
138 | 162 | } |
139 | | - return match |
140 | | - }) |
| 163 | + b.WriteString(match) |
| 164 | + } |
| 165 | + |
| 166 | + b.WriteString(template[last:]) |
| 167 | + return b.String() |
141 | 168 | } |
142 | 169 |
|
143 | 170 | // expandReferences resolves JSON path and step property references in the input. |
|
0 commit comments