1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
//! Rust-only utility (NOT a port — lives outside `src/ported/` by design).
//!
//! C resolves the quoting inside a `[...]` subscript by RE-LEXING the
//! subscript text: `getindex` (Src/params.c:2022) calls
//! `parse_subscript(s, scanflags & SCANPM_DQUOTED, ']')` at c:2029, which
//! untokenizes the text and pushes it back through the lexer
//! (`dquote_parse`, Src/lex.c:1751-1769). zshrs has no equivalent step on
//! that path — its `lex::parse_subscript` port throws away the tokenized
//! text C copies back at c:Src/lex.c:1772, and re-entering the real lexer
//! from inside paramsubst (and from the compiler, which resolves literal
//! assignment keys at compile time) would mean re-entrant lexer state on
//! the hottest expansion path.
//!
//! So the three C stages that decide what a backslash inside a subscript
//! MEANS — `dquote_parse`'s backslash arm, `getarg`'s marker disposition,
//! and the `remnulargs` / `parsestr` + `singsub` round that follows — are
//! expressed here as the one string transform they add up to. Each rule
//! cites the C line it comes from; see [`subscript_unescape`].
//!
//! Both callers are the "what key is this?" sites:
//! * `ported::subst::paramsubst` — `${A[\[k\]]}` (read)
//! * `extensions::compile_zsh::compile_assign` — `A[\[k\]]=v` (store)
use crate;
/// Backslash disposition inside a `[...]` subscript — the net effect of
/// the re-lex C runs on a subscript's SOURCE text.
///
/// `getindex` NEVER reads the subscript the way the outer lexer left it.
/// It calls `parse_subscript(s, scanflags & SCANPM_DQUOTED, ']')`
/// (c:Src/params.c:2029), and `parse_subscript` untokenizes the text and
/// re-lexes it through `dquote_parse(']', sub)`
/// (c:Src/lex.c:1751-1769 — `untokenize(t = dupstring_wlen(s, l));
/// inpush(t, 0, NULL); … err = dquote_parse(endchar, sub);`). That
/// re-lex is where a backslash inside a subscript acquires its meaning:
///
/// ```text
/// c:Src/lex.c:1497-1512
/// if (c != '\n') {
/// if (c == '$' || c == '\\' || (c == '}' && !intick && bct) ||
/// c == endchar || c == '`' ||
/// (endchar == ']' && (c == '[' || c == ']' ||
/// c == '(' || c == ')' ||
/// c == '{' || c == '}' ||
/// (c == '"' && sub))))
/// add(Bnull);
/// else {
/// /* lexstop is implicitly handled here */
/// add('\\');
/// goto cont;
/// }
/// } else if (sub || unset(CSHJUNKIEQUOTES) || endchar != '"')
/// continue;
/// ```
///
/// With `endchar == ']'` a backslash before one of ``$ \ ` ] [ ( ) { }``
/// (plus `"` when the subscript is inside double quotes, `sub`) becomes
/// the `Bnull` marker + the literal char; a backslash before ANY other
/// char stays a literal backslash. That asymmetry is exactly why
/// `A[\[k\]]` keys on `[k]` while `A[a\ b]` / `A[a\*b]` keep theirs.
/// Backslash-newline is dropped outright (c:1513).
///
/// `getarg` then disposes of the markers (c:Src/params.c:1538-1551):
///
/// ```text
/// if (inull(c)) {
/// c = t[1];
/// if (c == '[' || c == ']' || c == '(' || c == ')' ||
/// c == '{' || c == '}') {
/// if (ishash && i) *t = ztokens[*t - Pound];
/// needtok = 1; ++t;
/// } else if (c != '"')
/// *t = ztokens[*t - Pound];
/// continue;
/// }
/// ```
///
/// — a marker before a bracket/paren/brace (or before `"`) is KEPT and
/// later DELETED by `remnulargs` (c:1583-1584, hash key path), so the
/// escaped bracket reaches the hash table bare. Every other marker is
/// untokenized back to a literal `\` (`ztokens[Bnull - Pound]` is `\`,
/// c:Src/lex.c:38), which the `parsestr` + `singsub` round at
/// c:1585-1593 re-marks and drops one stage later — so ``\$``, `\\` and
/// ``\` `` also lose their backslash, just further down the pipeline.
///
/// zshrs has no equivalent re-lex step on this path (its
/// `lex::parse_subscript` discards the tokenized text C copies back at
/// c:Src/lex.c:1772), so a source-literal backslash reached the assoc
/// key verbatim: `A[\[k\]]=v` stored the 5-char key `\[k\]` where zsh
/// stores `[k]`. This function is that missing step, expressed as the
/// composite string transform the three C stages add up to.
///
/// * `sub` — C's `SCANPM_DQUOTED`: the subscript sits inside `"…"`.
/// * `resolve_dollar` — the caller has NO `parsestr`/`singsub` round
/// after this call (compile-time literal key), so apply that stage's
/// share of the work here as well.
///
/// Returns the rewritten text and whether an UNESCAPED `$` / `` ` ``
/// (i.e. a live expansion, which C resolves in `singsub` at c:1592)
/// is still present.
/// Same C stages as [`subscript_unescape`], stopped one step earlier and
/// re-encoded for a caller that still has to run C's `parsestr` + `singsub`
/// round (c:Src/params.c:1585-1592) through the word compiler.
///
/// [`subscript_unescape`] returns PLAIN text, which is right for a key the
/// caller stores verbatim but wrong for a key that still holds a live
/// expansion: its resolved `$` would be re-expanded by the word compiler and
/// its now-bare `[` would be read as a glob. C never has that problem because
/// its intermediate text is MARKED — `getarg` keeps the `Bnull` before a
/// bracket (c:1541-1548) and writes a literal `\` before the others
/// (c:1549-1550), and `parsestr` re-marks those (c:1588) before `singsub`
/// expands what is left. zshrs's word compiler consumes the same lexer
/// encoding, so emit the marker directly and let it do `singsub`'s job:
///
/// | source | C intermediate | emitted here |
/// |--------|---------------------------------------|--------------|
/// | `\[` `\]` `\(` `\)` `\{` `\}` (and `\"` when `sub`) | marker kept (c:1547), deleted by `remnulargs` (c:1583) | `Bnull` + char |
/// | `\$` `\\` `` \` `` | marker → `\` (c:1550), re-marked by `parsestr` (c:1588), dropped by `singsub`'s `prefork`/`remnulargs` (c:Src/subst.c:169) | `Bnull` + char |
/// | any other `\X` | ordinary text (c:Src/lex.c:1510 `add('\\')`) — survives BOTH re-lexes because the second one runs with `endchar == '\0'` and never marks `X` | `\` + char |
/// | `\` + newline | dropped (c:Src/lex.c:1513) | — |
/// | everything else | untouched | verbatim |
///
/// An unescaped `$` / `` ` `` is deliberately left live — that is exactly the
/// work c:1592 `singsub` still has to do.
///
/// * `sub` — C's `SCANPM_DQUOTED`: the subscript sits inside `"…"`.