zsh/pattern_data_escape.rs
1//! Rust-only utility (NOT a port — lives outside `src/ported/` by design).
2//!
3//! The DATA half of docs/BUGS.md #1090: how a backslash that is a
4//! CHARACTER OF A VALUE has to be spelled before it reaches
5//! `ported::pattern::patcompile`.
6//!
7//! C never needs this transform. Its pattern compiler consumes the
8//! LEXER's encoding, where a source-level quote already arrived as
9//! `Bnull`/`Bnullkeep` + payload (c:Src/zsh.h:195-200), so a RAW
10//! backslash in `patcompile`'s input can only be data. A substituted
11//! value acquires its pattern meaning in `zshtokenize`
12//! (c:Src/glob.c:3585-3653), reached from `strcatsub`'s
13//! `if (glbsub) shtokenize(dest)` (c:Src/subst.c:822/830) for
14//! `${~spec}` / `GLOB_SUBST`, and that function rewrites a backslash
15//! into a quote marker ONLY when the next character reaches its
16//! `ztokens` scan:
17//!
18//! ```text
19//! c:Src/glob.c:3597-3605 case Bnull: case Bnullkeep: case '\\':
20//! if (bslash) { s[-1] = … Bnullkeep/Bnull; break; }
21//! bslash = 1; continue;
22//! c:Src/glob.c:3640-3648 for (t = ztokens; *t; t++)
23//! if (*t == *s) {
24//! if (bslash) s[-1] = … Bnullkeep/Bnull;
25//! else *s = (t - ztokens) + Pound;
26//! break;
27//! }
28//! c:Src/glob.c:3651 bslash = 0;
29//! ```
30//!
31//! Before anything else — a space, a `$`, a `{` — no `switch` arm fires,
32//! c:3651 just clears `bslash`, and BOTH bytes survive in the string as
33//! ordinary literal data. That is why real zsh answers
34//!
35//! ```text
36//! p='a\ b'; [[ 'a b' == ${~p} ]] # no match — the pattern holds a backslash
37//! p='a\ b'; [[ 'a\ b' == ${~p} ]] # match
38//! ```
39//!
40//! `ported::pattern`'s input normalizer (src/ported/pattern.rs, the `\\`
41//! arm) reads a lone raw `\X` as a QUOTE of X — the spelling every
42//! SOURCE-level pattern path in zshrs hands it (the cond/case pattern
43//! builder in `extensions::compile_zsh`, `${v//\%/%%}`'s builder in
44//! `ported::subst`) — and spells a literal backslash as the pair `\\`.
45//! So doubling exactly the backslashes `zshtokenize` declines to consume
46//! is what carries C's `Bnull`-vs-raw split into the Rust encoding.
47//! Backslashes the tokenizer WOULD consume are left in place so the
48//! downstream tokenizer/normalizer still folds them into a quote at
49//! their original position.
50//!
51//! Callers are the "this pattern text came out of a VALUE" sites:
52//! * `ported::subst::paramsubst` — the search-subscript patterns
53//! (`${a[(I)…]}` / `(i)` / `(r)` / `(R)` / `(K)`), which reach
54//! `patcompile` through `tokenize` alone (c:Src/params.c:1727).
55//! * `fusevm_bridge`'s `BUILTIN_GLOB_SUBST_GUARD` /
56//! `BUILTIN_PAT_DATA_BACKSLASH` — the `${~spec}` and `setopt
57//! globsubst` legs of a `[[ … == pat ]]` RHS and a `case` arm, the
58//! `strcatsub` `shtokenize` C runs at c:Src/subst.c:822/830.
59
60/// The `switch` labels of `zshtokenize` that can consume a preceding
61/// backslash — c:Src/glob.c:3599 (`\\`), c:3606 (`<`), c:3623-3625
62/// (`(`/`|`/`)`) and c:3629-3639 (`>`/`^`/`#`/`~`/`[`/`]`/`*`/`?`/`=`/
63/// `-`/`!`).
64///
65/// A character that is in the `ztokens` TABLE (c:Src/lex.c:38) but has
66/// NO `switch` label — `$`, `{`, `}`, `` ` ``, `,`, `'`, `"` — never
67/// reaches the c:3640 scan, so its backslash stays data. zsh answers 2,
68/// not 1, for
69/// ```text
70/// a=('a$b' 'a\$b'); q='a\$b'; print ${a[(I)$q]}
71/// ```
72fn quotes_a_metachar(c: char) -> bool {
73 matches!(
74 c,
75 '<' | '('
76 | '|'
77 | ')'
78 | '>'
79 | '^'
80 | '#'
81 | '~'
82 | '['
83 | ']'
84 | '*'
85 | '?'
86 | '='
87 | '-'
88 | '!'
89 | '\\'
90 )
91}
92
93/// Rewrite a pattern string that came out of a VALUE so
94/// `ported::pattern`'s normalizer reads its backslashes the way
95/// `zshtokenize` does — see the module docs.
96///
97/// Every backslash `zshtokenize` would NOT consume (c:Src/glob.c:3651
98/// `bslash = 0` with nothing rewritten) is doubled, which is the
99/// normalizer's literal-backslash form. A trailing lone backslash is
100/// data too (c:3590 `for (; *s; s++)` ends before any arm can fire) and
101/// is doubled as well.
102pub fn escape_data_backslashes(v: &str) -> String {
103 if !v.contains('\\') {
104 return v.to_string();
105 }
106 let cs: Vec<char> = v.chars().collect();
107 let mut out = String::with_capacity(v.len() + 4);
108 let mut i = 0;
109 while i < cs.len() {
110 let c = cs[i];
111 if c != '\\' {
112 out.push(c);
113 i += 1;
114 continue;
115 }
116 if cs.get(i + 1).copied().is_some_and(quotes_a_metachar) {
117 // c:3600-3602 / c:3642-3643 — the escape is honored; leave the
118 // pair for the tokenizer to fold into `Bnull`/`Bnullkeep`.
119 out.push(c);
120 out.push(cs[i + 1]);
121 i += 2;
122 } else {
123 // c:3651 `bslash = 0` with nothing rewritten — a data backslash.
124 out.push('\\');
125 out.push('\\');
126 i += 1;
127 }
128 }
129 out
130}
131
132/// The same DATA-vs-QUOTE split as [`escape_data_backslashes`], applied one
133/// step LATER: to a string that has ALREADY been through `tokenize()` +
134/// `remnulargs()` and is about to be handed straight to `patcompile`.
135///
136/// `zshtokenize` (c:Src/glob.c:3585, flags 0) folds every backslash it
137/// honors into a `Bnull` written at the BACKSLASH's own position, leaving
138/// the escaped character raw (c:3600-3602 `s[-1] = … Bnull` and c:3642-3643
139/// for the `ztokens` scan). `remnulargs` (c:Src/glob.c:3658) then deletes
140/// those `Bnull`s. So in the string C finally hands to `patcompile` every
141/// surviving raw `\` is a literal backslash CHARACTER: `patcomppiece` has
142/// no `case '\\'` at all (c:Src/pattern.c:1579-1601 — only `case Bnullkeep`
143/// at c:1589 is special), so that byte compiles as ordinary text. This is
144/// why real zsh answers
145///
146/// ```text
147/// # the pattern is `\` + `(`, so it needs a literal backslash in the word
148/// compset -P '\\\(' # PREFIX='(' -> no match
149/// compset -P '\(' # PREFIX='(' -> match
150/// ```
151///
152/// `ported::pattern`'s normalizer instead reads a raw `\X` as a QUOTE of X
153/// (src/ported/pattern.rs, the `\\` arm) and spells a literal backslash as
154/// the pair `\\`. Doubling every remaining backslash is therefore the exact
155/// transposition of C's post-`remnulargs` encoding into the Rust one.
156///
157/// Distinct from [`escape_data_backslashes`], which runs BEFORE `tokenize`
158/// and must leave a `\<metachar>` pair alone so the tokenizer can still fold
159/// it into a quote. Here the tokenizer has already run, so no pair is
160/// pending and every backslash left is data.
161pub fn escape_tokenized_data_backslashes(v: &str) -> String {
162 if !v.contains('\\') {
163 return v.to_string();
164 }
165 v.replace('\\', "\\\\")
166}
167
168/// The SH_GLOB half of the same `strcatsub` step: `shtokenize` builds its
169/// flags from the option (c:Src/glob.c:3575-3580)
170///
171/// ```text
172/// int flags = ZSHTOK_SUBST;
173/// if (isset(SHGLOB))
174/// flags |= ZSHTOK_SHGLOB;
175/// ```
176///
177/// and `zshtokenize` then DECLINES to tokenize `(`, `|` and `)`
178/// (c:Src/glob.c:3617-3620)
179///
180/// ```text
181/// case '(':
182/// case '|':
183/// case ')':
184/// if (flags & ZSHTOK_SHGLOB)
185/// break;
186/// ```
187///
188/// so under SH_GLOB those three characters stay ordinary data. zsh applies
189/// that unconditionally — even a KSH_GLOB group loses its meaning:
190/// `zsh -fc 'setopt shglob kshglob; v=" x "; print "[${v##+([[:space:]])}]"'`
191/// prints `[ x ]`, unchanged.
192///
193/// zshrs cannot express the suppression by skipping its own tokenize pass,
194/// because the consumers tokenize the ASSEMBLED word once, after the value
195/// has been concatenated with any source-level pattern text around it;
196/// skipping there would de-meta the source half too. Spelling the three
197/// characters in the normalizer's literal form (`\X`) instead carries the
198/// suppression on exactly the bytes it belongs to.
199///
200/// `keep_ksh_groups` is the bash/ksh DROP-IN exemption — see
201/// [`dropin_keeps_ksh_groups`]. With it set, a `(` that opens a ksh-style
202/// extended group (`@(`, `*(`, `+(`, `?(`, `!(`) keeps its meaning, and so
203/// do that group's `|` separators and its closing `)`; every other paren
204/// and every `|` outside such a group is still literal, which is precisely
205/// how bash and ksh read them. Groups nest, so the decision is stacked.
206///
207/// Characters inside a `[…]` class are left alone: they are class members
208/// under every one of these rules, and `zshtokenize` — a flat scan — would
209/// not have touched them either.
210///
211/// A backslash pair is stepped over whole, as in [`escape_data_backslashes`],
212/// so an escape that is already present is never re-escaped into a literal
213/// backslash plus a live metacharacter.
214pub fn escape_shglob_parens(v: &str, keep_ksh_groups: bool) -> String {
215 if !v.contains(['(', '|', ')']) {
216 return v.to_string();
217 }
218 let cs: Vec<char> = v.chars().collect();
219 let mut out = String::with_capacity(v.len() + 4);
220 // One entry per open `(`: true when that group survives as a real
221 // ksh-glob group, false when its parens were made literal.
222 let mut groups: Vec<bool> = Vec::new();
223 let mut in_class = false;
224 let mut i = 0;
225 while i < cs.len() {
226 let c = cs[i];
227 if c == '\\' && i + 1 < cs.len() {
228 out.push(c);
229 out.push(cs[i + 1]);
230 i += 2;
231 continue;
232 }
233 if in_class {
234 if c == ']' {
235 in_class = false;
236 }
237 out.push(c);
238 i += 1;
239 continue;
240 }
241 match c {
242 '[' => {
243 in_class = true;
244 out.push(c);
245 }
246 '(' => {
247 let ksh = keep_ksh_groups
248 && i > 0
249 && matches!(cs[i - 1], '@' | '*' | '+' | '?' | '!')
250 && !(i >= 2 && cs[i - 2] == '\\');
251 groups.push(ksh);
252 if !ksh {
253 out.push('\\');
254 }
255 out.push(c);
256 }
257 ')' => {
258 // An unmatched `)` closes nothing, so it is ordinary text.
259 if !groups.pop().unwrap_or(false) {
260 out.push('\\');
261 }
262 out.push(c);
263 }
264 '|' => {
265 if !groups.last().copied().unwrap_or(false) {
266 out.push('\\');
267 }
268 out.push(c);
269 }
270 _ => out.push(c),
271 }
272 i += 1;
273 }
274 out
275}
276
277/// Whether SH_GLOB's `(` / `|` / `)` suppression applies at all right now.
278///
279/// The zsh rule is the option, so this is simply `isset(SHGLOB)` — EXCEPT in
280/// a bare Korn drop-in.
281///
282/// !!! DROP-IN GATE — no zsh C counterpart !!!
283/// `zshrs --ksh` reaches EMULATE_KSH, which raises SH_GLOB, but its
284/// reference is ksh, and ksh reads a BARE `(` as a grouping character in its
285/// own right — `ksh -c 'v="-a"; print -r -- "[${v##-(a|b*)}]"'` prints `[]`,
286/// i.e. the group matched and the whole value was stripped, where zsh under
287/// SH_GLOB leaves `-(a|b*)` as six literal characters. So the Korn drop-in
288/// suppresses nothing.
289///
290/// bash is the middle case and is handled by [`dropin_keeps_ksh_groups`]:
291/// `(` is special there ONLY after `@ * + ? !`.
292///
293/// False for none of `--zsh`, native zshrs, or a zsh user's own
294/// `emulate sh` / `emulate ksh` beyond what the option itself says — those
295/// keep zsh's answer.
296pub fn shglob_hides_parens() -> bool {
297 crate::ported::zsh_h::isset(crate::ported::zsh_h::SHGLOB)
298}
299
300/// Whether a bash DROP-IN is running with extended patterns on.
301///
302/// !!! DROP-IN GATE — no zsh C counterpart !!!
303/// `zshrs --bash` reaches EMULATE_SH, which raises SH_GLOB, but its
304/// reference is bash. bash gives `@(…)` / `+(…)` their extended meaning in
305/// exactly the positions zsh suppresses
306/// (`bash -c 'shopt -s extglob; v=" x "; echo "[${v##+([[:space:]])}]"'`
307/// prints `[x ]`, where zsh prints `[ x ]`), while still reading a BARE
308/// `(` as ordinary text — bash makes `(` special only after `@ * + ? !`.
309/// So the bash drop-in keeps the ksh groups and literalizes everything else.
310///
311/// False in `--zsh`, in native zshrs, and under a zsh user's own
312/// `emulate sh`, where zsh's answer is the correct one. bash's `extglob`
313/// shopt is zshrs's `kshglob` (src/extensions/dash_mode.rs SHOPT table), so
314/// the option carries the enable.
315pub fn dropin_keeps_ksh_groups() -> bool {
316 (crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode())
317 && crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHGLOB)
318}
319
320/// Whether a SOURCE-level `[[ … ]]` / `case` pattern must follow the
321/// EMULATED shell's paren rules instead of zsh's.
322///
323/// !!! DROP-IN GATE — no zsh C counterpart !!!
324/// zsh's answer for such a pattern comes from WHEN it was tokenized: the
325/// parser turned `(` into a grouping token before SH_GLOB was set, so the
326/// option later strands the `)` and the pattern is bad (c:Src/pattern.c:
327/// 500-510 + :913-917). That history is a zsh artifact. A bare POSIX-family
328/// drop-in has no such history — bash, ksh, dash and sh decide what `(`
329/// means when they match, so a pattern zsh rejects is simply ordinary text
330/// there:
331/// ```text
332/// bash -c "[[ '-a' = -(a|b*) ]] && echo M || echo N" # N
333/// ksh -c "[[ '-a' = -(a|b*) ]] && echo M || echo N" # N
334/// zsh -fc "setopt shglob; [[ '-a' = -(a|b*) ]]" # bad pattern
335/// ```
336/// [`dropin_keeps_ksh_groups`] then decides whether `@(…)` / `+(…)` survive
337/// inside that text.
338///
339/// `posix_faithful` is what separates the bare drop-in from the zsh-STYLE
340/// leg (`--sh --zsh`, or a zsh user typing `emulate sh`), which must keep
341/// zsh's answer.
342pub fn dropin_source_pattern_parens_literal() -> bool {
343 crate::dash_mode::posix_faithful() && crate::ported::zsh_h::isset(crate::ported::zsh_h::SHGLOB)
344}
345
346/// !!! WARNING: RUST-ONLY HELPER — NO C COUNTERPART !!!
347///
348/// Fills `ported::subst::DEFAULT_WORD_GLOB_BARS` for the default / alternate
349/// word of `${x:-word}` / `${x-word}` / `${x:+word}` / `${x+word}`. In C the
350/// lexer makes a `|` inside `${…}` the `Bar` token (c:Src/lex.c:1000-1008),
351/// multsub keeps the token in the substituted value (c:Src/subst.c:3207-3228),
352/// and globlist then reads a top-level alternation in the assembled word:
353/// `${x:-b|a}` matches the file `b`, `X${x:-b|a}` is the pattern `Xb` | `a`.
354/// zshrs untokenizes the word before BUILTIN_DEFAULT_WORD_GLOB, which puts
355/// the recorded `Bar`s back.
356///
357/// `word` is the SOURCE text of the default / alternate word as paramsubst
358/// holds it: an unquoted `|` is a raw `|`, while a quoted (`"b|a"`,
359/// `'b|a'`) or escaped (`b\|a`) one arrives as `Bnull |` and stays literal.
360/// A raw `|` inside `(…)` or `[…]` is left to the glob engine's own
361/// grouping. `value` is what the word substituted to. Only a word with no
362/// substitution syntax is recorded: its source text IS the value, so the
363/// `|` positions are known without running multsub again (a `$(…)` in the
364/// word must not run twice).
365///
366/// Returns whether the word holds a top-level source `|`, so the caller can
367/// arm the default-word glob for it.
368pub fn note_default_word_bars(word: &str, value: &str) -> bool {
369 use crate::ported::zsh_h::{
370 Bar, Bnull, Bnullkeep, Dnull, Inbrack, Inpar, Nularg, Outbrack, Outpar, Qstring, Qtick,
371 Snull, Tick,
372 };
373 let cs: Vec<char> = word.chars().collect();
374 let mut barred = String::with_capacity(word.len());
375 let mut paren = 0i32;
376 let mut bracket = false;
377 let mut found = false;
378 let mut substitutes = false;
379 let mut i = 0;
380 while i < cs.len() {
381 let c = cs[i];
382 if (c == Bnull || c == Bnullkeep) && i + 1 < cs.len() {
383 barred.push(cs[i + 1]); // quoted / escaped: literal
384 i += 2;
385 continue;
386 }
387 if c == Snull || c == Dnull || c == Nularg {
388 i += 1;
389 continue;
390 }
391 if matches!(c, '$' | '`' | '\u{e185}') || c == Qstring || c == Tick || c == Qtick {
392 substitutes = true;
393 }
394 match c {
395 '(' | Inpar => paren += 1,
396 ')' | Outpar => paren = (paren - 1).max(0),
397 '[' | Inbrack => bracket = true,
398 ']' | Outbrack => bracket = false,
399 _ => {}
400 }
401 if (c == '|' || c == Bar) && paren == 0 && !bracket {
402 found = true;
403 barred.push(Bar);
404 i += 1;
405 continue;
406 }
407 if ('\u{e184}'..='\u{e19c}').contains(&c) {
408 barred.push_str(&crate::ported::lex::untokenize(&c.to_string()));
409 } else {
410 barred.push(c);
411 }
412 i += 1;
413 }
414 if found && !substitutes && crate::ported::lex::untokenize(&barred) == value {
415 crate::ported::subst::DEFAULT_WORD_GLOB_BARS
416 .with(|b| b.borrow_mut().push((value.to_string(), barred)));
417 }
418 found
419}
420
421/// Engine selected by the options of a ksh93 `~(options)` / `~(options:pat)`
422/// pattern prefix.
423#[derive(Clone, Copy, PartialEq)]
424enum Ksh93PatEngine {
425 /// ksh glob (`K`, the default; `X`/`N` read the same here).
426 Glob,
427 /// `F`: fixed string, found anywhere in the subject.
428 Fixed,
429 /// `E` / `P`: extended regular expression, searched anywhere in the subject.
430 Ere,
431 /// `G`: basic regular expression, searched anywhere in the subject.
432 Bre,
433}
434
435/// Options parsed from `~(…)`: `(case-insensitive, engine)`, or `None` for a
436/// letter this port does not model. `-` turns the letters after it off, `+`
437/// on again.
438fn ksh93_pat_options(flags: &[char]) -> Option<(bool, Ksh93PatEngine)> {
439 let (mut icase, mut engine, mut off) = (false, Ksh93PatEngine::Glob, false);
440 for &c in flags {
441 match c {
442 '-' => off = true,
443 '+' => off = false,
444 'i' => icase = !off,
445 'F' if !off => engine = Ksh93PatEngine::Fixed,
446 'E' | 'P' if !off => engine = Ksh93PatEngine::Ere,
447 'G' if !off => engine = Ksh93PatEngine::Bre,
448 'K' | 'X' | 'N' | 'F' | 'E' | 'P' | 'G' => {}
449 _ => return None,
450 }
451 }
452 Some((icase, engine))
453}
454
455/// Index just past the `)` closing the group whose `(` is at `open`.
456fn ksh93_pat_group_end(cs: &[char], open: usize) -> Option<usize> {
457 let mut depth = 0;
458 let mut i = open;
459 while i < cs.len() {
460 match cs[i] {
461 '\\' => i += 1,
462 '(' => depth += 1,
463 ')' => {
464 depth -= 1;
465 if depth == 0 {
466 return Some(i + 1);
467 }
468 }
469 _ => {}
470 }
471 i += 1;
472 }
473 None
474}
475
476/// `ab`-style swap: the other-case twin of an ASCII letter.
477fn ksh93_pat_swapcase(c: char) -> char {
478 if c.is_ascii_lowercase() { c.to_ascii_uppercase() } else { c.to_ascii_lowercase() }
479}
480
481/// Rewrite the glob `cs` (normalized) / `orig` (as given) so that its letters
482/// match either case: `a` -> `[aA]`, `[a-c]` -> `[a-cA-C]`.
483fn ksh93_pat_fold_case(orig: &[char], cs: &[char], out: &mut String) {
484 let mut i = 0;
485 while i < cs.len() {
486 match cs[i] {
487 '\\' => {
488 out.push(orig[i]);
489 if let Some(&n) = orig.get(i + 1) {
490 out.push(n);
491 }
492 i += 2;
493 }
494 '[' => {
495 // A class runs to the first `]` after at least one member.
496 let start = i + 1 + usize::from(matches!(cs.get(i + 1), Some('!' | '^')));
497 match cs[(start + 1).min(cs.len())..].iter().position(|&c| c == ']') {
498 Some(rel) => {
499 let close = start + 1 + rel;
500 out.extend(&orig[i..close]);
501 out.extend(cs[start..close].iter().filter(|c| c.is_ascii_alphabetic()).map(|&c| ksh93_pat_swapcase(c)));
502 out.push(orig[close]);
503 i = close + 1;
504 }
505 None => {
506 out.push(orig[i]);
507 i += 1;
508 }
509 }
510 }
511 c if c.is_ascii_alphabetic() => {
512 out.extend(['[', c, ksh93_pat_swapcase(c), ']']);
513 i += 1;
514 }
515 _ => {
516 out.push(orig[i]);
517 i += 1;
518 }
519 }
520 }
521}
522
523/// Translate a POSIX basic regular expression to the ERE the `regex` crate
524/// reads: `\( \) \{ \} \| \+ \?` become operators, their bare forms literals.
525fn ksh93_bre_to_ere(bre: &[char]) -> String {
526 let mut out = String::new();
527 let mut i = 0;
528 while i < bre.len() {
529 match (bre[i], bre.get(i + 1)) {
530 ('\\', Some(&n)) if "(){}|+?".contains(n) => {
531 out.push(n);
532 i += 2;
533 }
534 ('\\', Some(&n)) => {
535 out.extend(['\\', n]);
536 i += 2;
537 }
538 (c, _) if "(){}|+?".contains(c) => {
539 out.extend(['\\', c]);
540 i += 1;
541 }
542 (c, _) => {
543 out.push(c);
544 i += 1;
545 }
546 }
547 }
548 out
549}
550
551/// ksh93 pattern options: `[[ ab == ~(i)AB ]]`, `~(i:AB)c`, `a~(i)B*`,
552/// `~(E)b+`, `~(F)a.c`, `~(G)a\(b\)c`. `Some(verdict)` when `pat` carries a
553/// `~(options)` prefix this port models, `None` to let the ordinary matcher
554/// run. Verified against ksh93u+m 1.0.10:
555///
556/// * `i` makes the pattern text after it case-insensitive until `~(-i)`; the
557/// scoped `~(i:pat)` form covers only `pat`.
558/// * `F`, `E`/`P` and `G` replace the glob with a fixed string / ERE / BRE
559/// that is SEARCHED for anywhere in the subject (`[[ abc == ~(E)b ]]` is
560/// true), unlike a glob, which must cover it.
561/// * `K`, `X`, `N` read as the default ksh glob.
562///
563/// !!! RUST-ONLY EXTENSION — no zsh C counterpart; --ksh only (mksh has no
564/// `~(…)` pattern options). In zsh `~(…)` is extended-glob negation. !!!
565pub fn ksh93_flagged_match(s: &str, pat: &str) -> Option<bool> {
566 use crate::ported::zsh_h::{Inpar, Outpar, Tilde};
567 if !crate::dash_mode::ksh93_mode() {
568 return None;
569 }
570 let orig: Vec<char> = pat.chars().collect();
571 let cs: Vec<char> = orig
572 .iter()
573 .map(|&c| match c {
574 Tilde => '~',
575 Inpar => '(',
576 Outpar => ')',
577 c => c,
578 })
579 .collect();
580 let first = (0..cs.len().saturating_sub(1)).find(|&i| {
581 cs[i] == '~' && cs[i + 1] == '(' && (i == 0 || cs[i - 1] != '\\')
582 })?;
583 let (icase, engine) = {
584 let end = cs[first + 2..].iter().position(|&c| !(c.is_ascii_alphabetic() || c == '-' || c == '+'))? + first + 2;
585 ksh93_pat_options(&cs[first + 2..end])?
586 };
587 if engine != Ksh93PatEngine::Glob {
588 // The regex / fixed-string engines take the whole pattern: no text
589 // around the `~(options)` group is modelled.
590 let end = cs[first + 2..].iter().position(|&c| !(c.is_ascii_alphabetic() || c == '-' || c == '+'))? + first + 2;
591 let body: Vec<char> = match cs[end] {
592 ')' if first == 0 => cs[end + 1..].to_vec(),
593 ':' if first == 0 && ksh93_pat_group_end(&cs, 1) == Some(cs.len()) => cs[end + 1..cs.len() - 1].to_vec(),
594 _ => return None,
595 };
596 let text: String = body.iter().collect();
597 return match engine {
598 Ksh93PatEngine::Fixed if icase => Some(s.to_lowercase().contains(&text.to_lowercase())),
599 Ksh93PatEngine::Fixed => Some(s.contains(&text)),
600 _ => {
601 let re = if engine == Ksh93PatEngine::Bre { ksh93_bre_to_ere(&body) } else { text };
602 regex::RegexBuilder::new(&re).case_insensitive(icase).build().ok().map(|r| r.is_match(s))
603 }
604 };
605 }
606 // Glob engine: walk the pattern once, tracking the case-fold state.
607 let mut out = String::new();
608 let mut fold = false;
609 let mut i = 0;
610 let mut lit_from = 0;
611 while i < cs.len() {
612 if cs[i] == '\\' {
613 i += 2;
614 continue;
615 }
616 if cs[i] == '~' && cs.get(i + 1) == Some(&'(') {
617 let Some(rel) = cs[i + 2..].iter().position(|&c| !(c.is_ascii_alphabetic() || c == '-' || c == '+')) else { break };
618 let end = i + 2 + rel;
619 let Some((ic, eng)) = ksh93_pat_options(&cs[i + 2..end]) else { return None };
620 if eng != Ksh93PatEngine::Glob || !matches!(cs[end], ')' | ':') {
621 return None;
622 }
623 flush_range(&mut out, &orig, &cs, lit_from, i, fold);
624 // `~(-i)` clears the fold; `~(i)` sets it; other letters leave it.
625 let off = cs[i + 2..end].contains(&'-');
626 let new_fold = if cs[i + 2..end].contains(&'i') { !off && ic } else { fold };
627 if cs[end] == ')' {
628 fold = new_fold;
629 i = end + 1;
630 } else {
631 let close = ksh93_pat_group_end(&cs, i + 1)?;
632 flush_range(&mut out, &orig, &cs, end + 1, close - 1, new_fold);
633 i = close;
634 }
635 lit_from = i;
636 continue;
637 }
638 i += 1;
639 }
640 flush_range(&mut out, &orig, &cs, lit_from, cs.len(), fold);
641 Some(crate::vm_helper::glob_match_static(s, &out))
642}
643
644/// [`ksh93_pat_fold_case`] over `orig[from..to]` when `fold`, else a copy.
645fn flush_range(out: &mut String, orig: &[char], cs: &[char], from: usize, to: usize, fold: bool) {
646 if fold {
647 ksh93_pat_fold_case(&orig[from..to], &cs[from..to], out);
648 } else {
649 out.extend(&orig[from..to]);
650 }
651}