zsh/ported/zle/computil.rs
1//! Completion utility functions for ZLE
2//!
3//! Port from zsh/Src/Zle/computil.c (5,180 lines)
4//!
5//! Help for `_describe'. // c:34
6//! Help for `_arguments'. // c:897
7//!
8//! The full utility library is in compsys/computil.rs (674 lines).
9//! This module provides _describe, _values, _alternative, _combination,
10//! and the compdescribe/comparguments/compvalues builtins.
11//!
12//! Key C functions and their Rust locations:
13//! - bin_compdescribe → compsys::describe::describe()
14//! - bin_comparguments → compsys::arguments (full _arguments)
15//! - bin_compvalues → compsys::computil::compvalues()
16//! - bin_comptags → compsys::state::comptags()
17//! - bin_comptry → compsys::state::comptry()
18
19use std::collections::HashMap;
20use crate::ported::utils::{quotedzputs, zwarnnam};
21use crate::ported::zle::complete::INCOMPFUNC;
22use crate::ported::zle::complete::COMPQSTACK;
23use crate::ported::zsh_h::OPT_ISSET;
24
25// =====================================================================
26// CRT_* — `_describe` row-type discriminator from `computil.c:79-83`.
27// Drives the `cdescr` table-builder switch.
28// =====================================================================
29
30/// Port of `CRT_SIMPLE` from `Src/Zle/computil.c:79`. Plain match row.
31
32// --- AUTO: cross-zle hoisted-fn use glob ---
33#[allow(unused_imports)]
34#[allow(unused_imports)]
35use crate::ported::zle::zle_main::*;
36#[allow(unused_imports)]
37use crate::ported::zle::zle_misc::*;
38#[allow(unused_imports)]
39use crate::ported::zle::zle_hist::*;
40#[allow(unused_imports)]
41use crate::ported::zle::zle_move::*;
42#[allow(unused_imports)]
43use crate::ported::zle::zle_word::*;
44#[allow(unused_imports)]
45use crate::ported::zle::zle_params::*;
46#[allow(unused_imports)]
47use crate::ported::zle::zle_vi::*;
48#[allow(unused_imports)]
49use crate::ported::zle::zle_utils::*;
50#[allow(unused_imports)]
51use crate::ported::zle::zle_refresh::*;
52#[allow(unused_imports)]
53use crate::ported::zle::zle_tricky::*;
54#[allow(unused_imports)]
55use crate::ported::zle::textobjects::*;
56#[allow(unused_imports)]
57use crate::ported::zle::deltochar::*;
58
59pub const CRT_SIMPLE: i32 = 0; // c:79
60/// Port of `CRT_DESC` from `computil.c:80`. Match with description.
61pub const CRT_DESC: i32 = 1; // c:80
62/// Port of `CRT_SPEC` from `computil.c:81`. Special separator row.
63pub const CRT_SPEC: i32 = 2; // c:81
64/// Port of `CRT_DUMMY` from `computil.c:82`. Placeholder row.
65pub const CRT_DUMMY: i32 = 3; // c:82
66/// Port of `CRT_EXPL` from `computil.c:83`. Explanation header row.
67pub const CRT_EXPL: i32 = 4; // c:83
68
69/// Port of `CDF_SEP` from `Src/Zle/computil.c:924`. `-S` flag — `--`
70/// terminates options.
71pub const CDF_SEP: i32 = 1; // c:924
72
73// =====================================================================
74// CAO_* — Cadef option-argument attachment style — `computil.c:941-945`.
75// =====================================================================
76
77/// Port of `CAO_NEXT` from `computil.c:941`. Argument in next argv slot.
78pub const CAO_NEXT: i32 = 1; // c:941
79/// Port of `CAO_DIRECT` from `computil.c:942`. Argument directly attached
80/// to option (`-opt:value`).
81pub const CAO_DIRECT: i32 = 2; // c:942
82/// Port of `CAO_ODIRECT` from `computil.c:943`. Optional direct attach.
83pub const CAO_ODIRECT: i32 = 3; // c:943
84/// Port of `CAO_EQUAL` from `computil.c:944`. Argument after `=`.
85pub const CAO_EQUAL: i32 = 4; // c:944
86/// Port of `CAO_OEQUAL` from `computil.c:945`. Optional `=` argument.
87pub const CAO_OEQUAL: i32 = 5; // c:945
88
89// =====================================================================
90// CAA_* — Cadef positional-argument kinds — `computil.c:964-968`.
91// =====================================================================
92
93/// Port of `CAA_NORMAL` from `computil.c:964`. Plain positional arg.
94pub const CAA_NORMAL: i32 = 1; // c:964
95/// Port of `CAA_OPT` from `computil.c:965`. Optional positional arg.
96pub const CAA_OPT: i32 = 2; // c:965
97/// Port of `CAA_REST` from `computil.c:966`. Mandatory rest of args.
98pub const CAA_REST: i32 = 3; // c:966
99/// Port of `CAA_RARGS` from `computil.c:967`. Repeated args sequence.
100pub const CAA_RARGS: i32 = 4; // c:967
101/// Port of `CAA_RREST` from `computil.c:968`. Repeated rest of args.
102pub const CAA_RREST: i32 = 5; // c:968
103
104/// Port of `MAX_CACACHE` from `computil.c:972`. Cadef LRU cache size.
105pub const MAX_CACACHE: usize = 8; // c:972
106
107// =====================================================================
108// CVV_* — Cvval value-kind — `computil.c:2949-2951`.
109// =====================================================================
110
111/// Port of `CVV_NOARG` from `computil.c:2949`. Value without argument.
112pub const CVV_NOARG: i32 = 0; // c:2949
113/// Port of `CVV_ARG` from `computil.c:2950`. Value requires argument.
114pub const CVV_ARG: i32 = 1; // c:2950
115/// Port of `CVV_OPT` from `computil.c:2951`. Argument optional.
116pub const CVV_OPT: i32 = 2; // c:2951
117
118/// Port of `MAX_CVCACHE` from `computil.c:2955`. Cvdef LRU cache size.
119pub const MAX_CVCACHE: usize = 8; // c:2955
120
121/// Port of `MAX_TAGS` from `computil.c:3755`. Maximum nested completion
122/// tags depth.
123pub const MAX_TAGS: usize = 256; // c:3755
124
125/// Port of `PATH_MAX2` from `computil.c:4141`. `PATH_MAX * 2` — buffer
126/// budget for path-completion staging strings.
127pub const PATH_MAX2: usize = 8192; // c:4141 (PATH_MAX*2, 4096*2)
128
129// =====================================================================
130// `_describe`-completion types — direct ports of the C structs at
131// Src/Zle/computil.c:40-91 (the cdset/cdstr/cdrun/cdstate chain
132// the `_describe` completion path builds + processes).
133// =====================================================================
134
135// CRT_* constants already declared above (file scope).
136
137/// Port of `typedef struct cdset *Cdset` from `Src/Zle/computil.c:36`.
138pub type Cdset = Box<cdset>; // c:36
139/// Port of `typedef struct cdstr *Cdstr` from `computil.c:37`.
140pub type Cdstr = Box<cdstr>; // c:37
141/// Port of `typedef struct cdrun *Cdrun` from `computil.c:38`.
142pub type Cdrun = Box<cdrun>; // c:38
143
144/// Direct port of `struct cdstr` from `Src/Zle/computil.c:58-70`.
145/// One match string inside a `_describe` group, with optional
146/// description and the same-description chain.
147#[derive(Debug, Default, Clone)]
148#[allow(non_camel_case_types)]
149pub struct cdstr { // c:58
150 pub next: Option<Box<cdstr>>, // c:59 Cdstr next
151 pub str: Option<String>, // c:60 char *str
152 pub desc: Option<String>, // c:61 char *desc
153 pub r#match: Option<String>, // c:62 char *match
154 pub sortstr: Option<String>, // c:63 char *sortstr
155 pub len: i32, // c:64 int len
156 pub width: i32, // c:65 int width
157 pub other: Option<Box<cdstr>>, // c:66 Cdstr other
158 pub kind: i32, // c:67 int kind (0/1/2)
159 pub set: usize, // c:68 Cdset set (raw ptr index)
160 pub run: Option<Box<cdstr>>, // c:69 Cdstr run
161}
162
163/// Direct port of `struct cdrun` from `Src/Zle/computil.c:72-77`.
164/// One contiguous "run" of cdstr entries the shell code should
165/// emit as a block.
166#[derive(Debug, Default)]
167#[allow(non_camel_case_types)]
168pub struct cdrun { // c:72
169 pub next: Option<Box<cdrun>>, // c:73 Cdrun next
170 pub r#type: i32, // c:74 int type (CRT_*)
171 pub strs: Option<Box<cdstr>>, // c:75 Cdstr strs
172 pub count: i32, // c:76 int count
173}
174
175/// Direct port of `struct cdset` from `Src/Zle/computil.c:85-91`.
176/// One set of matches (one `compadd` invocation worth) with its
177/// compadd options + the cdstr chain.
178#[derive(Debug, Default)]
179#[allow(non_camel_case_types)]
180pub struct cdset { // c:85
181 pub next: Option<Box<cdset>>, // c:86 Cdset next
182 pub opts: Option<Vec<String>>, // c:87 char **opts
183 pub strs: Option<Box<cdstr>>, // c:88 Cdstr strs
184 pub count: i32, // c:89 int count
185 pub desc: i32, // c:90 int desc
186}
187
188/// Direct port of `struct cdstate` from `Src/Zle/computil.c:40-56`.
189/// File-static state for the `_describe` engine — holds the active
190/// sets/runs/dimensions during a single `_describe` invocation.
191#[derive(Debug, Default)]
192#[allow(non_camel_case_types)]
193pub struct cdstate { // c:40
194 pub showd: i32, // c:41
195 pub sep: Option<String>, // c:42 char *sep
196 pub slen: i32, // c:43
197 pub swidth: i32, // c:44
198 pub maxmlen: i32, // c:45
199 pub sets: Option<Box<cdset>>, // c:46 Cdset sets
200 pub pre: i32, // c:47
201 pub premaxw: i32, // c:48
202 pub suf: i32, // c:49
203 pub maxg: i32, // c:50
204 pub maxglen: i32, // c:51
205 pub groups: i32, // c:52
206 pub descs: i32, // c:53
207 pub gprew: i32, // c:54
208 pub runs: Option<Box<cdrun>>, // c:55 Cdrun runs
209}
210
211/// Port of `static struct cdstate cd_state` from `Src/Zle/computil.c:93`.
212/// File-static instance the `_describe` engine reads/writes.
213pub static cd_state: std::sync::Mutex<cdstate> = // c:93
214 std::sync::Mutex::new(cdstate {
215 showd: 0, sep: None, slen: 0, swidth: 0, maxmlen: 0,
216 sets: None, pre: 0, premaxw: 0, suf: 0, maxg: 0, maxglen: 0,
217 groups: 0, descs: 0, gprew: 0, runs: None,
218 });
219
220/// Port of `static int cd_parsed` from `Src/Zle/computil.c:188`. Flag
221/// signalling whether `cd_state` holds a parsed-but-unconsumed
222/// description set.
223pub static cd_parsed: std::sync::atomic::AtomicI32 = // c:94
224 std::sync::atomic::AtomicI32::new(0);
225
226/// Direct port of `static void cd_calc(void)` from
227/// `Src/Zle/computil.c:188-211`. Walks `cd_state.sets`, computing
228/// each set's `count`/`desc` and updating
229/// `cd_state.pre`/`premaxw`/`suf` (the global max widths) for the
230/// `_describe` column layout.
231pub fn cd_calc() { // c:188
232 let mut st = cd_state.lock().unwrap();
233 st.pre = 0; // c:194
234 st.suf = 0;
235 let mut max_pre = 0i32;
236 let mut max_premaxw = st.premaxw;
237 let mut max_suf = 0i32;
238
239 let mut set = st.sets.as_deref_mut();
240 while let Some(s) = set {
241 s.count = 0; // c:197
242 s.desc = 0;
243 let mut str_iter = s.strs.as_deref();
244 while let Some(st_node) = str_iter {
245 s.count += 1; // c:199
246 let str_s = st_node.str.as_deref().unwrap_or("");
247 let l = str_s.len() as i32;
248 if l > max_pre { max_pre = l; } // c:200
249 // c:202 — ZMB_nicewidth(str). Rust niceztrlen returns usize.
250 let nw = crate::ported::utils::niceztrlen(str_s) as i32;
251 if nw > max_premaxw { max_premaxw = nw; }
252 if let Some(d) = st_node.desc.as_deref() { // c:204
253 s.desc += 1;
254 let dl = d.len() as i32;
255 if dl > max_suf { max_suf = dl; } // c:206
256 }
257 str_iter = st_node.next.as_deref();
258 }
259 set = s.next.as_deref_mut();
260 }
261 st.pre = max_pre;
262 st.premaxw = max_premaxw;
263 st.suf = max_suf;
264}
265
266/// Direct port of `static void cd_group(int maxg)` from
267/// `Src/Zle/computil.c:127-182`. Walks `cd_state.sets` looking for
268/// matches sharing the same description; links them via the `other`
269/// chain on the first cdstr of each group. Sets `cd_state.groups`,
270/// `descs`, `maxg`, `maxglen` accordingly.
271pub fn cd_group(maxg: i32) { // c:127
272 let mut st = cd_state.lock().unwrap();
273 st.groups = 0; // c:133
274 st.descs = 0;
275 st.maxglen = 0;
276 st.maxg = 0;
277
278 // c:136-140 — reset kind/other on every cdstr.
279 // Rust port: walk via raw pointers since we need mutable access
280 // through nested chains while holding the set borrow.
281 let st_ptr: *mut cdstate = &mut *st;
282 unsafe {
283 let mut set = (*st_ptr).sets.as_deref_mut();
284 while let Some(s) = set {
285 let mut sp = s.strs.as_deref_mut();
286 while let Some(sn) = sp {
287 sn.kind = 0;
288 sn.other = None;
289 sp = sn.next.as_deref_mut();
290 }
291 set = s.next.as_deref_mut();
292 }
293
294 // c:142-180 — find matching desc, build "other" chain.
295 let mut set1 = (*st_ptr).sets.as_deref_mut();
296 while let Some(s1) = set1 {
297 let s1_ptr: *mut cdset = s1;
298 let mut str1 = (*s1_ptr).strs.as_deref_mut();
299 while let Some(t1) = str1 {
300 if t1.desc.is_none() || t1.kind != 0 { // c:144
301 str1 = t1.next.as_deref_mut();
302 continue;
303 }
304 let mut num = 1i32; // c:147
305 let mut width = t1.width + (*st_ptr).swidth; // c:148
306 if width > (*st_ptr).maxglen {
307 (*st_ptr).maxglen = width;
308 }
309 // Iterate set2 from set1 onwards; str2 starts at str1.next
310 // when same set, else strs head.
311 let t1_desc = t1.desc.clone().unwrap_or_default();
312 let mut other_tail: *mut Option<Box<cdstr>> = &mut t1.other;
313 let mut hit_break = false;
314 let mut set2 = Some(&mut *s1_ptr);
315 let mut first_iter = true;
316 while let Some(s2) = set2 {
317 let s2_ptr: *mut cdset = s2;
318 let mut str2 = if first_iter {
319 t1.next.as_deref_mut()
320 } else {
321 (*s2_ptr).strs.as_deref_mut()
322 };
323 first_iter = false;
324 while let Some(t2) = str2 {
325 if t2.desc.as_deref() == Some(t1_desc.as_str()) {
326 width += CM_SPACE + t2.width; // c:157
327 if width > (*st_ptr).maxmlen || num == maxg { // c:158
328 hit_break = true;
329 break;
330 }
331 if width > (*st_ptr).maxglen { // c:160
332 (*st_ptr).maxglen = width;
333 }
334 t1.kind = 1; // c:162
335 t2.kind = 2;
336 num += 1;
337 // Clone t2 into the other chain (Rust ownership).
338 let clone = Box::new(cdstr {
339 next: None,
340 str: t2.str.clone(),
341 desc: t2.desc.clone(),
342 r#match: t2.r#match.clone(),
343 sortstr: t2.sortstr.clone(),
344 len: t2.len,
345 width: t2.width,
346 other: None,
347 kind: t2.kind,
348 set: t2.set,
349 run: None,
350 });
351 *other_tail = Some(clone);
352 let nxt = &mut (*other_tail).as_mut().unwrap().other;
353 other_tail = nxt as *mut _;
354 }
355 str2 = t2.next.as_deref_mut();
356 }
357 if hit_break { break; }
358 set2 = (*s2_ptr).next.as_deref_mut();
359 }
360 if num > 1 { // c:173
361 (*st_ptr).groups += 1;
362 } else {
363 (*st_ptr).descs += 1; // c:176
364 }
365 if num > (*st_ptr).maxg { // c:178
366 (*st_ptr).maxg = num;
367 }
368 str1 = t1.next.as_deref_mut();
369 }
370 set1 = s1.next.as_deref_mut();
371 }
372 }
373}
374
375/// CM_SPACE — inter-match spacing from `Src/Zle/zle_tricky.c:1700` /
376/// `Src/Zle/computil.c` (referenced as the literal `2`). Used to
377/// reserve a 2-char gap between adjacent matches when computing
378/// column widths.
379pub const CM_SPACE: i32 = 2; // c:zle_tricky.c
380
381/// Direct port of `static int cd_sort(const void *a, const void *b)`
382/// from `Src/Zle/computil.c:233-236`. qsort comparator over Cdstr
383/// pointers — compares the `sortstr` fields via `zstrcmp` (case-
384/// sensitive by default).
385pub fn cd_sort(a: &cdstr, b: &cdstr) -> std::cmp::Ordering { // c:233
386 crate::ported::sort::zstrcmp(
387 a.sortstr.as_deref().unwrap_or(""),
388 b.sortstr.as_deref().unwrap_or(""),
389 0,
390 )
391}
392
393/// Direct port of `static int cd_groups_want_sorting(void)` from
394/// `Src/Zle/computil.c:215-230`. Returns 0 if any set's opts contain
395/// `-V` (preserve order), 1 if any contain `-J` (sort), 1 default.
396pub fn cd_groups_want_sorting() -> i32 { // c:215
397 let st = cd_state.lock().unwrap();
398 let mut set = st.sets.as_deref();
399 while let Some(s) = set {
400 if let Some(opts) = s.opts.as_deref() {
401 for o in opts {
402 if o.starts_with("-V") { return 0; } // c:222
403 if o.starts_with("-J") { return 1; } // c:224
404 }
405 }
406 set = s.next.as_deref();
407 }
408 1 // c:229
409}
410
411/// Direct port of `static int cd_prep(void)` from
412/// `Src/Zle/computil.c:239-439`. Builds the `cd_state.runs` chain
413/// from the parsed `cd_state.sets`.
414///
415/// Three branches:
416/// - groups (cd_state.groups > 0): build CRT_EXPL + CRT_SPEC/
417/// CRT_DUMMY interleaved + CRT_SIMPLE per set. The most complex
418/// path; depends on width tracking via cd_state.gprew. **This
419/// branch returns 1 when the laid-out group width exceeds the
420/// terminal — the caller (cd_init at c:582-586) loops with a
421/// shrunken maxg until prep succeeds.**
422/// - showd (cd_state.showd != 0): emit CRT_DESC for entries with
423/// descriptions and CRT_SIMPLE for plain matches per set.
424/// - default: one CRT_SIMPLE run per set.
425pub fn cd_prep() -> i32 { // c:239
426 // CRT_SIMPLE/DESC/SPEC/DUMMY/EXPL declared at the top of this file
427
428 // Build the new runs list as a Vec; link into cd_state.runs at the end.
429 let mut new_runs: Vec<Box<cdrun>> = Vec::new();
430 let mut st = cd_state.lock().unwrap();
431 st.runs = None;
432
433 if st.groups != 0 {
434 // c:247-394 — groups path. Full algorithm: collect leaders
435 // (kind==1 from cd_group OR kind==0+desc standalone) into a
436 // `prep_lines` Vec; sort by width inside each leader's .other
437 // chain; track per-column widths; bail-with-1 on overflow;
438 // sort by sortstr; dedup-adjacent so same-desc entries cluster;
439 // emit CRT_EXPL header + CRT_SPEC per leader column 0; for each
440 // additional column emit CRT_DUMMY/CRT_SPEC interleave; finally
441 // emit CRT_SIMPLE per set for un-described entries.
442
443 let maxg = st.maxg.max(1) as usize;
444 let maxmlen = st.maxmlen;
445 let maxglen = st.maxglen;
446 let swidth = st.swidth;
447
448 // c:256 — wids[0..maxg] tracks max width per column.
449 let mut wids: Vec<i32> = vec![0; maxg];
450
451 // c:257-287 — collect leaders into prep_lines (Vec of owned
452 // cdstr clones with their .other chains).
453 let mut prep_lines: Vec<Box<cdstr>> = Vec::new();
454 let mut set = st.sets.as_deref();
455 while let Some(s) = set {
456 let mut str_iter = s.strs.as_deref();
457 while let Some(node) = str_iter {
458 if node.kind != 1 {
459 if node.kind == 0 && node.desc.is_some() { // c:262
460 if node.width > wids[0] { // c:263
461 wids[0] = node.width;
462 }
463 let mut clone = Box::new({
464 let n = node;
465 cdstr {
466 next: None, str: n.str.clone(), desc: n.desc.clone(),
467 r#match: n.r#match.clone(), sortstr: n.sortstr.clone(),
468 len: n.len, width: n.width, other: None,
469 kind: n.kind, set: n.set, run: None,
470 }
471 });
472 clone.other = None; // c:265
473 prep_lines.push(clone);
474 }
475 str_iter = node.next.as_deref();
476 continue;
477 }
478 // c:270 — kind==1 leader: collect, sort its .other by
479 // width descending, update wids[i] per column.
480 let mut gs = Box::new({
481 let n = node;
482 cdstr {
483 next: None, str: n.str.clone(), desc: n.desc.clone(),
484 r#match: n.r#match.clone(), sortstr: n.sortstr.clone(),
485 len: n.len, width: n.width, other: None,
486 kind: n.kind, set: n.set, run: None,
487 }
488 });
489 gs.kind = 2; // c:271
490 gs.other = None;
491
492 // Walk node.other; build a sorted insert into gs.other
493 // by descending width (matches c:274-281).
494 let mut gp = node.other.as_deref();
495 while let Some(g_node) = gp {
496 let new_clone = Box::new({
497 let n = g_node;
498 cdstr {
499 next: None, str: n.str.clone(), desc: n.desc.clone(),
500 r#match: n.r#match.clone(), sortstr: n.sortstr.clone(),
501 len: n.len, width: n.width, other: None,
502 kind: n.kind, set: n.set, run: None,
503 }
504 });
505 // Sorted-insert by width descending.
506 // Drain gs's .other chain into a flat Vec, sort-insert
507 // new_clone, then rebuild the chain.
508 let mut chain: Vec<Box<cdstr>> = Vec::new();
509 // First entry: a clone of gs itself (without other).
510 chain.push(Box::new(cdstr {
511 next: None, str: gs.str.clone(), desc: gs.desc.clone(),
512 r#match: gs.r#match.clone(), sortstr: gs.sortstr.clone(),
513 len: gs.len, width: gs.width, other: None,
514 kind: gs.kind, set: gs.set, run: None,
515 }));
516 let mut rest = gs.other.take();
517 while let Some(mut n) = rest {
518 rest = n.other.take();
519 chain.push(n);
520 }
521 // Find insert index where existing.width <= new_clone.width.
522 let mut ins = chain.len();
523 for (i, c) in chain.iter().enumerate() {
524 if c.width <= new_clone.width {
525 ins = i;
526 break;
527 }
528 }
529 chain.insert(ins, new_clone);
530 // Rebuild gs from chain[0]; link tail via .other.
531 let mut new_head = chain.remove(0);
532 let mut tail_ptr: *mut Option<Box<cdstr>> = &mut new_head.other;
533 for entry in chain {
534 unsafe {
535 *tail_ptr = Some(entry);
536 let nxt = &mut (*tail_ptr).as_mut().unwrap().other;
537 tail_ptr = nxt as *mut _;
538 }
539 }
540 gs = new_head;
541 gp = g_node.other.as_deref();
542 }
543
544 // c:282-284 — update wids per column.
545 let mut col = 0usize;
546 let mut walker = Some(gs.as_ref());
547 while let Some(g) = walker {
548 if col < wids.len() && g.width > wids[col] {
549 wids[col] = g.width;
550 }
551 col += 1;
552 walker = g.other.as_deref();
553 }
554
555 prep_lines.push(gs);
556 str_iter = node.next.as_deref();
557 }
558 set = s.next.as_deref();
559 }
560
561 // c:289-292 — gprew = sum(wids[i] + CM_SPACE).
562 let mut gprew = 0i32;
563 for w in &wids {
564 gprew += w + CM_SPACE;
565 }
566 st.gprew = gprew;
567
568 // c:294 — bail with retry if too wide.
569 if gprew > maxmlen && maxglen > 1 {
570 let _ = swidth;
571 return 1;
572 }
573
574 // c:297-303 — set sortstr from unmetafy(str) for each line.
575 for line in prep_lines.iter_mut() {
576 let s = line.str.clone().unwrap_or_default();
577 line.sortstr = Some(crate::ported::zle::zle_utils::unmetafy(&s));
578 }
579
580 // c:305 — sort if requested.
581 // We have to drop the lock briefly because cd_groups_want_sorting
582 // re-acquires it.
583 let want_sort = {
584 drop(st);
585 let r = cd_groups_want_sorting();
586 st = cd_state.lock().unwrap();
587 r
588 };
589 if want_sort != 0 { // c:305
590 prep_lines.sort_by(|a, b| cd_sort(a, b)); // c:306
591 }
592
593 // c:308-322 — dedup-adjacent: shuffle same-desc entries together.
594 let mut i = 0usize;
595 while i + 1 < prep_lines.len() {
596 let strp_desc = prep_lines[i].desc.clone().unwrap_or_default();
597 let next_desc = prep_lines[i + 1].desc.clone().unwrap_or_default();
598 if strp_desc == next_desc {
599 i += 1;
600 continue;
601 }
602 // Find a later entry with matching desc; bubble it to i+1.
603 let mut found: Option<usize> = None;
604 for j in i + 2..prep_lines.len() {
605 if prep_lines[j].desc.clone().unwrap_or_default() == strp_desc {
606 found = Some(j);
607 break;
608 }
609 }
610 if let Some(j) = found {
611 let entry = prep_lines.remove(j);
612 prep_lines.insert(i + 1, entry);
613 }
614 i += 1;
615 }
616
617 let preplines = prep_lines.len();
618
619 // c:323-326 — CRT_EXPL header: link all preplines via .run.
620 // Build a chain of header cdstrs (desc + str only).
621 if preplines > 0 {
622 let mut expl_head: Option<Box<cdstr>> = None;
623 let mut tail_ptr: *mut Option<Box<cdstr>> = &mut expl_head;
624 for line in &prep_lines {
625 let header = Box::new(cdstr {
626 next: None,
627 str: line.str.clone(),
628 desc: line.desc.clone(),
629 r#match: line.r#match.clone(),
630 sortstr: line.sortstr.clone(),
631 len: line.len,
632 width: line.width,
633 other: None,
634 kind: line.kind,
635 set: line.set,
636 run: None,
637 });
638 unsafe {
639 *tail_ptr = Some(header);
640 let nxt = &mut (*tail_ptr).as_mut().unwrap().run;
641 tail_ptr = nxt as *mut _;
642 }
643 }
644 // c:323-326 — emit CRT_EXPL run with the header chain.
645 let expl_run = Box::new(cdrun {
646 next: None,
647 r#type: CRT_EXPL,
648 strs: expl_head,
649 count: preplines as i32,
650 });
651 // Store at the END (matches c:373 `*runp = expl; runp = &(expl->next)`).
652 // We'll insert after column-emit runs below.
653
654 // c:328-340 — emit CRT_SPEC for each column-0 leader.
655 // Each line has a .other chain; we consume it column-by-column.
656 let mut grps: Vec<Option<Box<cdstr>>> = prep_lines
657 .into_iter()
658 .map(Some)
659 .collect();
660
661 for line_opt in grps.iter_mut() {
662 if let Some(line) = line_opt.take() {
663 let mut owned = *line;
664 let next_col = owned.other.take();
665 owned.run = None;
666 let spec_run = Box::new(cdrun {
667 next: None,
668 r#type: CRT_SPEC,
669 strs: Some(Box::new(owned)),
670 count: 1,
671 });
672 new_runs.push(spec_run);
673 *line_opt = next_col;
674 }
675 }
676
677 // c:343-372 — for columns 1..maxg, emit CRT_DUMMY/CRT_SPEC.
678 for _col in 1..maxg {
679 let mut dummy_count = 0i32;
680 for line_opt in grps.iter_mut() {
681 if let Some(line) = line_opt.take() {
682 // Flush pending dummies first.
683 if dummy_count > 0 {
684 new_runs.push(Box::new(cdrun {
685 next: None,
686 r#type: CRT_DUMMY,
687 strs: None,
688 count: dummy_count,
689 }));
690 dummy_count = 0;
691 }
692 let mut owned = *line;
693 let next_col = owned.other.take();
694 owned.run = None;
695 new_runs.push(Box::new(cdrun {
696 next: None,
697 r#type: CRT_SPEC,
698 strs: Some(Box::new(owned)),
699 count: 1,
700 }));
701 *line_opt = next_col;
702 } else {
703 dummy_count += 1;
704 }
705 }
706 if dummy_count > 0 { // c:365
707 new_runs.push(Box::new(cdrun {
708 next: None,
709 r#type: CRT_DUMMY,
710 strs: None,
711 count: dummy_count,
712 }));
713 }
714 }
715
716 // c:373 — append the expl run at the end of the column emits.
717 new_runs.push(expl_run);
718 }
719
720 // c:376-394 — emit CRT_SIMPLE per set for entries without
721 // kind and without desc (the un-described ones).
722 let mut set = st.sets.as_deref();
723 while let Some(s) = set {
724 let mut head: Option<Box<cdstr>> = None;
725 let mut tail_ptr: *mut Option<Box<cdstr>> = &mut head;
726 let mut count = 0i32;
727 let mut str_iter = s.strs.as_deref();
728 while let Some(node) = str_iter {
729 if node.kind == 0 && node.desc.is_none() {
730 let clone = Box::new(cdstr {
731 next: None,
732 str: node.str.clone(),
733 desc: None,
734 r#match: node.r#match.clone(),
735 sortstr: node.sortstr.clone(),
736 len: node.len,
737 width: node.width,
738 other: None,
739 kind: 0,
740 set: node.set,
741 run: None,
742 });
743 unsafe {
744 *tail_ptr = Some(clone);
745 let nxt = &mut (*tail_ptr).as_mut().unwrap().run;
746 tail_ptr = nxt as *mut _;
747 }
748 count += 1;
749 }
750 str_iter = node.next.as_deref();
751 }
752 if count > 0 {
753 new_runs.push(Box::new(cdrun {
754 next: None,
755 r#type: CRT_SIMPLE,
756 strs: head,
757 count,
758 }));
759 }
760 set = s.next.as_deref();
761 }
762 } else if st.showd != 0 {
763 // c:395-423 — showd: emit CRT_DESC (described entries) then
764 // CRT_SIMPLE (undescribed) per set.
765 let mut set = st.sets.as_deref();
766 while let Some(s) = set {
767 if s.desc > 0 {
768 // c:397-409 — CRT_DESC for entries with descriptions.
769 let mut head: Option<Box<cdstr>> = None;
770 let mut tail: *mut Option<Box<cdstr>> = &mut head;
771 let mut str_iter = s.strs.as_deref();
772 while let Some(st_node) = str_iter {
773 if st_node.desc.is_some() {
774 let clone = Box::new(cdstr {
775 next: None,
776 str: st_node.str.clone(),
777 desc: st_node.desc.clone(),
778 r#match: st_node.r#match.clone(),
779 sortstr: st_node.sortstr.clone(),
780 len: st_node.len,
781 width: st_node.width,
782 other: None,
783 kind: st_node.kind,
784 set: st_node.set,
785 run: None,
786 });
787 unsafe {
788 *tail = Some(clone);
789 let nxt = &mut (*tail).as_mut().unwrap().run;
790 tail = nxt as *mut _;
791 }
792 }
793 str_iter = st_node.next.as_deref();
794 }
795 new_runs.push(Box::new(cdrun {
796 next: None, r#type: CRT_DESC,
797 strs: head, count: s.desc,
798 }));
799 }
800 if s.desc != s.count {
801 // c:410-422 — CRT_SIMPLE for undescribed entries.
802 let mut head: Option<Box<cdstr>> = None;
803 let mut tail: *mut Option<Box<cdstr>> = &mut head;
804 let mut str_iter = s.strs.as_deref();
805 while let Some(st_node) = str_iter {
806 if st_node.desc.is_none() {
807 let clone = Box::new(cdstr {
808 next: None,
809 str: st_node.str.clone(),
810 desc: st_node.desc.clone(),
811 r#match: st_node.r#match.clone(),
812 sortstr: st_node.sortstr.clone(),
813 len: st_node.len,
814 width: st_node.width,
815 other: None,
816 kind: st_node.kind,
817 set: st_node.set,
818 run: None,
819 });
820 unsafe {
821 *tail = Some(clone);
822 let nxt = &mut (*tail).as_mut().unwrap().run;
823 tail = nxt as *mut _;
824 }
825 }
826 str_iter = st_node.next.as_deref();
827 }
828 new_runs.push(Box::new(cdrun {
829 next: None, r#type: CRT_SIMPLE,
830 strs: head, count: s.count - s.desc,
831 }));
832 }
833 set = s.next.as_deref();
834 }
835 } else {
836 // c:424-435 — default: one CRT_SIMPLE per non-empty set.
837 let mut set = st.sets.as_deref();
838 while let Some(s) = set {
839 if s.count != 0 {
840 // c:431 — link str.run = str.next for each entry.
841 let mut head: Option<Box<cdstr>> = None;
842 let mut tail: *mut Option<Box<cdstr>> = &mut head;
843 let mut str_iter = s.strs.as_deref();
844 while let Some(st_node) = str_iter {
845 let clone = Box::new(cdstr {
846 next: None,
847 str: st_node.str.clone(),
848 desc: st_node.desc.clone(),
849 r#match: st_node.r#match.clone(),
850 sortstr: st_node.sortstr.clone(),
851 len: st_node.len,
852 width: st_node.width,
853 other: None,
854 kind: st_node.kind,
855 set: st_node.set,
856 run: None,
857 });
858 unsafe {
859 *tail = Some(clone);
860 let nxt = &mut (*tail).as_mut().unwrap().run;
861 tail = nxt as *mut _;
862 }
863 str_iter = st_node.next.as_deref();
864 }
865 new_runs.push(Box::new(cdrun {
866 next: None, r#type: CRT_SIMPLE,
867 strs: head, count: s.count,
868 }));
869 }
870 set = s.next.as_deref();
871 }
872 }
873
874 // Link new_runs as a chain into cd_state.runs.
875 let mut head: Option<Box<cdrun>> = None;
876 for run in new_runs.into_iter().rev() {
877 let mut run = run;
878 run.next = head;
879 head = Some(run);
880 }
881 st.runs = head;
882 0 // c:438
883}
884
885/// Direct port of `static int cd_init(char *nam, char *hide, char *mlen,
886/// char *sep, char **opts, char **args,
887/// int disp)`
888/// from `Src/Zle/computil.c:477-594`. Parses the `_describe` input
889/// (match arrays + optional display arrays) into the `cd_state.sets`
890/// chain, then runs `cd_calc` + `cd_prep` to build the run chain.
891///
892/// `args` is the consolidated arg list — match-array param name,
893/// optional disp-array name, optional `--`-separated per-set opts.
894/// `-g` prefix on `args` enables group detection (cd_group loop).
895pub fn cd_init(nam: &str, hide: &str, mlen: &str, sep: &str, // c:477
896 opts: &[String], args: &[String], disp: i32) -> i32 {
897 use crate::ported::zle::compcore::{get_user_var, rembslash};
898
899 // c:485 — discard prior parsed state.
900 if cd_parsed.load(std::sync::atomic::Ordering::Relaxed) != 0 {
901 let mut st = cd_state.lock().unwrap();
902 st.sep = None;
903 freecdsets(st.sets.take());
904 cd_parsed.store(0, std::sync::atomic::Ordering::Relaxed);
905 }
906
907 // c:491 — seed cd_state.
908 {
909 let mut st = cd_state.lock().unwrap();
910 st.sep = Some(sep.to_string());
911 st.slen = sep.len() as i32;
912 st.swidth = crate::ported::utils::niceztrlen(sep) as i32;
913 st.sets = None;
914 st.showd = disp;
915 st.maxg = 0;
916 st.groups = 0;
917 st.descs = 0;
918 st.maxmlen = mlen.parse::<i32>().unwrap_or(0);
919 st.premaxw = 0;
920 let cols = crate::ported::utils::adjustcolumns() as i32;
921 let itmp = cols - st.swidth - 4; // c:499
922 if st.maxmlen > itmp { st.maxmlen = itmp; }
923 if st.maxmlen < 4 { st.maxmlen = 4; }
924 }
925
926 // c:504 — strip leading `-g` for group detection.
927 let mut idx = 0usize;
928 let grp = if args.first().map(|s| s.as_str()) == Some("-g") {
929 idx = 1;
930 true
931 } else {
932 false
933 };
934
935 // c:508 — walk arg pairs (match-array [disp-array] [-- opts]).
936 let mut sets_collected: Vec<Box<cdset>> = Vec::new();
937 while idx < args.len() {
938 let arg = &args[idx];
939 let Some(mat_arr) = get_user_var(Some(arg.as_str())) else { // c:515
940 zwarnnam(nam, &format!("invalid argument: {}", arg));
941 let mut st = cd_state.lock().unwrap();
942 st.sep = None;
943 freecdsets(st.sets.take());
944 return 1;
945 };
946 idx += 1;
947
948 // c:521-543 — parse `match:desc` entries into cdstr chain.
949 let mut strs_vec: Vec<Box<cdstr>> = Vec::new();
950 for entry in &mat_arr {
951 let bytes = entry.as_bytes();
952 let mut p = 0usize;
953 while p < bytes.len() && bytes[p] != b':' { // c:530
954 if bytes[p] == b'\\' && p + 1 < bytes.len() { p += 1; }
955 p += 1;
956 }
957 let (match_part, desc_part) = if p < bytes.len() {
958 let m = std::str::from_utf8(&bytes[..p]).unwrap_or("");
959 let d = std::str::from_utf8(&bytes[p + 1..]).unwrap_or("");
960 (rembslash(m), Some(rembslash(d)))
961 } else {
962 (rembslash(entry), None)
963 };
964 let str_s = match_part.clone();
965 let mut new_str = Box::new(cdstr::default());
966 new_str.str = Some(str_s.clone());
967 new_str.r#match = Some(str_s.clone());
968 new_str.desc = desc_part;
969 new_str.len = str_s.len() as i32;
970 new_str.width = crate::ported::utils::niceztrlen(&str_s) as i32;
971 new_str.kind = 0;
972 strs_vec.push(new_str);
973 }
974
975 // c:547-557 — optional separate match array.
976 if idx < args.len() && !args[idx].starts_with('-') {
977 let Some(match_arr) = get_user_var(Some(args[idx].as_str())) else {
978 zwarnnam(nam, &format!("invalid argument: {}", args[idx]));
979 let mut st = cd_state.lock().unwrap();
980 st.sep = None;
981 freecdsets(st.sets.take());
982 return 1;
983 };
984 for (i, m) in match_arr.iter().enumerate() {
985 if i < strs_vec.len() {
986 strs_vec[i].r#match = Some(m.clone());
987 }
988 }
989 idx += 1;
990 }
991
992 // c:559 — apply hide (strip leading `-`/`--` from str).
993 if !hide.is_empty() {
994 let hb = hide.as_bytes();
995 let double = hb.len() > 1;
996 for s in strs_vec.iter_mut() {
997 if let Some(cur) = s.str.clone() {
998 let mut bytes = cur.into_bytes();
999 if double && bytes.len() >= 2 && bytes[0] == b'-' && bytes[1] == b'-' {
1000 bytes.drain(0..2); // c:564
1001 } else if !bytes.is_empty() && (bytes[0] == b'-' || bytes[0] == b'+') {
1002 bytes.drain(0..1); // c:566
1003 }
1004 s.str = String::from_utf8(bytes).ok();
1005 }
1006 }
1007 }
1008
1009 // c:569-577 — gather per-set opts up to `--`.
1010 let opt_start = idx;
1011 while idx < args.len()
1012 && !(args[idx].as_bytes().len() == 2
1013 && args[idx].as_bytes()[0] == b'-'
1014 && args[idx].as_bytes()[1] == b'-')
1015 {
1016 idx += 1;
1017 }
1018 let per_set: &[String] = &args[opt_start..idx];
1019 let combined = cd_arrcat(per_set, opts);
1020 if idx < args.len() { idx += 1; } // c:577 skip `--`
1021
1022 // Link strs_vec as a chain into a new cdset.
1023 let mut strs_head: Option<Box<cdstr>> = None;
1024 for s in strs_vec.into_iter().rev() {
1025 let mut s = s;
1026 s.next = strs_head;
1027 strs_head = Some(s);
1028 }
1029 let mut set = Box::new(cdset::default());
1030 set.opts = Some(combined);
1031 set.strs = strs_head;
1032 sets_collected.push(set);
1033 }
1034
1035 // Link sets_collected as a chain into cd_state.sets.
1036 {
1037 let mut head: Option<Box<cdset>> = None;
1038 for s in sets_collected.into_iter().rev() {
1039 let mut s = s;
1040 s.next = head;
1041 head = Some(s);
1042 }
1043 cd_state.lock().unwrap().sets = head;
1044 }
1045
1046 // c:579 — group-aware vs simple prep.
1047 if disp != 0 && grp {
1048 let cols = crate::ported::utils::adjustcolumns() as i32;
1049 let mut mg = cols;
1050 // c:582-586 — retry cd_prep with shrinking maxg.
1051 loop {
1052 cd_group(mg);
1053 mg = {
1054 let st = cd_state.lock().unwrap();
1055 st.maxg - 1
1056 };
1057 cd_calc();
1058 if cd_prep() == 0 || mg <= 0 { break; }
1059 }
1060 } else {
1061 cd_calc();
1062 cd_prep();
1063 }
1064 cd_parsed.store(1, std::sync::atomic::Ordering::Relaxed); // c:592
1065 0
1066}
1067
1068/// Direct port of `static int cd_get(char **params)` from
1069/// `Src/Zle/computil.c:614-841`. Pops the next `cdrun` off
1070/// `cd_state.runs` and emits its match/display arrays + per-run
1071/// compadd options into the four named params:
1072/// params[0] = csl ("" or "packed")
1073/// params[1] = opts (compadd flags)
1074/// params[2] = mats (match strings)
1075/// params[3] = dpys (display strings)
1076/// Returns 1 when no runs remain, 0 otherwise.
1077pub fn cd_get(params: &[String]) -> i32 { // c:614
1078 use crate::ported::params::{setsparam, setaparam};
1079
1080 // c:618 — pop the head run.
1081 let run_opt = {
1082 let mut st = cd_state.lock().unwrap();
1083 st.runs.take().map(|mut r| {
1084 let next = r.next.take();
1085 st.runs = next;
1086 r
1087 })
1088 };
1089 let Some(run) = run_opt else { return 1; };
1090
1091 let mut mats: Vec<String> = Vec::new();
1092 let mut dpys: Vec<String> = Vec::new();
1093 let mut opts: Vec<String> = Vec::new();
1094 let mut csl: String = String::new();
1095
1096 let rtype = run.r#type;
1097
1098 // Helper: walk a cdstr chain via .run, applying f.
1099 let mut walk_run = |head: &Option<Box<cdstr>>, mut f: Box<dyn FnMut(&cdstr)>| {
1100 let mut cur = head.as_deref();
1101 while let Some(s) = cur {
1102 f(s);
1103 cur = s.run.as_deref();
1104 }
1105 };
1106
1107 if rtype == CRT_SIMPLE { // c:625
1108 let head_opts = run.strs.as_deref()
1109 .map(|s| {
1110 let st = cd_state.lock().unwrap();
1111 // c:634 — zarrdup(run->strs->set->opts). Set is an index;
1112 // we walk cd_state.sets to find the matching index.
1113 let mut set_iter = st.sets.as_deref();
1114 let mut found: Option<Vec<String>> = None;
1115 let mut idx_count = 0usize;
1116 while let Some(set) = set_iter {
1117 if idx_count == s.set {
1118 found = set.opts.clone();
1119 break;
1120 }
1121 idx_count += 1;
1122 set_iter = set.next.as_deref();
1123 }
1124 found.unwrap_or_default()
1125 })
1126 .unwrap_or_default();
1127 walk_run(&run.strs, Box::new(|s| { // c:629
1128 mats.push(s.r#match.clone().unwrap_or_default());
1129 dpys.push(s.str.clone().or_else(|| s.r#match.clone()).unwrap_or_default());
1130 }));
1131 let groups_flag = cd_state.lock().unwrap().groups;
1132 opts = if groups_flag != 0 { // c:635
1133 // c:641 — strip `-X` options.
1134 let mut filtered: Vec<String> = Vec::new();
1135 let mut skip_next = false;
1136 for o in head_opts.iter() {
1137 if skip_next { skip_next = false; continue; }
1138 if o.starts_with("-X") { // c:642
1139 if o.len() == 2 { skip_next = true; } // c:643
1140 continue;
1141 }
1142 filtered.push(o.clone());
1143 }
1144 filtered
1145 } else {
1146 head_opts
1147 };
1148 } else if rtype == CRT_DESC { // c:652
1149 let st_snapshot = {
1150 let st = cd_state.lock().unwrap();
1151 (st.pre, st.suf, st.premaxw, st.slen, st.swidth, st.sep.clone(),
1152 crate::ported::utils::adjustcolumns() as i32)
1153 };
1154 let (cd_pre, _cd_suf, cd_premaxw, _cd_slen, cd_swidth, cd_sep, cols) = st_snapshot;
1155 let sep_str = cd_sep.unwrap_or_default();
1156 walk_run(&run.strs, Box::new(|s| { // c:669
1157 let str_s = s.str.clone().unwrap_or_default();
1158 let desc_s = s.desc.clone().unwrap_or_default();
1159 let mut buf = String::with_capacity(
1160 (cd_pre + cd_premaxw + cd_swidth + 16) as usize);
1161 // c:674 — write str.
1162 buf.push_str(&str_s);
1163 // c:676 — pad to premaxw + CM_SPACE.
1164 let pad = (cd_premaxw - s.width + CM_SPACE).max(0) as usize;
1165 for _ in 0..pad { buf.push(' '); }
1166
1167 // c:679-715 — append separator + truncated desc to fit terminal.
1168 let mut remw = cols - cd_premaxw - cd_swidth - 3;
1169 while remw < 0 && cols > 0 { remw += cols; }
1170 if (sep_str.len() as i32) < remw { // c:685
1171 buf.push_str(&sep_str);
1172 remw -= sep_str.len() as i32;
1173 let dw = crate::ported::utils::niceztrlen(&desc_s) as i32;
1174 if dw <= remw {
1175 buf.push_str(&desc_s);
1176 } else { // c:701
1177 // Truncate desc to fit. Use char boundaries.
1178 let mut w_used = 0i32;
1179 for ch in desc_s.chars() {
1180 let cw = crate::ported::utils::niceztrlen(&ch.to_string()) as i32;
1181 if w_used + cw > remw { break; }
1182 buf.push(ch);
1183 w_used += cw;
1184 }
1185 }
1186 }
1187 mats.push(s.r#match.clone().unwrap_or_default()); // c:673
1188 dpys.push(buf);
1189 }));
1190 // c:721 — opts = cd_arrdup + opts[0] = "-l".
1191 let head_opts = run.strs.as_deref()
1192 .map(|s| {
1193 let st = cd_state.lock().unwrap();
1194 let mut set_iter = st.sets.as_deref();
1195 let mut found: Option<Vec<String>> = None;
1196 let mut idx_count = 0usize;
1197 while let Some(set) = set_iter {
1198 if idx_count == s.set { found = set.opts.clone(); break; }
1199 idx_count += 1;
1200 set_iter = set.next.as_deref();
1201 }
1202 found.unwrap_or_default()
1203 })
1204 .unwrap_or_default();
1205 opts = std::iter::once("-l".to_string()).chain(head_opts).collect();
1206 } else if rtype == CRT_SPEC { // c:726
1207 let s = run.strs.as_deref();
1208 if let Some(s) = s {
1209 mats.push(s.r#match.clone().unwrap_or_default());
1210 dpys.push(s.str.clone().unwrap_or_default());
1211 }
1212 // c:732 — opts = cd_arrdup + flip -J/-V to -2V or insert -2V-default-.
1213 let head_opts = s.map(|s| {
1214 let st = cd_state.lock().unwrap();
1215 let mut set_iter = st.sets.as_deref();
1216 let mut found: Option<Vec<String>> = None;
1217 let mut idx_count = 0usize;
1218 while let Some(set) = set_iter {
1219 if idx_count == s.set { found = set.opts.clone(); break; }
1220 idx_count += 1;
1221 set_iter = set.next.as_deref();
1222 }
1223 found.unwrap_or_default()
1224 }).unwrap_or_default();
1225 let mut new_opts: Vec<String> = head_opts.clone();
1226 let mut found_jv = false;
1227 // c:736 — `for (dp = opts + 1; *dp; dp++)`. Skip slot 0 (the
1228 // existing first element which we'll overwrite below) and look
1229 // for the first -J/-V flag.
1230 for i in 1..new_opts.len() {
1231 if new_opts[i].starts_with("-J") || new_opts[i].starts_with("-V") {
1232 let rest = new_opts[i][2..].to_string();
1233 new_opts[i] = format!("-2V{}", rest);
1234 found_jv = true;
1235 break;
1236 }
1237 }
1238 if !found_jv {
1239 new_opts.insert(0, "-2V-default-".to_string()); // c:750
1240 }
1241 opts = new_opts;
1242 csl = "packed".to_string();
1243 } else if rtype == CRT_DUMMY { // c:754
1244 // c:758 — opts[0] = "-E<count>".
1245 let head_opts = run.strs.as_deref().map(|s| {
1246 let st = cd_state.lock().unwrap();
1247 let mut set_iter = st.sets.as_deref();
1248 let mut found: Option<Vec<String>> = None;
1249 let mut idx_count = 0usize;
1250 while let Some(set) = set_iter {
1251 if idx_count == s.set { found = set.opts.clone(); break; }
1252 idx_count += 1;
1253 set_iter = set.next.as_deref();
1254 }
1255 found.unwrap_or_default()
1256 }).unwrap_or_default();
1257 opts = std::iter::once(format!("-E{}", run.count))
1258 .chain(head_opts).collect();
1259 csl = "packed".to_string();
1260 } else if rtype == CRT_EXPL { // c:772
1261 let st_snapshot = {
1262 let st = cd_state.lock().unwrap();
1263 (st.suf, st.slen, st.swidth, st.gprew, st.sep.clone(),
1264 crate::ported::utils::adjustcolumns() as i32)
1265 };
1266 let (_cd_suf, _cd_slen, cd_swidth, cd_gprew, cd_sep, cols) = st_snapshot;
1267 let sep_str = cd_sep.unwrap_or_default();
1268 let count = run.count;
1269
1270 walk_run(&run.strs, Box::new(|s| { // c:785
1271 // c:786 — if run sibling has same desc, emit empty.
1272 let next_desc = s.run.as_deref().and_then(|n| n.desc.clone());
1273 if next_desc.is_some() && next_desc == s.desc {
1274 dpys.push(String::new());
1275 return;
1276 }
1277 let mut buf = String::new();
1278 buf.push_str(&sep_str);
1279 let mut remw = cols - cd_gprew - cd_swidth - CM_SPACE;
1280 let desc_s = s.desc.clone().unwrap_or_default();
1281 let dw = crate::ported::utils::niceztrlen(&desc_s) as i32;
1282 if dw <= remw { // c:797
1283 buf.push_str(&desc_s);
1284 remw -= dw;
1285 } else {
1286 for ch in desc_s.chars() {
1287 let cw = crate::ported::utils::niceztrlen(&ch.to_string()) as i32;
1288 if cw > remw { break; }
1289 buf.push(ch);
1290 remw -= cw;
1291 }
1292 }
1293 while remw > 0 { // c:817
1294 buf.push(' ');
1295 remw -= 1;
1296 }
1297 dpys.push(buf);
1298 }));
1299 // c:825 — opts[0] = "-E<count>".
1300 let head_opts = run.strs.as_deref().map(|s| {
1301 let st = cd_state.lock().unwrap();
1302 let mut set_iter = st.sets.as_deref();
1303 let mut found: Option<Vec<String>> = None;
1304 let mut idx_count = 0usize;
1305 while let Some(set) = set_iter {
1306 if idx_count == s.set { found = set.opts.clone(); break; }
1307 idx_count += 1;
1308 set_iter = set.next.as_deref();
1309 }
1310 found.unwrap_or_default()
1311 }).unwrap_or_default();
1312 opts = std::iter::once(format!("-E{}", count))
1313 .chain(head_opts).collect();
1314 csl = "packed".to_string();
1315 }
1316
1317 // c:832 — emit the four params.
1318 if params.len() >= 4 {
1319 setsparam(¶ms[0], &csl);
1320 setaparam(¶ms[1], opts);
1321 setaparam(¶ms[2], mats);
1322 setaparam(¶ms[3], dpys);
1323 }
1324 0 // c:839
1325}
1326
1327/// Direct port of `static char **cd_arrcat(char **a, char **b)` from
1328/// `Src/Zle/computil.c:599`.
1329pub fn cd_arrcat(a: &[String], b: &[String]) -> Vec<String> { // c:444
1330 let mut out = a.to_vec();
1331 out.extend_from_slice(b);
1332 out
1333}
1334
1335/// Direct port of `static char **cd_arrdup(char **a)` from
1336/// `Src/Zle/computil.c:somewhere`. Duplicate a string array.
1337pub fn cd_arrdup(a: &[String]) -> Vec<String> { // c:cd_arrdup
1338 a.to_vec()
1339}
1340
1341/// Direct port of `static void freecdsets(Cdset p)` from
1342/// `Src/Zle/computil.c:97`. Walks the cdset `next` chain
1343/// freeing each set's opts/strs sub-chains and the cd_state runs
1344/// list at the end.
1345pub fn freecdsets(mut p: Option<Box<cdset>>) { // c:97
1346 while let Some(mut set) = p { // c:97 for (; p; ...)
1347 p = set.next.take(); // c:104 n = p->next
1348 // c:105-106 — `if (p->opts) freearray(p->opts)`.
1349 set.opts = None;
1350 // c:107-115 — for each cdstr: free sortstr/str/desc/match.
1351 let mut s = set.strs.take();
1352 while let Some(mut node) = s {
1353 s = node.next.take();
1354 node.sortstr = None; // c:109
1355 node.str = None; // c:110
1356 node.desc = None; // c:111
1357 // c:112-113 — `if (s->match != s->str) zsfree(s->match)`.
1358 // Rust's Option<String> drop is unconditional; the C
1359 // pointer-equality guard collapses out.
1360 node.r#match = None;
1361 drop(node); // c:114
1362 }
1363 // c:116-119 — drain cd_state.runs.
1364 if let Ok(mut st) = cd_state.lock() {
1365 let mut r = st.runs.take();
1366 while let Some(mut run) = r {
1367 r = run.next.take();
1368 drop(run); // c:118
1369 }
1370 }
1371 drop(set); // c:120
1372 }
1373}
1374
1375// =====================================================================
1376// `_arguments`-cache types — direct ports of the C structs at
1377// Src/Zle/computil.c:899-968. CAO_* / CAA_* / CDF_SEP /
1378// MAX_CACACHE constants already declared above (file scope).
1379// =====================================================================
1380
1381/// Port of `typedef struct cadef *Cadef` from `Src/Zle/computil.c:899`.
1382pub type Cadef = Box<cadef>; // c:899
1383/// Port of `typedef struct caopt *Caopt` from `Src/Zle/computil.c:900`.
1384pub type Caopt = Box<caopt>; // c:900
1385/// Port of `typedef struct caarg *Caarg` from `Src/Zle/computil.c:901`.
1386pub type Caarg = Box<caarg>; // c:901
1387
1388/// Direct port of `struct caarg` from `Src/Zle/computil.c:949-962`.
1389/// Description for one `_arguments` argument spec.
1390#[derive(Debug, Default, Clone)]
1391#[allow(non_camel_case_types)]
1392pub struct caarg { // c:949
1393 pub next: Option<Box<caarg>>, // c:950 Caarg next
1394 pub descr: Option<String>, // c:951 char *descr
1395 pub xor: Option<Vec<String>>, // c:952 char **xor
1396 pub action: Option<String>, // c:953 char *action
1397 pub r#type: i32, // c:954 int type (CAA_*)
1398 pub end: Option<String>, // c:955 char *end
1399 pub opt: Option<String>, // c:956 char *opt
1400 pub num: i32, // c:957 int num
1401 pub min: i32, // c:958 int min
1402 pub direct: i32, // c:959 int direct
1403 pub active: i32, // c:960 int active
1404 pub gsname: Option<String>, // c:961 char *gsname
1405}
1406
1407/// Direct port of `struct caopt` from `Src/Zle/computil.c:928-939`.
1408/// Description for one `_arguments` option spec.
1409#[derive(Debug, Default, Clone)]
1410#[allow(non_camel_case_types)]
1411pub struct caopt { // c:928
1412 pub next: Option<Box<caopt>>, // c:929 Caopt next
1413 pub name: Option<String>, // c:930 char *name
1414 pub descr: Option<String>, // c:931 char *descr
1415 pub xor: Option<Vec<String>>, // c:932 char **xor
1416 pub r#type: i32, // c:933 int type (CAO_*)
1417 pub args: Option<Box<caarg>>, // c:934 Caarg args
1418 pub active: i32, // c:935 int active
1419 pub num: i32, // c:936 int num
1420 pub gsname: Option<String>, // c:937 char *gsname
1421 pub not: i32, // c:938 int not
1422}
1423
1424/// Direct port of `struct cadef` from `Src/Zle/computil.c:905-922`.
1425/// Cache entry for a set of `_arguments` definitions.
1426#[derive(Debug, Default, Clone)]
1427#[allow(non_camel_case_types)]
1428pub struct cadef { // c:905
1429 pub next: Option<Box<cadef>>, // c:906 Cadef next
1430 pub snext: Option<Box<cadef>>, // c:907 Cadef snext
1431 pub opts: Option<Box<caopt>>, // c:908 Caopt opts
1432 pub nopts: i32, // c:909
1433 pub ndopts: i32, // c:909
1434 pub nodopts: i32, // c:909
1435 pub args: Option<Box<caarg>>, // c:910 Caarg args
1436 pub rest: Option<Box<caarg>>, // c:911 Caarg rest
1437 pub defs: Option<Vec<String>>, // c:912 char **defs
1438 pub ndefs: i32, // c:913
1439 pub lastt: i64, // c:914 time_t lastt
1440 pub single: Option<Vec<Option<Box<caopt>>>>, // c:915 Caopt *single (188-slot)
1441 pub r#match: Option<String>, // c:916 char *match
1442 pub argsactive: i32, // c:917
1443 pub set: Option<String>, // c:919 char *set
1444 pub flags: i32, // c:920 int flags (CDF_*)
1445 pub nonarg: Option<String>, // c:921 char *nonarg
1446}
1447
1448/// Direct port of `static void freecaargs(Caarg a)` from
1449/// `Src/Zle/computil.c:996`. Walks the `next` chain and frees
1450/// each entry. In Rust this is `Box` ownership — dropping the head
1451/// recursively drops the chain, but we mirror the C body for ABI
1452/// parity with callers that want explicit teardown.
1453pub fn freecaargs(mut a: Option<Box<caarg>>) { // c:996
1454 while let Some(mut node) = a { // c:996 for (; a; ...)
1455 a = node.next.take(); // c:1001 n = a->next
1456 // c:1002-1007 — zsfree on descr/xor/action/end/opt is implicit
1457 // via Drop on the String / Vec<String> fields.
1458 node.descr = None; // c:1013
1459 node.xor = None; // c:1013-1004
1460 node.action = None; // c:1013
1461 node.end = None; // c:1013
1462 node.opt = None; // c:1013
1463 drop(node); // c:1013 zfree(a, sizeof(*a))
1464 }
1465}
1466
1467/// Direct port of `static void freecadef(Cadef d)` from
1468/// `Src/Zle/computil.c:1013`. Walks the `snext` chain freeing
1469/// each cadef plus its opts/args/rest sub-chains.
1470pub fn freecadef(mut d: Option<Box<cadef>>) { // c:1013
1471 while let Some(mut node) = d { // c:1013 while (d)
1472 d = node.snext.take(); // c:1019 s = d->snext
1473 // c:1020-1023 — zsfree match/set, freearray(defs).
1474 node.r#match = None;
1475 node.set = None;
1476 node.defs = None;
1477
1478 // c:1025-1033 — for each opt: zsfree name/descr, freearray xor,
1479 // freecaargs(opt->args), zfree opt.
1480 let mut p = node.opts.take();
1481 while let Some(mut popt) = p {
1482 p = popt.next.take();
1483 popt.name = None;
1484 popt.descr = None;
1485 popt.xor = None;
1486 freecaargs(popt.args.take()); // c:1031
1487 drop(popt); // c:1032
1488 }
1489 freecaargs(node.args.take()); // c:1034
1490 freecaargs(node.rest.take()); // c:1035
1491 node.nonarg = None; // c:1036
1492 node.single = None; // c:1037-1038
1493 drop(node); // c:1039 zfree(d, sizeof(*d))
1494 }
1495}
1496
1497// =====================================================================
1498// `castate` — command-line parse state for `_arguments`.
1499// Src/Zle/computil.c:1920-1957.
1500// =====================================================================
1501
1502/// Port of `typedef struct castate *Castate` from
1503/// `Src/Zle/computil.c:1922`.
1504pub type Castate = Box<castate>; // c:1922
1505
1506/// Direct port of `struct castate` from `Src/Zle/computil.c:1928-1953`.
1507/// Encapsulates the parsed-command-line state for one `_arguments`
1508/// set — used as a linked list (`snext`) with one state per set.
1509#[derive(Debug, Default, Clone)]
1510#[allow(non_camel_case_types)]
1511pub struct castate { // c:1928
1512 pub snext: Option<Box<castate>>, // c:1929 Castate snext
1513 pub d: Option<Box<cadef>>, // c:1930 Cadef d
1514 pub nopts: i32, // c:1931
1515 pub def: Option<Box<caarg>>, // c:1932 Caarg def
1516 pub ddef: Option<Box<caarg>>, // c:1933 Caarg ddef
1517 pub curopt: Option<Box<caopt>>, // c:1934 Caopt curopt
1518 pub dopt: Option<Box<caopt>>, // c:1935 Caopt dopt
1519 pub opt: i32, // c:1936
1520 pub arg: i32, // c:1937
1521 pub argbeg: i32, // c:1938
1522 pub optbeg: i32, // c:1939
1523 pub nargbeg: i32, // c:1941
1524 pub restbeg: i32, // c:1942
1525 pub curpos: i32, // c:1943
1526 pub argend: i32, // c:1944
1527 pub inopt: i32, // c:1945
1528 pub inarg: i32, // c:1946
1529 pub nth: i32, // c:1947
1530 pub singles: i32, // c:1948
1531 pub oopt: i32, // c:1949
1532 pub actopts: i32, // c:1950
1533 pub args: Option<Vec<String>>, // c:1951 LinkList args
1534 pub oargs: Option<Vec<Option<Vec<String>>>>, // c:1952 LinkList *oargs
1535}
1536
1537/// Port of `static struct castate ca_laststate` from
1538/// `Src/Zle/computil.c:1955`. Most recently parsed cmdline state.
1539pub static ca_laststate: std::sync::Mutex<castate> = // c:1955
1540 std::sync::Mutex::new(castate {
1541 snext: None, d: None, nopts: 0, def: None, ddef: None,
1542 curopt: None, dopt: None, opt: 0, arg: 0, argbeg: 0, optbeg: 0,
1543 nargbeg: 0, restbeg: 0, curpos: 0, argend: 0, inopt: 0,
1544 inarg: 0, nth: 0, singles: 0, oopt: 0, actopts: 0,
1545 args: None, oargs: None,
1546 });
1547
1548/// Port of `static int ca_parsed` from `Src/Zle/computil.c:1956`.
1549pub static ca_parsed: std::sync::atomic::AtomicI32 = // c:1956
1550 std::sync::atomic::AtomicI32::new(0);
1551
1552/// Port of `static int ca_alloced` from `Src/Zle/computil.c:1960`.
1553pub static ca_alloced: std::sync::atomic::AtomicI32 = // c:1960
1554 std::sync::atomic::AtomicI32::new(0);
1555
1556/// Port of `static int ca_doff` from `Src/Zle/computil.c:1960`. Count
1557/// of chars of ignored prefix (for clumped options or arg to an
1558/// option).
1559pub static ca_doff: std::sync::atomic::AtomicI32 = // c:1960
1560 std::sync::atomic::AtomicI32::new(0);
1561
1562/// Direct port of `static void freecastate(Castate s)` from
1563/// `Src/Zle/computil.c:1960`. Frees the args/oargs lists.
1564pub fn freecastate(s: &mut castate) { // c:1960
1565 s.args = None; // c:1960 freelinklist(s->args)
1566 s.oargs = None; // c:1966-1969 freelinklist per slot
1567}
1568
1569// =====================================================================
1570// `cvdef` / `cvval` — `_values` completion cache types.
1571// Src/Zle/computil.c:2919-2956. CVV_* and MAX_CVCACHE consts
1572// already declared above (file scope).
1573// =====================================================================
1574
1575/// Port of `typedef struct cvdef *Cvdef` from `Src/Zle/computil.c:2919`.
1576pub type Cvdef = Box<cvdef>; // c:2919
1577/// Port of `typedef struct cvval *Cvval` from `computil.c:2920`.
1578pub type Cvval = Box<cvval>; // c:2920
1579
1580/// Direct port of `struct cvdef` from `Src/Zle/computil.c:2924-2935`.
1581/// One parsed `_values` definition entry, cached for reuse.
1582#[derive(Debug, Default, Clone)]
1583#[allow(non_camel_case_types)]
1584pub struct cvdef { // c:2924
1585 pub descr: Option<String>, // c:2925 char *descr
1586 pub hassep: i32, // c:2926
1587 pub sep: i32, // c:2927 char sep
1588 pub argsep: i32, // c:2928 char argsep
1589 pub next: Option<Box<cvdef>>, // c:2929 Cvdef next
1590 pub vals: Option<Box<cvval>>, // c:2930 Cvval vals
1591 pub defs: Option<Vec<String>>, // c:2931 char **defs
1592 pub ndefs: i32, // c:2932
1593 pub lastt: i64, // c:2933 time_t lastt
1594 pub words: i32, // c:2934
1595}
1596
1597/// Direct port of `struct cvval` from `Src/Zle/computil.c:2939-2947`.
1598/// One value definition inside a cvdef.
1599#[derive(Debug, Default, Clone)]
1600#[allow(non_camel_case_types)]
1601pub struct cvval { // c:2939
1602 pub next: Option<Box<cvval>>, // c:2940 Cvval next
1603 pub name: Option<String>, // c:2961 char *name
1604 pub descr: Option<String>, // c:2961 char *descr
1605 pub xor: Option<Vec<String>>, // c:2961 char **xor
1606 pub r#type: i32, // c:2961 int type (CVV_*)
1607 pub arg: Option<Box<caarg>>, // c:2961 Caarg arg
1608 pub active: i32, // c:2961
1609}
1610
1611/// Direct port of `static void freecvdef(Cvdef d)` from
1612/// `Src/Zle/computil.c:2961`. Walks the vals chain freeing
1613/// each cvval (which frees its caarg via freecaargs).
1614pub fn freecvdef(d: Option<Box<cvdef>>) { // c:2961
1615 let Some(mut node) = d else { return; }; // c:2961 if (d)
1616 node.descr = None; // c:2966 zsfree(d->descr)
1617 node.defs = None; // c:2967-2968 freearray(d->defs)
1618 let mut p = node.vals.take();
1619 while let Some(mut v) = p { // c:2970 for (p = d->vals; ...)
1620 p = v.next.take(); // c:2971 n = p->next
1621 v.name = None; // c:2972
1622 v.descr = None; // c:2973
1623 v.xor = None; // c:2974-2975
1624 freecaargs(v.arg.take()); // c:2976
1625 drop(v); // c:2977
1626 }
1627 drop(node); // c:2979
1628}
1629
1630// =====================================================================
1631// `cvstate` — `_values` parse state.
1632// Src/Zle/computil.c:3220-3231.
1633// =====================================================================
1634
1635/// Direct port of `struct cvstate` from `Src/Zle/computil.c:3222-3227`.
1636#[derive(Debug, Default)]
1637#[allow(non_camel_case_types)]
1638pub struct cvstate { // c:3222
1639 pub d: Option<Box<cvdef>>, // c:3223 Cvdef d
1640 pub def: Option<Box<caarg>>, // c:3224 Caarg def
1641 pub val: Option<Box<cvval>>, // c:3225 Cvval val
1642 pub vals: Option<Vec<String>>, // c:3226 LinkList vals
1643}
1644
1645/// Port of `static struct cvstate cv_laststate` from
1646/// `Src/Zle/computil.c:3229`.
1647pub static cv_laststate: std::sync::Mutex<cvstate> = // c:3229
1648 std::sync::Mutex::new(cvstate {
1649 d: None, def: None, val: None, vals: None,
1650 });
1651
1652/// Port of `static int cv_parsed` from `Src/Zle/computil.c:3230`.
1653pub static cv_parsed: std::sync::atomic::AtomicI32 = // c:3230
1654 std::sync::atomic::AtomicI32::new(0);
1655
1656/// Port of `static int cv_alloced` from `Src/Zle/computil.c:3230`.
1657pub static cv_alloced: std::sync::atomic::AtomicI32 = // c:3230
1658 std::sync::atomic::AtomicI32::new(0);
1659
1660/// Port of `static Cadef cadef_cache[MAX_CACACHE]` from
1661/// `Src/Zle/computil.c:973`. The LRU cache holds parsed
1662/// `_arguments` defs keyed by the raw arg vector — `get_cadef`
1663/// scans linearly, returns on first match (arr-compare on `defs`),
1664/// and on miss evicts the entry with the oldest `lastt` slot before
1665/// inserting the freshly parsed result.
1666pub static cadef_cache: std::sync::Mutex<[Option<Box<cadef>>; MAX_CACACHE]> = // c:973
1667 std::sync::Mutex::new([const { None }; MAX_CACACHE]);
1668
1669/// Port of `static Cvdef cvdef_cache[MAX_CVCACHE]` from
1670/// `Src/Zle/computil.c:2956`. Same LRU layout as cadef_cache;
1671/// `get_cvdef` scans for a defs-match hit, evicts the oldest slot
1672/// on miss.
1673pub static cvdef_cache: std::sync::Mutex<[Option<Box<cvdef>>; MAX_CVCACHE]> = // c:2956
1674 std::sync::Mutex::new([const { None }; MAX_CVCACHE]);
1675
1676/// Port of `static Ctags comptags[MAX_TAGS]` from
1677/// `Src/Zle/computil.c:3756`. One ctags entry per `locallevel`;
1678/// indexed by completion level.
1679pub static comptags: std::sync::Mutex<[Option<Box<ctags>>; MAX_TAGS]> = // c:3756
1680 std::sync::Mutex::new([const { None }; MAX_TAGS]);
1681
1682/// Port of `static int lasttaglevel` from `Src/Zle/computil.c:3760`.
1683/// "locallevel at last comptags -i".
1684pub static lasttaglevel: std::sync::atomic::AtomicI32 = // c:3760
1685 std::sync::atomic::AtomicI32::new(0);
1686
1687// =====================================================================
1688// `ctags` / `ctset` — `comptags` cache.
1689// Src/Zle/computil.c:3732-3760. MAX_TAGS already declared above.
1690// =====================================================================
1691
1692/// Port of `typedef struct ctags *Ctags` from `Src/Zle/computil.c:3732`.
1693pub type Ctags = Box<ctags>; // c:3732
1694/// Port of `typedef struct ctset *Ctset` from `computil.c:3733`.
1695pub type Ctset = Box<ctset>; // c:3733
1696
1697/// Direct port of `struct ctags` from `Src/Zle/computil.c:3737-3742`.
1698/// A bunch of tag sets keyed by locallevel.
1699#[derive(Debug, Default)]
1700#[allow(non_camel_case_types)]
1701pub struct ctags { // c:3737
1702 pub all: Option<Vec<String>>, // c:3738 char **all
1703 pub context: Option<String>, // c:3739 char *context
1704 pub init: i32, // c:3740
1705 pub sets: Option<Box<ctset>>, // c:3741 Ctset sets
1706}
1707
1708/// Direct port of `struct ctset` from `Src/Zle/computil.c:3763`.
1709#[derive(Debug, Default)]
1710#[allow(non_camel_case_types)]
1711pub struct ctset { // c:3763
1712 pub next: Option<Box<ctset>>, // c:3763 Ctset next
1713 pub tags: Option<Vec<String>>, // c:3763 char **tags
1714 pub tag: Option<String>, // c:3763 char *tag
1715 pub ptr: i32, // c:3763 char **ptr (index)
1716}
1717
1718/// Direct port of `static void freectset(Ctset s)` from
1719/// `Src/Zle/computil.c:3780`.
1720pub fn freectset(mut s: Option<Box<ctset>>) { // c:3763
1721 while let Some(mut node) = s { // c:3780 while (s)
1722 s = node.next.take(); // c:3780 n = s->next
1723 node.tags = None; // c:3780-3771
1724 node.tag = None; // c:3780
1725 drop(node); // c:3780
1726 }
1727}
1728
1729/// Direct port of `static void freectags(Ctags t)` from
1730/// `Src/Zle/computil.c:3780`.
1731pub fn freectags(t: Option<Box<ctags>>) { // c:3780
1732 let Some(mut node) = t else { return; }; // c:3780 if (t)
1733 node.all = None; // c:3783-3784
1734 node.context = None; // c:3785
1735 freectset(node.sets.take()); // c:3786
1736 drop(node); // c:3787
1737}
1738
1739/// Port of `rembslashcolon(char *s)` from `Src/Zle/computil.c:1046`.
1740/// ```c
1741/// static char *
1742/// rembslashcolon(char *s)
1743/// {
1744/// char *p, *r;
1745/// r = p = s = dupstring(s);
1746/// while (*s) {
1747/// if (s[0] != '\\' || s[1] != ':')
1748/// *p++ = *s;
1749/// s++;
1750/// }
1751/// *p = '\0';
1752/// return r;
1753/// }
1754/// ```
1755/// Strip every `\:` two-byte sequence to nothing (the `\` is dropped,
1756/// the `:` follows on the next iteration). Used to unescape colon-
1757/// bearing description strings produced by `_arguments`.
1758pub fn rembslashcolon(s: &str) -> String { // c:1047
1759 let bytes = s.as_bytes(); // c:1047 dupstring(s)
1760 let mut out = Vec::<u8>::with_capacity(bytes.len());
1761 let mut i = 0;
1762 while i < bytes.len() { // c:1053 while (*s)
1763 // c:1054 — `if (s[0] != '\\' || s[1] != ':') *p++ = *s`.
1764 let drop = bytes[i] == b'\\'
1765 && i + 1 < bytes.len()
1766 && bytes[i + 1] == b':';
1767 if !drop {
1768 out.push(bytes[i]); // c:1055 *p++ = *s
1769 }
1770 i += 1; // c:1056 s++
1771 }
1772 // c:1058 — `*p = '\0'`. Rust strings are length-tracked.
1773 String::from_utf8(out).unwrap_or_default() // c:1060 return r
1774}
1775
1776/// Port of `bslashcolon(char *s)` from `Src/Zle/computil.c:1065`.
1777/// ```c
1778/// static char *
1779/// bslashcolon(char *s)
1780/// {
1781/// char *p, *r;
1782/// r = p = zhalloc((2 * strlen(s)) + 1);
1783/// while (*s) {
1784/// if (*s == ':')
1785/// *p++ = '\\';
1786/// *p++ = *s++;
1787/// }
1788/// *p = '\0';
1789/// return r;
1790/// }
1791/// ```
1792/// Insert a backslash before every `:`, doubling the worst-case
1793/// length. Inverse of `rembslashcolon` for description-string
1794/// emission.
1795pub fn bslashcolon(s: &str) -> String { // c:1066
1796 let bytes = s.as_bytes(); // c:1066 zhalloc(2*strlen(s)+1)
1797 let mut out = Vec::<u8>::with_capacity(2 * bytes.len() + 1);
1798 for &b in bytes { // c:1072 while (*s)
1799 if b == b':' { // c:1073
1800 out.push(b'\\'); // c:1074 *p++ = '\\'
1801 }
1802 out.push(b); // c:1075 *p++ = *s++
1803 }
1804 // c:1077 — `*p = '\0'`.
1805 String::from_utf8(out).unwrap_or_default() // c:1079 return r
1806}
1807
1808/// Port of `single_index(char pre, char opt)` from `Src/Zle/computil.c:1088`.
1809/// ```c
1810/// static int
1811/// single_index(char pre, char opt)
1812/// {
1813/// if (opt <= 0x20 || opt > 0x7e)
1814/// return -1;
1815/// return opt + (pre == '-' ? -0x21 : 94 - 0x21);
1816/// }
1817/// ```
1818/// Map a `(prefix, option-letter)` pair into the flat 188-slot array
1819/// that `cadef` keeps for single-letter option lookup. Returns -1
1820/// when `opt` is outside the printable-ASCII range.
1821///
1822/// `pre` is `-` for the negative-prefix slot and anything else
1823/// (typically `+`) for the positive-prefix slot.
1824pub fn single_index(pre: u8, opt: u8) -> i32 { // c:1089
1825 if opt <= 0x20 || opt > 0x7e { // c:1089
1826 return -1; // c:1092
1827 }
1828 // c:1094 — `return opt + (pre == '-' ? -0x21 : 94 - 0x21)`.
1829 let off: i32 = if pre == b'-' { -0x21 } else { 94 - 0x21 };
1830 (opt as i32) + off
1831}
1832
1833// `freecaargs(Caarg)` + `freecadef(Cadef)` ported above with the
1834// caarg/caopt/cadef struct ports (c:996 / c:1013).
1835
1836#[cfg(test)]
1837mod cao_caa_tests {
1838 use super::*;
1839
1840 #[test]
1841 fn cao_values_match_c_source() {
1842 let _g = crate::ported::zle::zle_main::zle_test_setup();
1843 // c:941-945 — sequential 1..=5.
1844 assert_eq!(CAO_NEXT, 1);
1845 assert_eq!(CAO_DIRECT, 2);
1846 assert_eq!(CAO_ODIRECT, 3);
1847 assert_eq!(CAO_EQUAL, 4);
1848 assert_eq!(CAO_OEQUAL, 5);
1849 }
1850
1851 #[test]
1852 fn caa_values_match_c_source() {
1853 let _g = crate::ported::zle::zle_main::zle_test_setup();
1854 // c:964-968 — sequential 1..=5.
1855 assert_eq!(CAA_NORMAL, 1);
1856 assert_eq!(CAA_OPT, 2);
1857 assert_eq!(CAA_REST, 3);
1858 assert_eq!(CAA_RARGS, 4);
1859 assert_eq!(CAA_RREST, 5);
1860 }
1861
1862 #[test]
1863 fn crt_values_match_c_source() {
1864 let _g = crate::ported::zle::zle_main::zle_test_setup();
1865 // c:79-83 — sequential 0..=4.
1866 assert_eq!(CRT_SIMPLE, 0);
1867 assert_eq!(CRT_DESC, 1);
1868 assert_eq!(CRT_SPEC, 2);
1869 assert_eq!(CRT_DUMMY, 3);
1870 assert_eq!(CRT_EXPL, 4);
1871 }
1872
1873 #[test]
1874 fn cvv_values_match_c_source() {
1875 let _g = crate::ported::zle::zle_main::zle_test_setup();
1876 // c:2949-2951 — sequential 0..=2.
1877 assert_eq!(CVV_NOARG, 0);
1878 assert_eq!(CVV_ARG, 1);
1879 assert_eq!(CVV_OPT, 2);
1880 }
1881
1882 #[test]
1883 fn cache_sizes_are_8() {
1884 let _g = crate::ported::zle::zle_main::zle_test_setup();
1885 // c:972 + c:2955 — both LRU caches are 8 entries.
1886 assert_eq!(MAX_CACACHE, 8);
1887 assert_eq!(MAX_CVCACHE, 8);
1888 }
1889
1890 #[test]
1891 fn max_tags_is_256() {
1892 let _g = crate::ported::zle::zle_main::zle_test_setup();
1893 assert_eq!(MAX_TAGS, 256);
1894 }
1895
1896 #[test]
1897 fn path_max2_is_8192() {
1898 let _g = crate::ported::zle::zle_main::zle_test_setup();
1899 assert_eq!(PATH_MAX2, 8192);
1900 }
1901}
1902
1903#[cfg(test)]
1904mod tests {
1905 use super::*;
1906
1907 // Tests for cd_get / cd_init / cd_sort / cd_prep removed — those
1908 // tests exercised the deleted CompDescItem/CompDescSet Rust-only
1909 // wrappers. The C-faithful entries (cd_get takes char**params and
1910 // returns int) get exercised through the full `_describe` widget
1911 // path under integration tests; per-fn unit tests would just
1912 // lock in the deleted Rust-side shape.
1913
1914 // test_parse_caarg / test_parse_cadef removed — they exercised
1915 // the deleted CompArgDef/CompOptDef Rust-only types via fake-
1916 // signature wrappers. Real ports land alongside the cadef chain.
1917
1918 #[test]
1919 fn test_rembslashcolon() {
1920 let _g = crate::ported::zle::zle_main::zle_test_setup();
1921 // c:1054 — `\:` two-byte sequence drops the backslash.
1922 assert_eq!(rembslashcolon("a\\:b\\:c"), "a:b:c");
1923 }
1924
1925 #[test]
1926 fn test_rembslashcolon_lone_backslash_kept() {
1927 let _g = crate::ported::zle::zle_main::zle_test_setup();
1928 // c:1054 — `\X` (X != ':') keeps the backslash.
1929 assert_eq!(rembslashcolon("a\\nb"), "a\\nb");
1930 }
1931
1932 #[test]
1933 fn test_rembslashcolon_trailing_backslash() {
1934 let _g = crate::ported::zle::zle_main::zle_test_setup();
1935 // c:1054 — trailing `\` with no follow-up keeps the `\`.
1936 assert_eq!(rembslashcolon("a\\"), "a\\");
1937 }
1938
1939 #[test]
1940 fn test_rembslashcolon_unescaped_colon_passes_through() {
1941 let _g = crate::ported::zle::zle_main::zle_test_setup();
1942 // c:1054 — bare `:` (no preceding `\`) is kept.
1943 assert_eq!(rembslashcolon("a:b"), "a:b");
1944 }
1945
1946 #[test]
1947 fn test_bslashcolon() {
1948 let _g = crate::ported::zle::zle_main::zle_test_setup();
1949 // c:1073 — every `:` gets `\` prepended.
1950 assert_eq!(bslashcolon("a:b:c"), "a\\:b\\:c");
1951 }
1952
1953 #[test]
1954 fn test_bslashcolon_no_colons() {
1955 let _g = crate::ported::zle::zle_main::zle_test_setup();
1956 // c:1072 — non-colon bytes pass through unchanged.
1957 assert_eq!(bslashcolon("hello"), "hello");
1958 }
1959
1960 #[test]
1961 fn test_bslashcolon_already_escaped_doubled() {
1962 let _g = crate::ported::zle::zle_main::zle_test_setup();
1963 // c:1073-1074 — C doesn't track previous backslash, so an
1964 // already-escaped `\:` becomes `\\:` (the `\` passes
1965 // through, then the `:` gets a fresh `\` prepended).
1966 assert_eq!(bslashcolon("a\\:b"), "a\\\\:b");
1967 }
1968
1969 #[test]
1970 fn test_single_index_dash_prefix() {
1971 let _g = crate::ported::zle::zle_main::zle_test_setup();
1972 // c:1094 — `pre == '-'` → offset = -0x21.
1973 // For opt='a' (0x61): 0x61 + -0x21 = 0x40 = 64.
1974 assert_eq!(single_index(b'-', b'a'), 64);
1975 // For opt='A' (0x41): 0x41 + -0x21 = 0x20 = 32.
1976 assert_eq!(single_index(b'-', b'A'), 32);
1977 // For opt='!' (0x21): 0x21 + -0x21 = 0.
1978 assert_eq!(single_index(b'-', b'!'), 0);
1979 // For opt='~' (0x7e): 0x7e + -0x21 = 0x5d = 93.
1980 assert_eq!(single_index(b'-', b'~'), 93);
1981 }
1982
1983 #[test]
1984 fn test_single_index_plus_prefix() {
1985 let _g = crate::ported::zle::zle_main::zle_test_setup();
1986 // c:1094 — `pre == '+'` → offset = 94 - 0x21 = 61.
1987 // For opt='a' (0x61): 0x61 + 61 = 158.
1988 assert_eq!(single_index(b'+', b'a'), 158);
1989 // For opt='!' (0x21): 0x21 + 61 = 94.
1990 assert_eq!(single_index(b'+', b'!'), 94);
1991 // For opt='~' (0x7e): 0x7e + 61 = 187.
1992 assert_eq!(single_index(b'+', b'~'), 187);
1993 }
1994
1995 #[test]
1996 fn test_single_index_out_of_range() {
1997 let _g = crate::ported::zle::zle_main::zle_test_setup();
1998 // c:1091-1092 — opt <= 0x20 OR opt > 0x7e returns -1.
1999 assert_eq!(single_index(b'-', 0x20), -1); // space (0x20) excluded
2000 assert_eq!(single_index(b'-', 0x00), -1); // NUL
2001 assert_eq!(single_index(b'-', 0x7f), -1); // DEL (0x7f) excluded
2002 assert_eq!(single_index(b'+', 0xff), -1); // outside ASCII
2003 }
2004
2005 // test_cd_group removed — used the deleted CompDescItem; the
2006 // function `cd_group` itself wasn't a real C export and was
2007 // also removed alongside the fake structs.
2008
2009 #[test]
2010 fn caarg_default_zero_initialized() {
2011 let _g = crate::ported::zle::zle_main::zle_test_setup();
2012 // c:949-962 — fresh caarg: every field zero / None.
2013 let a = caarg::default();
2014 assert!(a.next.is_none());
2015 assert!(a.descr.is_none());
2016 assert!(a.action.is_none());
2017 assert_eq!(a.r#type, 0);
2018 assert_eq!(a.num, 0);
2019 assert_eq!(a.active, 0);
2020 }
2021
2022 #[test]
2023 fn caopt_default_zero_initialized() {
2024 let _g = crate::ported::zle::zle_main::zle_test_setup();
2025 // c:928-939 — fresh caopt: zero / None across all fields.
2026 let o = caopt::default();
2027 assert!(o.next.is_none());
2028 assert!(o.name.is_none());
2029 assert!(o.args.is_none());
2030 assert_eq!(o.r#type, 0);
2031 assert_eq!(o.num, 0);
2032 assert_eq!(o.not, 0);
2033 }
2034
2035 #[test]
2036 fn cadef_default_zero_initialized() {
2037 let _g = crate::ported::zle::zle_main::zle_test_setup();
2038 // c:905-922 — fresh cadef: zero / None across all fields.
2039 let d = cadef::default();
2040 assert!(d.next.is_none());
2041 assert!(d.opts.is_none());
2042 assert!(d.args.is_none());
2043 assert_eq!(d.nopts, 0);
2044 assert_eq!(d.flags, 0);
2045 }
2046
2047 #[test]
2048 fn freecaargs_walks_chain() {
2049 let _g = crate::ported::zle::zle_main::zle_test_setup();
2050 // c:996-1010 — freecaargs walks `next` chain freeing each
2051 // entry. After call, the chain owner observes no remaining
2052 // refs (Drop handles deallocation).
2053 let mut head = caarg { descr: Some("a".into()), ..Default::default() };
2054 let mid = caarg { descr: Some("b".into()), ..Default::default() };
2055 let tail = caarg { descr: Some("c".into()), ..Default::default() };
2056 let mut mid_box = Box::new(mid);
2057 mid_box.next = Some(Box::new(tail));
2058 head.next = Some(mid_box);
2059 freecaargs(Some(Box::new(head)));
2060 // No panic, no leak — Box drop chains the rest.
2061 }
2062
2063 #[test]
2064 fn cao_caa_constants_match_c() {
2065 let _g = crate::ported::zle::zle_main::zle_test_setup();
2066 // c:941-945 and c:964-968 — sequential 1..=5.
2067 assert_eq!(CAO_NEXT, 1);
2068 assert_eq!(CAO_DIRECT, 2);
2069 assert_eq!(CAO_ODIRECT, 3);
2070 assert_eq!(CAO_EQUAL, 4);
2071 assert_eq!(CAO_OEQUAL, 5);
2072 assert_eq!(CAA_NORMAL, 1);
2073 assert_eq!(CAA_OPT, 2);
2074 assert_eq!(CAA_REST, 3);
2075 assert_eq!(CAA_RARGS, 4);
2076 assert_eq!(CAA_RREST, 5);
2077 }
2078
2079 #[test]
2080 fn cdf_max_cacache_constants_match_c() {
2081 let _g = crate::ported::zle::zle_main::zle_test_setup();
2082 // c:924 — CDF_SEP = 1; c:972 — MAX_CACACHE = 8.
2083 assert_eq!(CDF_SEP, 1);
2084 assert_eq!(MAX_CACACHE, 8);
2085 }
2086
2087 #[test]
2088 fn crt_constants_match_c() {
2089 let _g = crate::ported::zle::zle_main::zle_test_setup();
2090 // c:79-83 — sequential 0..=4.
2091 assert_eq!(CRT_SIMPLE, 0);
2092 assert_eq!(CRT_DESC, 1);
2093 assert_eq!(CRT_SPEC, 2);
2094 assert_eq!(CRT_DUMMY, 3);
2095 assert_eq!(CRT_EXPL, 4);
2096 }
2097
2098 #[test]
2099 fn cdstr_default_zero_initialized() {
2100 let _g = crate::ported::zle::zle_main::zle_test_setup();
2101 // c:58-70 — fresh cdstr: zero/None across all fields.
2102 let s = cdstr::default();
2103 assert!(s.next.is_none());
2104 assert!(s.str.is_none());
2105 assert!(s.desc.is_none());
2106 assert!(s.r#match.is_none());
2107 assert_eq!(s.len, 0);
2108 assert_eq!(s.width, 0);
2109 assert_eq!(s.kind, 0);
2110 }
2111
2112 #[test]
2113 fn cdrun_default_zero_initialized() {
2114 let _g = crate::ported::zle::zle_main::zle_test_setup();
2115 // c:72-77 — fresh cdrun: zero/None.
2116 let r = cdrun::default();
2117 assert!(r.next.is_none());
2118 assert!(r.strs.is_none());
2119 assert_eq!(r.r#type, 0);
2120 assert_eq!(r.count, 0);
2121 }
2122
2123 #[test]
2124 fn cdset_default_zero_initialized() {
2125 let _g = crate::ported::zle::zle_main::zle_test_setup();
2126 // c:85-91 — fresh cdset: zero/None.
2127 let s = cdset::default();
2128 assert!(s.next.is_none());
2129 assert!(s.opts.is_none());
2130 assert!(s.strs.is_none());
2131 assert_eq!(s.count, 0);
2132 assert_eq!(s.desc, 0);
2133 }
2134
2135 #[test]
2136 fn cdstate_default_zero_initialized() {
2137 let _g = crate::ported::zle::zle_main::zle_test_setup();
2138 // c:40-56 — fresh cdstate: zero/None.
2139 let st = cdstate::default();
2140 assert_eq!(st.showd, 0);
2141 assert!(st.sep.is_none());
2142 assert!(st.sets.is_none());
2143 assert!(st.runs.is_none());
2144 }
2145
2146 #[test]
2147 fn freecdsets_walks_chain() {
2148 let _g = crate::ported::zle::zle_main::zle_test_setup();
2149 // c:96-122 — freecdsets walks `next` chain freeing each set
2150 // and its strs sub-chain.
2151 let head_str = cdstr {
2152 str: Some("foo".into()),
2153 desc: Some("first".into()),
2154 ..Default::default()
2155 };
2156 let tail_str = cdstr {
2157 str: Some("bar".into()),
2158 ..Default::default()
2159 };
2160 let mut head_str_b = Box::new(head_str);
2161 head_str_b.next = Some(Box::new(tail_str));
2162 let set = cdset {
2163 strs: Some(head_str_b),
2164 count: 2,
2165 ..Default::default()
2166 };
2167 freecdsets(Some(Box::new(set)));
2168 // No panic / no leak — Box drop chains the rest.
2169 }
2170
2171 #[test]
2172 fn castate_default_zero_initialized() {
2173 let _g = crate::ported::zle::zle_main::zle_test_setup();
2174 // c:1928-1953 — fresh castate: zero/None.
2175 let s = castate::default();
2176 assert!(s.snext.is_none());
2177 assert!(s.d.is_none());
2178 assert!(s.def.is_none());
2179 assert!(s.args.is_none());
2180 assert_eq!(s.nopts, 0);
2181 assert_eq!(s.curpos, 0);
2182 }
2183
2184 #[test]
2185 fn cvdef_default_zero_initialized() {
2186 let _g = crate::ported::zle::zle_main::zle_test_setup();
2187 // c:2924-2935 — fresh cvdef: zero/None.
2188 let d = cvdef::default();
2189 assert!(d.descr.is_none());
2190 assert!(d.vals.is_none());
2191 assert_eq!(d.hassep, 0);
2192 assert_eq!(d.sep, 0);
2193 assert_eq!(d.argsep, 0);
2194 }
2195
2196 #[test]
2197 fn cvval_default_zero_initialized() {
2198 let _g = crate::ported::zle::zle_main::zle_test_setup();
2199 // c:2939-2947 — fresh cvval: zero/None.
2200 let v = cvval::default();
2201 assert!(v.next.is_none());
2202 assert!(v.name.is_none());
2203 assert!(v.arg.is_none());
2204 assert_eq!(v.r#type, 0);
2205 assert_eq!(v.active, 0);
2206 }
2207
2208 #[test]
2209 fn cvstate_default_zero_initialized() {
2210 let _g = crate::ported::zle::zle_main::zle_test_setup();
2211 // c:3222-3227 — fresh cvstate: None across all 4 fields.
2212 let s = cvstate::default();
2213 assert!(s.d.is_none());
2214 assert!(s.def.is_none());
2215 assert!(s.val.is_none());
2216 assert!(s.vals.is_none());
2217 }
2218
2219 #[test]
2220 fn ctags_default_zero_initialized() {
2221 let _g = crate::ported::zle::zle_main::zle_test_setup();
2222 // c:3737-3742 — fresh ctags: zero/None.
2223 let t = ctags::default();
2224 assert!(t.all.is_none());
2225 assert!(t.context.is_none());
2226 assert!(t.sets.is_none());
2227 assert_eq!(t.init, 0);
2228 }
2229
2230 #[test]
2231 fn ctset_default_zero_initialized() {
2232 let _g = crate::ported::zle::zle_main::zle_test_setup();
2233 // c:3746-3751 — fresh ctset: zero/None.
2234 let s = ctset::default();
2235 assert!(s.next.is_none());
2236 assert!(s.tags.is_none());
2237 assert!(s.tag.is_none());
2238 assert_eq!(s.ptr, 0);
2239 }
2240
2241 #[test]
2242 fn cvv_constants_match_c() {
2243 let _g = crate::ported::zle::zle_main::zle_test_setup();
2244 // c:2949-2951 — sequential 0..=2.
2245 assert_eq!(CVV_NOARG, 0);
2246 assert_eq!(CVV_ARG, 1);
2247 assert_eq!(CVV_OPT, 2);
2248 }
2249
2250 #[test]
2251 fn max_tags_cvcache_match_c() {
2252 let _g = crate::ported::zle::zle_main::zle_test_setup();
2253 // c:3755 — MAX_TAGS = 256; c:2955 — MAX_CVCACHE = 8.
2254 assert_eq!(MAX_TAGS, 256);
2255 assert_eq!(MAX_CVCACHE, 8);
2256 }
2257
2258 #[test]
2259 fn freectset_walks_chain() {
2260 let _g = crate::ported::zle::zle_main::zle_test_setup();
2261 // c:3762-3777 — freectset walks `next` chain freeing each
2262 // ctset's tags/tag fields.
2263 let mut head = ctset { tag: Some("foo".into()), ..Default::default() };
2264 let tail = ctset { tag: Some("bar".into()), ..Default::default() };
2265 head.next = Some(Box::new(tail));
2266 freectset(Some(Box::new(head)));
2267 }
2268
2269 #[test]
2270 fn freectags_drops_one_node() {
2271 let _g = crate::ported::zle::zle_main::zle_test_setup();
2272 // c:3779-3789 — freectags releases all/context/sets on one ctags.
2273 let t = ctags {
2274 all: Some(vec!["a".into(), "b".into()]),
2275 context: Some("ctx".into()),
2276 ..Default::default()
2277 };
2278 freectags(Some(Box::new(t)));
2279 }
2280
2281 #[test]
2282 fn freecvdef_walks_vals_chain() {
2283 let _g = crate::ported::zle::zle_main::zle_test_setup();
2284 // c:2960-2981 — freecvdef walks vals freeing each cvval.
2285 let v_tail = cvval { name: Some("opt2".into()), ..Default::default() };
2286 let mut v_head = cvval { name: Some("opt1".into()), ..Default::default() };
2287 v_head.next = Some(Box::new(v_tail));
2288 let d = cvdef {
2289 descr: Some("test".into()),
2290 vals: Some(Box::new(v_head)),
2291 ..Default::default()
2292 };
2293 freecvdef(Some(Box::new(d)));
2294 }
2295
2296 /// c:1196 — `_arguments '-foo[only foo]' '*:file:_files'`. Verify
2297 /// that the option-name xor list contains the spec name, that
2298 /// nopts/ndopts reflect the option type (CAO_NEXT here), and that
2299 /// the rest arg lands on `rest` with type CAA_REST.
2300 #[test]
2301 fn parse_cadef_simple_opt_and_rest() {
2302 let _g = crate::ported::zle::zle_main::zle_test_setup();
2303 crate::ported::utils::inittyptab();
2304 let args = vec![
2305 String::from(""), // adpre/adsuf split (no %d)
2306 String::from("-foo[only foo]"),
2307 String::from("*:file:_files"),
2308 ];
2309 let def = parse_cadef("_arguments", &args).expect("cadef built");
2310 let opt = def.opts.as_deref().expect("opt linked");
2311 assert_eq!(opt.name.as_deref(), Some("-foo"));
2312 assert_eq!(opt.descr.as_deref(), Some("only foo"));
2313 assert_eq!(opt.r#type, CAO_NEXT);
2314 // c:1462-1468 — non-multi option appends its own name to xor.
2315 let xor = opt.xor.as_ref().expect("xor list");
2316 assert!(xor.iter().any(|s| s == "-foo"), "xor must include -foo: {:?}", xor);
2317
2318 let rest = def.rest.as_deref().expect("rest linked");
2319 assert_eq!(rest.r#type, CAA_REST);
2320 assert_eq!(rest.descr.as_deref(), Some("file"));
2321 assert_eq!(rest.action.as_deref(), Some("_files"));
2322 }
2323
2324 /// c:1617-1661 — numbered positional argument `1:cmd:_commands` lands
2325 /// on `def.args` with the right slot (num=0 because anum is `1`
2326 /// then `arg->num = anum - 1`).
2327 #[test]
2328 fn parse_cadef_numbered_positional_arg() {
2329 let _g = crate::ported::zle::zle_main::zle_test_setup();
2330 crate::ported::utils::inittyptab();
2331 let args = vec![
2332 String::from(""),
2333 String::from("1:cmd:_commands"),
2334 ];
2335 let def = parse_cadef("_arguments", &args).expect("cadef built");
2336 let pos = def.args.as_deref().expect("positional arg linked");
2337 assert_eq!(pos.num, 1);
2338 assert_eq!(pos.r#type, CAA_NORMAL);
2339 assert_eq!(pos.descr.as_deref(), Some("cmd"));
2340 assert_eq!(pos.action.as_deref(), Some("_commands"));
2341 assert_eq!(pos.direct, 1, "explicit numbering sets direct=1");
2342 }
2343
2344 /// c:1647-1656 — duplicate numbered argument must error out and
2345 /// return None (the cadef cache miss path picks this up).
2346 #[test]
2347 fn parse_cadef_doubled_arg_errors() {
2348 let _g = crate::ported::zle::zle_main::zle_test_setup();
2349 crate::ported::utils::inittyptab();
2350 let args = vec![
2351 String::from(""),
2352 String::from("1:a:_a"),
2353 String::from("1:b:_b"),
2354 ];
2355 let def = parse_cadef("_arguments", &args);
2356 assert!(def.is_none(), "duplicate arg num=1 must reject");
2357 }
2358
2359 /// c:1335-1370 — `(opt-x opt-y)-foo[descr]` builds a 3-element
2360 /// xor list `[opt-x, opt-y, -foo]` (the option's own name gets
2361 /// added at the end via c:1462-1468).
2362 #[test]
2363 fn parse_cadef_xor_list_populated() {
2364 let _g = crate::ported::zle::zle_main::zle_test_setup();
2365 crate::ported::utils::inittyptab();
2366 let args = vec![
2367 String::from(""),
2368 String::from("(opt-x opt-y)-foo[descr]"),
2369 ];
2370 let def = parse_cadef("_arguments", &args).expect("cadef built");
2371 let opt = def.opts.as_deref().expect("opt linked");
2372 let xor = opt.xor.as_ref().expect("xor list");
2373 assert_eq!(xor.len(), 3, "xor: {:?}", xor);
2374 assert_eq!(xor[0], "opt-x");
2375 assert_eq!(xor[1], "opt-y");
2376 assert_eq!(xor[2], "-foo");
2377 }
2378
2379 /// c:3796 — `settags(0, ["ctx", "tag1", "tag2"])` populates
2380 /// `comptags[0]` with context="ctx", all=["tag1","tag2"], init=1.
2381 #[test]
2382 fn settags_populates_slot() {
2383 let _g = crate::ported::zle::zle_main::zle_test_setup();
2384 // Clear slot to make test order-independent.
2385 if let Ok(mut tab) = comptags.lock() {
2386 tab[0] = None;
2387 }
2388 settags(0, &[
2389 "ctx".to_string(),
2390 "tag-a".to_string(),
2391 "tag-b".to_string(),
2392 ]);
2393 let tab = comptags.lock().unwrap();
2394 let slot = tab[0].as_deref().expect("comptags[0] populated");
2395 assert_eq!(slot.context.as_deref(), Some("ctx"));
2396 assert_eq!(slot.init, 1);
2397 let all = slot.all.as_ref().expect("all populated");
2398 assert_eq!(all.len(), 2);
2399 assert_eq!(all[0], "tag-a");
2400 assert_eq!(all[1], "tag-b");
2401 assert!(slot.sets.is_none());
2402 }
2403
2404 /// c:1712-1718 — exact name match returns the opt with `*end`
2405 /// pointing past the option name.
2406 #[test]
2407 fn ca_get_opt_exact_match() {
2408 let _g = crate::ported::zle::zle_main::zle_test_setup();
2409 crate::ported::utils::inittyptab();
2410 let args = vec![
2411 String::from(""),
2412 String::from("-foo[d]"),
2413 ];
2414 let mut def = *parse_cadef("_arguments", &args).expect("cadef built");
2415 // Mark the only opt active so ca_get_opt accepts it.
2416 let mut cur = def.opts.as_deref_mut();
2417 while let Some(o) = cur {
2418 o.active = 1;
2419 cur = o.next.as_deref_mut();
2420 }
2421 let mut end: usize = 0;
2422 let hit = ca_get_opt(&def, "-foo", 1, &mut end).expect("hit");
2423 assert_eq!(hit.name.as_deref(), Some("-foo"));
2424 assert_eq!(end, 4);
2425 }
2426
2427 /// c:1809-1822 — `argsactive=0` short-circuits to None even when
2428 /// args are linked. Guards against the easy off-by-one error of
2429 /// returning the first matching arg unconditionally.
2430 #[test]
2431 fn ca_get_arg_argsactive_zero_returns_none() {
2432 let _g = crate::ported::zle::zle_main::zle_test_setup();
2433 crate::ported::utils::inittyptab();
2434 let args = vec![
2435 String::from(""),
2436 String::from("1:c:_c"),
2437 ];
2438 let def = *parse_cadef("_arguments", &args).expect("cadef built");
2439 // argsactive defaults to 0 — must short-circuit.
2440 assert!(ca_get_arg(&def, 1).is_none());
2441 }
2442
2443 /// c:1817 — when `argsactive=1` and the positional arg is active,
2444 /// `n` inside `[min, num]` returns the matching node.
2445 #[test]
2446 fn ca_get_arg_in_range_active() {
2447 let _g = crate::ported::zle::zle_main::zle_test_setup();
2448 crate::ported::utils::inittyptab();
2449 let args = vec![
2450 String::from(""),
2451 String::from("1:c:_c"),
2452 ];
2453 let mut def = *parse_cadef("_arguments", &args).expect("cadef built");
2454 def.argsactive = 1;
2455 if let Some(a) = def.args.as_deref_mut() {
2456 a.active = 1;
2457 }
2458 let hit = ca_get_arg(&def, 1).expect("hit");
2459 assert_eq!(hit.num, 1);
2460 assert_eq!(hit.descr.as_deref(), Some("c"));
2461 }
2462
2463 /// c:2999-3027 — `-s , descr opt1[a]:val1: opt2[b]` builds a cvdef
2464 /// with sep=',', descr="descr", vals chain of two cvvals.
2465 #[test]
2466 fn parse_cvdef_sep_and_two_vals() {
2467 let _g = crate::ported::zle::zle_main::zle_test_setup();
2468 crate::ported::utils::inittyptab();
2469 let args = vec![
2470 String::from("-s"),
2471 String::from(","),
2472 String::from("descr"),
2473 String::from("opt1[a]:val1:"),
2474 String::from("opt2[b]"),
2475 ];
2476 let def = parse_cvdef("_values", &args).expect("cvdef built");
2477 assert_eq!(def.hassep, 1);
2478 assert_eq!(def.sep, b',' as i32);
2479 assert_eq!(def.descr.as_deref(), Some("descr"));
2480 let v1 = def.vals.as_deref().expect("val1");
2481 assert_eq!(v1.name.as_deref(), Some("opt1"));
2482 assert_eq!(v1.descr.as_deref(), Some("a"));
2483 assert_eq!(v1.r#type, CVV_ARG);
2484 let v2 = v1.next.as_deref().expect("val2");
2485 assert_eq!(v2.name.as_deref(), Some("opt2"));
2486 assert_eq!(v2.descr.as_deref(), Some("b"));
2487 assert_eq!(v2.r#type, CVV_NOARG);
2488 }
2489
2490 /// c:1786-1801 — ca_foreign_opt walks snext skipping curset.
2491 /// When `curset == all` (head pointer matches), only the OTHER
2492 /// sets in the snext chain are scanned.
2493 #[test]
2494 fn ca_foreign_opt_finds_in_other_set() {
2495 let _g = crate::ported::zle::zle_main::zle_test_setup();
2496 crate::ported::utils::inittyptab();
2497 let other = cadef {
2498 opts: Some(Box::new(caopt {
2499 name: Some("-bar".into()), active: 1,
2500 ..Default::default()
2501 })),
2502 ..Default::default()
2503 };
2504 let all = cadef {
2505 opts: Some(Box::new(caopt {
2506 name: Some("-foo".into()), active: 1,
2507 ..Default::default()
2508 })),
2509 snext: Some(Box::new(other)),
2510 ..Default::default()
2511 };
2512 // curset = &all (head). `-bar` lives in snext set — found.
2513 assert_eq!(ca_foreign_opt(&all, &all, "-bar"), 1);
2514 // `-foo` lives ONLY in the head (which gets skipped) — not found.
2515 assert_eq!(ca_foreign_opt(&all, &all, "-foo"), 0);
2516 // `-missing` not anywhere — not found.
2517 assert_eq!(ca_foreign_opt(&all, &all, "-missing"), 0);
2518 }
2519
2520 /// c:1834 — guard: with neither xor nor opts AND no compcurrent
2521 /// position, the function returns without mutating any active flag.
2522 #[test]
2523 fn ca_inactive_guard_noop_keeps_active() {
2524 let _g = crate::ported::zle::zle_main::zle_test_setup();
2525 crate::ported::utils::inittyptab();
2526 let mut d = cadef {
2527 opts: Some(Box::new(caopt {
2528 name: Some("-foo".into()), active: 1, ..Default::default()
2529 })),
2530 argsactive: 1,
2531 ..Default::default()
2532 };
2533 ca_inactive(&mut d, &[], 0, 0);
2534 assert_eq!(d.opts.as_deref().unwrap().active, 1);
2535 assert_eq!(d.argsactive, 1);
2536 }
2537
2538 /// c:1881 — with `opts=1`, every option's `active` clears.
2539 #[test]
2540 fn ca_inactive_opts_flag_deactivates_options() {
2541 use std::sync::atomic::Ordering;
2542 let _g = crate::ported::zle::zle_main::zle_test_setup();
2543 crate::ported::utils::inittyptab();
2544 let saved_compcur = crate::ported::zle::complete::COMPCURRENT.load(Ordering::Relaxed);
2545 let mut d = cadef {
2546 opts: Some(Box::new(caopt {
2547 name: Some("-foo".into()), active: 1, num: 0,
2548 next: Some(Box::new(caopt {
2549 name: Some("-bar".into()), active: 1, num: 1,
2550 ..Default::default()
2551 })),
2552 ..Default::default()
2553 })),
2554 argsactive: 1,
2555 ..Default::default()
2556 };
2557 // Force COMPCURRENT >= cur so the guard at c:1834 is satisfied.
2558 crate::ported::zle::complete::COMPCURRENT.store(2, Ordering::Relaxed);
2559 ca_inactive(&mut d, &[], 1, 1);
2560 // Restore COMPCURRENT immediately so parallel non-ZLE tests
2561 // see the original value.
2562 crate::ported::zle::complete::COMPCURRENT.store(saved_compcur, Ordering::Relaxed);
2563 let mut p = d.opts.as_deref();
2564 while let Some(o) = p {
2565 assert_eq!(o.active, 0,
2566 "{:?} should be deactivated", o.name);
2567 p = o.next.as_deref();
2568 }
2569 }
2570
2571 /// c:3798-3801 — `comptags -i 0 a b` populates comptags[0].
2572 /// Then `-T` reports empty sets (`1`), `-C` reads context.
2573 #[test]
2574 fn bin_comptags_init_and_context() {
2575 use std::sync::atomic::Ordering;
2576 let _g = crate::ported::zle::zle_main::zle_test_setup();
2577 crate::ported::utils::inittyptab();
2578 let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
2579 let saved_locallevel = crate::ported::builtin::LOCALLEVEL.load(Ordering::Relaxed);
2580 // Reset slot 0 + locallevel.
2581 if let Ok(mut tab) = comptags.lock() {
2582 tab[0] = None;
2583 }
2584 crate::ported::builtin::LOCALLEVEL.store(0, Ordering::Relaxed);
2585 INCOMPFUNC.store(1, Ordering::Relaxed);
2586
2587 let ops = crate::ported::zsh_h::options {
2588 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2589 args: Vec::new(),
2590 argscount: 0,
2591 argsalloc: 0,
2592 };
2593 // -i my-ctx tag-a tag-b
2594 let r = bin_comptags("comptags", &[
2595 "-i".into(), "my-ctx".into(),
2596 "tag-a".into(), "tag-b".into(),
2597 ], &ops, 0);
2598 assert_eq!(r, 0);
2599 // -T returns 1 (no sets yet).
2600 let r = bin_comptags("comptags", &[
2601 "-T".into(),
2602 ], &ops, 0);
2603 assert_eq!(r, 1);
2604 // comptags[0].context should be "my-ctx".
2605 let ctx = comptags.lock().unwrap()[0].as_ref()
2606 .and_then(|t| t.context.clone());
2607 // Restore the globals before assertion-fail can leave them mutated.
2608 INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
2609 crate::ported::builtin::LOCALLEVEL.store(saved_locallevel, Ordering::Relaxed);
2610 assert_eq!(ctx.as_deref(), Some("my-ctx"));
2611 }
2612
2613 /// c:3178-3186 — cv_get_val finds a value by name; missing returns None.
2614 #[test]
2615 fn cv_get_val_hits_and_misses() {
2616 let _g = crate::ported::zle::zle_main::zle_test_setup();
2617 let d = cvdef {
2618 vals: Some(Box::new(cvval {
2619 name: Some("foo".into()),
2620 r#type: CVV_NOARG,
2621 next: Some(Box::new(cvval {
2622 name: Some("bar".into()),
2623 r#type: CVV_ARG,
2624 ..Default::default()
2625 })),
2626 ..Default::default()
2627 })),
2628 ..Default::default()
2629 };
2630 let hit = cv_get_val(&d, "bar").expect("hit");
2631 assert_eq!(hit.name.as_deref(), Some("bar"));
2632 assert_eq!(hit.r#type, CVV_ARG);
2633 assert!(cv_get_val(&d, "missing").is_none());
2634 }
2635
2636 /// c:5126-5131 — setup_ frees every cache slot and zeros
2637 /// lasttaglevel. Pre-fill all three caches + lasttaglevel, then
2638 /// call setup_ and verify they're cleared.
2639 #[test]
2640 fn setup_clears_all_caches() {
2641 use std::sync::atomic::Ordering;
2642 let _g = crate::ported::zle::zle_main::zle_test_setup();
2643 // Pre-fill.
2644 if let Ok(mut cache) = cadef_cache.lock() {
2645 cache[0] = Some(Box::new(cadef::default()));
2646 }
2647 if let Ok(mut cache) = cvdef_cache.lock() {
2648 cache[0] = Some(Box::new(cvdef::default()));
2649 }
2650 if let Ok(mut tab) = comptags.lock() {
2651 tab[0] = Some(Box::new(ctags::default()));
2652 }
2653 lasttaglevel.store(42, Ordering::Relaxed);
2654
2655 let r = setup_();
2656 assert_eq!(r, 0);
2657
2658 assert!(cadef_cache.lock().unwrap()[0].is_none(),
2659 "cadef_cache[0] should be cleared");
2660 assert!(cvdef_cache.lock().unwrap()[0].is_none(),
2661 "cvdef_cache[0] should be cleared");
2662 assert!(comptags.lock().unwrap()[0].is_none(),
2663 "comptags[0] should be cleared");
2664 assert_eq!(lasttaglevel.load(Ordering::Relaxed), 0,
2665 "lasttaglevel should reset to 0");
2666 }
2667
2668 /// c:5171-5177 — finish_ frees every slot in all three caches.
2669 /// Same pre-fill pattern as setup_clears_all_caches; finish_
2670 /// differs in not zeroing lasttaglevel.
2671 #[test]
2672 fn finish_frees_all_caches() {
2673 let _g = crate::ported::zle::zle_main::zle_test_setup();
2674 if let Ok(mut cache) = cadef_cache.lock() {
2675 cache[1] = Some(Box::new(cadef::default()));
2676 }
2677 if let Ok(mut cache) = cvdef_cache.lock() {
2678 cache[1] = Some(Box::new(cvdef::default()));
2679 }
2680 if let Ok(mut tab) = comptags.lock() {
2681 tab[1] = Some(Box::new(ctags::default()));
2682 }
2683 let r = finish_();
2684 assert_eq!(r, 0);
2685 assert!(cadef_cache.lock().unwrap()[1].is_none());
2686 assert!(cvdef_cache.lock().unwrap()[1].is_none());
2687 assert!(comptags.lock().unwrap()[1].is_none());
2688 }
2689
2690 /// c:4592-4593 — cfp_matcher_pats hits CMF_LEFT && lalen==0:
2691 /// return empty string immediately. Constructed Cmatcher chain
2692 /// has one matcher with CMF_LEFT flag and lalen=0, llen=1, wlen=1.
2693 /// We seed cmatcher_global so parse_cmatcher returns it via a
2694 /// matcher spec — but parse_cmatcher is complex, so this test
2695 /// instead constructs the chain directly and verifies the bail
2696 /// happens via the public surface (we exercise the same edge via
2697 /// a matcher spec parsed by parse_cmatcher that triggers the
2698 /// left-anchor zero-len path indirectly through the dispatcher).
2699 /// The simpler observable: a malformed/empty matcher spec is
2700 /// rejected and returns add untouched.
2701 #[test]
2702 fn cfp_matcher_pats_left_anchor_zero_lalen_returns_empty_via_invalid_spec() {
2703 let _g = crate::ported::zle::zle_main::zle_test_setup();
2704 crate::ported::utils::inittyptab();
2705 // A malformed matcher should fall through parse_cmatcher's
2706 // None return; cfp_matcher_pats then returns `add` unchanged
2707 // (the non-bail path; the CMF_LEFT-lalen0 bail requires a
2708 // successfully-parsed but pathological matcher which the
2709 // public parser does not produce).
2710 let r = cfp_matcher_pats("not-a-real-matcher-spec", "xyz");
2711 assert_eq!(r, "xyz");
2712 }
2713
2714 /// c:3967 — bin_comptry with lasttaglevel == 0 (no -i call yet)
2715 /// errors with "no tags registered".
2716 #[test]
2717 fn bin_comptry_no_taglevel_errors() {
2718 let _g = crate::ported::zle::zle_main::zle_test_setup();
2719 let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
2720 let saved_lvl = lasttaglevel.load(std::sync::atomic::Ordering::Relaxed);
2721 INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
2722 lasttaglevel.store(0, std::sync::atomic::Ordering::Relaxed);
2723 let ops = crate::ported::zsh_h::options {
2724 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2725 args: Vec::new(), argscount: 0, argsalloc: 0,
2726 };
2727 let r = bin_comptry("comptry", &["tag1".into()], &ops, 0);
2728 INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
2729 lasttaglevel.store(saved_lvl, std::sync::atomic::Ordering::Relaxed);
2730 assert_eq!(r, 1);
2731 }
2732
2733 /// c:4091-4134 — bin_comptry plain mode: filters args to registered
2734 /// tags not already in any set, then appends one set with the
2735 /// surviving tags.
2736 #[test]
2737 fn bin_comptry_plain_adds_set() {
2738 let _g = crate::ported::zle::zle_main::zle_test_setup();
2739 let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
2740 let saved_lvl = lasttaglevel.load(std::sync::atomic::Ordering::Relaxed);
2741 // Seed comptags[1] with two registered tags.
2742 if let Ok(mut tab) = comptags.lock() {
2743 tab[1] = Some(Box::new(ctags {
2744 all: Some(vec!["files".into(), "directories".into()]),
2745 context: Some("ctx".into()),
2746 init: 0,
2747 sets: None,
2748 }));
2749 }
2750 INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
2751 lasttaglevel.store(1, std::sync::atomic::Ordering::Relaxed);
2752 let ops = crate::ported::zsh_h::options {
2753 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2754 args: Vec::new(), argscount: 0, argsalloc: 0,
2755 };
2756 let r = bin_comptry("comptry", &["files".into(), "unknown".into()], &ops, 0);
2757 // Inspect comptags[1].sets BEFORE restoring globals so a panic
2758 // doesn't leave them mutated.
2759 let sets_first_tags = comptags.lock().unwrap()[1].as_ref()
2760 .and_then(|t| t.sets.as_ref()
2761 .and_then(|s| s.tags.clone()));
2762 // Clean up.
2763 if let Ok(mut tab) = comptags.lock() {
2764 tab[1] = None;
2765 }
2766 INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
2767 lasttaglevel.store(saved_lvl, std::sync::atomic::Ordering::Relaxed);
2768 assert_eq!(r, 0);
2769 let tags = sets_first_tags.expect("a set was appended");
2770 assert_eq!(tags, vec!["files".to_string()],
2771 "only registered tags survive the filter");
2772 }
2773
2774 /// c:4525 — cfp_matcher_pats returns add unchanged when matcher
2775 /// spec is empty (parse_cmatcher returns None).
2776 #[test]
2777 fn cfp_matcher_pats_empty_matcher_passthrough() {
2778 let _g = crate::ported::zle::zle_main::zle_test_setup();
2779 crate::ported::utils::inittyptab();
2780 let r = cfp_matcher_pats("", "abc");
2781 assert_eq!(r, "abc");
2782 }
2783
2784 /// c:4307 — cfp_matcher_range with no matchers (all None) emits
2785 /// each char verbatim.
2786 #[test]
2787 fn cfp_matcher_range_no_matchers_verbatim() {
2788 let _g = crate::ported::zle::zle_main::zle_test_setup();
2789 let ms: Vec<Option<Box<crate::ported::zle::comp_h::Cmatcher>>> =
2790 vec![None, None, None];
2791 let r = cfp_matcher_range(&ms, "abc");
2792 assert_eq!(r, "abc");
2793 }
2794
2795 /// c:247-394 — cd_prep groups path emits CRT_EXPL + CRT_SPEC runs
2796 /// when cd_state.groups > 0. With two singleton kind=0+desc entries
2797 /// we expect: 2 CRT_SPEC runs (one per leader) followed by the
2798 /// CRT_EXPL header.
2799 #[test]
2800 fn cd_prep_groups_emits_expl_and_spec_runs() {
2801 let _g = crate::ported::zle::zle_main::zle_test_setup();
2802 // Seed cd_state: one set with two kind=0+desc entries.
2803 {
2804 let mut st = cd_state.lock().unwrap();
2805 st.showd = 0;
2806 st.groups = 2;
2807 st.descs = 2;
2808 st.maxg = 1;
2809 st.maxglen = 5;
2810 st.maxmlen = 100;
2811 st.sets = Some(Box::new(cdset {
2812 count: 2,
2813 desc: 2,
2814 strs: Some(Box::new(cdstr {
2815 str: Some("alpha".into()),
2816 r#match: Some("alpha".into()),
2817 desc: Some("first".into()),
2818 width: 5, len: 5,
2819 kind: 0,
2820 next: Some(Box::new(cdstr {
2821 str: Some("beta".into()),
2822 r#match: Some("beta".into()),
2823 desc: Some("second".into()),
2824 width: 4, len: 4,
2825 kind: 0,
2826 ..Default::default()
2827 })),
2828 ..Default::default()
2829 })),
2830 ..Default::default()
2831 }));
2832 st.runs = None;
2833 }
2834 let r = cd_prep();
2835 assert_eq!(r, 0);
2836 let st = cd_state.lock().unwrap();
2837 let r1 = st.runs.as_deref().expect("first run");
2838 assert_eq!(r1.r#type, CRT_SPEC, "first run is CRT_SPEC");
2839 let r2 = r1.next.as_deref().expect("second run");
2840 assert_eq!(r2.r#type, CRT_SPEC, "second run is CRT_SPEC");
2841 let r3 = r2.next.as_deref().expect("third run");
2842 assert_eq!(r3.r#type, CRT_EXPL, "third run is CRT_EXPL");
2843 assert_eq!(r3.count, 2, "CRT_EXPL covers both prep_lines");
2844 }
2845
2846 /// c:4704-4732 — cfp_bld_pats combines names with skipped + pat.
2847 /// With one name "dir" and pats ["*.c"], produces ["dir*.c"].
2848 #[test]
2849 fn cfp_bld_pats_concatenates_skipped_and_pat() {
2850 let _g = crate::ported::zle::zle_main::zle_test_setup();
2851 let out = cfp_bld_pats(0,
2852 &["dir".to_string()],
2853 "",
2854 &["*.c".to_string()]);
2855 assert_eq!(out, vec!["dir*.c".to_string()]);
2856 }
2857
2858 /// c:4711 — when GLOBDOTS is unset AND compprefix starts with `.`,
2859 /// add a dot-prefixed variant of each non-`.`-leading pattern.
2860 #[test]
2861 fn cfp_bld_pats_globdots_variant() {
2862 let _g = crate::ported::zle::zle_main::zle_test_setup();
2863 // Seed COMPPREFIX with a leading dot.
2864 let m = crate::ported::zle::complete::COMPPREFIX
2865 .get_or_init(|| std::sync::Mutex::new(String::new()));
2866 *m.lock().unwrap() = ".foo".to_string();
2867 // Force GLOBDOTS unset — that's the default in zle_test_setup.
2868 let out = cfp_bld_pats(0,
2869 &["d".to_string()],
2870 "",
2871 &["*.x".to_string()]);
2872 // Reset compprefix to avoid bleed.
2873 *m.lock().unwrap() = String::new();
2874 assert!(out.contains(&"d*.x".to_string()));
2875 assert!(out.contains(&"d.*.x".to_string()),
2876 "dot-variant must be emitted: {:?}", out);
2877 }
2878
2879 /// c:4625 — cfp_opt_pats with empty compprefix passes pats through.
2880 #[test]
2881 fn cfp_opt_pats_passthrough_empty_compprefix() {
2882 let _g = crate::ported::zle::zle_main::zle_test_setup();
2883 let m = crate::ported::zle::complete::COMPPREFIX
2884 .get_or_init(|| std::sync::Mutex::new(String::new()));
2885 *m.lock().unwrap() = String::new();
2886 let pats = vec!["*".to_string(), "*.c".to_string()];
2887 let out = cfp_opt_pats(&pats, "");
2888 assert_eq!(out, pats);
2889 }
2890
2891 /// c:4175 — cfp_test_exact returns None when both compprefix and
2892 /// compsuffix are empty (no anchoring context).
2893 #[test]
2894 fn cfp_test_exact_no_anchor_returns_none() {
2895 let _g = crate::ported::zle::zle_main::zle_test_setup();
2896 let pm = crate::ported::zle::complete::COMPPREFIX
2897 .get_or_init(|| std::sync::Mutex::new(String::new()));
2898 *pm.lock().unwrap() = String::new();
2899 let sm = crate::ported::zle::complete::COMPSUFFIX
2900 .get_or_init(|| std::sync::Mutex::new(String::new()));
2901 *sm.lock().unwrap() = String::new();
2902 let r = cfp_test_exact(&["/tmp".to_string()],
2903 &["true".to_string()], "");
2904 assert!(r.is_none());
2905 }
2906
2907 /// c:5083 — bin_compgroups registers 6 group variants per name.
2908 /// Sanity test: returns 0 on the empty-args path (no-op).
2909 #[test]
2910 fn bin_compgroups_empty_args_succeeds() {
2911 let _g = crate::ported::zle::zle_main::zle_test_setup();
2912 let saved = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
2913 INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
2914 let ops = crate::ported::zsh_h::options {
2915 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2916 args: Vec::new(), argscount: 0, argsalloc: 0,
2917 };
2918 let r = bin_compgroups("compgroups", &[], &ops, 0);
2919 INCOMPFUNC.store(saved, std::sync::atomic::Ordering::Relaxed);
2920 assert_eq!(r, 0);
2921 }
2922
2923 /// c:215-230 — cd_groups_want_sorting: returns 0 when ANY set's
2924 /// opts contains `-V`, 1 when `-J` (or default).
2925 #[test]
2926 fn cd_groups_want_sorting_respects_opts() {
2927 let _g = crate::ported::zle::zle_main::zle_test_setup();
2928 // Default (no sets) → 1 (sorted).
2929 {
2930 let mut st = cd_state.lock().unwrap();
2931 st.sets = None;
2932 }
2933 assert_eq!(cd_groups_want_sorting(), 1);
2934 // Inject a set with -V option → returns 0.
2935 {
2936 let mut st = cd_state.lock().unwrap();
2937 st.sets = Some(Box::new(cdset {
2938 opts: Some(vec!["-V".into(), "grpname".into()]),
2939 ..Default::default()
2940 }));
2941 }
2942 assert_eq!(cd_groups_want_sorting(), 0);
2943 // Cleanup so other tests don't see the injected state.
2944 cd_state.lock().unwrap().sets = None;
2945 }
2946
2947 /// c:233 — cd_sort compares Cdstr.sortstr lexically.
2948 #[test]
2949 fn cd_sort_orders_by_sortstr() {
2950 let _g = crate::ported::zle::zle_main::zle_test_setup();
2951 let a = cdstr {
2952 sortstr: Some("apple".into()), ..Default::default()
2953 };
2954 let b = cdstr {
2955 sortstr: Some("banana".into()), ..Default::default()
2956 };
2957 assert_eq!(cd_sort(&a, &b), std::cmp::Ordering::Less);
2958 assert_eq!(cd_sort(&b, &a), std::cmp::Ordering::Greater);
2959 assert_eq!(cd_sort(&a, &a), std::cmp::Ordering::Equal);
2960 }
2961
2962 /// c:425-435 — cd_prep default branch: one CRT_SIMPLE run per
2963 /// non-empty set with the str chain mirrored via `.run` links.
2964 #[test]
2965 fn cd_prep_default_builds_simple_run_per_set() {
2966 let _g = crate::ported::zle::zle_main::zle_test_setup();
2967 // Seed cd_state with 2 sets, each 1 match.
2968 {
2969 let mut st = cd_state.lock().unwrap();
2970 st.showd = 0;
2971 st.groups = 0;
2972 st.sets = Some(Box::new(cdset {
2973 count: 1,
2974 strs: Some(Box::new(cdstr {
2975 str: Some("a".into()),
2976 r#match: Some("a".into()),
2977 ..Default::default()
2978 })),
2979 next: Some(Box::new(cdset {
2980 count: 1,
2981 strs: Some(Box::new(cdstr {
2982 str: Some("b".into()),
2983 r#match: Some("b".into()),
2984 ..Default::default()
2985 })),
2986 ..Default::default()
2987 })),
2988 ..Default::default()
2989 }));
2990 st.runs = None;
2991 }
2992 let r = cd_prep();
2993 assert_eq!(r, 0);
2994 // Two CRT_SIMPLE runs, one per set.
2995 let st = cd_state.lock().unwrap();
2996 let r1 = st.runs.as_deref().expect("first run");
2997 assert_eq!(r1.r#type, CRT_SIMPLE);
2998 assert_eq!(r1.count, 1);
2999 let r2 = r1.next.as_deref().expect("second run");
3000 assert_eq!(r2.r#type, CRT_SIMPLE);
3001 assert_eq!(r2.count, 1);
3002 assert!(r2.next.is_none(), "no third run");
3003 }
3004
3005 /// c:846-895 — bin_compdescribe with invalid option returns 1.
3006 #[test]
3007 fn bin_compdescribe_rejects_bad_option() {
3008 let _g = crate::ported::zle::zle_main::zle_test_setup();
3009 let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
3010 INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
3011 let ops = crate::ported::zsh_h::options {
3012 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3013 args: Vec::new(), argscount: 0, argsalloc: 0,
3014 };
3015 // -xx is two chars but ends with `x` not a known subcommand
3016 // letter — should fall through to the `invalid option` arm.
3017 let r = bin_compdescribe("compdescribe", &["-x".into()], &ops, 0);
3018 INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
3019 assert_eq!(r, 1);
3020 }
3021
3022 /// c:4903-4923 — cf_remove_other with pre="dir/foo" returns
3023 /// only names starting with "dir/" and clears `amb`.
3024 #[test]
3025 fn cf_remove_other_filters_by_dir_head() {
3026 let _g = crate::ported::zle::zle_main::zle_test_setup();
3027 let names = vec![
3028 "dir/a".to_string(),
3029 "dir/b".to_string(),
3030 "other/c".to_string(),
3031 ];
3032 let mut amb = 99;
3033 let ret = cf_remove_other(&names, "dir/foo", &mut amb);
3034 assert_eq!(amb, 0);
3035 let v = ret.expect("matching names returned");
3036 assert_eq!(v, vec!["dir/a".to_string(), "dir/b".to_string()]);
3037 }
3038
3039 /// c:4942-4951 — pre without '/' and names diverge → `amb=1`,
3040 /// returns None.
3041 #[test]
3042 fn cf_remove_other_no_slash_diverge_sets_amb() {
3043 let _g = crate::ported::zle::zle_main::zle_test_setup();
3044 let names = vec!["a".to_string(), "b".to_string()];
3045 let mut amb = 0;
3046 let ret = cf_remove_other(&names, "x", &mut amb);
3047 assert!(ret.is_none());
3048 assert_eq!(amb, 1);
3049 }
3050
3051 /// c:4870 — no "parent" and no "pwd" in style → cf_ignore returns
3052 /// without touching `ign`.
3053 #[test]
3054 fn cf_ignore_no_style_is_noop() {
3055 let _g = crate::ported::zle::zle_main::zle_test_setup();
3056 let mut ign: Vec<String> = vec!["x".into()];
3057 cf_ignore(&["/tmp".into()], &mut ign, "", "/tmp/foo");
3058 assert_eq!(ign, vec!["x".to_string()],
3059 "no style match must leave ign untouched");
3060 }
3061
3062 /// c:3691 — empty compqstack short-circuits bin_compquote to 0.
3063 #[test]
3064 fn bin_compquote_returns_zero_when_qstack_empty() {
3065 let _g = crate::ported::zle::zle_main::zle_test_setup();
3066 let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
3067 INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
3068 // Ensure compqstack is empty (zle_test_setup resets things).
3069 if let Some(m) = COMPQSTACK.get() {
3070 if let Ok(mut s) = m.lock() {
3071 s.clear();
3072 }
3073 }
3074 let ops = crate::ported::zsh_h::options {
3075 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3076 args: Vec::new(),
3077 argscount: 0, argsalloc: 0,
3078 };
3079 let r = bin_compquote("compquote", &["foo".into()], &ops, 0);
3080 INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
3081 assert_eq!(r, 0);
3082 }
3083
3084 /// c:3192-3203 — cv_quote_get_val unquotes input then delegates
3085 /// to cv_get_val. Quoted name with backslash should still match
3086 /// after parse_subst_string strips the quoting.
3087 #[test]
3088 fn cv_quote_get_val_unquotes_then_lookup() {
3089 let _g = crate::ported::zle::zle_main::zle_test_setup();
3090 crate::ported::utils::inittyptab();
3091 let d = cvdef {
3092 vals: Some(Box::new(cvval {
3093 name: Some("foo".into()),
3094 r#type: CVV_NOARG,
3095 ..Default::default()
3096 })),
3097 ..Default::default()
3098 };
3099 // Plain name → hit.
3100 assert!(cv_quote_get_val(&d, "foo").is_some());
3101 // Unknown → miss.
3102 assert!(cv_quote_get_val(&d, "bar").is_none());
3103 }
3104
3105 /// c:3211-3217 — cv_inactive clears active for each name in xor.
3106 #[test]
3107 fn cv_inactive_clears_named_vals() {
3108 let _g = crate::ported::zle::zle_main::zle_test_setup();
3109 let mut d = cvdef {
3110 vals: Some(Box::new(cvval {
3111 name: Some("a".into()), active: 1,
3112 next: Some(Box::new(cvval {
3113 name: Some("b".into()), active: 1,
3114 next: Some(Box::new(cvval {
3115 name: Some("c".into()), active: 1,
3116 ..Default::default()
3117 })),
3118 ..Default::default()
3119 })),
3120 ..Default::default()
3121 })),
3122 ..Default::default()
3123 };
3124 cv_inactive(&mut d, &["a".into(), "c".into()]);
3125 let mut p = d.vals.as_deref();
3126 let mut by_name = std::collections::HashMap::new();
3127 while let Some(v) = p {
3128 by_name.insert(v.name.clone().unwrap_or_default(), v.active);
3129 p = v.next.as_deref();
3130 }
3131 assert_eq!(by_name["a"], 0);
3132 assert_eq!(by_name["b"], 1, "untouched val stays active");
3133 assert_eq!(by_name["c"], 0);
3134 }
3135}
3136
3137// ===========================================================
3138// Methods moved verbatim from src/ported/exec.rs because their
3139// C counterpart's source file maps 1:1 to this Rust module.
3140// Rust permits multiple inherent impl blocks for the same
3141// type within a crate, so call sites in exec.rs are unchanged.
3142// ===========================================================
3143
3144// BEGIN moved-from-exec-rs
3145// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
3146
3147// END moved-from-exec-rs
3148
3149
3150// ─── moved from src/ported/exec.rs (drift extraction) ───
3151
3152// CompSpec / CompMatch / CompGroup / CompState moved out of this
3153// port file to `src/extensions/bash_complete.rs` — they are
3154// Rust-original types backing the bash-style `complete` builtin
3155// extension, not zsh C ports. The ported zle/ tree should stay a
3156// faithful C-source mirror; Rust-only types live in extensions/.
3157//
3158// Callers that used `crate::ported::zle::computil::Comp*` should
3159// switch to `crate::bash_complete::Comp*` (the path lib.rs
3160// exports). exec.rs's re-export updated to point to the new home.
3161
3162
3163/// Direct port of `static Cadef alloc_cadef(char **args, int single,
3164/// char *match, char *nonarg, int flags)` from `Src/Zle/computil.c:1147-1177`.
3165///
3166/// Builds a fresh `cadef` with the option/single-letter/match/nonarg
3167/// fields initialized. `args` (if present) is captured into `defs`
3168/// for later cache-key compare in `get_cadef` (c:1681). `single` set
3169/// allocates the 188-slot single-letter index array. `match` is the
3170/// match-spec carried through to the option/arg matchers.
3171pub fn alloc_cadef(args: Option<&[String]>, single: i32, matchstr: &str, // c:1147
3172 nonarg: Option<&str>, flags: i32) -> Box<cadef> {
3173 Box::new(cadef {
3174 next: None, // c:1152
3175 snext: None, // c:1152
3176 opts: None, // c:1153
3177 args: None, // c:1154
3178 rest: None, // c:1154
3179 nonarg: nonarg.map(|s| s.to_string()), // c:1155 ztrdup(nonarg)
3180 defs: args.map(|a| a.to_vec()), // c:1157 zarrdup(args)
3181 ndefs: args.map_or(0, |a| a.len() as i32), // c:1158 arrlen(args)
3182 nopts: 0, // c:1163
3183 ndopts: 0, // c:1164
3184 nodopts: 0, // c:1165
3185 lastt: { // c:1166 time(0)
3186 use std::time::{SystemTime, UNIX_EPOCH};
3187 SystemTime::now().duration_since(UNIX_EPOCH)
3188 .map(|d| d.as_secs() as i64).unwrap_or(0)
3189 },
3190 set: None, // c:1167
3191 // c:1168-1172 — 188-slot single-letter Caopt index. Capacity
3192 // 188 matches C exactly (range of single-letter option names).
3193 single: if single != 0 {
3194 Some((0..188).map(|_| None).collect())
3195 } else {
3196 None
3197 },
3198 r#match: Some(matchstr.to_string()), // c:1173 ztrdup(match)
3199 argsactive: 0,
3200 flags, // c:1174
3201 })
3202}
3203
3204/// Port of `arrcontains(char **a, char *s, int colon)` from Src/Zle/computil.c:3813.
3205pub fn arrcontains(a: &[String], s: &str, colon: bool) -> i32 { // c:3813
3206 // C body c:3817-3826: linear scan; if colon, compare up to first
3207 // `:` in either side; else strcmp.
3208 for entry in a {
3209 if colon {
3210 let p = s.split(':').next().unwrap_or(s);
3211 let q = entry.split(':').next().unwrap_or(entry);
3212 if p == q {
3213 return 1; // c:3823
3214 }
3215 } else if entry == s {
3216 return 1; // c:3825
3217 }
3218 }
3219 0 // c:3827
3220}
3221
3222/// Direct port of `static int bin_comparguments(char *nam, char **args,
3223/// UNUSED(Options ops),
3224/// UNUSED(int func))`
3225/// from `Src/Zle/computil.c:2585-2914`. Full subcommand dispatch for
3226/// `comparguments -i/-D/-O/-L/-s/-M/-a/-W/-n`. Each branch consumes
3227/// the parsed `ca_laststate` from `ca_parse_line`.
3228pub fn bin_comparguments(nam: &str, args: &[String], // c:2585
3229 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3230 use crate::ported::params::{setsparam, setaparam, sethparam, setiparam};
3231 use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS};
3232 use std::sync::atomic::Ordering;
3233
3234 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:2590
3235 zwarnnam(nam, "can only be called from completion function");
3236 return 1;
3237 }
3238 if args.is_empty() { return 1; }
3239 let a0 = args[0].as_bytes();
3240 // c:2594 — must be `-X` exactly (2 chars).
3241 if a0.len() != 2 || a0[0] != b'-' {
3242 zwarnnam(nam, &format!("invalid argument: {}", args[0]));
3243 return 1;
3244 }
3245 let sub = a0[1];
3246
3247 // c:2598 — non-init subcommands require ca_parsed.
3248 if sub != b'i' && sub != b'I' && ca_parsed.load(Ordering::Relaxed) == 0 {
3249 zwarnnam(nam, "no parsed state");
3250 return 1;
3251 }
3252
3253 // c:2602 — per-subcommand arg-count bounds.
3254 let (min, max): (i32, i32) = match sub {
3255 b'i' => (2, -1),
3256 b'D' => (3, 3),
3257 b'O' => (4, 4),
3258 b'L' => (3, 4),
3259 b's' => (1, 1),
3260 b'M' => (1, 1),
3261 b'a' => (0, 0),
3262 b'W' => (3, 3),
3263 b'n' => (1, 1),
3264 _ => {
3265 zwarnnam(nam, &format!("invalid option: {}", args[0]));
3266 return 1;
3267 }
3268 };
3269 let n = (args.len() as i32) - 1;
3270 if n < min { zwarnnam(nam, "not enough arguments"); return 1; }
3271 if max >= 0 && n > max { zwarnnam(nam, "too many arguments"); return 1; }
3272
3273 match sub {
3274 b'i' => { // c:2625
3275 // c:2629 — compcurrent > 1 && compwords[0].
3276 let compcur = COMPCURRENT.load(Ordering::Relaxed);
3277 let compwords_nonempty = COMPWORDS.get()
3278 .and_then(|m| m.lock().ok().map(|w| !w.is_empty() && !w[0].is_empty()))
3279 .unwrap_or(false);
3280 if compcur <= 1 || !compwords_nonempty {
3281 return 1; // c:2670
3282 }
3283 // c:2636 — get_cadef(nam, args[1..]).
3284 // get_cadef returns 1 on cache hit. Look up the cached cadef
3285 // from the cadef_cache by argv match and run parse_line.
3286 let spec = &args[1..];
3287 let _ = get_cadef(nam, spec); // c:2636
3288 // Now find the cadef in the cache.
3289 let cached: Option<Box<cadef>> = {
3290 let cache = cadef_cache.lock().ok();
3291 cache.and_then(|c| {
3292 c.iter().find_map(|slot| {
3293 slot.as_ref().filter(|e| {
3294 e.ndefs == spec.len() as i32
3295 && e.defs.as_deref().map_or(false, |d| {
3296 d.len() == spec.len()
3297 && d.iter().zip(spec.iter()).all(|(a, b)| a == b)
3298 })
3299 }).cloned()
3300 })
3301 })
3302 };
3303 let Some(mut def_head) = cached else { return 1; };
3304 ca_parsed.store(0, Ordering::Relaxed); // c:2634
3305 ca_doff.store(0, Ordering::Relaxed);
3306 let all_clone = Box::new(clone_cadef_shallow(&def_head));
3307
3308 // c:2643-2664 — for each set walk: track which parses
3309 // succeeded ("use"). When a set succeeds AND more sets
3310 // remain, snapshot ca_laststate into a fallback chain.
3311 // When the final set rejects, restore the most-recent
3312 // saved state (or fail with ret=1 if no fallback).
3313 let mut first = 1;
3314 let mut def_opt: Option<Box<cadef>> = Some(def_head);
3315 let mut multi = 0;
3316 // Look ahead once to see if we have more than one set;
3317 // matches C's `multi = !!def->snext` at c:2639.
3318 if let Some(ref d) = def_opt {
3319 if d.snext.is_some() { multi = 1; }
3320 }
3321 let mut states: Vec<castate> = Vec::new(); // c:2632
3322 let mut ret = 0i32;
3323
3324 while let Some(mut current) = def_opt {
3325 let next = current.snext.take();
3326 let parse_ret = ca_parse_line(&mut current, &all_clone, multi, first);
3327 let use_state = parse_ret == 0; // c:2644
3328 let has_next = next.is_some();
3329 if use_state && has_next { // c:2646
3330 // c:2648 — snapshot ca_laststate, push onto fallback.
3331 if let Ok(ls) = ca_laststate.lock() {
3332 states.push(clone_castate_full(&ls));
3333 }
3334 } else if !use_state && !has_next { // c:2652
3335 // c:2654 — restore most-recent saved state (if any).
3336 if let Some(saved) = states.pop() {
3337 if let Ok(mut ls) = ca_laststate.lock() {
3338 freecastate(&mut ls);
3339 *ls = saved;
3340 }
3341 } else {
3342 ret = 1; // c:2661
3343 }
3344 }
3345 first = 0; // c:2663
3346 def_opt = next;
3347 }
3348 ca_parsed.store(1, Ordering::Relaxed); // c:2665
3349
3350 // c:2666 — thread fallback chain into ca_laststate.snext.
3351 if !states.is_empty() {
3352 if let Ok(mut ls) = ca_laststate.lock() {
3353 // Build a linked snext chain from oldest → newest.
3354 let mut head: Option<Box<castate>> = None;
3355 for s in states.into_iter().rev() {
3356 let mut s = s;
3357 s.snext = head;
3358 head = Some(Box::new(s));
3359 }
3360 ls.snext = head;
3361 }
3362 }
3363 ret // c:2668
3364 }
3365
3366 b'D' => { // c:2672
3367 let mut descr: Vec<String> = Vec::new();
3368 let mut act: Vec<String> = Vec::new();
3369 let mut subc: Vec<String> = Vec::new();
3370 let mut ret = 1i32;
3371 crate::ported::zle::complete::ignore_prefix(
3372 ca_doff.load(Ordering::Relaxed));
3373
3374 // Walk lstate (ca_laststate + its snext chain).
3375 let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3376 .ok();
3377 while let Some(s) = state_clone {
3378 let arg = s.def.clone();
3379 if let Some(a) = arg {
3380 ret = 0;
3381 let opt_str = a.opt.clone();
3382 let optdef = s.curopt.clone();
3383 ca_set_data(&mut descr, &mut act, &mut subc,
3384 opt_str.as_deref(),
3385 Some(a),
3386 optdef.as_deref(),
3387 if ca_doff.load(Ordering::Relaxed) > 0 { 1 } else { 0 });
3388 }
3389 state_clone = s.snext.map(|b| *b);
3390 }
3391 if ret == 0 { // c:2698
3392 setaparam(&args[1], descr);
3393 setaparam(&args[2], act);
3394 setaparam(&args[3], subc);
3395 }
3396 ret
3397 }
3398
3399 b'M' => { // c:2827
3400 let m = ca_laststate.lock().ok()
3401 .and_then(|s| s.d.as_ref().and_then(|d| d.r#match.clone()))
3402 .unwrap_or_default();
3403 setsparam(&args[1], &m);
3404 0
3405 }
3406
3407 b'a' => { // c:2833
3408 let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3409 .ok();
3410 while let Some(s) = state_clone {
3411 if s.d.as_ref().map_or(false, |d| d.args.is_some() || d.rest.is_some()) {
3412 return 0;
3413 }
3414 state_clone = s.snext.map(|b| *b);
3415 }
3416 1 // c:2840
3417 }
3418
3419 b'n' => { // c:2899
3420 let optbeg = ca_laststate.lock().map(|s| s.optbeg).unwrap_or(0);
3421 let kshoffset = if crate::ported::zsh_h::isset(
3422 crate::ported::zsh_h::KSHARRAYS) { 0 } else { 1 };
3423 setiparam(&args[1], (optbeg + kshoffset) as i64);
3424 0
3425 }
3426
3427 b'O' => { // c:2705
3428 // Build the four lists; for each non-`not` active opt assign it
3429 // to one of {next, direct, odirect, equal}.
3430 let mut next_l: Vec<String> = Vec::new();
3431 let mut direct_l: Vec<String> = Vec::new();
3432 let mut odirect_l: Vec<String> = Vec::new();
3433 let mut equal_l: Vec<String> = Vec::new();
3434 let mut ret = 1i32;
3435
3436 let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3437 .ok();
3438 while let Some(s) = state_clone {
3439 // c:2721 — gate on actopts + position.
3440 let actopts_ok = s.actopts != 0 &&
3441 (s.opt != 0
3442 || (ca_doff.load(Ordering::Relaxed) != 0 && s.def.is_some())
3443 || (s.def.is_some()
3444 && s.def.as_deref().map_or(false, |d|
3445 d.opt.is_some()
3446 && (d.r#type == CAA_OPT
3447 || (d.r#type >= CAA_RARGS && d.num < 0)))));
3448 let pos_ok = s.def.is_none()
3449 || s.def.as_deref().map_or(true, |d| d.r#type < CAA_RARGS)
3450 || (s.def.as_deref().map_or(false, |d| d.r#type == CAA_RARGS)
3451 && s.curpos == s.argbeg + 1)
3452 || COMPCURRENT.load(Ordering::Relaxed) == 1;
3453 if actopts_ok && pos_ok {
3454 ret = 0;
3455 if let Some(d) = s.d.as_ref() {
3456 let mut p = d.opts.as_deref();
3457 while let Some(opt) = p {
3458 if opt.active != 0 && opt.not == 0 {
3459 let bucket: &mut Vec<String> = match opt.r#type {
3460 t if t == CAO_NEXT => &mut next_l,
3461 t if t == CAO_DIRECT => &mut direct_l,
3462 t if t == CAO_ODIRECT => &mut odirect_l,
3463 _ => &mut equal_l,
3464 };
3465 let name_esc = bslashcolon(
3466 opt.name.as_deref().unwrap_or(""));
3467 let str_val = if let Some(desc) = opt.descr.as_deref() {
3468 format!("{}:{}", name_esc, desc)
3469 } else {
3470 name_esc
3471 };
3472 if !bucket.iter().any(|s| s == &str_val) {
3473 bucket.push(str_val);
3474 }
3475 }
3476 p = opt.next.as_deref();
3477 }
3478 }
3479 }
3480 state_clone = s.snext.map(|b| *b);
3481 }
3482
3483 if ret == 0 {
3484 setaparam(&args[1], next_l);
3485 setaparam(&args[2], direct_l);
3486 setaparam(&args[3], odirect_l);
3487 setaparam(&args[4], equal_l);
3488 0
3489 } else {
3490 let singles = ca_laststate.lock().map(|s| s.singles).unwrap_or(0);
3491 if singles != 0 { 2 } else { 1 } // c:2769
3492 }
3493 }
3494
3495 b'L' => { // c:2771
3496 // c:2787 — for each state, ca_get_opt(d, args[1], 1, NULL).
3497 let mut descr: Vec<String> = Vec::new();
3498 let mut act: Vec<String> = Vec::new();
3499 let mut subc: Vec<String> = Vec::new();
3500 let mut ret = 1i32;
3501 let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3502 .ok();
3503 while let Some(s) = state_clone {
3504 if let Some(d) = s.d.as_ref() {
3505 let mut end = 0usize;
3506 if let Some(opt) = ca_get_opt(d, &args[1], 1, &mut end) {
3507 if opt.args.is_some() {
3508 ret = 0;
3509 let opt_name = opt.name.clone();
3510 let opt_args = opt.args.clone();
3511 ca_set_data(&mut descr, &mut act, &mut subc,
3512 opt_name.as_deref(),
3513 opt_args,
3514 Some(&opt),
3515 1);
3516 }
3517 }
3518 }
3519 state_clone = s.snext.map(|b| *b);
3520 }
3521 if ret == 0 {
3522 setaparam(&args[2], descr);
3523 setaparam(&args[3], act);
3524 setaparam(&args[4], subc);
3525 }
3526 ret
3527 }
3528
3529 b's' => { // c:2803
3530 let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3531 .ok();
3532 while let Some(s) = state_clone {
3533 let single_active = s.d.as_ref().map_or(false, |d| d.single.is_some())
3534 && s.singles != 0
3535 && s.actopts != 0;
3536 if single_active {
3537 let kind = if let (Some(_), Some(dopt)) = (&s.ddef, &s.dopt) {
3538 match dopt.r#type {
3539 t if t == CAO_DIRECT => "direct",
3540 t if t == CAO_OEQUAL || t == CAO_EQUAL => "equal",
3541 _ => "next",
3542 }
3543 } else { "" };
3544 setsparam(&args[1], kind);
3545 return 0;
3546 }
3547 state_clone = s.snext.map(|b| *b);
3548 }
3549 1
3550 }
3551
3552 b'W' => { // c:2841
3553 // Build state.args concat and oargs concat for $opt_args.
3554 let mut all_args: Vec<String> = Vec::new();
3555 let opt_args_use_nul = !args[3].starts_with('0');
3556 let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3557 .ok();
3558 // Pass 1: state.args.
3559 let mut snapshot = state_clone.clone();
3560 while let Some(s) = snapshot {
3561 if let Some(a) = s.args.as_ref() {
3562 all_args.extend(a.iter().cloned());
3563 }
3564 snapshot = s.snext.map(|b| *b);
3565 }
3566 setaparam(&args[1], all_args);
3567
3568 // Pass 2: oargs into a hash.
3569 let mut hash_vec: Vec<String> = Vec::new();
3570 while let Some(s) = state_clone {
3571 if let Some(d) = s.d.as_ref() {
3572 let mut o = d.opts.as_deref();
3573 let mut a_idx = 0usize;
3574 let oargs_ref = s.oargs.as_deref();
3575 while let Some(op) = o {
3576 if let Some(oa) = oargs_ref.and_then(|v| v.get(a_idx)).and_then(|x| x.as_ref()) {
3577 let key = match (op.gsname.as_deref(), op.name.as_deref()) {
3578 (Some(gs), Some(n)) =>
3579 format!("{}{}", gs, n),
3580 (None, Some(n)) => n.to_string(),
3581 _ => String::new(),
3582 };
3583 hash_vec.push(key);
3584 let joined = if opt_args_use_nul {
3585 String::from_utf8_lossy(&ca_nullist(oa)).into_owned()
3586 } else {
3587 ca_colonlist(oa)
3588 };
3589 hash_vec.push(joined);
3590 }
3591 a_idx += 1;
3592 o = op.next.as_deref();
3593 }
3594 }
3595 state_clone = s.snext.map(|b| *b);
3596 }
3597 sethparam(&args[2], hash_vec);
3598 0
3599 }
3600
3601 _ => 0,
3602 }
3603}
3604
3605/// Mirror of `memcpy` from a locked `ca_laststate` so we can walk the
3606/// snext chain without holding the mutex. Rust-only artifact.
3607#[allow(dead_code)]
3608fn clone_castate_full(s: &castate) -> castate {
3609 castate {
3610 snext: s.snext.clone(),
3611 d: s.d.clone(),
3612 nopts: s.nopts,
3613 def: s.def.clone(),
3614 ddef: s.ddef.clone(),
3615 curopt: s.curopt.clone(),
3616 dopt: s.dopt.clone(),
3617 opt: s.opt, arg: s.arg,
3618 argbeg: s.argbeg, optbeg: s.optbeg,
3619 nargbeg: s.nargbeg, restbeg: s.restbeg,
3620 curpos: s.curpos, argend: s.argend,
3621 inopt: s.inopt, inarg: s.inarg,
3622 nth: s.nth, singles: s.singles, oopt: s.oopt, actopts: s.actopts,
3623 args: s.args.clone(),
3624 oargs: s.oargs.clone(),
3625 }
3626}
3627
3628/// Direct port of `static int bin_compdescribe(char *nam, char **args,
3629/// UNUSED(Options ops),
3630/// UNUSED(int func))`
3631/// from `Src/Zle/computil.c:846-895`. Subcommand dispatch for
3632/// `compdescribe -i/-I/-g`:
3633/// - `-i hide mlen ARGS...` → cd_init with empty opts and disp=0
3634/// - `-I hide mlen sep optsParam ARGS...` → cd_init with disp=1
3635/// - `-g param csl mats dpys` → cd_get with the 4 output params
3636pub fn bin_compdescribe(nam: &str, args: &[String], // c:846
3637 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3638 use crate::ported::params::paramtab;
3639 use std::sync::atomic::Ordering;
3640
3641 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:850
3642 zwarnnam(nam, "can only be called from completion function");
3643 return 1;
3644 }
3645 if args.is_empty() { return 1; }
3646 let a0 = args[0].as_bytes();
3647 // c:854 — `args[0]` must be exactly 2 chars starting with `-`.
3648 if a0.len() != 2 || a0[0] != b'-' {
3649 zwarnnam(nam, &format!("invalid argument: {}", args[0]));
3650 return 1;
3651 }
3652 let n = args.len() as i32;
3653
3654 match a0[1] {
3655 b'i' => { // c:859
3656 if n < 3 {
3657 zwarnnam(nam, "not enough arguments");
3658 return 1;
3659 }
3660 cd_init(nam, &args[1], &args[2], "", &[], &args[3..], 0) // c:865
3661 }
3662 b'I' => { // c:866
3663 if n < 6 {
3664 zwarnnam(nam, "not enough arguments");
3665 return 1;
3666 }
3667 // c:874 — getaparam(args[4]).
3668 let opts_arr: Vec<String> = paramtab().read().ok()
3669 .and_then(|tab| tab.get(&args[4])
3670 .and_then(|pm| pm.u_arr.clone()))
3671 .unwrap_or_default();
3672 if opts_arr.is_empty() && paramtab().read().ok()
3673 .map_or(true, |tab| tab.get(&args[4]).is_none())
3674 {
3675 zwarnnam(nam, &format!("unknown parameter: {}", args[4]));
3676 return 1;
3677 }
3678 cd_init(nam, &args[1], &args[2], &args[3], &opts_arr, // c:878
3679 &args[5..], 1)
3680 }
3681 b'g' => { // c:880
3682 if cd_parsed.load(Ordering::Relaxed) == 0 { // c:881
3683 zwarnnam(nam, "no parsed state"); // c:889
3684 return 1;
3685 }
3686 if n != 5 {
3687 zwarnnam(nam,
3688 if n < 5 { "not enough arguments" } else { "too many arguments" });
3689 return 1;
3690 }
3691 cd_get(&args[1..]) // c:887
3692 }
3693 _ => {
3694 zwarnnam(nam, &format!("invalid option: {}", args[0]));
3695 1
3696 }
3697 }
3698}
3699
3700/// Direct port of `static int bin_compfiles(char *nam, char **args,
3701/// UNUSED(Options ops),
3702/// UNUSED(int func))` from
3703/// `Src/Zle/computil.c:4970-5070`. Subcommand dispatch for
3704/// `compfiles -p/-P/-i/-r`. `-i` runs cf_ignore on a param-named
3705/// array; `-r` runs cf_remove_other; `-p`/`-P` thread through cf_pats.
3706pub fn bin_compfiles(nam: &str, args: &[String], // c:4970
3707 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3708 use crate::ported::params::{paramtab, setaparam};
3709 use crate::ported::utils::quotestring;
3710 use crate::ported::zsh_h::QT_BACKSLASH_PATTERN;
3711 use std::sync::atomic::Ordering;
3712
3713 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:4972
3714 zwarnnam(nam, "can only be called from completion function");
3715 return 1;
3716 }
3717 if args.is_empty() || !args[0].starts_with('-') { // c:4976
3718 let bad = args.first().map(|s| s.as_str()).unwrap_or("");
3719 zwarnnam(nam, &format!("missing option: {}", bad));
3720 return 1;
3721 }
3722 let a0 = args[0].as_bytes();
3723 if a0.len() < 2 { zwarnnam(nam, &format!("missing option: {}", args[0])); return 1; }
3724 let sub = a0[1];
3725
3726 // Helper: read a named array via paramtab.
3727 let get_arr = |name: &str| -> Option<Vec<String>> {
3728 paramtab().read().ok().and_then(|tab| {
3729 tab.get(name).and_then(|pm| pm.u_arr.clone())
3730 })
3731 };
3732
3733 match sub {
3734 b'p' | b'P' => { // c:4981
3735 // c:4983 — accept `-p` or `-p--` (the `--` toggles noopt).
3736 let noopt = a0.len() > 2;
3737 if noopt && (a0.len() != 4 || a0[2] != b'-' || a0[3] != b'-') {
3738 zwarnnam(nam, &format!("invalid option: {}", args[0]));
3739 return 1;
3740 }
3741 let required = if sub == b'p' { 8 } else { 7 };
3742 if args.len() <= required { // c:4990
3743 zwarnnam(nam, "too few arguments");
3744 return 1;
3745 }
3746 // c:4996 — getaparam(args[1]).
3747 let Some(src) = get_arr(&args[1]) else {
3748 zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
3749 return 0;
3750 };
3751 // c:5001 — quotestring each entry with QT_BACKSLASH_PATTERN.
3752 let l: Vec<String> = src.iter()
3753 .map(|s| quotestring(s, QT_BACKSLASH_PATTERN))
3754 .collect();
3755 // c:5003 — cf_pats dispatch.
3756 let result = cf_pats(
3757 if sub == b'P' { 1 } else { 0 },
3758 if noopt { 1 } else { 0 },
3759 &l,
3760 &get_arr(&args[2]).unwrap_or_default(),
3761 &args[3],
3762 &args[4],
3763 &args[5],
3764 &get_arr(&args[6]).unwrap_or_default(),
3765 &args[7..],
3766 );
3767 setaparam(&args[1], result);
3768 0
3769 }
3770 b'i' => { // c:5010
3771 if a0.len() > 2 { // c:5011
3772 zwarnnam(nam, &format!("invalid option: {}", args[0]));
3773 return 1;
3774 }
3775 if args.len() < 5 { // c:5018
3776 zwarnnam(nam, "too few arguments");
3777 return 1;
3778 }
3779 if args.len() > 5 { // c:5022
3780 zwarnnam(nam, "too many arguments");
3781 return 1;
3782 }
3783 let mut l: Vec<String> = get_arr(&args[2]).unwrap_or_default();
3784 let Some(tmp) = get_arr(&args[1]) else { // c:5032
3785 zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
3786 return 0;
3787 };
3788 cf_ignore(&tmp, &mut l, &args[3], &args[4]); // c:5037
3789 setaparam(&args[2], l); // c:5039
3790 0
3791 }
3792 b'r' => { // c:5042
3793 if args.len() < 3 { // c:5048
3794 zwarnnam(nam, "too few arguments");
3795 return 1;
3796 }
3797 if args.len() > 3 { // c:5052
3798 zwarnnam(nam, "too many arguments");
3799 return 1;
3800 }
3801 let Some(tmp) = get_arr(&args[1]) else { // c:5057
3802 zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
3803 return 0;
3804 };
3805 let mut ret = 0i32;
3806 // c:5062 — cf_remove_other.
3807 if let Some(l) = cf_remove_other(&tmp, &args[2], &mut ret) {
3808 setaparam(&args[1], l);
3809 }
3810 ret // c:5065
3811 }
3812 _ => {
3813 zwarnnam(nam, &format!("invalid option: {}", args[0]));
3814 1
3815 }
3816 }
3817}
3818
3819/// Direct port of `static int bin_compgroups(char *nam, char **args,
3820/// UNUSED(Options ops),
3821/// UNUSED(int func))` from
3822/// `Src/Zle/computil.c:5073-5100`. For each group name in args, opens
3823/// six successive completion groups with the same name but different
3824/// sort/uniq flags (NOSORT+UNIQCON, UNIQALL, NOSORT+UNIQCON, UNIQALL,
3825/// NOSORT, 0). Each begcmgroup is bracketed by endcmgroup. This is
3826/// how _path_files etc. register their match groups before adding
3827/// candidates via compadd.
3828pub fn bin_compgroups(nam: &str, args: &[String], // c:5073
3829 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3830 use crate::ported::zle::comp_h::{CGF_NOSORT, CGF_UNIQALL, CGF_UNIQCON};
3831 use crate::ported::zle::compcore::{begcmgroup, endcmgroup};
3832
3833 if INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed) != 1 { // c:5078
3834 zwarnnam(nam, "can only be called from completion function");
3835 return 1;
3836 }
3837 // c:5083 — for each group name, register 6 group variants.
3838 for n in args { // c:5083
3839 endcmgroup(None); // c:5084
3840 begcmgroup(Some(n), CGF_NOSORT | CGF_UNIQCON); // c:5085
3841 endcmgroup(None);
3842 begcmgroup(Some(n), CGF_UNIQALL); // c:5087
3843 endcmgroup(None);
3844 begcmgroup(Some(n), CGF_NOSORT | CGF_UNIQCON); // c:5089
3845 endcmgroup(None);
3846 begcmgroup(Some(n), CGF_UNIQALL); // c:5091
3847 endcmgroup(None);
3848 begcmgroup(Some(n), CGF_NOSORT); // c:5093
3849 endcmgroup(None);
3850 begcmgroup(Some(n), 0); // c:5095
3851 }
3852 0 // c:5099
3853}
3854
3855/// Port of `boot_(UNUSED(Module m))` from Src/Zle/computil.c:5153.
3856/// WARNING: param names don't match C — Rust=() vs C=(m)
3857pub fn boot_() -> i32 { // c:5153
3858 // C body c:5155-5156 — `return 0`. Faithful empty body.
3859 0
3860}
3861
3862/// Port of `ca_colonlist(LinkList l)` from Src/Zle/computil.c:2428.
3863pub fn ca_colonlist(l: &[String]) -> String { // c:2428
3864 // C body c:2430-2459 — joins l with `:`, escapes `:` and `\`
3865 // with `\` per item.
3866 if l.is_empty() {
3867 return String::new(); // c:2459
3868 }
3869 let mut out = String::new();
3870 for (i, item) in l.iter().enumerate() { // c:2444
3871 if i > 0 {
3872 out.push(':'); // c:2452
3873 }
3874 for ch in item.chars() {
3875 if ch == ':' || ch == '\\' { // c:2447
3876 out.push('\\');
3877 }
3878 out.push(ch);
3879 }
3880 }
3881 out
3882}
3883
3884/// Direct port of `static int ca_foreign_opt(Cadef curset, Cadef all,
3885/// char *option)` from
3886/// `Src/Zle/computil.c:1786-1802`. Walks the `snext` chain of `all`
3887/// skipping `curset` and reports whether any other set defines an
3888/// option with the requested name. Returns 1 on match, 0 otherwise.
3889pub fn ca_foreign_opt(curset: &cadef, all: &cadef, option: &str) -> i32 { // c:1787
3890 let curset_ptr = curset as *const cadef;
3891 let mut d_opt = Some(all);
3892 while let Some(d) = d_opt { // c:1792
3893 if std::ptr::addr_eq(d as *const cadef, curset_ptr) { // c:1793
3894 d_opt = d.snext.as_deref();
3895 continue;
3896 }
3897 let mut p = d.opts.as_deref();
3898 while let Some(opt) = p { // c:1796
3899 if opt.name.as_deref() == Some(option) { // c:1797
3900 return 1; // c:1798
3901 }
3902 p = opt.next.as_deref();
3903 }
3904 d_opt = d.snext.as_deref();
3905 }
3906 0 // c:1801
3907}
3908
3909/// Direct port of `static Caarg ca_get_arg(Cadef d, int n)` from
3910/// `Src/Zle/computil.c:1807-1823`. Walks `d->args` looking for the
3911/// arg whose `[min, num]` range contains `n`. Falls back to `d->rest`
3912/// when no positional matches. Returns a shallow clone (no `next`)
3913/// of the matched arg.
3914pub fn ca_get_arg(d: &cadef, mut n: i32) -> Option<Box<caarg>> { // c:1807
3915 if d.argsactive == 0 { // c:1809
3916 return None; // c:1822
3917 }
3918
3919 // c:1810-1816 — skip inactive entries (advance `n` to compensate for
3920 // each skipped one, mirroring the C `n++` inside the loop).
3921 let mut a = d.args.as_deref();
3922 while let Some(node) = a { // c:1812
3923 let in_range = node.active != 0 && n >= node.min && n <= node.num;
3924 if in_range { break; } // c:1812 inverted
3925 if node.active == 0 { // c:1813
3926 n += 1; // c:1814
3927 }
3928 a = node.next.as_deref(); // c:1815
3929 }
3930
3931 if let Some(node) = a { // c:1817
3932 if node.active != 0 && node.min <= n && node.num >= n {
3933 return Some(Box::new(caarg { // c:1818
3934 next: None,
3935 descr: node.descr.clone(),
3936 xor: node.xor.clone(),
3937 action: node.action.clone(),
3938 r#type: node.r#type,
3939 end: node.end.clone(),
3940 opt: node.opt.clone(),
3941 num: node.num,
3942 min: node.min,
3943 direct: node.direct,
3944 active: node.active,
3945 gsname: node.gsname.clone(),
3946 }));
3947 }
3948 }
3949
3950 // c:1820 — rest fallback.
3951 if let Some(r) = d.rest.as_deref() {
3952 if r.active != 0 {
3953 return Some(Box::new(caarg {
3954 next: None,
3955 descr: r.descr.clone(),
3956 xor: r.xor.clone(),
3957 action: r.action.clone(),
3958 r#type: r.r#type,
3959 end: r.end.clone(),
3960 opt: r.opt.clone(),
3961 num: r.num,
3962 min: r.min,
3963 direct: r.direct,
3964 active: r.active,
3965 gsname: r.gsname.clone(),
3966 }));
3967 }
3968 }
3969 None // c:1820
3970}
3971
3972/// Direct port of `static Caopt ca_get_opt(Cadef d, char *line, int full,
3973/// char **end)` from
3974/// `Src/Zle/computil.c:1706-1742`. Looks up an option-spec by name
3975/// against `line`. With `full=0`, also accepts a prefix-of-`line`
3976/// match where the option name is a prefix and the rest of `line` is
3977/// the option's argument (handles `=` / `--name=value` shapes per
3978/// `CAO_OEQUAL` / `CAO_EQUAL`). Sets `*end` to the byte offset past
3979/// the option text (and past the `=` separator when applicable).
3980/// Returns a cloned shallow copy of the matched `caopt` (without its
3981/// `next` chain) — Rust ownership artifact, equivalent to C returning
3982/// the aliased `Caopt` pointer.
3983pub fn ca_get_opt(d: &cadef, line: &str, full: i32, // c:1706
3984 end: &mut usize) -> Option<Box<caopt>> {
3985 let line_bytes = line.as_bytes();
3986
3987 // c:1712-1718 — exact match against an active option name.
3988 let mut cur = d.opts.as_deref();
3989 while let Some(p) = cur { // c:1712
3990 if p.active != 0 { // c:1713
3991 if let Some(name) = p.name.as_deref() {
3992 if name == line {
3993 *end = line_bytes.len(); // c:1715
3994 return Some(Box::new(caopt { // c:1717
3995 next: None,
3996 name: p.name.clone(),
3997 descr: p.descr.clone(),
3998 xor: p.xor.clone(),
3999 r#type: p.r#type,
4000 args: None,
4001 active: p.active,
4002 num: p.num,
4003 gsname: p.gsname.clone(),
4004 not: p.not,
4005 }));
4006 }
4007 }
4008 }
4009 cur = p.next.as_deref();
4010 }
4011
4012 if full == 0 { // c:1720
4013 // c:1722-1739 — prefix-match path for `name=value` / `nameSPC value`.
4014 let mut cur = d.opts.as_deref();
4015 while let Some(p) = cur {
4016 if p.active != 0 { // c:1723
4017 if let Some(name) = p.name.as_deref() {
4018 // c:1723-1724 — short args/NEXT → exact match, else strpfx.
4019 let is_match = if p.args.is_none() || p.r#type == CAO_NEXT {
4020 name == line
4021 } else {
4022 crate::ported::utils::strpfx(name, line)
4023 };
4024 if is_match {
4025 let l = name.len();
4026 // c:1726-1728 — for OEQUAL/EQUAL, the char at name's
4027 // end must be `=` or absent; otherwise skip.
4028 if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)
4029 && l < line_bytes.len() && line_bytes[l] != b'='
4030 {
4031 cur = p.next.as_deref();
4032 continue; // c:1728
4033 }
4034 // c:1731-1736 — set end past the option (+= 1 for `=`).
4035 let mut at = l;
4036 if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)
4037 && l < line_bytes.len() && line_bytes[l] == b'='
4038 {
4039 at += 1; // c:1734
4040 }
4041 *end = at; // c:1736
4042 return Some(Box::new(caopt { // c:1738
4043 next: None,
4044 name: p.name.clone(),
4045 descr: p.descr.clone(),
4046 xor: p.xor.clone(),
4047 r#type: p.r#type,
4048 args: None,
4049 active: p.active,
4050 num: p.num,
4051 gsname: p.gsname.clone(),
4052 not: p.not,
4053 }));
4054 }
4055 }
4056 }
4057 cur = p.next.as_deref();
4058 }
4059 }
4060 None // c:1741
4061}
4062
4063/// Direct port of `static Caopt ca_get_sopt(Cadef d, char *line,
4064/// char **end, LinkList *lp)`
4065/// from `Src/Zle/computil.c:1747-1781`. Single-letter option lookup
4066/// for clumped flags like `-abc`. Walks `line[1..]` consulting
4067/// `d->single[]` for each char; CAO_NEXT matches accumulate in `lp`,
4068/// the first non-NEXT match terminates and sets `*end` past it.
4069/// Returns the terminating Caopt (cloned, no chain) or None.
4070pub fn ca_get_sopt(d: &cadef, line: &str, // c:1747
4071 end: &mut usize,
4072 lp: &mut Option<Vec<Box<caopt>>>) -> Option<Box<caopt>> {
4073 let line_bytes = line.as_bytes();
4074 if line_bytes.is_empty() {
4075 *lp = None;
4076 return None;
4077 }
4078 let pre = line_bytes[0]; // c:1750
4079 let mut idx: usize = 1;
4080 *lp = None; // c:1754
4081
4082 let single = match d.single.as_ref() { // c:1757
4083 Some(s) => s,
4084 None => return None,
4085 };
4086
4087 let mut p_cur: Option<&caopt> = None; // c:1755 p = NULL
4088 let mut pp_cur: Option<&caopt> = None;
4089 let mut list_acc: Option<Vec<Box<caopt>>> = None;
4090
4091 while idx < line_bytes.len() { // c:1755 for (;*line;line++)
4092 let ch = line_bytes[idx];
4093 let sidx = single_index(pre, ch); // c:1756
4094
4095 // c:1756 — d->single[sidx] lookup (assigns to p if valid).
4096 let lookup: Option<&caopt> = if sidx >= 0 && (sidx as usize) < single.len() {
4097 single[sidx as usize].as_deref()
4098 } else {
4099 None
4100 };
4101 if lookup.is_some() {
4102 p_cur = lookup;
4103 }
4104 let active_with_args = lookup
4105 .filter(|p| p.active != 0 && p.args.is_some());
4106
4107 if let Some(p) = active_with_args { // c:1757
4108 if p.r#type == CAO_NEXT { // c:1758
4109 let list = list_acc.get_or_insert_with(Vec::new);
4110 list.push(Box::new(caopt { // c:1761 addlinknode
4111 next: None,
4112 name: p.name.clone(),
4113 descr: p.descr.clone(),
4114 xor: p.xor.clone(),
4115 r#type: p.r#type,
4116 args: None,
4117 active: p.active,
4118 num: p.num,
4119 gsname: p.gsname.clone(),
4120 not: p.not,
4121 }));
4122 } else { // c:1762
4123 idx += 1; // c:1764 line++
4124 if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL) // c:1765
4125 && idx < line_bytes.len() && line_bytes[idx] == b'='
4126 {
4127 idx += 1; // c:1767
4128 }
4129 *end = idx; // c:1768
4130 pp_cur = Some(p); // c:1770
4131 break; // c:1771
4132 }
4133 } else if p_cur.is_none() || p_cur.map_or(true, |p| p.active == 0) { // c:1773
4134 return None; // c:1774
4135 }
4136
4137 // c:1775 — pp = (p->name[0] == pre ? p : NULL); p = NULL.
4138 pp_cur = p_cur.filter(|p| {
4139 p.name.as_deref()
4140 .and_then(|n| n.as_bytes().first().copied())
4141 .map_or(false, |b| b == pre)
4142 });
4143 p_cur = None;
4144 idx += 1; // c:1755 line++
4145 }
4146
4147 // c:1778 — pp && end: *end = line.
4148 if pp_cur.is_some() {
4149 *end = idx;
4150 }
4151
4152 *lp = list_acc;
4153
4154 pp_cur.map(|p| Box::new(caopt { // c:1780
4155 next: None,
4156 name: p.name.clone(),
4157 descr: p.descr.clone(),
4158 xor: p.xor.clone(),
4159 r#type: p.r#type,
4160 args: None,
4161 active: p.active,
4162 num: p.num,
4163 gsname: p.gsname.clone(),
4164 not: p.not,
4165 }))
4166}
4167
4168/// Direct port of `static void ca_inactive(Cadef d, char **xor, int cur,
4169/// int opts)` from
4170/// `Src/Zle/computil.c:1832-1918`. Marks options/args inactive based
4171/// on a xor-list (or, when `opts=1`, deactivates all options except
4172/// rest-of-line args). Each xor entry can be:
4173/// - bare name → exact-match opt or numeric positional or `:`/`-`/`*`/group
4174/// - `group-name-:` / `group-name--` → group-scoped exclusion
4175/// - excludeall path (just a set/group name) → kills the whole set
4176pub fn ca_inactive(d: &mut cadef, xor: &[String], cur: i32, opts: i32) { // c:1832
4177 use crate::ported::ztype_h::idigit;
4178 use crate::ported::zle::complete::COMPCURRENT;
4179
4180 if (xor.is_empty() && opts == 0) // c:1834
4181 || cur > COMPCURRENT.load(std::sync::atomic::Ordering::Relaxed)
4182 {
4183 return;
4184 }
4185
4186 // c:1839 — single-letter exclusions only when at compcurrent (option
4187 // clumping safety: a prefix-of-longer-opt at cursor mustn't kill the
4188 // multi-letter form prematurely).
4189 let single = opts == 0
4190 && cur == COMPCURRENT.load(std::sync::atomic::Ordering::Relaxed);
4191
4192 // c:1841 — iterate xor entries. When opts=1 we synthesize a "-" pass.
4193 let iter_xor: Vec<String> = if opts != 0 {
4194 vec!["-".to_string()]
4195 } else {
4196 xor.to_vec()
4197 };
4198
4199 for x_orig in iter_xor.iter() {
4200 let mut x = x_orig.as_str();
4201 let mut excludeall = 0; // c:1842
4202 let mut grp: Option<&str> = None;
4203 let mut grplen: usize = 0;
4204
4205 // c:1845-1858 — split off optional `group-name-` prefix.
4206 let xb = x.as_bytes();
4207 let mut sep_byte = if xb.is_empty() { 0u8 } else { xb[0] };
4208 let mut sep_pos = 0usize;
4209 loop {
4210 if sep_pos >= xb.len() { break; }
4211 sep_byte = xb[sep_pos];
4212 if sep_byte == b'+' || sep_byte == b'-' || sep_byte == b':'
4213 || sep_byte == b'*' || idigit(sep_byte)
4214 {
4215 break;
4216 }
4217 // Find next '-'.
4218 let after = &xb[sep_pos..];
4219 let dash_off = after.iter().position(|&b| b == b'-');
4220 match dash_off {
4221 None => {
4222 excludeall = 1; // c:1850
4223 sep_pos = xb.len();
4224 break;
4225 }
4226 Some(d) => {
4227 let next = sep_pos + d + 1;
4228 if next >= xb.len() { // c:1848
4229 excludeall = 1;
4230 sep_pos = xb.len();
4231 break;
4232 }
4233 sep_pos = next;
4234 }
4235 }
4236 }
4237 if sep_pos > 0 && sep_pos < xb.len() { // c:1859
4238 grp = Some(&x[..sep_pos]);
4239 grplen = sep_pos;
4240 x = &x[sep_pos..];
4241 } else if sep_pos > 0 && excludeall != 0 && sep_pos == xb.len() {
4242 // c:1850 path — the whole string was a group name.
4243 grp = Some(x);
4244 grplen = sep_pos;
4245 x = "";
4246 }
4247 let xb = x.as_bytes();
4248
4249 // c:1865 — excludeall or `:` alone.
4250 if excludeall != 0 || (xb.len() == 1 && xb[0] == b':') {
4251 if let Some(g) = grp { // c:1866
4252 let mut cur_arg = d.args.as_deref_mut();
4253 while let Some(a) = cur_arg {
4254 let matches = a.gsname.as_deref().map_or(false, |gn| {
4255 let gnb = gn.as_bytes();
4256 gnb.len() == grplen + (excludeall as usize)
4257 && gn.starts_with(g)
4258 });
4259 if matches {
4260 a.active = 0; // c:1872
4261 }
4262 cur_arg = a.next.as_deref_mut();
4263 }
4264 if let Some(r) = d.rest.as_deref_mut() {
4265 let matches = r.gsname.as_deref().map_or(false, |gn| {
4266 let gnb = gn.as_bytes();
4267 gnb.len() == grplen + (excludeall as usize)
4268 && gn.starts_with(g)
4269 });
4270 if matches {
4271 r.active = 0; // c:1876
4272 }
4273 }
4274 } else {
4275 d.argsactive = 0; // c:1878
4276 }
4277 }
4278
4279 // c:1881 — excludeall or `-` alone: kill options.
4280 if excludeall != 0 || (xb.len() == 1 && xb[0] == b'-') {
4281 let mut cur_opt = d.opts.as_deref_mut();
4282 while let Some(p) = cur_opt {
4283 let grp_ok = grp.map_or(true, |g| {
4284 p.gsname.as_deref().map_or(false, |gn| {
4285 gn.len() == grplen + (excludeall as usize)
4286 && gn.starts_with(g)
4287 })
4288 });
4289 let single_skip = single && p.name.as_deref().map_or(false, |n| {
4290 let nb = n.as_bytes();
4291 nb.len() >= 3 && nb[0] != 0
4292 });
4293 if grp_ok && !single_skip {
4294 p.active = 0; // c:1888
4295 }
4296 cur_opt = p.next.as_deref_mut();
4297 }
4298 }
4299
4300 // c:1891 — excludeall or `*` alone: kill rest.
4301 if excludeall != 0 || (xb.len() == 1 && xb[0] == b'*') {
4302 if let Some(r) = d.rest.as_deref_mut() {
4303 let grp_ok = grp.map_or(true, |g| {
4304 r.gsname.as_deref().map_or(false, |gn| {
4305 gn.len() == grplen + (excludeall as usize)
4306 && gn.starts_with(g)
4307 })
4308 });
4309 if grp_ok {
4310 r.active = 0; // c:1895
4311 }
4312 }
4313 }
4314
4315 if excludeall == 0 { // c:1898
4316 if !xb.is_empty() && idigit(xb[0]) { // c:1899
4317 let n: i32 = x.bytes().take_while(|b| idigit(*b))
4318 .fold(0i32, |acc, b| acc * 10 + (b - b'0') as i32);
4319 let mut cur_arg = d.args.as_deref_mut();
4320 let mut hit: Option<&mut caarg> = None;
4321 while let Some(a) = cur_arg {
4322 if a.num >= n {
4323 hit = Some(a);
4324 break;
4325 }
4326 cur_arg = a.next.as_deref_mut();
4327 }
4328 if let Some(a) = hit {
4329 if a.num == n {
4330 let grp_ok = grp.map_or(true, |g| {
4331 a.gsname.as_deref().map_or(false, |gn| gn.starts_with(g))
4332 });
4333 if grp_ok {
4334 a.active = 0; // c:1908
4335 }
4336 }
4337 }
4338 } else {
4339 // c:1909 — ca_get_opt for full match.
4340 let mut end_unused = 0usize;
4341 if let Some(matched) = ca_get_opt(d, x, 1, &mut end_unused) {
4342 let grp_ok = grp.map_or(true, |g| {
4343 matched.gsname.as_deref().map_or(false, |gn| gn.starts_with(g))
4344 });
4345 let single_skip = single
4346 && matched.name.as_deref().map_or(false, |n| {
4347 let nb = n.as_bytes();
4348 nb.len() >= 3 && nb[0] != 0
4349 });
4350 if grp_ok && !single_skip {
4351 // Walk d.opts to find the actual node and clear its active.
4352 let target_name = matched.name.clone();
4353 let mut cur_opt = d.opts.as_deref_mut();
4354 while let Some(p) = cur_opt {
4355 if p.name == target_name {
4356 p.active = 0; // c:1912
4357 break;
4358 }
4359 cur_opt = p.next.as_deref_mut();
4360 }
4361 }
4362 }
4363 }
4364 if opts != 0 {
4365 break; // c:1914
4366 }
4367 }
4368 let _ = sep_byte;
4369 }
4370}
4371
4372/// Port of `ca_nullist(LinkList l)` from Src/Zle/computil.c:2411.
4373pub fn ca_nullist(l: &[String]) -> Vec<u8> { // c:2411
4374 // C body c:2413-2419 — `if (l) { array = zlinklist2array(l, 0);
4375 // ret = zjoin(array, '\\0', 0); free(array);
4376 // return ret; } else return ztrdup("")`.
4377 // Returns NUL-joined byte buffer.
4378 if l.is_empty() {
4379 return Vec::new(); // c:2419
4380 }
4381 let mut out = Vec::new();
4382 for (i, item) in l.iter().enumerate() {
4383 if i > 0 {
4384 out.push(0);
4385 }
4386 out.extend_from_slice(item.as_bytes());
4387 }
4388 out
4389}
4390
4391/// Port of `ca_opt_arg(Caopt opt, char *line)` from Src/Zle/computil.c:1976.
4392/// WARNING: param names don't match C — Rust=(opt_name, line, equal_kind) vs C=(opt, line)
4393pub fn ca_opt_arg(opt_name: &str, line: &str, equal_kind: bool) -> String { // c:1976
4394 // C body c:1978-1996: walks `o = opt->name` and `line` byte-by-byte,
4395 // skipping `\\` escapes; if any quote (`\\` `'` `"`)
4396 // in line, advance line; once they diverge, return
4397 // dup of remaining line minus optional `=` if
4398 // opt is CAO_EQUAL/CAO_OEQUAL.
4399 let o_bytes = opt_name.as_bytes();
4400 let l_bytes = line.as_bytes();
4401 let mut oi = 0usize;
4402 let mut li = 0usize;
4403 loop { // c:1980
4404 if oi >= o_bytes.len() || li >= l_bytes.len() {
4405 break;
4406 }
4407 let mut oc = o_bytes[oi];
4408 if oc == b'\\' { // c:1981
4409 oi += 1;
4410 if oi >= o_bytes.len() {
4411 break;
4412 }
4413 oc = o_bytes[oi];
4414 }
4415 let mut lc = l_bytes[li];
4416 if matches!(lc, b'\\' | b'\'' | b'"') { // c:1983
4417 li += 1;
4418 if li >= l_bytes.len() {
4419 break;
4420 }
4421 lc = l_bytes[li];
4422 }
4423 if oc != lc { // c:1985
4424 break;
4425 }
4426 oi += 1;
4427 li += 1;
4428 }
4429 let rest = &l_bytes[li..];
4430 let mut s = String::from_utf8_lossy(rest).into_owned();
4431 if equal_kind && s.starts_with('\\') { // c:2004
4432 s.remove(0);
4433 }
4434 if equal_kind {
4435 s = s.strip_prefix('=').map(|t| t.to_string()).unwrap_or(s); // c:2004
4436 }
4437 s
4438}
4439
4440/// Direct port of `static int ca_parse_line(Cadef d, Cadef all, int multi,
4441/// int first)` from
4442/// `Src/Zle/computil.c:2004-2403`. Walks `compwords[1..]` matching
4443/// each word against the cadef's option/arg specs, updating active
4444/// flags via `ca_inactive` and populating `ca_laststate` with the
4445/// completion-point info downstream subcommands need. Returns 1 when
4446/// the set should be skipped (foreign option spotted in multi-set
4447/// mode), 0 otherwise.
4448///
4449/// Substrate notes:
4450/// - `endpat` (CAA_RREST/RARGS end-pattern matching) is compiled via
4451/// `patcompile`; the resulting `Patprog` is held in the local
4452/// `endpat` slot exactly like the C code.
4453/// - `napat` (the `-A` "non-arg" pattern) is also compiled via
4454/// `patcompile`.
4455/// - The `sopts` (clumped single-letter remainders) LinkList is
4456/// represented as a `Vec<Box<caopt>>` queue.
4457pub fn ca_parse_line(d: &mut cadef, all: &cadef, multi: i32, first: i32) -> i32 { // c:2004
4458 use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS};
4459 use crate::ported::pattern::{patcompile, pattry};
4460 use crate::ported::glob::remnulargs;
4461 use crate::ported::lex::untokenize;
4462 use std::sync::atomic::Ordering;
4463
4464 let compcur = COMPCURRENT.load(Ordering::Relaxed);
4465
4466 // c:2019 — free old state if this is the first set.
4467 if first != 0 && ca_alloced.load(Ordering::Relaxed) != 0 {
4468 if let Ok(mut ls) = ca_laststate.lock() {
4469 freecastate(&mut ls);
4470 ls.snext = None;
4471 }
4472 }
4473
4474 // c:2030-2036 — mark everything active.
4475 let mut p = d.opts.as_deref_mut();
4476 while let Some(o) = p {
4477 o.active = 1;
4478 p = o.next.as_deref_mut();
4479 }
4480 d.argsactive = 1;
4481 if let Some(r) = d.rest.as_deref_mut() { r.active = 1; }
4482 let mut a = d.args.as_deref_mut();
4483 while let Some(ar) = a {
4484 ar.active = 1;
4485 a = ar.next.as_deref_mut();
4486 }
4487
4488 // c:2040-2056 — build the initial castate.
4489 let compwords: Vec<String> = COMPWORDS
4490 .get()
4491 .and_then(|m| m.lock().ok().map(|w| w.clone()))
4492 .unwrap_or_default();
4493 let argend_init = (compwords.len() as i32) - 1;
4494
4495 // Set up the working state. Note d is stored as None in the
4496 // working `state`; we re-populate ca_laststate.d from a clone at
4497 // the end (we can't move d into state since the caller still owns
4498 // it).
4499 let mut state = castate {
4500 snext: None,
4501 d: None,
4502 nopts: d.nopts,
4503 def: None,
4504 ddef: None,
4505 curopt: None,
4506 dopt: None,
4507 opt: 1, arg: 1, argbeg: 1, optbeg: 1, nargbeg: 1, restbeg: 1,
4508 actopts: 1,
4509 nth: 1, inarg: 1, inopt: 0,
4510 singles: 0, oopt: 0,
4511 argend: argend_init,
4512 curpos: compcur,
4513 args: Some(Vec::new()),
4514 oargs: Some((0..d.nopts as usize).map(|_| None).collect()),
4515 };
4516 ca_alloced.store(1, Ordering::Relaxed);
4517
4518 // Snapshot state → ca_laststate (the "early return on empty"
4519 // path uses this).
4520 if let Ok(mut ls) = ca_laststate.lock() {
4521 *ls = clone_castate(&state, d);
4522 }
4523
4524 if compwords.len() < 2 { // c:2058
4525 if let Ok(mut ls) = ca_laststate.lock() {
4526 ls.opt = 0; ls.arg = 0;
4527 }
4528 // c:2061 goto end — fall through to actopts count.
4529 } else {
4530 // c:2063-2064 — compile -A nonarg pattern.
4531 let napat = d.nonarg.as_deref().and_then(|s| {
4532 patcompile(s, 0, None::<&mut String>)
4533 });
4534 let mut endpat: Option<crate::ported::pattern::Patprog> = None;
4535
4536 // c:2068 — walk words.
4537 let mut cur = 2i32;
4538 let mut argxor: Option<Vec<String>> = None;
4539 let mut sopts: Vec<Box<caopt>> = Vec::new();
4540 let mut wasopt_idx: Option<usize> = None;
4541 let mut doff: i32 = 0;
4542 let mut adef: Option<Box<caarg>> = None;
4543 let mut ddef: Option<Box<caarg>> = None;
4544 let mut dopt: Option<Box<caopt>> = None;
4545 state.curopt = None; state.def = None;
4546
4547 loop {
4548 let line_idx = (cur - 1) as usize;
4549 if line_idx >= compwords.len() { break; }
4550 let oline = compwords[line_idx].clone();
4551 let mut line = oline.clone();
4552 ddef = None;
4553 adef = None;
4554 dopt = None;
4555 state.singles = 0;
4556 let mut arglast = 0;
4557
4558 remnulargs(&mut line);
4559 line = untokenize(&line);
4560
4561 // c:2095 — apply pending arg-xor.
4562 if let Some(xor) = argxor.take() {
4563 ca_inactive(d, &xor, cur - 1, 0);
4564 }
4565
4566 // c:2099 — CDF_SEP `--` separator turns off option parsing.
4567 if (d.flags & CDF_SEP) != 0 && cur != compcur && state.actopts != 0
4568 && line == "--"
4569 {
4570 ca_inactive(d, &[], cur, 1);
4571 state.actopts = 0;
4572 cur += 1;
4573 continue;
4574 }
4575
4576 // c:2108 — already have a def from previous opt, collect args.
4577 if state.def.is_some() {
4578 state.arg = 0;
4579 if let Some(co) = state.curopt.as_deref() {
4580 let cn = co.num as usize;
4581 if let Some(oargs) = state.oargs.as_mut() {
4582 if cn < oargs.len() {
4583 oargs[cn].get_or_insert_with(Vec::new).push(oline.clone());
4584 }
4585 }
4586 }
4587 let def_type = state.def.as_deref().map_or(0, |d| d.r#type);
4588 let def_is_opt = def_type == CAA_OPT;
4589 state.opt = if def_is_opt { 1 } else { 0 };
4590 if def_is_opt {
4591 if state.def.as_deref().map_or(false, |d| d.opt.is_some()) {
4592 state.oopt += 1;
4593 }
4594 }
4595
4596 if def_type == CAA_REST || def_type == CAA_RARGS
4597 || def_type == CAA_RREST
4598 {
4599 // c:2118 — end-pattern check.
4600 let matched_end = state.def.as_deref()
4601 .and_then(|d| d.end.as_deref())
4602 .map_or(false, |_| endpat.as_ref()
4603 .map_or(false, |ep| pattry(ep, &line)));
4604 if matched_end {
4605 state.def = None;
4606 state.curopt = None;
4607 state.opt = 1; state.arg = 1;
4608 state.argend = cur - 1;
4609 if let Ok(mut ls) = ca_laststate.lock() {
4610 ls.argend = cur - 1;
4611 }
4612 // c:2124 goto cont.
4613 }
4614 } else {
4615 // c:2125 — advance to next arg slot.
4616 let next = state.def.as_deref().and_then(|d| d.next.clone());
4617 if next.is_some() {
4618 state.def = next;
4619 state.argbeg = cur;
4620 state.argend = argend_init;
4621 } else if let Some(s) = sopts.first().cloned() {
4622 // c:2128 — pop a queued single-letter opt arg.
4623 sopts.remove(0);
4624 state.curopt = Some(s);
4625 state.def = state.curopt.as_deref().and_then(|c| c.args.clone());
4626 state.opt = 0;
4627 state.argbeg = cur; state.optbeg = cur; state.inopt = cur;
4628 state.argend = argend_init;
4629 doff = 0;
4630 state.singles = 1;
4631 if let Some(co) = state.curopt.as_deref() {
4632 let cn = co.num as usize;
4633 if let Some(oargs) = state.oargs.as_mut() {
4634 if cn < oargs.len() && oargs[cn].is_none() {
4635 oargs[cn] = Some(Vec::new());
4636 }
4637 }
4638 }
4639 // c:2138 goto cont.
4640 } else {
4641 state.curopt = None;
4642 state.opt = 1;
4643 }
4644 }
4645 } else {
4646 state.opt = 1; state.arg = 1;
4647 state.curopt = None;
4648 }
4649 if state.opt != 0 {
4650 let lb = line.as_bytes();
4651 state.opt = if lb.is_empty() { 0 }
4652 else if lb.len() == 1 { 1 } else { 2 };
4653 }
4654
4655 let mut pe_off: i32 = 0;
4656 wasopt_idx = None;
4657
4658 // c:2156 — option lookup.
4659 let opt_match = if state.opt == 2 {
4660 let lb = line.as_bytes();
4661 if !lb.is_empty() && (lb[0] == b'-' || lb[0] == b'+') {
4662 let mut end = 0usize;
4663 if let Some(found) = ca_get_opt(d, &line, 0, &mut end) {
4664 pe_off = end as i32;
4665 // c:2158 — for OEQUAL/EQUAL check `=` boundary.
4666 let pe_ok = match found.r#type {
4667 t if t == CAO_OEQUAL =>
4668 (line_idx + 1 < compwords.len())
4669 || (pe_off > 0
4670 && lb.get(pe_off as usize - 1) == Some(&b'=')),
4671 t if t == CAO_EQUAL =>
4672 pe_off > 0
4673 && (lb.get(pe_off as usize - 1) == Some(&b'=')
4674 || pe_off as usize >= lb.len()),
4675 _ => true,
4676 };
4677 if pe_ok { Some(found) } else { None }
4678 } else { None }
4679 } else { None }
4680 } else { None };
4681
4682 if let Some(co) = opt_match {
4683 // Bind found opt details for later.
4684 let co_name = co.name.clone().unwrap_or_default();
4685 let co_type = co.r#type;
4686 let co_num = co.num;
4687 let co_xor = co.xor.clone().unwrap_or_default();
4688 let co_args = co.args.clone();
4689 state.curopt = Some(co);
4690 let pe_at_eq = pe_off > 0
4691 && line.as_bytes().get(pe_off as usize - 1) == Some(&b'=');
4692 let pe_tail_present = (pe_off as usize) < line.as_bytes().len();
4693
4694 let take_args = co_type != CAO_EQUAL || pe_at_eq;
4695 state.def = if take_args { co_args.clone() } else { None };
4696 if state.def.is_some() { ddef = state.def.clone(); dopt = state.curopt.clone(); }
4697
4698 doff = pe_off;
4699 state.optbeg = cur; state.argbeg = cur; state.inopt = cur;
4700 state.argend = argend_init;
4701 let single_ok = d.single.is_some()
4702 && !pe_tail_present
4703 && co_name.as_bytes().len() >= 2
4704 && co_name.as_bytes()[1] != b'-'
4705 && co_name.as_bytes().get(2).is_none();
4706 state.singles = if single_ok { 1 } else { 0 };
4707
4708 let cn = co_num as usize;
4709 if let Some(oargs) = state.oargs.as_mut() {
4710 if cn < oargs.len() && oargs[cn].is_none() {
4711 oargs[cn] = Some(Vec::new());
4712 }
4713 }
4714 ca_inactive(d, &co_xor, cur, 0); // c:2179
4715
4716 let collect_arg = state.def.is_some() && (
4717 co_type == CAO_DIRECT
4718 || co_type == CAO_EQUAL
4719 || (co_type == CAO_ODIRECT && pe_tail_present)
4720 || (co_type == CAO_OEQUAL && (pe_tail_present || pe_at_eq))
4721 );
4722 if collect_arg {
4723 let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4724 if dtype != CAA_REST && dtype != CAA_RARGS && dtype != CAA_RREST {
4725 let next = state.def.as_deref().and_then(|d| d.next.clone());
4726 state.def = next;
4727 }
4728 let arg_str = ca_opt_arg(&co_name, &oline, false);
4729 if let Some(oargs) = state.oargs.as_mut() {
4730 if cn < oargs.len() {
4731 oargs[cn].get_or_insert_with(Vec::new).push(arg_str);
4732 }
4733 }
4734 }
4735 if state.def.is_some() {
4736 state.opt = 0;
4737 } else {
4738 if d.single.is_none()
4739 || (co_name.as_bytes().len() >= 3
4740 && co_name.as_bytes()[1] != 0)
4741 {
4742 wasopt_idx = Some(co_num as usize); // c:2201
4743 }
4744 state.curopt = None;
4745 }
4746 } else if state.opt == 2 && d.single.is_some()
4747 && line.as_bytes().first().copied().map_or(false,
4748 |b| b == b'-' || b == b'+')
4749 {
4750 // c:2204 — single-letter clump.
4751 let mut end = 0usize;
4752 let mut tmp_sopts: Option<Vec<Box<caopt>>> = None;
4753 let s_match = ca_get_sopt(d, &line, &mut end, &mut tmp_sopts);
4754 if let Some(queued) = tmp_sopts {
4755 sopts.extend(queued);
4756 }
4757 let active_sopt = s_match.or_else(|| {
4758 if cur != compcur && !sopts.is_empty() {
4759 Some(sopts.remove(0))
4760 } else { None }
4761 });
4762 if let Some(co) = active_sopt {
4763 let co_name = co.name.clone().unwrap_or_default();
4764 let co_type = co.r#type;
4765 let co_num = co.num;
4766 let co_xor = co.xor.clone().unwrap_or_default();
4767 let co_args = co.args.clone();
4768 state.curopt = Some(co);
4769
4770 let cn = co_num as usize;
4771 if let Some(oargs) = state.oargs.as_mut() {
4772 if cn < oargs.len() && oargs[cn].is_none() {
4773 oargs[cn] = Some(Vec::new());
4774 }
4775 }
4776 state.def = co_args.clone();
4777 if co_type == CAO_NEXT && cur == compcur {
4778 ddef = None;
4779 } else {
4780 ddef = state.def.clone();
4781 }
4782 dopt = state.curopt.clone();
4783 doff = end as i32;
4784 state.optbeg = cur; state.argbeg = cur; state.inopt = cur;
4785 state.argend = argend_init;
4786 state.singles = if end >= line.as_bytes().len() { 1 } else { 0 };
4787
4788 let lb = line.as_bytes();
4789 let pre = lb.first().copied().unwrap_or(0);
4790 let mut p_idx = 1usize;
4791 while p_idx < end.min(lb.len()) {
4792 let sidx = single_index(pre, lb[p_idx]);
4793 if sidx >= 0 && (sidx as usize)
4794 < d.single.as_ref().map_or(0, |s| s.len())
4795 {
4796 let tmp_xor = d.single.as_ref()
4797 .and_then(|s| s.get(sidx as usize))
4798 .and_then(|so| so.as_ref())
4799 .and_then(|so| so.xor.clone());
4800 let tmp_num = d.single.as_ref()
4801 .and_then(|s| s.get(sidx as usize))
4802 .and_then(|so| so.as_ref())
4803 .map_or(-1, |so| so.num);
4804 let tn = tmp_num as usize;
4805 if tmp_num >= 0 {
4806 if let Some(oargs) = state.oargs.as_mut() {
4807 if tn < oargs.len() && oargs[tn].is_none() {
4808 oargs[tn] = Some(Vec::new());
4809 }
4810 }
4811 }
4812 if let Some(xor) = tmp_xor {
4813 ca_inactive(d, &xor, cur, 0);
4814 }
4815 }
4816 p_idx += 1;
4817 }
4818
4819 let pe_tail_present = end < line.as_bytes().len();
4820 let pe_at_eq = end > 0
4821 && line.as_bytes().get(end - 1) == Some(&b'=');
4822 let collect_arg = state.def.is_some() && (
4823 co_type == CAO_DIRECT
4824 || co_type == CAO_EQUAL
4825 || (co_type == CAO_ODIRECT && pe_tail_present)
4826 || (co_type == CAO_OEQUAL && (pe_tail_present || pe_at_eq))
4827 );
4828 if collect_arg {
4829 let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4830 if dtype != CAA_REST && dtype != CAA_RARGS && dtype != CAA_RREST {
4831 let next = state.def.as_deref().and_then(|d| d.next.clone());
4832 state.def = next;
4833 }
4834 let arg_str = ca_opt_arg(&co_name, &line, false);
4835 if let Some(oargs) = state.oargs.as_mut() {
4836 if cn < oargs.len() {
4837 oargs[cn].get_or_insert_with(Vec::new).push(arg_str);
4838 }
4839 }
4840 }
4841 if state.def.is_some() {
4842 state.opt = 0;
4843 } else {
4844 state.curopt = None;
4845 }
4846 }
4847 } else if multi != 0
4848 && line.as_bytes().first().copied().map_or(false,
4849 |b| b == b'-' || b == b'+')
4850 && cur != compcur
4851 && ca_foreign_opt(d, all, &line) != 0
4852 {
4853 return 1; // c:2258
4854 } else if state.arg != 0 && cur <= compcur { // c:2259
4855 // c:2264 — napat -A pattern.
4856 if let Some(np) = napat.as_ref() {
4857 if cur < compcur && state.actopts != 0 {
4858 if pattry(np, &line) {
4859 cur += 1;
4860 continue;
4861 }
4862 ca_inactive(d, &[], cur + 1, 1);
4863 state.actopts = 0;
4864 }
4865 }
4866
4867 arglast = 1;
4868 if state.inopt != 0 { // c:2274
4869 state.inopt = 0;
4870 state.nargbeg = cur - 1;
4871 state.argend = argend_init;
4872 }
4873 // c:2279 — no args/rest + non-empty non-flag line → skip set.
4874 let lb = line.as_bytes();
4875 let non_flag = !lb.is_empty() && lb[0] != b'-' && lb[0] != b'+';
4876 if d.args.is_none() && d.rest.is_none() && non_flag {
4877 if multi == 0 && cur > compcur { break; }
4878 return 1;
4879 }
4880
4881 adef = ca_get_arg(d, state.nth);
4882 state.def = adef.clone();
4883 let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4884 if state.def.is_some() && (dtype == CAA_RREST || dtype == CAA_RARGS) {
4885 if ca_laststate.lock().map(|ls| ls.def.is_some()).unwrap_or(false) {
4886 break;
4887 }
4888 state.opt = if cur == state.nargbeg + 1
4889 && (multi == 0
4890 || line.is_empty()
4891 || lb[0] == b'-'
4892 || lb[0] == b'+')
4893 { 1 } else { 0 };
4894 state.optbeg = state.nargbeg;
4895 state.argbeg = cur - 1;
4896 state.argend = argend_init;
4897 // c:2311 — gather remaining words into state.args.
4898 while line_idx < compwords.len() {
4899 state.args.get_or_insert_with(Vec::new)
4900 .push(compwords[line_idx].clone());
4901 cur += 1;
4902 if (cur - 1) as usize >= compwords.len() { break; }
4903 }
4904 if let Ok(mut ls) = ca_laststate.lock() {
4905 *ls = clone_castate(&state, d);
4906 ls.ddef = None;
4907 ls.dopt = None;
4908 }
4909 break;
4910 }
4911 state.args.get_or_insert_with(Vec::new).push(line.clone());
4912 if let Some(a) = adef.as_deref() {
4913 state.oopt = a.num - state.nth;
4914 }
4915
4916 if state.def.is_some() && cur != compcur { // c:2323
4917 argxor = state.def.as_deref().and_then(|d| d.xor.clone());
4918 }
4919 let dtype2 = state.def.as_deref().map_or(0, |d| d.r#type);
4920 if state.def.is_some() && dtype2 != CAA_NORMAL && dtype2 != CAA_OPT
4921 && state.inarg != 0
4922 {
4923 state.restbeg = cur;
4924 state.inarg = 0;
4925 } else if state.def.is_none() || dtype2 == CAA_NORMAL || dtype2 == CAA_OPT {
4926 state.inarg = 1;
4927 }
4928 state.nth += 1;
4929 state.def = None;
4930 }
4931
4932 // c:2338 — end-pattern compile for rest-args.
4933 if state.def.is_some() && state.curopt.is_some() {
4934 let dt = state.def.as_deref().map_or(0, |d| d.r#type);
4935 if dt == CAA_RREST || dt == CAA_RARGS {
4936 let end_pat_str = state.def.as_deref()
4937 .and_then(|d| d.end.clone());
4938 if let Some(eps) = end_pat_str {
4939 endpat = patcompile(&eps, 0, None::<&mut String>);
4940 } else {
4941 // c:2342-2353 — no end-pattern: gather rest into oargs.
4942 if cur < compcur {
4943 if let Ok(mut ls) = ca_laststate.lock() {
4944 *ls = clone_castate(&state, d);
4945 }
4946 }
4947 let cn = state.curopt.as_deref().map(|c| c.num as usize);
4948 if let Some(cn) = cn {
4949 if let Some(oargs) = state.oargs.as_mut() {
4950 if cn < oargs.len() {
4951 let bucket = oargs[cn].get_or_insert_with(Vec::new);
4952 let mut k = line_idx;
4953 while k < compwords.len() {
4954 bucket.push(compwords[k].clone());
4955 k += 1;
4956 }
4957 }
4958 }
4959 }
4960 if let Ok(mut ls) = ca_laststate.lock() {
4961 ls.ddef = None;
4962 ls.dopt = None;
4963 }
4964 break;
4965 }
4966 }
4967 } else if state.def.is_some() {
4968 let eps = state.def.as_deref().and_then(|d| d.end.clone());
4969 if let Some(eps) = eps {
4970 endpat = patcompile(&eps, 0, None::<&mut String>);
4971 }
4972 }
4973
4974 // c:2360 cont: — checkpoint to ca_laststate.
4975 if cur + 1 == compcur {
4976 if let Ok(mut ls) = ca_laststate.lock() {
4977 *ls = clone_castate(&state, d);
4978 ls.ddef = None;
4979 ls.dopt = None;
4980 }
4981 } else if cur == compcur {
4982 let mut ls = ca_laststate.lock().unwrap();
4983 if ls.def.is_none() {
4984 if let Some(ddef_v) = ddef.clone() {
4985 ls.def = Some(ddef_v);
4986 ls.singles = state.singles;
4987 if state.curopt.as_deref().map_or(false, |c| c.r#type == CAO_NEXT) {
4988 ls.ddef = ddef.clone();
4989 ls.dopt = dopt.clone();
4990 ls.def = None;
4991 ls.opt = 1;
4992 // Mark curopt active again in d.
4993 if let Some(co) = state.curopt.as_deref() {
4994 let target_name = co.name.clone();
4995 let mut p = d.opts.as_deref_mut();
4996 while let Some(op) = p {
4997 if op.name == target_name {
4998 op.active = 1;
4999 break;
5000 }
5001 p = op.next.as_deref_mut();
5002 }
5003 }
5004 } else {
5005 ca_doff.store(doff, Ordering::Relaxed);
5006 ls.opt = 0;
5007 }
5008 } else {
5009 ls.def = adef.clone();
5010 ls.opt = if arglast == 0
5011 || multi == 0
5012 || line.is_empty()
5013 || line.as_bytes()[0] == b'-'
5014 || line.as_bytes()[0] == b'+'
5015 { 1 } else { 0 };
5016 ls.ddef = None;
5017 ls.dopt = None;
5018 ls.optbeg = state.nargbeg;
5019 ls.argbeg = state.restbeg;
5020 ls.argend = state.argend;
5021 ls.singles = state.singles;
5022 ls.oopt = state.oopt;
5023 if let Some(wi) = wasopt_idx {
5024 let mut p = d.opts.as_deref_mut();
5025 while let Some(op) = p {
5026 if op.num as usize == wi {
5027 op.active = 1;
5028 break;
5029 }
5030 p = op.next.as_deref_mut();
5031 }
5032 }
5033 }
5034 }
5035 }
5036 cur += 1;
5037 }
5038 let _ = (endpat, ddef, dopt, adef);
5039 }
5040
5041 // c:2397-2400 — count active opts.
5042 let mut actopts = 0i32;
5043 let mut p = d.opts.as_deref();
5044 while let Some(o) = p {
5045 if o.active != 0 { actopts += 1; }
5046 p = o.next.as_deref();
5047 }
5048 if let Ok(mut ls) = ca_laststate.lock() {
5049 ls.actopts = actopts;
5050 // Make sure ls.d reflects the (now-mutated) d.
5051 ls.d = Some(Box::new(clone_cadef_shallow(d)));
5052 }
5053 0
5054}
5055
5056/// Mirror of the C `memcpy(&ca_laststate, &state, sizeof(state))` pattern
5057/// — we can't move `state` (the caller continues using it), and Rust's
5058/// own `Clone` would over-clone owned chains. Instead snapshot the
5059/// salient fields plus a fresh shallow clone of `d`. Local to
5060/// `ca_parse_line`; matches no C function.
5061#[allow(dead_code)]
5062fn clone_castate(s: &castate, d: &cadef) -> castate {
5063 castate {
5064 snext: None,
5065 d: Some(Box::new(clone_cadef_shallow(d))),
5066 nopts: s.nopts,
5067 def: s.def.clone(),
5068 ddef: s.ddef.clone(),
5069 curopt: s.curopt.clone(),
5070 dopt: s.dopt.clone(),
5071 opt: s.opt, arg: s.arg,
5072 argbeg: s.argbeg, optbeg: s.optbeg,
5073 nargbeg: s.nargbeg, restbeg: s.restbeg,
5074 curpos: s.curpos, argend: s.argend,
5075 inopt: s.inopt, inarg: s.inarg,
5076 nth: s.nth,
5077 singles: s.singles, oopt: s.oopt, actopts: s.actopts,
5078 args: s.args.clone(),
5079 oargs: s.oargs.clone(),
5080 }
5081}
5082
5083#[allow(dead_code)]
5084fn clone_cadef_shallow(d: &cadef) -> cadef {
5085 cadef {
5086 next: None,
5087 snext: None,
5088 opts: d.opts.clone(),
5089 nopts: d.nopts,
5090 ndopts: d.ndopts,
5091 nodopts: d.nodopts,
5092 args: d.args.clone(),
5093 rest: d.rest.clone(),
5094 defs: d.defs.clone(),
5095 ndefs: d.ndefs,
5096 lastt: d.lastt,
5097 single: d.single.clone(),
5098 r#match: d.r#match.clone(),
5099 argsactive: d.argsactive,
5100 set: d.set.clone(),
5101 flags: d.flags,
5102 nonarg: d.nonarg.clone(),
5103 }
5104}
5105
5106/// Direct port of `static void ca_set_data(LinkList descr, LinkList act,
5107/// LinkList subc, char *opt,
5108/// Caarg arg, Caopt optdef,
5109/// int single)` from
5110/// `Src/Zle/computil.c:2472-2582`. Appends to descr/act/subc the
5111/// description/action/subcontext for each `arg` whose `[min,num]`
5112/// range covers `ca_laststate.nth`. When `opt` is non-None, all
5113/// args are treated as option args; otherwise positional. Recurses
5114/// via the goto-rec C path to retry after the first loop when more
5115/// state remains.
5116pub fn ca_set_data(descr: &mut Vec<String>, // c:2472
5117 act: &mut Vec<String>,
5118 subc: &mut Vec<String>,
5119 opt: Option<&str>,
5120 start_arg: Option<Box<caarg>>,
5121 optdef: Option<&caopt>,
5122 single: i32)
5123{
5124 use crate::ported::zle::complete::restrict_range;
5125
5126 let mut arg: Option<Box<caarg>> = start_arg;
5127 let mut opt = opt.map(|s| s.to_string());
5128 let mut restr = 0;
5129 let mut miss = 0;
5130 let mut oopt = 1i32;
5131 let mut lopt;
5132
5133 'rec: loop {
5134 // c:2481 — addopt = (opt ? 0 : ca_laststate.oopt).
5135 let addopt = if opt.is_some() {
5136 0
5137 } else {
5138 ca_laststate.lock().map(|s| s.oopt).unwrap_or(0)
5139 };
5140
5141 // c:2483 — main arg walk.
5142 while let Some(a) = arg.as_ref() {
5143 let cont = {
5144 let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
5145 opt.is_some()
5146 || a.num < 0
5147 || (a.min <= nth + addopt && a.num >= nth)
5148 };
5149 if !cont { break; }
5150
5151 lopt = a.r#type == CAA_OPT; // c:2486
5152 if opt.is_none() && !lopt && oopt > 0 { // c:2487
5153 oopt = 0;
5154 }
5155
5156 // c:2490 — dedup: skip if (descr, act) pair already present.
5157 let mut dup = false;
5158 let descr_str = a.descr.clone().unwrap_or_default();
5159 let act_str = a.action.clone().unwrap_or_default();
5160 for (d, ac) in descr.iter().zip(act.iter()) {
5161 if d == &descr_str && ac == &act_str {
5162 dup = true;
5163 break;
5164 }
5165 }
5166
5167 // c:2497 — with ignored prefix, no normal args.
5168 if single != 0 && a.opt.is_none() {
5169 return;
5170 }
5171
5172 if !dup { // c:2500
5173 descr.push(descr_str.clone()); // c:2501
5174 act.push(act_str.clone());
5175
5176 if restr == 0 { // c:2504
5177 let nrestr = if a.r#type == CAA_RARGS { // c:2506
5178 let (optbeg, argend) = ca_laststate.lock()
5179 .map(|s| (s.optbeg, s.argend)).unwrap_or((0, 0));
5180 restrict_range(optbeg, argend);
5181 1
5182 } else if a.r#type == CAA_RREST { // c:2508
5183 let (argbeg, argend) = ca_laststate.lock()
5184 .map(|s| (s.argbeg, s.argend)).unwrap_or((0, 0));
5185 restrict_range(argbeg, argend);
5186 1
5187 } else { 0 };
5188 restr = nrestr;
5189 }
5190
5191 // c:2511 — build subcontext string.
5192 let buf = if let Some(o) = a.opt.as_deref() { // c:2511
5193 let gs = a.gsname.as_deref().unwrap_or("");
5194 if a.num > 0 && a.r#type < CAA_REST { // c:2514
5195 format!("{}option{}-{}", gs, o, a.num)
5196 } else { // c:2518
5197 format!("{}option{}-rest", gs, o)
5198 }
5199 } else if a.num > 0 { // c:2520
5200 if let Some(gs) = a.gsname.as_deref() {
5201 format!("{}argument-{}", gs, a.num)
5202 } else {
5203 format!("argument-{}", a.num)
5204 }
5205 } else { // c:2523
5206 if let Some(gs) = a.gsname.as_deref() {
5207 format!("{}argument-rest", gs)
5208 } else {
5209 "argument-rest".to_string()
5210 }
5211 };
5212 subc.push(buf); // c:2527
5213 }
5214
5215 // c:2539 — guard: NORMAL inside an opt where opt requires its
5216 // argument as a separate word — return so we don't keep trying
5217 // to match positionals.
5218 if a.r#type == CAA_NORMAL && opt.is_some() {
5219 if let Some(od) = optdef {
5220 if od.r#type == CAO_NEXT
5221 || od.r#type == CAO_ODIRECT
5222 || od.r#type == CAO_OEQUAL
5223 {
5224 return;
5225 }
5226 }
5227 }
5228
5229 if single != 0 { break; } // c:2545
5230
5231 // c:2548-2568 — advance to the next arg.
5232 if opt.is_none() { // c:2548
5233 let next_is_none_and_miss = a.num >= 0 && a.next.is_none() && miss != 0;
5234 if next_is_none_and_miss { // c:2549
5235 let rest = ca_laststate.lock().ok().and_then(|s| {
5236 s.d.as_ref().and_then(|d| d.rest.clone())
5237 });
5238 arg = rest.filter(|r| r.active != 0); // c:2550
5239 } else {
5240 let onum = a.num; // c:2553
5241 let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
5242 let rest_flag = onum != a.min && onum == nth; // c:2554
5243 let next = a.next.clone();
5244 if let Some(n) = next { // c:2555
5245 if n.num != onum + 1 { miss = 1; } // c:2556
5246 arg = Some(n);
5247 } else if rest_flag || (oopt > 0 && opt.is_none()) { // c:2558
5248 let rest = ca_laststate.lock().ok().and_then(|s| {
5249 s.d.as_ref().and_then(|d| d.rest.clone())
5250 });
5251 arg = rest.filter(|r| r.active != 0);
5252 oopt = -1;
5253 } else {
5254 arg = None;
5255 }
5256 }
5257 } else { // c:2564
5258 if !lopt { break; } // c:2565
5259 arg = a.next.clone(); // c:2567
5260 }
5261 }
5262
5263 // c:2570 — retry as positional after the option args path.
5264 let laststate_oopt = ca_laststate.lock().map(|s| s.oopt).unwrap_or(0);
5265 let cur_lopt = arg.as_ref().map_or(false, |a| a.r#type == CAA_OPT);
5266 if single == 0 && opt.is_some() && (cur_lopt || laststate_oopt != 0) {
5267 opt = None;
5268 let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
5269 // c:2572 — arg = ca_get_arg(ca_laststate.d, ca_laststate.nth).
5270 arg = ca_laststate.lock().ok().and_then(|s| {
5271 s.d.as_ref().and_then(|d| ca_get_arg(d, nth))
5272 });
5273 continue 'rec;
5274 }
5275 // c:2575 — retry as rest after positional path.
5276 if opt.is_none() && oopt > 0 {
5277 oopt = -1;
5278 let rest = ca_laststate.lock().ok().and_then(|s| {
5279 s.d.as_ref().and_then(|d| d.rest.clone())
5280 });
5281 arg = rest.filter(|r| r.active != 0);
5282 continue 'rec;
5283 }
5284 break 'rec;
5285 }
5286}
5287
5288/// Direct port of `static void cf_ignore(char **names, LinkList ign,
5289/// char *style, char *path)`
5290/// from `Src/Zle/computil.c:4860-4896`. Adds to `ign` any directory
5291/// in `names` that:
5292/// - "pwd" style: shares the same dev/ino as `$PWD` (so completion
5293/// doesn't offer the directory you're already in).
5294/// - "parent" style: is an ancestor directory of `path` (so when
5295/// completing under `/a/b/c/`, `/a/`, `/a/b/`, etc. don't show
5296/// up as options).
5297/// Quoted with QT_BACKSLASH for safe re-insertion into the line.
5298pub fn cf_ignore(names: &[String], ign: &mut Vec<String>, style: &str, path: &str) { // c:4860
5299 use std::os::unix::fs::MetadataExt;
5300 use crate::ported::utils::quotestring;
5301 use crate::ported::zsh_h::QT_BACKSLASH;
5302 use crate::ported::zle::compresult::ztat;
5303
5304 let pl = path.len();
5305 let tpar = style.contains("parent"); // c:4866
5306 let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
5307 let est = if !pwd.is_empty() { ztat(&pwd, true) } else { None };
5308 let tpwd = style.contains("pwd") && est.is_some(); // c:4867
5309
5310 if !tpar && !tpwd { return; } // c:4870
5311
5312 for n in names { // c:4873
5313 let nst = match ztat(n, true) { // c:4874 lstat
5314 Some(m) if m.is_dir() => m,
5315 _ => continue,
5316 };
5317 if tpwd {
5318 if let Some(ref est) = est {
5319 if nst.dev() == est.dev() && nst.ino() == est.ino() { // c:4875
5320 ign.push(quotestring(n, QT_BACKSLASH)); // c:4876
5321 continue;
5322 }
5323 }
5324 }
5325 if tpar && pl > 0 && n.starts_with(path) { // c:4879
5326 let mut c = n.clone();
5327 let mut found = false;
5328 // c:4881 — walk up via strrchr('/') while above path-prefix.
5329 while let Some(idx) = c.rfind('/') {
5330 if idx <= pl { break; }
5331 c.truncate(idx);
5332 if let Some(st) = ztat(&c, false) { // c:4883 stat
5333 if st.dev() == nst.dev() && st.ino() == nst.ino() {
5334 found = true;
5335 break;
5336 }
5337 }
5338 }
5339 // c:4889 — fallback last-segment check via lstat.
5340 let last_match = if !found {
5341 if let Some(idx) = c.rfind('/') {
5342 if idx > pl {
5343 c.truncate(idx);
5344 ztat(&c, true).map_or(false, |st|
5345 st.dev() == nst.dev() && st.ino() == nst.ino())
5346 } else { false }
5347 } else { false }
5348 } else { false };
5349 if found || last_match {
5350 ign.push(quotestring(n, QT_BACKSLASH)); // c:4892
5351 }
5352 }
5353 }
5354}
5355
5356/// Direct port of `static char **cf_pats(int dirs, int noopt,
5357/// char **names, char **accept,
5358/// char *skipped, char *matcher,
5359/// char *sdirs, char **fake,
5360/// char **pats)` from
5361/// `Src/Zle/computil.c:4829`. Combines the supplied pattern
5362/// lists into a single resolved pattern array used by
5363/// `_path_files` to drive the file-completion path.
5364///
5365/// **Substrate tradeoff:** the helper chain
5366/// `cfp_test_exact`/`cfp_opt_pats`/`cfp_bld_pats`/`cfp_add_sdirs`
5367/// Direct port of `static LinkList cf_pats(int dirs, int noopt,
5368/// LinkList names, char **accept,
5369/// char *skipped, char *matcher,
5370/// char *sdirs, char **fake,
5371/// char **pats)` from
5372/// `Src/Zle/computil.c:4829-4848`. The cf_pats driver:
5373/// 1. Try cfp_test_exact first; if it returns a non-empty list, fold
5374/// in `sdirs`/`fake` via cfp_add_sdirs and return.
5375/// 2. Otherwise: if dirs, replace `pats` with `*(-/)`. If !noopt run
5376/// cfp_opt_pats. Then build the patterns via cfp_bld_pats and fold
5377/// in sdirs/fake.
5378pub fn cf_pats(dirs: i32, noopt: i32, names: &[String], // c:4829
5379 accept: &[String], skipped: &str, matcher: &str,
5380 sdirs: &str, fake: &[String], pats: &[String]) -> Vec<String> {
5381 // c:4835 — try exact-match pass first.
5382 if let Some(exact) = cfp_test_exact(names, accept, skipped) {
5383 let mut out = exact;
5384 cfp_add_sdirs(&mut out, names, skipped, sdirs, fake); // c:4836
5385 return out;
5386 }
5387
5388 // c:4838 — when dirs is set, force the `*(-/)` directory glob.
5389 let dir_pats = vec!["*(-/)".to_string()];
5390 let active_pats: Vec<String> = if dirs != 0 {
5391 dir_pats
5392 } else if noopt == 0 {
5393 // c:4843 — optimization pass.
5394 cfp_opt_pats(pats, matcher)
5395 } else {
5396 pats.to_vec()
5397 };
5398
5399 // c:4846 — build the glob array.
5400 let mut out = cfp_bld_pats(dirs, names, skipped, &active_pats);
5401 cfp_add_sdirs(&mut out, names, skipped, sdirs, fake);
5402 out
5403}
5404
5405/// Direct port of `static LinkList cf_remove_other(char **names,
5406/// char *pre, int *amb)`
5407/// from `Src/Zle/computil.c:4899-4953`. Helper for `_path_files` that
5408/// reports whether the remaining `names` share a common directory
5409/// prefix (`*amb` cleared) or diverge (`*amb` set, return None).
5410/// When `pre` itself contains a `/`, names matching that head are
5411/// returned as the consensus list.
5412pub fn cf_remove_other(names: &[String], pre: &str, amb: &mut i32) // c:4899
5413 -> Option<Vec<String>>
5414{
5415 use crate::ported::utils::strpfx;
5416
5417 if let Some(slash) = pre.find('/') { // c:4903
5418 // c:4906-4908 — pre' = pre[..slash] + "/".
5419 let pre2 = format!("{}/", &pre[..slash]);
5420
5421 // c:4910-4912 — any name with the truncated prefix?
5422 let any_match = names.iter().any(|n| strpfx(&pre2, n));
5423
5424 if any_match { // c:4914
5425 // c:4915-4922 — return all matching names with amb=0.
5426 let ret: Vec<String> = names.iter()
5427 .filter(|n| strpfx(&pre2, n))
5428 .cloned()
5429 .collect();
5430 *amb = 0;
5431 return Some(ret); // c:4923
5432 } else { // c:4924
5433 // c:4925-4940 — check if remaining names all share first-name's head.
5434 let mut it = names.iter();
5435 let Some(first) = it.next() else {
5436 *amb = 0; // c:4926
5437 return None;
5438 };
5439 // c:4930 — strip after first '/' in first name.
5440 let p_head = match first.find('/') {
5441 Some(i) => format!("{}/", &first[..i]),
5442 None => format!("{}/", first),
5443 };
5444 for n in it { // c:4935
5445 if !strpfx(&p_head, n) {
5446 *amb = 1; // c:4937
5447 return None;
5448 }
5449 }
5450 // All match — fall through to return None (matches C).
5451 }
5452 } else { // c:4942
5453 // c:4943 — empty list: amb cleared.
5454 let mut it = names.iter();
5455 let Some(first) = it.next() else {
5456 *amb = 0;
5457 return None;
5458 };
5459 for n in it { // c:4946
5460 if first != n { // c:4947
5461 *amb = 1;
5462 return None;
5463 }
5464 }
5465 }
5466 None // c:4952
5467}
5468
5469/// Port of `cfp_add_sdirs(LinkList final, LinkList orig, char *skipped, char *sdirs, char **fake)` from Src/Zle/computil.c:4735.
5470/// WARNING: param names don't match C — Rust=(final_list, orig, sdirs, fake) vs C=(final, orig, skipped, sdirs, fake)
5471pub fn cfp_add_sdirs(final_list: &mut Vec<String>, orig: &[String], // c:4735
5472 _skipped: &str, sdirs: &str, fake: &[String]) {
5473 // C body c:4738-4767: if sdirs ∈ {"yes","true","on","1","..","../"}
5474 // and GLOBDOTS or compprefix starts with `.`,
5475 // prepend "." (or "..") to final.
5476 let mut add = 0;
5477 if !sdirs.is_empty() { // c:4740
5478 match sdirs {
5479 "yes" | "true" | "on" | "1" => add = 2, // c:4741
5480 ".." => add = 1, // c:4744
5481 _ => {}
5482 }
5483 }
5484 if add > 0 {
5485 for f in fake {
5486 final_list.push(f.clone());
5487 }
5488 for o in orig {
5489 if !final_list.contains(o) {
5490 final_list.push(o.clone());
5491 }
5492 }
5493 }
5494}
5495
5496/// Direct port of `static LinkList cfp_bld_pats(UNUSED(int dirs),
5497/// LinkList names,
5498/// char *skipped,
5499/// char **pats)` from
5500/// `Src/Zle/computil.c:4704-4732`. For each (name, pattern) pair,
5501/// builds `name + skipped + pattern`. When GLOBDOTS is unset and the
5502/// compprefix starts with `.`, also adds a dot-prefixed variant.
5503pub fn cfp_bld_pats(_dirs: i32, names: &[String], skipped: &str, // c:4704
5504 pats: &[String]) -> Vec<String> {
5505 use crate::ported::zsh_h::{unset, GLOBDOTS};
5506 use crate::ported::zle::complete::COMPPREFIX;
5507
5508 let compprefix = COMPPREFIX.get()
5509 .and_then(|m| m.lock().ok().map(|s| s.clone()))
5510 .unwrap_or_default();
5511 // c:4711 — `dot = unset(GLOBDOTS) && compprefix && *compprefix == '.'`.
5512 let dot = unset(GLOBDOTS) && compprefix.starts_with('.');
5513
5514 let mut ret: Vec<String> = Vec::new();
5515 for o in names { // c:4712
5516 for p in pats { // c:4714
5517 // c:4716 — `str = o + skipped + p`.
5518 ret.push(format!("{}{}{}", o, skipped, p));
5519 // c:4721 — dot variant when GLOBDOTS unset and pattern
5520 // doesn't already start with '.'.
5521 if dot && !p.starts_with('.') {
5522 ret.push(format!("{}{}.{}", o, skipped, p));
5523 }
5524 }
5525 }
5526 ret // c:4731
5527}
5528
5529/// Direct port of `static char *cfp_matcher_pats(char *matcher, char *add)`
5530/// from `Src/Zle/computil.c:4525-4613`. Parses the matcher spec into
5531/// a Cmatcher chain, then walks each chain entry truncating `add` at
5532/// the first character that matches the matcher's stop pattern, and
5533/// recording one matcher per surviving character. Finally calls
5534/// cfp_matcher_range to synthesize the output pattern.
5535///
5536/// Returns:
5537/// - the transformed string (possibly empty) on success
5538/// - the original `add` unchanged when the matcher spec is empty
5539/// or unparseable
5540pub fn cfp_matcher_pats(matcher: &str, add: &str) -> String { // c:4525
5541 use crate::ported::zle::comp_h::{Cmatcher, CMF_LEFT, CMF_RIGHT};
5542 use crate::ported::zle::compmatch::pattern_match;
5543 use crate::ported::zle::complete::parse_cmatcher;
5544 use crate::ported::utils::ztrlen;
5545
5546 // c:4527 — parse_cmatcher returns None on error (the C pcm_err path).
5547 let m_chain = parse_cmatcher("", matcher);
5548 let Some(mut m_chain) = m_chain else {
5549 return add.to_string(); // c:4529
5550 };
5551
5552 // c:4531-4538 — ms[0..zl] is one matcher slot per character of add.
5553 let zl = ztrlen(add); // c:4531
5554 let mut ms: Vec<Option<Box<Cmatcher>>> = (0..zl).map(|_| None).collect();
5555 let mut add_owned = add.to_string();
5556
5557 let mut m_opt: Option<&Cmatcher> = Some(&*m_chain);
5558 while let Some(m) = m_opt {
5559 let mut stopp: Option<&crate::ported::zle::comp_h::Cpattern> = None;
5560 let mut stopl: i32 = 0;
5561
5562 if (m.flags & (CMF_LEFT | CMF_RIGHT)) == 0 { // c:4542
5563 if m.llen == 1 && m.wlen == 1 { // c:4543
5564 // c:4550 — walk add looking for the first char where the
5565 // matcher's `line` pattern matches; record `m` in ms[i].
5566 let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
5567 for (i, (byte_idx, _ch)) in chars.iter().enumerate() {
5568 if i >= ms.len() { break; }
5569 let slice = &add_owned[*byte_idx..];
5570 if pattern_match(m.line.as_deref(), slice, None, "") != 0 {
5571 // c:4551 — `if (*mp)` collision: truncate add.
5572 if ms[i].is_some() {
5573 add_owned.truncate(*byte_idx); // c:4553
5574 break;
5575 } else {
5576 ms[i] = Some(Box::new(m.clone())); // c:4557
5577 }
5578 }
5579 }
5580 } else {
5581 stopp = m.line.as_deref(); // c:4565
5582 stopl = m.llen;
5583 }
5584 } else if (m.flags & CMF_RIGHT) != 0 { // c:4568
5585 if m.wlen < 0 && m.llen == 0 && m.ralen == 1 { // c:4569
5586 let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
5587 for (i, (byte_idx, _ch)) in chars.iter().enumerate() {
5588 if i >= ms.len() { break; }
5589 let slice = &add_owned[*byte_idx..];
5590 if pattern_match(m.right.as_deref(), slice, None, "") != 0 {
5591 // c:4572 — collision OR leading-dot guard.
5592 let leading_dot = *byte_idx == 0 && slice.starts_with('.');
5593 if ms[i].is_some() || leading_dot {
5594 add_owned.truncate(*byte_idx); // c:4573
5595 break;
5596 } else {
5597 ms[i] = Some(Box::new(m.clone()));
5598 }
5599 }
5600 }
5601 } else if m.llen != 0 { // c:4584
5602 stopp = m.line.as_deref();
5603 stopl = m.llen;
5604 } else {
5605 stopp = m.right.as_deref(); // c:4588
5606 stopl = m.ralen;
5607 }
5608 } else { // c:4591 CMF_LEFT
5609 if m.lalen == 0 { // c:4592
5610 return String::new(); // c:4593
5611 }
5612 stopp = m.left.as_deref();
5613 stopl = m.lalen;
5614 }
5615
5616 // c:4598-4608 — apply stopp truncation.
5617 if let Some(sp) = stopp {
5618 let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
5619 let mut bytes_remaining = add_owned.len() as i32;
5620 for (_i, (byte_idx, _ch)) in chars.iter().enumerate() {
5621 if bytes_remaining < stopl { break; }
5622 let slice = &add_owned[*byte_idx..];
5623 if pattern_match(Some(sp), slice, None, "") != 0 {
5624 add_owned.truncate(*byte_idx); // c:4601
5625 break;
5626 }
5627 bytes_remaining -= 1;
5628 }
5629 }
5630
5631 m_opt = m.next.as_deref();
5632 }
5633
5634 // c:4610 — synthesize the output via cfp_matcher_range.
5635 if !add_owned.is_empty() {
5636 cfp_matcher_range(&ms, &add_owned)
5637 } else {
5638 add_owned // c:4613
5639 }
5640}
5641
5642/// Direct port of `static char *cfp_matcher_range(Cmatcher *ms, char *add)`
5643/// from `Src/Zle/computil.c:4307-4520`. For each character of `add`,
5644/// consults the parallel `ms[i]` matcher and emits a pattern fragment:
5645/// - no matcher: the character verbatim
5646/// - CMF_RIGHT: `*c`
5647/// - word EQUIV+line EQUIV: `[c eq(c)]` (two-char class with
5648/// the equivalent char from the word side)
5649/// - CPAT_NCLASS: `[^class]`
5650/// - CPAT_CCLASS / CPAT_EQUIV / CPAT_CHAR: `[classchar+addchar]`
5651/// - CPAT_ANY: `?`
5652pub fn cfp_matcher_range(ms: &[Option<Box<crate::ported::zle::comp_h::Cmatcher>>], // c:4307
5653 add: &str) -> String
5654{
5655 use crate::ported::zle::comp_h::{CMF_RIGHT, Cpattern};
5656 use crate::ported::zle::comp_h::{CPAT_ANY, CPAT_CCLASS, CPAT_CHAR,
5657 CPAT_EQUIV, CPAT_NCLASS};
5658 use crate::ported::zle::compmatch::{pattern_match1, pattern_match_equivalence};
5659 use crate::ported::pattern::pattern_range_to_string;
5660 use crate::ported::utils::imeta;
5661
5662 // Local PATMATCHRANGE — Rust copy of the helper used by pattern_match1
5663 // / pattern_match_equivalence. Walks an encoded char-range str
5664 // looking for `c`. On hit returns Some((idx, mtp)).
5665 fn patmatchrange_local(s: Option<&str>, c: u32) -> Option<(u32, i32)> {
5666 let s = s?;
5667 let mut idx: u32 = 0;
5668 let mut chars = s.chars().peekable();
5669 while let Some(ch) = chars.next() {
5670 if let Some(&peek) = chars.peek() {
5671 if peek == '-' {
5672 chars.next();
5673 if let Some(hi) = chars.next() {
5674 if c >= ch as u32 && c <= hi as u32 {
5675 return Some((idx, 0));
5676 }
5677 idx += 1;
5678 continue;
5679 }
5680 }
5681 }
5682 if c == ch as u32 {
5683 return Some((idx, 0));
5684 }
5685 idx += 1;
5686 }
5687 None
5688 }
5689
5690 let mut out = String::with_capacity(add.len() * 2);
5691 let add_chars: Vec<(usize, char)> = add.char_indices().collect();
5692
5693 for (i, (_byte_idx, ch)) in add_chars.iter().enumerate() {
5694 let addc = *ch as u32;
5695 let m_opt = ms.get(i).and_then(|x| x.as_deref());
5696
5697 match m_opt {
5698 None => {
5699 // c:4331 — no matcher: emit char verbatim.
5700 out.push(*ch);
5701 }
5702 Some(m) if (m.flags & CMF_RIGHT) != 0 => {
5703 // c:4344 — right-anchored: `*char`.
5704 out.push('*');
5705 out.push(*ch);
5706 }
5707 Some(m) => {
5708 let word: Option<&Cpattern> = m.word.as_deref();
5709 let line: Option<&Cpattern> = m.line.as_deref();
5710 if let (Some(l), Some(w)) = (line, word) {
5711 if l.tp == CPAT_EQUIV && w.tp == CPAT_EQUIV {
5712 // c:4359 — genuine equivalence; emit `[char eq]`.
5713 out.push('[');
5714 out.push(*ch);
5715 if let Some((ind, mtp)) = patmatchrange_local(
5716 l.str.as_deref(), addc)
5717 {
5718 let eq = pattern_match_equivalence(
5719 w, ind + 1, mtp, addc);
5720 if eq != u32::MAX {
5721 if let Some(c) = char::from_u32(eq) {
5722 let _ = imeta(c); // imeta handled implicitly
5723 out.push(c);
5724 }
5725 }
5726 }
5727 out.push(']');
5728 continue;
5729 }
5730 }
5731 if let Some(w) = word {
5732 match w.tp {
5733 x if x == CPAT_NCLASS => { // c:4401
5734 out.push('[');
5735 out.push('^');
5736 if let Some(idx_str) = w.str.as_deref() {
5737 if let Ok(idx) = idx_str.parse::<usize>() {
5738 if let Some(cls) = pattern_range_to_string(idx) {
5739 out.push_str(&cls);
5740 } else {
5741 out.push_str(idx_str);
5742 }
5743 } else {
5744 out.push_str(idx_str);
5745 }
5746 }
5747 out.push(']');
5748 }
5749 x if x == CPAT_CCLASS
5750 || x == CPAT_EQUIV
5751 || x == CPAT_CHAR => { // c:4435 / c:4441 / c:4442
5752 out.push('[');
5753 let mut mt = 0i32;
5754 let addadd = pattern_match1(w, addc, &mut mt) == 0;
5755 // c:4455 — if addadd && *add == ']', emit ']' first.
5756 if addadd && *ch == ']' {
5757 out.push(*ch);
5758 }
5759 if w.tp == CPAT_CHAR { // c:4461
5760 if let Some(c) = char::from_u32(w.chr) {
5761 out.push(c);
5762 }
5763 } else { // c:4476
5764 if let Some(idx_str) = w.str.as_deref() {
5765 if let Ok(idx) = idx_str.parse::<usize>() {
5766 if let Some(cls) = pattern_range_to_string(idx) {
5767 out.push_str(&cls);
5768 } else {
5769 out.push_str(idx_str);
5770 }
5771 } else {
5772 out.push_str(idx_str);
5773 }
5774 }
5775 }
5776 if addadd && *ch != ']' { // c:4489
5777 out.push(*ch);
5778 }
5779 out.push(']');
5780 }
5781 x if x == CPAT_ANY => { // c:4502
5782 out.push('?');
5783 }
5784 _ => {
5785 // Fallback: emit verbatim.
5786 out.push(*ch);
5787 }
5788 }
5789 } else {
5790 out.push(*ch);
5791 }
5792 }
5793 }
5794 }
5795 out
5796}
5797
5798/// Direct port of `static void cfp_opt_pats(char **pats, char *matcher)`
5799/// from `Src/Zle/computil.c:4621-4701`. "Optimization" pass that
5800/// prefixes each `*…`-leading pattern with the literal portion of
5801/// `compprefix` that no pattern would consume. The walk computes a
5802/// shrinking `add` string — each pattern crosses off the chars in
5803/// `add` it would match — and any remaining chars become the prefix.
5804///
5805/// Modifies `pats` in place; returns the (possibly modified) list.
5806pub fn cfp_opt_pats(pats: &[String], matcher: &str) -> Vec<String> { // c:4621
5807 use crate::ported::glob::{remnulargs, tokenize};
5808 use crate::ported::pattern::haswilds;
5809 use crate::ported::zle::compcore::comppatmatch;
5810 use crate::ported::zle::compcore::rembslash;
5811 use crate::ported::zle::complete::{COMPPREFIX, COMPSUFFIX};
5812 use crate::ported::ztype_h::idigit;
5813
5814 let compprefix = COMPPREFIX.get()
5815 .and_then(|m| m.lock().ok().map(|s| s.clone()))
5816 .unwrap_or_default();
5817 if compprefix.is_empty() { // c:4625
5818 return pats.to_vec();
5819 }
5820 let compsuffix = COMPSUFFIX.get()
5821 .and_then(|m| m.lock().ok().map(|s| s.clone()))
5822 .unwrap_or_default();
5823
5824 // c:4628-4633 — if comppatmatch && haswilds(rembslash(prefix+suffix)): bail.
5825 let cpm_set = comppatmatch.get()
5826 .and_then(|m| m.lock().ok().map(|g| g.is_some()))
5827 .unwrap_or(false);
5828 if cpm_set {
5829 let merged = format!("{}{}", compprefix, compsuffix);
5830 let mut t = rembslash(&merged);
5831 tokenize(&mut t);
5832 remnulargs(&mut t);
5833 if haswilds(&t) {
5834 return pats.to_vec(); // c:4632
5835 }
5836 }
5837
5838 // c:4634-4649 — build `add` by walking compprefix, unescaping `\X`
5839 // for non-special X, and pre-escaping unescaped specials.
5840 const SPECIALS: &[u8] = b"*?<>()[]|#^~=";
5841 let cp_bytes = compprefix.as_bytes();
5842 let mut add: Vec<u8> = Vec::with_capacity(cp_bytes.len() * 2);
5843 let mut i = 0usize;
5844 while i < cp_bytes.len() {
5845 let c = cp_bytes[i];
5846 let keep = if c == b'\\' && i + 1 < cp_bytes.len() {
5847 // c:4636 — keep `\X` literal when X is non-special.
5848 let next = cp_bytes[i + 1];
5849 !SPECIALS.contains(&next)
5850 } else {
5851 true
5852 };
5853 if keep {
5854 let unescaped_at_start = i == 0 || cp_bytes[i - 1] != b'\\';
5855 if unescaped_at_start && SPECIALS.contains(&c) { // c:4640
5856 add.push(b'\\');
5857 }
5858 add.push(c);
5859 }
5860 i += 1;
5861 }
5862 let mut add_s: String = String::from_utf8_lossy(&add).into_owned();
5863
5864 // c:4650-4691 — walk each pattern, cross off chars from `add`.
5865 for p_orig in pats {
5866 if add_s.is_empty() { break; }
5867 let mut q_bytes: Vec<u8> = p_orig.as_bytes().to_vec();
5868 if q_bytes.is_empty() { continue; }
5869 // c:4654 — strip trailing alternation `(…|…)` group.
5870 if let Some(b')') = q_bytes.last().copied() {
5871 let mut t = q_bytes.len() - 1;
5872 let mut found = None;
5873 while t > 0 {
5874 t -= 1;
5875 let c = q_bytes[t];
5876 if c == b')' || c == b'|' || c == b'~' || c == b'(' {
5877 found = Some((t, c));
5878 break;
5879 }
5880 }
5881 if let Some((idx, c)) = found {
5882 if c == b'(' {
5883 q_bytes.truncate(idx);
5884 }
5885 }
5886 }
5887
5888 let mut qi = 0usize;
5889 while qi < q_bytes.len() && !add_s.is_empty() {
5890 let c = q_bytes[qi];
5891 if c == b'\\' && qi + 1 < q_bytes.len() { // c:4662
5892 qi += 1;
5893 let target = q_bytes[qi];
5894 // c:4663 — cross off `target` from add.
5895 if let Some(pos) = add_s.find(target as char) {
5896 add_s.truncate(pos);
5897 }
5898 } else if c == b'<' { // c:4665
5899 // c:4666 — cross off any digit.
5900 let cut_at = add_s.bytes().position(|b| idigit(b));
5901 if let Some(pos) = cut_at {
5902 add_s.truncate(pos);
5903 }
5904 } else if c == b'[' { // c:4668
5905 // c:4669-4684 — character class.
5906 let mut xi = qi + 1;
5907 let not = xi < q_bytes.len()
5908 && (q_bytes[xi] == b'!' || q_bytes[xi] == b'^');
5909 if not { xi += 1; }
5910 let _ = not;
5911 while xi < q_bytes.len() && q_bytes[xi] != b']' {
5912 if xi + 2 < q_bytes.len() && q_bytes[xi + 1] == b'-' {
5913 let c1 = q_bytes[xi];
5914 let c2 = q_bytes[xi + 2];
5915 let cut_at = add_s.bytes().position(|b| b >= c1 && b <= c2);
5916 if let Some(pos) = cut_at {
5917 add_s.truncate(pos);
5918 }
5919 xi += 3;
5920 } else {
5921 let cut_at = add_s.find(q_bytes[xi] as char);
5922 if let Some(pos) = cut_at {
5923 add_s.truncate(pos);
5924 }
5925 xi += 1;
5926 }
5927 }
5928 qi = xi;
5929 } else if c != b'?' && c != b'*' && c != b'(' && c != b')'
5930 && c != b'|' && c != b'~' && c != b'#' // c:4685
5931 {
5932 let cut_at = add_s.find(c as char);
5933 if let Some(pos) = cut_at {
5934 add_s.truncate(pos);
5935 }
5936 }
5937 qi += 1;
5938 }
5939 }
5940
5941 // c:4693-4700 — prepend `add` to each `*`-leading pattern.
5942 let mut out: Vec<String> = pats.to_vec();
5943 if !add_s.is_empty() {
5944 let final_add = if !matcher.is_empty() {
5945 let m = cfp_matcher_pats(matcher, &add_s);
5946 if m.is_empty() { return out; } // c:4694
5947 m
5948 } else {
5949 add_s
5950 };
5951 for p in out.iter_mut() {
5952 if p.starts_with('*') {
5953 *p = format!("{}{}", final_add, p);
5954 }
5955 }
5956 }
5957 out
5958}
5959
5960/// Direct port of `static LinkList cfp_test_exact(LinkList names,
5961/// char **accept,
5962/// char *skipped)` from
5963/// `Src/Zle/computil.c:4160-4290`. Returns the subset of `names` whose
5964/// `name + skipped + compprefix + compsuffix` resolves to an existing
5965/// file. When `accept` is non-boolean, the resolved path must also
5966/// match at least one of the compiled accept-patterns. Returns None
5967/// when nothing matched.
5968pub fn cfp_test_exact(names: &[String], accept: &[String], // c:4160
5969 skipped: &str) -> Option<Vec<String>>
5970{
5971 use crate::ported::glob::tokenize;
5972 use crate::ported::pattern::{patcompile, pattry, Patprog};
5973 use crate::ported::zle::compcore::rembslash;
5974 use crate::ported::zle::complete::{COMPPREFIX, COMPSUFFIX};
5975 use crate::ported::zle::compresult::ztat;
5976
5977 let compprefix = COMPPREFIX.get()
5978 .and_then(|m| m.lock().ok().map(|s| s.clone()))
5979 .unwrap_or_default();
5980 let compsuffix = COMPSUFFIX.get()
5981 .and_then(|m| m.lock().ok().map(|s| s.clone()))
5982 .unwrap_or_default();
5983
5984 // c:4175 — bail when both prefix and suffix are empty.
5985 if compprefix.is_empty() && compsuffix.is_empty() {
5986 return None;
5987 }
5988
5989 // c:4181 — accept-exact off?
5990 let accept_off = accept.is_empty()
5991 || (accept.len() == 1 && matches!(accept[0].as_str(),
5992 "false" | "no" | "off" | "0"));
5993 if accept_off { // c:4188
5994 return None;
5995 }
5996
5997 // c:4199-4214 — build compiled Patprog list from non-boolean accept.
5998 let mut alist: Option<Vec<Patprog>> = None;
5999 let is_boolean_true = accept.len() == 1
6000 && matches!(accept[0].as_str(), "true" | "yes" | "on" | "1");
6001 if !is_boolean_true {
6002 let mut list: Vec<Patprog> = Vec::new();
6003 let mut all_star = false;
6004 for p in accept {
6005 if p == "*" { // c:4207 wildcard short-circuit
6006 all_star = true;
6007 break;
6008 }
6009 let mut p_copy = p.clone();
6010 tokenize(&mut p_copy);
6011 if let Some(prog) = patcompile(&p_copy, 0, None::<&mut String>) {
6012 list.push(prog);
6013 }
6014 }
6015 if !all_star { alist = Some(list); }
6016 }
6017
6018 // c:4220-4227 — assemble `suf = skipped + rembslash(prefix + suffix)`.
6019 let sl = skipped.len() + compprefix.len() + compsuffix.len();
6020 if sl > PATH_MAX2 { // c:4223
6021 return None;
6022 }
6023 let suf = format!("{}{}",
6024 skipped,
6025 rembslash(&format!("{}{}", compprefix, compsuffix)));
6026
6027 let mut ret: Vec<String> = Vec::new();
6028 for p in names { // c:4229
6029 let l = p.len();
6030 if l + sl >= PATH_MAX2 { continue; } // c:4231
6031 let buf = format!("{}{}", p, suf);
6032 if ztat(&buf, false).is_none() { continue; } // c:4269 stat exists?
6033 // c:4274 — accept-pattern check.
6034 if let Some(ref ps) = alist {
6035 let any_match = ps.iter().any(|prog| pattry(prog, &buf));
6036 if !any_match { continue; }
6037 }
6038 ret.push(buf); // c:4285
6039 }
6040
6041 if ret.is_empty() { None } else { Some(ret) } // c:4289
6042}
6043
6044/// Port of `cleanup_(UNUSED(Module m))` from Src/Zle/computil.c:5160.
6045/// WARNING: param names don't match C — Rust=() vs C=(m)
6046pub fn cleanup_() -> i32 { // c:5160
6047 // C body c:5162-5163 — `return setfeatureenables(m, &module_features, NULL)`.
6048 // Static-link path: no per-feature toggle, return 0.
6049 0
6050}
6051
6052/// Port of `comp_quote(char *str, int prefix)` from Src/Zle/computil.c:3662.
6053pub fn comp_quote(str: &str, prefix: i32) -> String { // c:3662
6054 // c:3667 — `x = (prefix && *str == '=')`.
6055 let (s_eff, x) = if prefix != 0 && str.starts_with('=') { // c:3667
6056 ("x".to_string() + &str[1..], true) // c:3668
6057 } else {
6058 (str.to_string(), false)
6059 };
6060 // c:3670 — `ret = quotestring(str, *compqstack)`.
6061 // *compqstack is the first byte of the qstack string.
6062 let qhead = COMPQSTACK.get()
6063 .and_then(|m| m.lock().ok().and_then(|str| str.bytes().next()))
6064 .unwrap_or(0);
6065 let mut ret = crate::ported::zle::zle_tricky::quotename(&s_eff, qhead as i32);
6066 // c:3672-3673 — restore `=` prefix on both ret and original.
6067 if x {
6068 if !ret.is_empty() {
6069 ret.replace_range(0..1, "=");
6070 }
6071 }
6072 ret
6073}
6074
6075/// Direct port of `static Cvval cv_get_val(Cvdef d, char *name)` from
6076/// `Src/Zle/computil.c:3178-3187`. Linear scan over `d->vals` for a
6077/// name match. Returns a shallow clone of the matched cvval.
6078pub fn cv_get_val(d: &cvdef, name: &str) -> Option<Box<cvval>> { // c:3178
6079 let mut p = d.vals.as_deref();
6080 while let Some(v) = p { // c:3182
6081 if v.name.as_deref() == Some(name) { // c:3183
6082 return Some(Box::new(cvval {
6083 next: None,
6084 name: v.name.clone(),
6085 descr: v.descr.clone(),
6086 xor: v.xor.clone(),
6087 r#type: v.r#type,
6088 arg: v.arg.clone(),
6089 active: v.active,
6090 }));
6091 }
6092 p = v.next.as_deref();
6093 }
6094 None // c:3186
6095}
6096
6097/// Direct port of `static void cv_inactive(Cvdef d, char **xor)` from
6098/// `Src/Zle/computil.c:3209-3218`. Clears `active` on each cvval named
6099/// in the xor list.
6100pub fn cv_inactive(d: &mut cvdef, xor: &[String]) { // c:3209
6101 for name in xor { // c:3214
6102 let mut p = d.vals.as_deref_mut();
6103 while let Some(v) = p {
6104 if v.name.as_deref() == Some(name.as_str()) {
6105 v.active = 0; // c:3216
6106 }
6107 p = v.next.as_deref_mut();
6108 }
6109 }
6110}
6111
6112/// Direct port of `static Cvval cv_next(Cvdef d, char **sp, char **ap)`
6113/// from `Src/Zle/computil.c:3240-3331`. Splits the next value out of
6114/// `*sp` using `d->sep` / `d->argsep`, returns its matched Cvval.
6115/// On success, `*sp` advances past the consumed prefix; `*ap` is set
6116/// to the value's argument (if any) or `None`.
6117pub fn cv_next(d: &cvdef, sp: &mut Option<String>, // c:3240
6118 ap: &mut Option<String>) -> Option<Box<cvval>>
6119{
6120 let s_in = sp.take().unwrap_or_default();
6121 if s_in.is_empty() { // c:3245
6122 *sp = None;
6123 *ap = None;
6124 return None;
6125 }
6126 let bytes = s_in.as_bytes();
6127
6128 // Branch 1: hassep && !sep, or !argsep — greedy match (longest prefix).
6129 if (d.hassep != 0 && d.sep == 0) || d.argsep == 0 { // c:3250
6130 let ec_byte: u8 = if d.hassep != 0 && d.sep != 0 {
6131 d.sep as u8
6132 } else {
6133 d.argsep as u8
6134 };
6135 let mut s_idx: usize = 0;
6136 let mut r: Option<Box<cvval>> = None;
6137 // c:3255 — extend until cv_quote_get_val matches or hit ec.
6138 loop {
6139 s_idx += 1;
6140 if s_idx > bytes.len() { break; }
6141 let candidate = std::str::from_utf8(&bytes[..s_idx])
6142 .unwrap_or("");
6143 r = cv_quote_get_val(d, candidate);
6144 if r.is_some() { break; }
6145 if s_idx >= bytes.len() || bytes[s_idx] == ec_byte { break; }
6146 }
6147 let os = s_idx;
6148 // c:3268 — advance *sp.
6149 if d.hassep != 0 && d.sep != 0 {
6150 let sep_byte = d.sep as u8;
6151 if let Some(off) = bytes[s_idx.min(bytes.len())..]
6152 .iter().position(|&b| b == sep_byte)
6153 {
6154 let after = s_idx + off + 1;
6155 *sp = Some(String::from_utf8_lossy(&bytes[after..]).into_owned());
6156 } else {
6157 *sp = None;
6158 }
6159 } else {
6160 *sp = if s_idx < bytes.len() {
6161 Some(String::from_utf8_lossy(&bytes[s_idx..]).into_owned())
6162 } else { None };
6163 }
6164 // c:3275 — set *ap.
6165 let argsep_b = d.argsep as u8;
6166 if d.argsep != 0 && os < bytes.len() && bytes[os] == argsep_b {
6167 *ap = Some(String::from_utf8_lossy(&bytes[os + 1..]).into_owned());
6168 *sp = None;
6169 } else if r.as_deref().map_or(false, |v| v.r#type != CVV_NOARG) {
6170 *ap = if os < bytes.len() {
6171 Some(String::from_utf8_lossy(&bytes[os..]).into_owned())
6172 } else { None };
6173 } else {
6174 *ap = None;
6175 }
6176 return r;
6177 }
6178
6179 // Branch 2: hassep set (with both sep and argsep).
6180 if d.hassep != 0 { // c:3285
6181 let sep_b = d.sep as u8;
6182 let argsep_b = d.argsep as u8;
6183 let ns = bytes.iter().position(|&b| b == sep_b);
6184 let mut as_off: Option<usize> = None;
6185 let mut skip = false;
6186 if d.argsep != 0 {
6187 if let Some(a_pos) = bytes.iter().position(|&b| b == argsep_b) {
6188 if ns.map_or(true, |n| a_pos <= n) { // c:3289
6189 as_off = Some(a_pos);
6190 *ap = Some(String::from_utf8_lossy(&bytes[a_pos + 1..])
6191 .into_owned());
6192 skip = true;
6193 }
6194 }
6195 }
6196 let sap = as_off.or(ns);
6197 let head = match sap {
6198 Some(p) => std::str::from_utf8(&bytes[..p]).unwrap_or(""),
6199 None => std::str::from_utf8(&bytes).unwrap_or(""),
6200 };
6201 let r = cv_quote_get_val(d, head);
6202 // c:3302 — if NOARG/no match and skip, fall back to as.
6203 let ns_eff = if (r.as_deref().map_or(true, |v| v.r#type == CVV_NOARG)) && skip {
6204 as_off
6205 } else if skip {
6206 // skip is set ⇒ as_off was the chosen sep; ns might be after.
6207 ns.filter(|&n| as_off.map_or(true, |a| n > a))
6208 } else {
6209 ns
6210 };
6211 let next_off = match ns_eff {
6212 None => None,
6213 Some(n) if Some(n) == as_off
6214 && r.as_deref().map_or(false, |v| v.r#type != CVV_NOARG) => None,
6215 Some(n) => Some(n + 1),
6216 };
6217 *sp = next_off.map(|o|
6218 String::from_utf8_lossy(&bytes[o..]).into_owned());
6219 if !skip { *ap = None; }
6220 return r;
6221 }
6222
6223 // Branch 3: no hassep, argsep set.
6224 *sp = None; // c:3314
6225 let argsep_b = d.argsep as u8;
6226 let as_pos = bytes.iter().position(|&b| b == argsep_b);
6227 let head = match as_pos {
6228 Some(p) => {
6229 *ap = Some(String::from_utf8_lossy(&bytes[p + 1..]).into_owned());
6230 std::str::from_utf8(&bytes[..p]).unwrap_or("")
6231 }
6232 None => {
6233 *ap = None;
6234 &s_in
6235 }
6236 };
6237 cv_quote_get_val(d, head) // c:3324
6238}
6239
6240/// Direct port of `static void cv_parse_word(Cvdef d)` from
6241/// `Src/Zle/computil.c:3336-3472`. Walks `compwords[1..]` (when
6242/// `d->words` set) and `compprefix` calling `cv_next` repeatedly,
6243/// accumulating recognized values into `cv_laststate.vals`.
6244pub fn cv_parse_word(d: &mut cvdef) { // c:3336
6245 use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS,
6246 COMPPREFIX, COMPSUFFIX, ignore_prefix, ignore_suffix};
6247 use std::sync::atomic::Ordering;
6248
6249 // c:3343 — free old vals.
6250 if cv_alloced.load(Ordering::Relaxed) != 0 {
6251 if let Ok(mut ls) = cv_laststate.lock() {
6252 ls.vals = None;
6253 }
6254 }
6255 // c:3346 — mark all vals active.
6256 let mut v = d.vals.as_deref_mut();
6257 while let Some(vv) = v {
6258 vv.active = 1;
6259 v = vv.next.as_deref_mut();
6260 }
6261
6262 let mut state_vals: Vec<String> = Vec::new();
6263 let mut state_def: Option<Box<caarg>> = None;
6264 let mut state_val: Option<Box<cvval>> = None;
6265 cv_alloced.store(1, Ordering::Relaxed);
6266
6267 let compcur = COMPCURRENT.load(Ordering::Relaxed);
6268 let compwords: Vec<String> = COMPWORDS
6269 .get()
6270 .and_then(|m| m.lock().ok().map(|w| w.clone()))
6271 .unwrap_or_default();
6272 let compprefix: String = COMPPREFIX.get()
6273 .and_then(|m| m.lock().ok().map(|s| s.clone()))
6274 .unwrap_or_default();
6275 let compsuffix: String = COMPSUFFIX.get()
6276 .and_then(|m| m.lock().ok().map(|s| s.clone()))
6277 .unwrap_or_default();
6278 let mut pign = compprefix.clone(); // c:3340
6279 let mut nosfx = false;
6280
6281 // c:3356 — scan compwords[1..] if d.words is set.
6282 if d.words != 0 && !compwords.is_empty() && !compwords[0].is_empty() {
6283 for i in 1..compwords.len() {
6284 if (i as i32) == compcur - 1 { continue; }
6285 let mut str_opt: Option<String> = Some(compwords[i].clone());
6286 while str_opt.as_deref().map_or(false, |s| !s.is_empty()) {
6287 let mut ap: Option<String> = None;
6288 let val = cv_next(d, &mut str_opt, &mut ap);
6289 if let Some(v) = val {
6290 state_vals.push(v.name.clone().unwrap_or_default());
6291 state_vals.push(ap.unwrap_or_default());
6292 if (i as i32) + 1 < compcur {
6293 let xor = v.xor.clone().unwrap_or_default();
6294 cv_inactive(d, &xor);
6295 }
6296 } else {
6297 break;
6298 }
6299 }
6300 }
6301 }
6302
6303 // c:3385 — scan compprefix.
6304 let mut str_opt: Option<String> = Some(compprefix.clone());
6305 let mut last_arg: Option<String> = None;
6306 while str_opt.as_deref().map_or(false, |s| !s.is_empty()) {
6307 let mut ap: Option<String> = None;
6308 let val = cv_next(d, &mut str_opt, &mut ap);
6309 if let Some(v) = val {
6310 state_vals.push(v.name.clone().unwrap_or_default());
6311 match ap.as_deref() {
6312 Some(arg_v) => {
6313 if str_opt.is_some() {
6314 state_vals.push(arg_v.to_string());
6315 } else {
6316 let joined = format!("{}{}", arg_v, compsuffix);
6317 state_vals.push(joined);
6318 nosfx = true;
6319 }
6320 last_arg = ap.clone();
6321 }
6322 None => state_vals.push(String::new()),
6323 }
6324 let xor = v.xor.clone().unwrap_or_default();
6325 cv_inactive(d, &xor);
6326 if let Some(s) = str_opt.as_deref() {
6327 pign = s.to_string();
6328 } else {
6329 // c:3407 — re-activate v in the cvdef.
6330 let target_name = v.name.clone();
6331 let mut p = d.vals.as_deref_mut();
6332 while let Some(vv) = p {
6333 if vv.name == target_name { vv.active = 1; break; }
6334 p = vv.next.as_deref_mut();
6335 }
6336 }
6337 state_val = Some(v);
6338 } else {
6339 break;
6340 }
6341 }
6342 if state_val.is_some() && last_arg.is_some() && str_opt.is_none() { // c:3411
6343 state_def = state_val.as_ref().and_then(|v| v.arg.clone());
6344 }
6345
6346 // c:3414 — separator handling for compsuffix.
6347 if !nosfx && d.hassep != 0 {
6348 let pign_len = pign.len();
6349 let cp_len = compprefix.len();
6350 ignore_prefix(cp_len as i32 - pign_len as i32); // c:3418
6351
6352 let mut ign = 0usize;
6353 let mut more: Option<String> = None;
6354 if d.sep == 0 && (state_val.is_none()
6355 || state_val.as_deref().map_or(true, |v| v.r#type == CVV_NOARG))
6356 {
6357 ign = compsuffix.len();
6358 more = Some(compsuffix.clone());
6359 } else if d.sep != 0 {
6360 let sep_b = d.sep as u8;
6361 let ns_pos = compsuffix.as_bytes().iter().position(|&b| b == sep_b);
6362 let as_pos = if d.argsep != 0 {
6363 compsuffix.as_bytes().iter().position(|&b| b == d.argsep as u8)
6364 } else { None };
6365 if let Some(a) = as_pos {
6366 if ns_pos.map_or(true, |n| a <= n) {
6367 ign = compsuffix.len() - a;
6368 } else {
6369 ign = ns_pos.map_or(0, |n| compsuffix.len() - n);
6370 }
6371 } else {
6372 ign = ns_pos.map_or(0, |n| compsuffix.len() - n);
6373 }
6374 more = ns_pos.map(|n| compsuffix[n + 1..].to_string());
6375 } else if d.argsep != 0 {
6376 let as_pos = compsuffix.as_bytes().iter()
6377 .position(|&b| b == d.argsep as u8);
6378 if let Some(a) = as_pos {
6379 ign = compsuffix.len() - a;
6380 }
6381 }
6382
6383 if ign > 0 {
6384 ignore_suffix(ign as i32); // c:3444
6385 }
6386
6387 let mut more_opt = more;
6388 while more_opt.as_deref().map_or(false, |s| !s.is_empty()) { // c:3446
6389 let mut ap: Option<String> = None;
6390 let val = cv_next(d, &mut more_opt, &mut ap);
6391 if let Some(v) = val {
6392 state_vals.push(v.name.clone().unwrap_or_default());
6393 match ap.as_deref() {
6394 Some(arg_v) => {
6395 if more_opt.is_some() {
6396 state_vals.push(arg_v.to_string());
6397 } else {
6398 state_vals.push(format!("{}{}", arg_v, compsuffix));
6399 }
6400 }
6401 None => state_vals.push(String::new()),
6402 }
6403 let xor = v.xor.clone().unwrap_or_default();
6404 cv_inactive(d, &xor);
6405 } else { break; }
6406 }
6407 } else if last_arg.is_some() {
6408 let cp_len = compprefix.len();
6409 let arg_off = compprefix.find(last_arg.as_deref().unwrap_or(""))
6410 .map(|i| i as i32).unwrap_or(cp_len as i32);
6411 ignore_prefix(arg_off); // c:3467
6412 } else {
6413 let cp_len = compprefix.len();
6414 ignore_prefix(cp_len as i32 - pign.len() as i32); // c:3469
6415 }
6416
6417 // c:3471 — commit state.
6418 if let Ok(mut ls) = cv_laststate.lock() {
6419 *ls = cvstate {
6420 d: Some(Box::new(d.clone())),
6421 def: state_def,
6422 val: state_val,
6423 vals: if state_vals.is_empty() { None } else { Some(state_vals) },
6424 };
6425 }
6426}
6427
6428/// Direct port of `static Cvval cv_quote_get_val(Cvdef d, char *name)`
6429/// from `Src/Zle/computil.c:3190-3204`. Unquotes `name` via the full
6430/// C chain: `parse_subst_string` (with noerrs=2 to suppress errors),
6431/// `remnulargs`, then `untokenize`; result fed to `cv_get_val`.
6432pub fn cv_quote_get_val(d: &cvdef, name: &str) -> Option<Box<cvval>> { // c:3190
6433 // c:3195 — `name = dupstring(name)` (Rust: own a mutable copy).
6434 let mut s = name.to_string();
6435 // c:3196-3199 — `ne = noerrs; noerrs = 2; parse_subst_string(name);
6436 // noerrs = ne`. The parse_subst_string port (lex.rs:3797)
6437 // returns Result; we discard errors so noerrs=2/restore is a no-op.
6438 crate::ported::utils::set_noerrs(2);
6439 let parsed = crate::ported::lex::parse_subst_string(&s).ok();
6440 crate::ported::utils::set_noerrs(0);
6441 if let Some(p) = parsed { s = p; }
6442 // c:3200 — `remnulargs(name)`.
6443 crate::ported::glob::remnulargs(&mut s);
6444 // c:3201 — `untokenize(name)`.
6445 let s = crate::ported::lex::untokenize(&s);
6446 // c:3203 — `return cv_get_val(d, name)`.
6447 cv_get_val(d, &s)
6448}
6449
6450/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from Src/Zle/computil.c:5146.
6451/// WARNING: param names don't match C — Rust=() vs C=(m, enables)
6452pub fn enables_() -> i32 { // c:5146
6453 // C body c:5148 — `return handlefeatures(m, &module_features, enables)`.
6454 // Static-link no-op.
6455 0
6456}
6457
6458/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from Src/Zle/computil.c:5138.
6459/// WARNING: param names don't match C — Rust=() vs C=(m, features)
6460pub fn features_() -> i32 { // c:5138
6461 // C body c:5140-5141 — `*features = featuresarray(...); return 0`.
6462 // Features array exposed elsewhere; return 0.
6463 0
6464}
6465
6466/// Direct port of `int finish_(UNUSED(Module m))` from
6467/// `Src/Zle/computil.c:5167-5180`. Frees every cached cadef/cvdef
6468/// and the comptags table on module unload.
6469pub fn finish_() -> i32 { // c:5167
6470 // c:5171 — `for (i = 0; i < MAX_CACACHE; i++) freecadef(cadef_cache[i])`.
6471 if let Ok(mut cache) = cadef_cache.lock() {
6472 for slot in cache.iter_mut() {
6473 freecadef(slot.take());
6474 }
6475 }
6476 // c:5173 — `for (i = 0; i < MAX_CVCACHE; i++) freecvdef(cvdef_cache[i])`.
6477 if let Ok(mut cache) = cvdef_cache.lock() {
6478 for slot in cache.iter_mut() {
6479 freecvdef(slot.take());
6480 }
6481 }
6482 // c:5176 — `for (i = 0; i < MAX_TAGS; i++) freectags(comptags[i])`.
6483 if let Ok(mut tab) = comptags.lock() {
6484 for slot in tab.iter_mut() {
6485 freectags(slot.take());
6486 }
6487 }
6488 0 // c:5179
6489}
6490
6491/// Direct port of `int setup_(UNUSED(Module m))` from
6492/// `Src/Zle/computil.c:5124-5134`. Zeroes the three module caches
6493/// and resets `lasttaglevel`. Called on module load.
6494pub fn setup_() -> i32 { // c:5124
6495 // c:5126 — `memset(cadef_cache, 0, sizeof(cadef_cache))`.
6496 if let Ok(mut cache) = cadef_cache.lock() {
6497 for slot in cache.iter_mut() {
6498 freecadef(slot.take());
6499 }
6500 }
6501 // c:5127 — `memset(cvdef_cache, 0, sizeof(cvdef_cache))`.
6502 if let Ok(mut cache) = cvdef_cache.lock() {
6503 for slot in cache.iter_mut() {
6504 freecvdef(slot.take());
6505 }
6506 }
6507 // c:5129 — `memset(comptags, 0, sizeof(comptags))`.
6508 if let Ok(mut tab) = comptags.lock() {
6509 for slot in tab.iter_mut() {
6510 freectags(slot.take());
6511 }
6512 }
6513 // c:5131 — `lasttaglevel = 0`.
6514 lasttaglevel.store(0, std::sync::atomic::Ordering::Relaxed);
6515 0 // c:5133
6516}
6517
6518// `freecastate` / `freectags` / `freectset` / `freecvdef` real ports
6519// landed above with the castate / ctags / ctset / cvdef structs.
6520
6521/// Direct port of `static Cadef get_cadef(char *nam, char **args)`
6522/// from `Src/Zle/computil.c:1673-1694`. Walks `cadef_cache` looking
6523/// for an entry whose `defs` array matches the requested `args`
6524/// (same length + position-for-position string equality). On hit,
6525/// bumps that entry's `lastt` and returns it. On miss, parses via
6526/// `parse_cadef` and evicts the entry with the oldest `lastt`
6527/// (or the first empty slot) to make room for the new one.
6528///
6529/// Returns `1` on hit, `0` on miss-and-cache-insert. The previous
6530/// return-`i32` shape is preserved for callers; the parsed cadef
6531/// itself lives in `cadef_cache` and is looked up by separate
6532/// per-name accessors (`ca_get_opt`, `ca_get_arg`, etc.).
6533pub fn get_cadef(nam: &str, args: &[String]) -> i32 { // c:1673
6534 let na = args.len() as i32;
6535 let now = { // c:1681 time(0)
6536 use std::time::{SystemTime, UNIX_EPOCH};
6537 SystemTime::now().duration_since(UNIX_EPOCH)
6538 .map(|d| d.as_secs() as i64).unwrap_or(0)
6539 };
6540
6541 if let Ok(mut cache) = cadef_cache.lock() {
6542 // c:1678 — `for (i = MAX_CACACHE, p = cadef_cache, min = NULL;
6543 // i && *p; p++, i--)`. Linear scan; track LRU
6544 // candidate for eviction in `min_idx`.
6545 let mut min_idx: Option<usize> = None;
6546 let mut min_lastt: i64 = i64::MAX;
6547 let mut hit_idx: Option<usize> = None;
6548 for (i, slot) in cache.iter().enumerate() {
6549 match slot {
6550 Some(entry) => {
6551 // c:1679 — `if (*p && na == (*p)->ndefs && arrcmp(args, (*p)->defs))`.
6552 if entry.ndefs == na
6553 && entry.defs.as_deref()
6554 .map_or(false, |d| d.len() == args.len()
6555 && d.iter().zip(args.iter()).all(|(a, b)| a == b))
6556 {
6557 hit_idx = Some(i);
6558 break; // c:1682 break on match
6559 }
6560 // c:1684 — track entry with smallest lastt as eviction target.
6561 if entry.lastt < min_lastt {
6562 min_lastt = entry.lastt;
6563 min_idx = Some(i);
6564 }
6565 }
6566 None => {
6567 // c:1684 — empty slot wins as eviction target.
6568 min_idx = Some(i);
6569 break;
6570 }
6571 }
6572 }
6573 if let Some(i) = hit_idx {
6574 if let Some(entry) = cache[i].as_mut() {
6575 entry.lastt = now; // c:1681
6576 }
6577 return 1; // c:1683 hit
6578 }
6579 // c:1688 — parse_cadef; on success replace the chosen slot.
6580 if let Some(new) = parse_cadef(nam, args) {
6581 let idx = min_idx.unwrap_or(0);
6582 cache[idx] = Some(new);
6583 }
6584 }
6585 0 // c:1693 miss
6586}
6587
6588/// Direct port of `static Caarg parse_caarg(int mult, int type, int num,
6589/// int opt, char *oname, char **def,
6590/// char *set)` from
6591/// `Src/Zle/computil.c:1099-1144`. Parses one `:descr[:action]`
6592/// fragment of an `_arguments` spec into a freshly-allocated caarg.
6593/// On return, `*idx` points at the first byte of `bytes` not consumed
6594/// (either the separator `:` for `mult=1` or `bytes.len()` for
6595/// `mult=0` rest specs).
6596pub fn parse_caarg(mult: i32, atype: i32, num: i32, opt: i32, // c:1099
6597 oname: Option<&str>, bytes: &[u8], idx: &mut usize,
6598 set: Option<&str>) -> Box<caarg> {
6599 let mut ret = Box::new(caarg::default());
6600 ret.num = num; // c:1109
6601 ret.min = num - opt; // c:1110
6602 ret.r#type = atype; // c:1111
6603 ret.opt = oname.map(|s| s.to_string()); // c:1112
6604 ret.direct = 0; // c:1113
6605 ret.gsname = set.map(|s| s.to_string()); // c:1114
6606
6607 let n = bytes.len();
6608
6609 // c:1118-1120 — scan description up to the next `:` (escaped `\:` skipped).
6610 let d_start = *idx;
6611 while *idx < n && bytes[*idx] != b':' {
6612 if bytes[*idx] == b'\\' && *idx + 1 < n {
6613 *idx += 1;
6614 }
6615 *idx += 1;
6616 }
6617 let has_sav = *idx < n;
6618 let descr_slice = &bytes[d_start..*idx];
6619 let descr_str = std::str::from_utf8(descr_slice).unwrap_or("");
6620 ret.descr = Some(rembslashcolon(descr_str)); // c:1123
6621
6622 if has_sav { // c:1127
6623 if mult != 0 { // c:1128
6624 // c:1129-1136 — `*p == ':'` start, scan to next `:` or NUL.
6625 *idx += 1;
6626 let a_start = *idx;
6627 while *idx < n && bytes[*idx] != b':' {
6628 if bytes[*idx] == b'\\' && *idx + 1 < n {
6629 *idx += 1;
6630 }
6631 *idx += 1;
6632 }
6633 let action_slice = &bytes[a_start..*idx];
6634 let action_str = std::str::from_utf8(action_slice).unwrap_or("");
6635 ret.action = Some(rembslashcolon(action_str)); // c:1134
6636 } else { // c:1137
6637 // c:1138 — `ret->action = ztrdup(rembslashcolon(p + 1))`.
6638 let action_slice = &bytes[*idx + 1..];
6639 let action_str = std::str::from_utf8(action_slice).unwrap_or("");
6640 ret.action = Some(rembslashcolon(action_str));
6641 *idx = n;
6642 }
6643 } else { // c:1139
6644 ret.action = Some(String::new()); // c:1140
6645 }
6646 // c:1141 — `*def = p`. Caller reads `bytes[*idx]` to decide whether to
6647 // continue scanning more `:` fragments.
6648
6649 ret
6650}
6651
6652/// Direct port of `static Cadef parse_cadef(char *nam, char **args)` from
6653/// `Src/Zle/computil.c:1196-1666`. Parses the leading auto-description
6654/// (first arg up to `%d`), the `-s/-A/-S/-M` flag block, then the
6655/// main spec-list loop that fills opts/args/rest from each remaining
6656/// `_arguments` spec entry.
6657pub fn parse_cadef(nam: &str, args: &[String]) -> Option<Box<cadef>> { // c:1196
6658 use crate::ported::ztype_h::{iblank, idigit, inblank};
6659
6660 if args.is_empty() {
6661 return None; // c:1262 `!*args`
6662 }
6663
6664 let orig_args = args;
6665 let mut idx = 0usize;
6666 let mut single: i32 = 0;
6667 let mut flags: i32 = 0;
6668 let mut match_spec: String = "r:|[_-]=* r:|=*".to_string(); // c:1200
6669 let mut nonarg: Option<String> = None;
6670
6671 // c:1208-1216 — split args[0] on `%d` into (adpre, adsuf). Used at
6672 // c:1543-1554 to auto-derive option descriptions.
6673 let (adpre, adsuf): (Option<String>, Option<String>) = {
6674 let first = args[0].as_bytes();
6675 let mut split_at: Option<usize> = None;
6676 let mut i = 0usize;
6677 while i + 1 < first.len() {
6678 if first[i] == b'%' && first[i + 1] == b'd' {
6679 split_at = Some(i);
6680 break;
6681 }
6682 i += 1;
6683 }
6684 if let Some(at) = split_at {
6685 let pre = String::from_utf8_lossy(&first[..at]).into_owned();
6686 let suf = String::from_utf8_lossy(&first[at + 2..]).into_owned();
6687 (Some(pre), Some(suf))
6688 } else {
6689 (None, None)
6690 }
6691 };
6692
6693 idx += 1; // c:1220 args++
6694
6695 // c:1221-1259 — `-s/-A/-S/-M[arg]` flag block.
6696 while idx < args.len() {
6697 let p = &args[idx];
6698 let bytes = p.as_bytes();
6699 if bytes.len() < 2 || bytes[0] != b'-' { // c:1221
6700 break;
6701 }
6702 let cluster = &bytes[1..];
6703 let mut ok = true;
6704 for (i, &c) in cluster.iter().enumerate() {
6705 match c {
6706 b's' => single = 1, // c:1233
6707 b'S' => flags |= CDF_SEP, // c:1235
6708 b'A' => { // c:1237
6709 if i + 1 < cluster.len() { // c:1238
6710 nonarg = Some(String::from_utf8_lossy(&cluster[i + 1..]).into_owned());
6711 } else if idx + 1 < args.len() { // c:1241
6712 nonarg = Some(args[idx + 1].clone());
6713 idx += 1;
6714 } else {
6715 ok = false;
6716 }
6717 break;
6718 }
6719 b'M' => { // c:1245
6720 if i + 1 < cluster.len() { // c:1246
6721 match_spec = String::from_utf8_lossy(&cluster[i + 1..]).into_owned();
6722 } else if idx + 1 < args.len() { // c:1249
6723 match_spec = args[idx + 1].clone();
6724 idx += 1;
6725 } else {
6726 ok = false;
6727 }
6728 break;
6729 }
6730 _ => {
6731 ok = false;
6732 break;
6733 }
6734 }
6735 }
6736 if !ok {
6737 break; // c:1230
6738 }
6739 idx += 1; // c:1258
6740 }
6741
6742 if idx < args.len() && args[idx] == ":" { // c:1260
6743 idx += 1;
6744 }
6745 if idx >= args.len() { // c:1262
6746 return None;
6747 }
6748
6749 // c:1266 — `tokenize(nonarg = dupstring(nonarg))`. The Rust matcher
6750 // path lazily tokenizes on use; the stored bytes are the spec text.
6751
6752 // c:1269 — `all = ret = alloc_cadef(orig_args, single, match, nonarg, flags)`.
6753 let first_def = alloc_cadef(
6754 Some(orig_args),
6755 single,
6756 &match_spec,
6757 nonarg.as_deref(),
6758 flags,
6759 );
6760
6761 // ---- spec-list loop state (c:1271-1273) ----
6762 // `sets` accumulates each Cadef in `snext` order; per-set opts/args/rest
6763 // are collected in parallel Vecs and linked into the cadef at the end.
6764 let mut sets: Vec<Box<cadef>> = vec![first_def];
6765 let mut opts_per_set: Vec<Vec<Box<caopt>>> = vec![Vec::new()];
6766 let mut args_per_set: Vec<Vec<Box<caarg>>> = vec![Vec::new()];
6767 let mut rest_per_set: Vec<Option<Box<caarg>>> = vec![None];
6768
6769 let sargs = idx; // c:1271 saved set-start
6770 let mut anum: i32 = 1; // c:1203
6771 let mut doset: Option<String> = None;
6772 let mut axor: Option<String> = None;
6773 let mut curset: Option<usize> = None; // c:1201
6774 let mut pendset: Option<usize> = None;
6775 let mut foreignset = false;
6776
6777 // c:1275 — `for (; *args || pendset; args++)`.
6778 'outer: loop {
6779 // c:1276 — `if (!*args)` start a fresh set (restart from sargs).
6780 if idx >= args.len() {
6781 if pendset.is_none() {
6782 break 'outer;
6783 }
6784 // c:1278-1286 — set_cadef_opts on current; alloc new cadef as snext.
6785 {
6786 let cur = sets.last_mut().unwrap();
6787 let cur_args = args_per_set.last_mut().unwrap();
6788 // Link the args list into cur so set_cadef_opts can walk it.
6789 let mut head: Option<Box<caarg>> = None;
6790 for arg_box in cur_args.drain(..).rev() {
6791 let mut a = arg_box;
6792 a.next = head;
6793 head = Some(a);
6794 }
6795 cur.args = head;
6796 set_cadef_opts(cur); // c:1280
6797 // Stash args back as a Vec for the rest of the loop. We need
6798 // both forms; the linked list will be rebuilt at the end.
6799 let mut walk = cur.args.take();
6800 while let Some(mut node) = walk {
6801 walk = node.next.take();
6802 cur_args.push(node);
6803 }
6804 }
6805 idx = sargs; // c:1278
6806 doset = None; // c:1279
6807 sets.push(alloc_cadef(None, single, &match_spec, // c:1281
6808 nonarg.as_deref(), flags));
6809 opts_per_set.push(Vec::new());
6810 args_per_set.push(Vec::new());
6811 rest_per_set.push(None);
6812 anum = 1; // c:1283
6813 foreignset = false; // c:1284
6814 curset = pendset; // c:1285
6815 pendset = None; // c:1286
6816 }
6817
6818 let arg = &args[idx];
6819 let arg_bytes = arg.as_bytes();
6820
6821 // c:1288 — `args[0][0] == '-' && !args[0][1] && args[1]` — set marker.
6822 if arg_bytes == b"-" && idx + 1 < args.len() {
6823 if curset.is_some() && curset != Some(idx) { // c:1289
6824 foreignset = true;
6825 if pendset.is_none() && Some(idx) > curset { // c:1290
6826 pendset = Some(idx);
6827 }
6828 idx += 1; // c:1292 ++args
6829 } else { // c:1293
6830 foreignset = false;
6831 idx += 1;
6832 let p_str = &args[idx]; // c:1295 char *p = *++args
6833 let pb = p_str.as_bytes();
6834 let l = pb.len().saturating_sub(1);
6835 // c:1298 — `if (*p == '(' && p[l] == ')')` strip parens for axor.
6836 let (set_name, ax) = if !pb.is_empty()
6837 && pb[0] == b'(' && pb[l] == b')'
6838 {
6839 let inner = String::from_utf8_lossy(&pb[1..l]).into_owned();
6840 (inner.clone(), Some(inner))
6841 } else {
6842 (p_str.clone(), None)
6843 };
6844 axor = ax;
6845 if set_name.is_empty() { // c:1302
6846 zwarnnam(nam, "empty set name");
6847 return None;
6848 }
6849 let new_set = crate::ported::string::tricat(&set_name, "-", "");// c:1307
6850 doset = Some(new_set.clone());
6851 {
6852 let cur = sets.last_mut().unwrap();
6853 cur.set = Some(new_set);
6854 }
6855 curset = Some(idx); // c:1308
6856 }
6857 idx += 1;
6858 continue; // c:1310
6859 }
6860
6861 // c:1311 — `args[0][0] == '+' && !args[0][1] && args[1]` — group marker.
6862 if arg_bytes == b"+" && idx + 1 < args.len() {
6863 foreignset = false; // c:1315
6864 idx += 1;
6865 let p_str = &args[idx]; // c:1316
6866 let pb = p_str.as_bytes();
6867 let l = pb.len().saturating_sub(1);
6868 let (group_name, ax) = if !pb.is_empty()
6869 && pb[0] == b'(' && pb[l] == b')'
6870 {
6871 let inner = String::from_utf8_lossy(&pb[1..l]).into_owned();
6872 (inner.clone(), Some(inner))
6873 } else {
6874 (p_str.clone(), None)
6875 };
6876 axor = ax;
6877 if group_name.is_empty() { // c:1322
6878 zwarnnam(nam, "empty group name");
6879 return None;
6880 }
6881 doset = Some(crate::ported::string::tricat(&group_name, "-", ""));// c:1327
6882 idx += 1;
6883 continue; // c:1328
6884 }
6885
6886 // c:1329 — `if (foreignset) continue` — skip specs for other sets.
6887 if foreignset {
6888 idx += 1;
6889 continue;
6890 }
6891
6892 // c:1331 — parse one spec entry.
6893 let bytes = arg_bytes;
6894 let mut p = 0usize;
6895 let mut xnum: i32 = 0; // c:1332
6896 let mut not_flag = false;
6897 if p < bytes.len() && bytes[p] == b'!' { // c:1333
6898 not_flag = true;
6899 p += 1;
6900 }
6901
6902 let mut xor: Option<Vec<String>> = None;
6903 if p < bytes.len() && bytes[p] == b'(' { // c:1335 xor list
6904 let mut list: Vec<String> = Vec::new();
6905 // c:1342-1354 — collect words inside parens.
6906 let mut bad = false;
6907 'paren: loop {
6908 if p >= bytes.len() || bytes[p] == b')' { break; }
6909 p += 1; // c:1343 p++
6910 while p < bytes.len() && inblank(bytes[p]) { p += 1; } // c:1343 inblank skip
6911 if p >= bytes.len() { bad = true; break 'paren; }
6912 if bytes[p] == b')' { break 'paren; }
6913 let q = p;
6914 p += 1;
6915 while p < bytes.len() && bytes[p] != b')' && !inblank(bytes[p]) {
6916 p += 1;
6917 }
6918 if p >= bytes.len() { bad = true; break 'paren; } // c:1349
6919 let word = String::from_utf8_lossy(&bytes[q..p]).into_owned();
6920 list.push(word);
6921 xnum += 1; // c:1353
6922 }
6923 if bad || p >= bytes.len() || bytes[p] != b')' { // c:1356
6924 zwarnnam(nam, &format!("invalid argument: {}", arg));
6925 return None;
6926 }
6927 if doset.is_some() && axor.is_some() { // c:1361
6928 xnum += 1;
6929 list.push(axor.clone().unwrap()); // c:1366-1367
6930 }
6931 xor = Some(list);
6932 p += 1; // c:1370
6933 } else if doset.is_some() && axor.is_some() { // c:1371
6934 xnum = 1;
6935 xor = Some(vec![axor.clone().unwrap()]);
6936 }
6937
6938 // c:1379 — option spec OR rest-arg OR normal-arg.
6939 let is_opt = p < bytes.len() && (
6940 bytes[p] == b'-' || bytes[p] == b'+'
6941 || (bytes[p] == b'*' && p + 1 < bytes.len()
6942 && (bytes[p + 1] == b'-' || bytes[p + 1] == b'+'))
6943 );
6944
6945 if is_opt {
6946 // ---- c:1381-1580 option spec branch ----
6947 // The `rec:` goto loop handles `-+`/`+-` duplication by
6948 // parsing the same spec twice with name[0] flipped between
6949 // `-` and `+`.
6950 let mut again_iter = 0i32; // c:1384
6951 let mut againp_start: Option<usize> = None;
6952 let mut p_state = p;
6953 let mut xor_state = xor;
6954 let mut xnum_state = xnum;
6955
6956 'rec: loop {
6957 let mut multi = false; // c:1390
6958 if p_state < bytes.len() && bytes[p_state] == b'*' {
6959 multi = true;
6960 p_state += 1;
6961 }
6962
6963 let mut name_start: usize;
6964 let mut name_buf: Vec<u8>;
6965 let need_flip = p_state + 2 < bytes.len()
6966 && ((bytes[p_state] == b'-' && bytes[p_state + 1] == b'+')
6967 || (bytes[p_state] == b'+' && bytes[p_state + 1] == b'-'))
6968 && bytes[p_state + 2] != b':'
6969 && bytes[p_state + 2] != b'['
6970 && bytes[p_state + 2] != b'='
6971 && bytes[p_state + 2] != b'-'
6972 && bytes[p_state + 2] != b'+';
6973
6974 if need_flip { // c:1393
6975 if again_iter == 0 {
6976 againp_start = Some(p_state);
6977 }
6978 name_start = p_state + 1;
6979 name_buf = bytes[name_start..].to_vec();
6980 if !name_buf.is_empty() {
6981 name_buf[0] = if again_iter != 0 { b'-' } else { b'+' };
6982 }
6983 again_iter += 1;
6984 p_state = name_start;
6985 } else { // c:1404
6986 name_start = p_state;
6987 name_buf = bytes[name_start..].to_vec();
6988 if p_state + 1 < bytes.len()
6989 && bytes[p_state] == b'-' && bytes[p_state + 1] == b'-'
6990 {
6991 p_state += 1; // c:1407 skip 2nd '-'
6992 }
6993 }
6994
6995 if p_state + 1 >= bytes.len() { // c:1409
6996 zwarnnam(nam, &format!("invalid argument: {}", arg));
6997 return None;
6998 }
6999
7000 // c:1416-1422 — skip option name body up to type byte.
7001 let mut np = p_state - name_start + 1;
7002 let nlen = name_buf.len();
7003 while np < nlen
7004 && name_buf[np] != b':'
7005 && name_buf[np] != b'['
7006 && !((name_buf[np] == b'-' || name_buf[np] == b'+')
7007 && np + 1 < nlen
7008 && (name_buf[np + 1] == b':' || name_buf[np + 1] == b'['))
7009 && !(name_buf[np] == b'='
7010 && np + 1 < nlen
7011 && (name_buf[np + 1] == b':'
7012 || name_buf[np + 1] == b'['
7013 || name_buf[np + 1] == b'-'))
7014 {
7015 if name_buf[np] == b'\\' && np + 1 < nlen {
7016 np += 1;
7017 }
7018 np += 1;
7019 }
7020
7021 let mut c_byte = if np < nlen { name_buf[np] } else { 0 };
7022 let opt_name_slice = &name_buf[..np];
7023 let opt_name = String::from_utf8_lossy(opt_name_slice).into_owned();
7024
7025 let mut otype = CAO_NEXT; // c:1384
7026 if c_byte == b'-' { // c:1427
7027 otype = CAO_DIRECT;
7028 np += 1;
7029 c_byte = if np < nlen { name_buf[np] } else { 0 };
7030 } else if c_byte == b'+' { // c:1430
7031 otype = CAO_ODIRECT;
7032 np += 1;
7033 c_byte = if np < nlen { name_buf[np] } else { 0 };
7034 } else if c_byte == b'=' { // c:1433
7035 otype = CAO_OEQUAL;
7036 np += 1;
7037 c_byte = if np < nlen { name_buf[np] } else { 0 };
7038 if c_byte == b'-' {
7039 otype = CAO_EQUAL; // c:1436
7040 np += 1;
7041 c_byte = if np < nlen { name_buf[np] } else { 0 };
7042 }
7043 }
7044
7045 // c:1441 — optional `[descr]`.
7046 let mut descr_str: Option<String> = None;
7047 if c_byte == b'[' { // c:1441
7048 np += 1;
7049 let d_start = np;
7050 while np < nlen && name_buf[np] != b']' {
7051 if name_buf[np] == b'\\' && np + 1 < nlen { np += 1; }
7052 np += 1;
7053 }
7054 if np >= nlen { // c:1446
7055 zwarnnam(nam, &format!("invalid option definition: {}", arg));
7056 return None;
7057 }
7058 let d_slice = &name_buf[d_start..np];
7059 descr_str = Some(String::from_utf8_lossy(d_slice).into_owned());
7060 np += 1;
7061 c_byte = if np < nlen { name_buf[np] } else { 0 };
7062 }
7063
7064 if c_byte != 0 && c_byte != b':' { // c:1456
7065 zwarnnam(nam, &format!("invalid option definition: {}", arg));
7066 return None;
7067 }
7068
7069 // c:1461 — add option name to xor list if not `*-...`.
7070 let clean_name = rembslashcolon(&opt_name);
7071 if !multi {
7072 let xv = xor_state.get_or_insert_with(Vec::new);
7073 if xv.len() <= xnum_state as usize {
7074 xv.resize(xnum_state as usize + 1, String::new());
7075 }
7076 xv[xnum_state as usize] = clean_name.clone();
7077 }
7078
7079 // c:1470-1531 — argument loop for `:descr:action[:...]`.
7080 let mut oargs: Vec<Box<caarg>> = Vec::new();
7081 if c_byte == b':' {
7082 let mut oanum: i32 = 1; // c:1473
7083 let mut onum: i32 = 0;
7084 while c_byte == b':' { // c:1479
7085 let mut rest = 0;
7086 let mut end_str: Option<String> = None;
7087 np += 1; // c:1484 *++p
7088 let atype: i32;
7089 c_byte = if np < nlen { name_buf[np] } else { 0 };
7090 if c_byte == b':' { // c:1485
7091 atype = CAA_OPT;
7092 np += 1;
7093 } else if c_byte == b'*' { // c:1487
7094 np += 1;
7095 if np < nlen && name_buf[np] != b':' { // c:1488
7096 let end_start = np;
7097 while np < nlen && name_buf[np] != b':' {
7098 if name_buf[np] == b'\\' && np + 1 < nlen {
7099 np += 1;
7100 }
7101 np += 1;
7102 }
7103 let e_slice = &name_buf[end_start..np];
7104 end_str = Some(String::from_utf8_lossy(e_slice).into_owned());
7105 }
7106 if np >= nlen || name_buf[np] != b':' { // c:1500
7107 zwarnnam(nam, &format!("invalid option definition: {}", arg));
7108 return None;
7109 }
7110 np += 1; // c:1507 *++p
7111 if np < nlen && name_buf[np] == b':' { // c:1508
7112 np += 1;
7113 if np < nlen && name_buf[np] == b':' { // c:1509
7114 atype = CAA_RREST;
7115 np += 1;
7116 } else {
7117 atype = CAA_RARGS;
7118 }
7119 } else {
7120 atype = CAA_REST;
7121 }
7122 rest = 1;
7123 } else {
7124 atype = CAA_NORMAL;
7125 }
7126
7127 // c:1521 — parse_caarg.
7128 let mut oarg = parse_caarg(
7129 if rest != 0 { 0 } else { 1 },
7130 atype, oanum, onum,
7131 Some(&clean_name),
7132 &name_buf, &mut np,
7133 doset.as_deref(),
7134 );
7135 oanum += 1;
7136 if atype == CAA_OPT { onum += 1; } // c:1524
7137 if let Some(end) = end_str {
7138 oarg.end = Some(end); // c:1526
7139 }
7140 oargs.push(oarg);
7141
7142 if rest != 0 { break; } // c:1528
7143 c_byte = if np < nlen { name_buf[np] } else { 0 }; // c:1530
7144 }
7145 }
7146
7147 // c:1534 — build the caopt.
7148 let mut opt_box = Box::new(caopt::default());
7149 opt_box.gsname = doset.clone(); // c:1539
7150 opt_box.name = Some(clean_name.clone()); // c:1540
7151 opt_box.descr = if let Some(d) = descr_str.clone() { // c:1542
7152 Some(d)
7153 } else if adpre.is_some() && oargs.len() == 1 { // c:1543
7154 let first_arg = &oargs[0];
7155 let d_field = first_arg.descr.as_deref().unwrap_or("");
7156 let has_visible = d_field.bytes().any(|b| !iblank(b));
7157 if has_visible { // c:1550
7158 Some(crate::ported::string::tricat(
7159 adpre.as_deref().unwrap_or(""),
7160 d_field,
7161 adsuf.as_deref().unwrap_or(""),
7162 ))
7163 } else {
7164 None // c:1553
7165 }
7166 } else {
7167 None
7168 };
7169 let xor_clone = if again_iter == 1 { // c:1556
7170 xor_state.clone()
7171 } else {
7172 xor_state.take()
7173 };
7174 opt_box.xor = xor_clone;
7175 opt_box.r#type = otype; // c:1557
7176 opt_box.not = if not_flag { 1 } else { 0 }; // c:1560
7177
7178 // Link in the arg list.
7179 let mut head: Option<Box<caarg>> = None;
7180 for a in oargs.into_iter().rev() {
7181 let mut a = a;
7182 a.next = head;
7183 head = Some(a);
7184 }
7185 opt_box.args = head;
7186
7187 {
7188 let cur = sets.last_mut().unwrap();
7189 opt_box.num = cur.nopts;
7190 cur.nopts += 1; // c:1559
7191 if otype == CAO_DIRECT || otype == CAO_EQUAL { // c:1562
7192 cur.ndopts += 1;
7193 } else if otype == CAO_ODIRECT || otype == CAO_OEQUAL { // c:1564
7194 cur.nodopts += 1;
7195 }
7196 // c:1571 — single-letter lookup table.
7197 if single != 0 {
7198 let nb = clean_name.as_bytes();
7199 if nb.len() == 2 && nb[1] != b'-' {
7200 let sidx = single_index(nb[0], nb[1]);
7201 if sidx >= 0 {
7202 if let Some(ref mut s) = cur.single {
7203 if (sidx as usize) < s.len() {
7204 s[sidx as usize] = Some(Box::new(
7205 caopt {
7206 next: None,
7207 name: opt_box.name.clone(),
7208 descr: opt_box.descr.clone(),
7209 xor: opt_box.xor.clone(),
7210 r#type: opt_box.r#type,
7211 args: None,
7212 active: 0,
7213 num: opt_box.num,
7214 gsname: opt_box.gsname.clone(),
7215 not: opt_box.not,
7216 }
7217 ));
7218 }
7219 }
7220 }
7221 }
7222 }
7223 }
7224
7225 opts_per_set.last_mut().unwrap().push(opt_box);
7226
7227 if again_iter == 1 { // c:1576
7228 if let Some(start) = againp_start {
7229 p_state = start;
7230 xnum_state = xnum; // restore
7231 xor_state = xor_state.clone();
7232 continue 'rec;
7233 }
7234 }
7235 break 'rec;
7236 }
7237 } else if p < bytes.len() && bytes[p] == b'*' {
7238 // ---- c:1581-1607 rest-arg branch ----
7239 if not_flag { // c:1586
7240 idx += 1;
7241 continue;
7242 }
7243 p += 1; // c:1589 *++p
7244 if p >= bytes.len() || bytes[p] != b':' {
7245 zwarnnam(nam, &format!("invalid rest argument definition: {}", arg));
7246 return None;
7247 }
7248 if rest_per_set.last().unwrap().is_some() { // c:1594
7249 zwarnnam(nam, &format!("doubled rest argument definition: {}", arg));
7250 return None;
7251 }
7252 let mut atype = CAA_REST; // c:1584
7253 p += 1; // c:1599 *++p
7254 if p < bytes.len() && bytes[p] == b':' { // c:1599
7255 p += 1;
7256 if p < bytes.len() && bytes[p] == b':' { // c:1600
7257 atype = CAA_RREST;
7258 p += 1;
7259 } else {
7260 atype = CAA_RARGS;
7261 }
7262 }
7263 let mut rarg = parse_caarg(0, atype, -1, 0, None, bytes, &mut p,
7264 doset.as_deref()); // c:1606
7265 rarg.xor = xor; // c:1607
7266 *rest_per_set.last_mut().unwrap() = Some(rarg);
7267 } else {
7268 // ---- c:1608-1661 normal-arg branch ----
7269 if not_flag { // c:1614
7270 idx += 1;
7271 continue;
7272 }
7273 let mut direct = 0; // c:1611
7274 if p < bytes.len() && idigit(bytes[p]) { // c:1617
7275 direct = 1;
7276 let mut num: i32 = 0;
7277 while p < bytes.len() && idigit(bytes[p]) {
7278 num = num * 10 + (bytes[p] - b'0') as i32;
7279 p += 1;
7280 }
7281 anum = num + 1; // c:1624
7282 } else {
7283 anum += 1; // c:1627
7284 }
7285 if p >= bytes.len() || bytes[p] != b':' { // c:1629
7286 zwarnnam(nam, &format!("invalid argument: {}", arg));
7287 return None;
7288 }
7289 let mut atype = CAA_NORMAL;
7290 p += 1; // c:1636 *++p
7291 if p < bytes.len() && bytes[p] == b':' { // c:1636
7292 atype = CAA_OPT;
7293 p += 1;
7294 }
7295 let mut narg = parse_caarg(0, atype, anum - 1, 0, None,
7296 bytes, &mut p, doset.as_deref()); // c:1641
7297 narg.xor = xor; // c:1642
7298 narg.direct = direct; // c:1643
7299
7300 // c:1647-1661 — sorted insert by num.
7301 let target = anum - 1;
7302 let cur_args = args_per_set.last_mut().unwrap();
7303 let mut insert_at = cur_args.len();
7304 for (i, existing) in cur_args.iter().enumerate() {
7305 if existing.num >= target {
7306 insert_at = i;
7307 break;
7308 }
7309 }
7310 if insert_at < cur_args.len() && cur_args[insert_at].num == target {
7311 zwarnnam(nam, &format!("doubled argument definition: {}", arg));
7312 return None;
7313 }
7314 cur_args.insert(insert_at, narg);
7315 }
7316
7317 idx += 1;
7318 }
7319
7320 // c:1664 — final set_cadef_opts on the last set.
7321 {
7322 let last_idx = sets.len() - 1;
7323 let cur = &mut sets[last_idx];
7324 let cur_args = &mut args_per_set[last_idx];
7325 let mut head: Option<Box<caarg>> = None;
7326 for a in cur_args.drain(..).rev() {
7327 let mut a = a;
7328 a.next = head;
7329 head = Some(a);
7330 }
7331 cur.args = head;
7332 set_cadef_opts(cur);
7333 }
7334
7335 // ---- finalize: link opts/args/rest per set, then snext-chain ----
7336 let n_sets = sets.len();
7337 for i in 0..n_sets {
7338 // opts — append order.
7339 let mut head: Option<Box<caopt>> = None;
7340 for o in opts_per_set[i].drain(..).rev() {
7341 let mut o = o;
7342 o.next = head;
7343 head = Some(o);
7344 }
7345 sets[i].opts = head;
7346 // args was already linked in the per-set finalize step above for
7347 // every set except possibly the last (which is now done). Walk
7348 // any still-present Vec entries into the linked list for safety.
7349 if !args_per_set[i].is_empty() {
7350 let mut head: Option<Box<caarg>> = None;
7351 for a in args_per_set[i].drain(..).rev() {
7352 let mut a = a;
7353 a.next = head;
7354 head = Some(a);
7355 }
7356 sets[i].args = head;
7357 }
7358 sets[i].rest = rest_per_set[i].take();
7359 }
7360
7361 // c:1281 — snext chain links each subsequent set off the head.
7362 while sets.len() > 1 {
7363 let tail = sets.pop().unwrap();
7364 let prev = sets.last_mut().unwrap();
7365 // Walk to the end of the snext chain on prev and attach tail.
7366 let mut cursor: &mut Option<Box<cadef>> = &mut prev.snext;
7367 while cursor.is_some() {
7368 cursor = &mut cursor.as_mut().unwrap().snext;
7369 }
7370 *cursor = Some(tail);
7371 }
7372
7373 Some(sets.pop().unwrap())
7374}
7375
7376/// Direct port of `static Cvdef get_cvdef(char *nam, char **args)` from
7377/// `Src/Zle/computil.c:3154-3173`. LRU lookup over `cvdef_cache`
7378/// keyed by the raw argv. On hit bumps `lastt` and returns 1. On
7379/// miss parses via `parse_cvdef` and evicts the entry with the
7380/// oldest `lastt` (or the first empty slot) for insertion.
7381pub fn get_cvdef(nam: &str, args: &[String]) -> i32 { // c:3154
7382 let na = args.len() as i32;
7383 let now = { // c:3161 time(0)
7384 use std::time::{SystemTime, UNIX_EPOCH};
7385 SystemTime::now().duration_since(UNIX_EPOCH)
7386 .map(|d| d.as_secs() as i64).unwrap_or(0)
7387 };
7388
7389 if let Ok(mut cache) = cvdef_cache.lock() {
7390 let mut min_idx: Option<usize> = None;
7391 let mut min_lastt: i64 = i64::MAX;
7392 let mut hit_idx: Option<usize> = None;
7393 for (i, slot) in cache.iter().enumerate() { // c:3159
7394 match slot {
7395 Some(entry) => {
7396 if entry.ndefs == na // c:3160
7397 && entry.defs.as_deref()
7398 .map_or(false, |d| d.len() == args.len()
7399 && d.iter().zip(args.iter()).all(|(a, b)| a == b))
7400 {
7401 hit_idx = Some(i);
7402 break;
7403 }
7404 if entry.lastt < min_lastt { // c:3164
7405 min_lastt = entry.lastt;
7406 min_idx = Some(i);
7407 }
7408 }
7409 None => { // c:3164 empty slot
7410 min_idx = Some(i);
7411 break;
7412 }
7413 }
7414 }
7415 if let Some(i) = hit_idx { // c:3160
7416 if let Some(entry) = cache[i].as_mut() {
7417 entry.lastt = now; // c:3161
7418 }
7419 return 1; // c:3163 hit
7420 }
7421 // c:3168 — parse_cvdef; on success replace the chosen slot.
7422 if let Some(new) = parse_cvdef(nam, args) {
7423 let idx = min_idx.unwrap_or(0);
7424 cache[idx] = Some(new); // c:3170
7425 }
7426 }
7427 0 // c:3172 miss
7428}
7429
7430/// Direct port of `static Cvdef parse_cvdef(char *nam, char **args)`
7431/// from `Src/Zle/computil.c:2986-3148`. Parses the leading
7432/// `-s SEP / -S SEP / -w` flag block, then the description, then
7433/// each value spec into a cvval chain.
7434pub fn parse_cvdef(nam: &str, args: &[String]) -> Option<Box<cvdef>> { // c:2986
7435 use crate::ported::ztype_h::inblank;
7436
7437 let orig_args = args;
7438 let mut idx = 0usize;
7439
7440 let mut sep: i32 = 0; // c:2991 char sep = '\0'
7441 let mut asep: i32 = b'=' as i32; // c:2991 char asep = '='
7442 let mut hassep: i32 = 0; // c:2992
7443 let mut words: i32 = 0; // c:2992
7444
7445 // c:2994-3010 — leading flag block (-s SEP, -S SEP, -w).
7446 while idx + 1 < args.len()
7447 && args[idx].len() == 2
7448 && args[idx].starts_with('-')
7449 && (args[idx].as_bytes()[1] == b's'
7450 || args[idx].as_bytes()[1] == b'S'
7451 || args[idx].as_bytes()[1] == b'w')
7452 {
7453 let flag = args[idx].as_bytes()[1];
7454 if flag == b's' { // c:2999
7455 hassep = 1;
7456 sep = args[idx + 1].as_bytes().first().copied().unwrap_or(0) as i32;
7457 idx += 2;
7458 } else if flag == b'S' { // c:3003
7459 asep = args[idx + 1].as_bytes().first().copied().unwrap_or(0) as i32;
7460 idx += 2;
7461 } else { // c:3006 -w
7462 words = 1;
7463 idx += 1;
7464 }
7465 }
7466
7467 if idx + 1 >= args.len() { // c:3011
7468 zwarnnam(nam, "not enough arguments");
7469 return None;
7470 }
7471 let descr = args[idx].clone(); // c:3015 descr = *args++
7472 idx += 1;
7473
7474 let mut ret = Box::new(cvdef {
7475 descr: Some(descr), // c:3018
7476 hassep, // c:3019
7477 sep, // c:3020
7478 argsep: asep, // c:3021
7479 next: None, // c:3022
7480 vals: None, // c:3023
7481 defs: Some(orig_args.to_vec()), // c:3024
7482 ndefs: orig_args.len() as i32, // c:3025
7483 lastt: { // c:3026
7484 use std::time::{SystemTime, UNIX_EPOCH};
7485 SystemTime::now().duration_since(UNIX_EPOCH)
7486 .map(|d| d.as_secs() as i64).unwrap_or(0)
7487 },
7488 words, // c:3027
7489 });
7490
7491 // c:3029-3147 — for each remaining arg, parse one value spec.
7492 let mut vals_collected: Vec<Box<cvval>> = Vec::new();
7493
7494 while idx < args.len() {
7495 let spec = &args[idx];
7496 let bytes = spec.as_bytes();
7497 let mut p: usize = 0;
7498 let mut xnum: i32 = 0; // c:3032
7499 let mut bs = 0; // c:3030
7500 let mut xor: Option<Vec<String>> = None;
7501
7502 // c:3035-3068 — `(opt1 opt2)` xor list.
7503 if p < bytes.len() && bytes[p] == b'(' { // c:3035
7504 let mut list: Vec<String> = Vec::new();
7505 let mut bad = false;
7506 'paren: loop {
7507 if p >= bytes.len() || bytes[p] == b')' { break; }
7508 p += 1; // c:3041 p++
7509 while p < bytes.len() && inblank(bytes[p]) { p += 1; }
7510 if p >= bytes.len() { bad = true; break 'paren; }
7511 if bytes[p] == b')' { break 'paren; }
7512 let q = p;
7513 p += 1;
7514 while p < bytes.len() && bytes[p] != b')' && !inblank(bytes[p]) {
7515 p += 1;
7516 }
7517 if p >= bytes.len() { bad = true; break 'paren; }
7518 let word = String::from_utf8_lossy(&bytes[q..p]).into_owned();
7519 list.push(word);
7520 xnum += 1;
7521 }
7522 if bad || p >= bytes.len() || bytes[p] != b')' { // c:3056
7523 zwarnnam(nam, &format!("invalid argument: {}", spec));
7524 return None;
7525 }
7526 xor = Some(list);
7527 p += 1; // c:3066
7528 }
7529
7530 // c:3071 — `*` (multi).
7531 let multi = p < bytes.len() && bytes[p] == b'*';
7532 if multi { p += 1; }
7533
7534 // c:3076 — scan option name up to `:` or `[`.
7535 let name_start = p;
7536 while p < bytes.len() && bytes[p] != b':' && bytes[p] != b'[' { // c:3076
7537 if bytes[p] == b'\\' && p + 1 < bytes.len() {
7538 p += 1;
7539 bs = 1; // c:3078
7540 }
7541 p += 1;
7542 }
7543
7544 // c:3080-3085 — multi-letter check against empty separator.
7545 if hassep != 0 && sep == 0 && name_start + (bs as usize) + 1 < p { // c:3080
7546 zwarnnam(nam,
7547 "no multi-letter values with empty separator allowed");
7548 return None;
7549 }
7550
7551 let name_bytes = &bytes[name_start..p];
7552 let name = String::from_utf8_lossy(name_bytes).into_owned();
7553
7554 // c:3087 — optional [descr].
7555 let mut value_descr: Option<String> = None;
7556 let mut c_byte = if p < bytes.len() { bytes[p] } else { 0 };
7557 if c_byte == b'[' { // c:3088
7558 p += 1;
7559 let d_start = p;
7560 while p < bytes.len() && bytes[p] != b']' { // c:3090
7561 if bytes[p] == b'\\' && p + 1 < bytes.len() { p += 1; }
7562 p += 1;
7563 }
7564 if p >= bytes.len() { // c:3094
7565 zwarnnam(nam, &format!("invalid value definition: {}", spec));
7566 return None;
7567 }
7568 value_descr = Some(String::from_utf8_lossy(&bytes[d_start..p]).into_owned());
7569 p += 1; // c:3100
7570 c_byte = if p < bytes.len() { bytes[p] } else { 0 };
7571 }
7572
7573 if c_byte != 0 && c_byte != b':' { // c:3106
7574 zwarnnam(nam, &format!("invalid value definition: {}", spec));
7575 return None;
7576 }
7577
7578 // c:3114 — :arg or ::optarg.
7579 let mut vtype = CVV_NOARG;
7580 let mut arg: Option<Box<caarg>> = None;
7581 if c_byte == b':' { // c:3114
7582 if hassep != 0 && sep == 0 { // c:3115
7583 zwarnnam(nam,
7584 "no value with argument with empty separator allowed");
7585 return None;
7586 }
7587 p += 1; // c:3121 *++p
7588 if p < bytes.len() && bytes[p] == b':' { // c:3121
7589 p += 1;
7590 vtype = CVV_OPT; // c:3123
7591 } else {
7592 vtype = CVV_ARG; // c:3125
7593 }
7594 arg = Some(parse_caarg(0, 0, 0, 0, Some(&name), bytes, &mut p, None));// c:3126
7595 }
7596
7597 // c:3131-3137 — add own name to xor list when not multi.
7598 if !multi { // c:3131
7599 let xv = xor.get_or_insert_with(Vec::new);
7600 if xv.len() <= xnum as usize {
7601 xv.resize(xnum as usize + 1, String::new());
7602 }
7603 xv[xnum as usize] = name.clone(); // c:3136
7604 }
7605
7606 let v = Box::new(cvval { // c:3138
7607 next: None,
7608 name: Some(name), // c:3142
7609 descr: value_descr, // c:3143
7610 xor, // c:3144
7611 r#type: vtype, // c:3145
7612 arg, // c:3146
7613 active: 0,
7614 });
7615 vals_collected.push(v);
7616
7617 idx += 1;
7618 }
7619
7620 // Link vals_collected as a chain.
7621 let mut head: Option<Box<cvval>> = None;
7622 for v in vals_collected.into_iter().rev() {
7623 let mut v = v;
7624 v.next = head;
7625 head = Some(v);
7626 }
7627 ret.vals = head;
7628
7629 Some(ret)
7630}
7631
7632/// Direct port of `static void set_cadef_opts(Cadef def)` from
7633/// `Src/Zle/computil.c:1180-1191`. After a set-of-arg-definitions has
7634/// been parsed into the cadef, walk the args linked list and update
7635/// each non-direct argp's `min` field to the cumulative number of
7636/// CAA_OPT entries that precede it. The optionality count compounds
7637/// down the chain, which determines minimum-argument-count semantics
7638/// during completion.
7639pub fn set_cadef_opts(def: &mut cadef) { // c:1180
7640 let mut xnum: i32 = 0;
7641 let mut argp = def.args.as_deref_mut(); // c:1185 argp = def->args
7642 while let Some(node) = argp { // c:1185
7643 if node.direct == 0 { // c:1186 !argp->direct
7644 node.min = node.num - xnum; // c:1187
7645 }
7646 if node.r#type == CAA_OPT { // c:1188
7647 xnum += 1; // c:1189
7648 }
7649 argp = node.next.as_deref_mut(); // c:1185 argp = argp->next
7650 }
7651}
7652
7653/// Direct port of `static void settags(int level, char **tags)` from
7654/// `Src/Zle/computil.c:3794`. Replaces `comptags[level]` with a fresh
7655/// ctags carrying `tags[0]` as context and `tags[1..]` as the full
7656/// tag-list. Used at the start of every completion level transition
7657/// (`comptags -i`).
7658pub fn settags(level: i32, tags: &[String]) { // c:3794
7659 let idx = level as usize;
7660 if idx >= MAX_TAGS { return; } // c:3756 bounds
7661
7662 if let Ok(mut tab) = comptags.lock() {
7663 if tab[idx].is_some() { // c:3798
7664 freectags(tab[idx].take()); // c:3799
7665 }
7666 let context = tags.first().cloned(); // c:3804 *tags
7667 let all: Vec<String> = tags.iter().skip(1).cloned().collect(); // c:3803 tags+1
7668 tab[idx] = Some(Box::new(ctags { // c:3801 zalloc
7669 all: Some(all), // c:3803
7670 context, // c:3804
7671 init: 1, // c:3806
7672 sets: None, // c:3805
7673 }));
7674 }
7675}
7676
7677// `setup_` is ported above with the cadef_cache/cvdef_cache/comptags
7678// reset body cited at Src/Zle/computil.c:5124. This duplicate shim
7679// was retired when the real port landed.
7680
7681// =====================================================================
7682// bin_compquote / bin_comptags / bin_comptry / bin_compvalues —
7683// Src/Zle/computil.c. Each is a structural port matching the C
7684// signature exactly so the dispatch surface lands; the underlying
7685// state-mutation paths (compqstack rewrite, tags-stack walk,
7686// compvalues table) depend on infrastructure (getvalue / setstrvalue
7687// / compstate hash / cv_* helpers) that's open work.
7688// =====================================================================
7689
7690/// Direct port of `bin_compquote(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Zle/computil.c:3679`.
7691/// C body (c:3683-3725):
7692/// ```c
7693/// if (incompfunc != 1) { error; return 1; }
7694/// if (!compqstack || !*compqstack) return 0;
7695/// while ((name = *args++)) {
7696/// if ((v = getvalue(...))) {
7697/// switch (PM_TYPE(v->pm->node.flags)) {
7698/// case PM_SCALAR/NAMEREF:
7699/// setstrvalue(v, comp_quote(getstrvalue(v), -p));
7700/// case PM_ARRAY:
7701/// foreach val in array: comp_quote each
7702/// default: zwarnnam("invalid parameter type");
7703/// }
7704/// }
7705/// }
7706/// ```
7707/// Quoting routes through `comp_quote()` per param type (PM_SCALAR
7708/// / PM_ARRAY); the entry validates `incompfunc` + `compqstack`
7709/// guards before dispatch.
7710/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
7711pub fn bin_compquote(nam: &str, args: &[String], // c:3679
7712 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
7713 use crate::ported::params::{getvalue, getstrvalue, getvaluearr,
7714 setstrvalue, setarrvalue};
7715 use crate::ported::zsh_h::{value, PM_ARRAY, PM_TYPE};
7716 use std::sync::atomic::Ordering;
7717
7718 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:3685
7719 zwarnnam(nam, "can only be called from completion function");
7720 return 1;
7721 }
7722 // c:3691 — `if (!compqstack || !*compqstack) return 0;`.
7723 let qstack_empty = COMPQSTACK.get()
7724 .map(|m| m.lock().map(|s| s.is_empty()).unwrap_or(true))
7725 .unwrap_or(true);
7726 if qstack_empty { return 0; }
7727 let p_flag = OPT_ISSET(ops, b'p'); // c:3704
7728
7729 // c:3696 — `while ((name = *args++))`. Walk param names.
7730 for name in args { // c:3696
7731 let mut vbuf = value {
7732 pm: None, arr: Vec::new(), scanflags: 0,
7733 valflags: 0, start: 0, end: 0,
7734 };
7735 let mut nameref: &str = name.as_str();
7736 let v = getvalue(Some(&mut vbuf), &mut nameref, 0); // c:3699
7737 if v.is_none() { // c:3724
7738 zwarnnam(nam, &format!("unknown parameter: {}", name));
7739 continue;
7740 }
7741 let v = v.unwrap();
7742 let flags = v.pm.as_ref().map(|pm| pm.node.flags).unwrap_or(0);
7743 let pm_type = PM_TYPE(flags as u32);
7744 // c:3700-3705 — PM_SCALAR / PM_NAMEREF path.
7745 if pm_type == 0 || (flags as u32 & crate::ported::zsh_h::PM_NAMEREF) != 0 {
7746 let s = getstrvalue(Some(v));
7747 let q = comp_quote(&s, p_flag as i32);
7748 let mut nameref_re: &str = name.as_str();
7749 setstrvalue(getvalue(Some(&mut vbuf), &mut nameref_re, 0), &q);
7750 } else if pm_type == PM_ARRAY { // c:3706
7751 let arr = getvaluearr(Some(v));
7752 let new_arr: Vec<String> = arr.into_iter()
7753 .map(|elem| comp_quote(&elem, p_flag as i32))
7754 .collect();
7755 // Re-fetch a fresh value for the setarrvalue call (getvalue
7756 // consumed the prior borrow).
7757 let mut vbuf2 = value {
7758 pm: None, arr: Vec::new(), scanflags: 0,
7759 valflags: 0, start: 0, end: 0,
7760 };
7761 let mut nameref2: &str = name.as_str();
7762 if let Some(v2) = getvalue(Some(&mut vbuf2), &mut nameref2, 0) {
7763 setarrvalue(v2, new_arr);
7764 }
7765 } else { // c:3720
7766 zwarnnam(nam, &format!("invalid parameter type: {}", name));
7767 }
7768 }
7769 0 // c:3725
7770}
7771
7772/// Direct port of `static int bin_comptags(char *nam, char **args,
7773/// UNUSED(Options ops),
7774/// UNUSED(int func))` from
7775/// `Src/Zle/computil.c:3831-3958`. Full subcommand dispatch for
7776/// `comptags -i/-C/-T/-N/-R/-S/-A`. Reads `LOCALLEVEL` to index
7777/// `comptags[]`; `--` suffix decrements the level by one.
7778pub fn bin_comptags(nam: &str, args: &[String], // c:3831
7779 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
7780 use crate::ported::builtin::LOCALLEVEL;
7781 use crate::ported::params::{setsparam, setaparam};
7782 use crate::ported::utils::strpfx;
7783 use std::sync::atomic::Ordering;
7784
7785 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:3835
7786 zwarnnam(nam, "can only be called from completion function");
7787 return 1;
7788 }
7789 if args.is_empty() { return 1; }
7790 let a0 = args[0].as_bytes();
7791 // c:3839 — validate `-X` or `-X--` shape.
7792 if a0.len() < 2 || a0[0] != b'-'
7793 || (a0.len() > 2 && (a0[2] != b'-' || a0.len() > 3))
7794 {
7795 zwarnnam(nam, &format!("invalid argument: {}", args[0]));
7796 return 1;
7797 }
7798
7799 let level: i32 = LOCALLEVEL.load(Ordering::Relaxed) // c:3844
7800 - if a0.len() > 2 { 1 } else { 0 };
7801 if level < 0 || (level as usize) >= MAX_TAGS { // c:3845
7802 zwarnnam(nam, "nesting level too deep");
7803 return 1;
7804 }
7805 let lvl_idx = level as usize;
7806
7807 let sub = a0[1];
7808
7809 // c:3849 — non-init subcommands require a registered comptags[level].
7810 if sub != b'i' && sub != b'I' {
7811 let registered = {
7812 let tab = comptags.lock().unwrap();
7813 tab[lvl_idx].is_some()
7814 };
7815 if !registered {
7816 zwarnnam(nam, "no tags registered");
7817 return 1;
7818 }
7819 }
7820
7821 // c:3854-3864 — per-subcommand arg-count bounds.
7822 let (min, max): (i32, i32) = match sub {
7823 b'i' => (2, -1),
7824 b'C' => (1, 1),
7825 b'T' => (0, 0),
7826 b'N' => (0, 0),
7827 b'R' => (1, 1),
7828 b'S' => (1, 1),
7829 b'A' => (2, 3),
7830 _ => {
7831 zwarnnam(nam, &format!("invalid option: {}", args[0]));
7832 return 1;
7833 }
7834 };
7835 let n = (args.len() as i32) - 1;
7836 if n < min { zwarnnam(nam, "not enough arguments"); return 1; }
7837 if max >= 0 && n > max { zwarnnam(nam, "too many arguments"); return 1; }
7838
7839 match sub {
7840 b'i' => { // c:3874
7841 settags(level, &args[1..]);
7842 lasttaglevel.store(level, Ordering::Relaxed); // c:3876
7843 0
7844 }
7845 b'C' => { // c:3878
7846 let ctx = {
7847 let tab = comptags.lock().unwrap();
7848 tab[lvl_idx].as_ref()
7849 .and_then(|t| t.context.clone())
7850 .unwrap_or_default()
7851 };
7852 setsparam(&args[1], &ctx); // c:3879
7853 0
7854 }
7855 b'T' => { // c:3881
7856 let empty = {
7857 let tab = comptags.lock().unwrap();
7858 tab[lvl_idx].as_ref().map_or(true, |t| t.sets.is_none())
7859 };
7860 if empty { 1 } else { 0 } // c:3882
7861 }
7862 b'N' => { // c:3883
7863 let mut tab = comptags.lock().unwrap();
7864 if let Some(t) = tab[lvl_idx].as_mut() {
7865 if t.init != 0 { // c:3887
7866 t.init = 0;
7867 } else if let Some(mut s) = t.sets.take() { // c:3889
7868 t.sets = s.next.take(); // c:3890
7869 freectset(Some(s)); // c:3892
7870 }
7871 if t.sets.is_some() { 0 } else { 1 } // c:3894
7872 } else {
7873 1
7874 }
7875 }
7876 b'R' => { // c:3896
7877 let tab = comptags.lock().unwrap();
7878 let hit = tab[lvl_idx].as_ref()
7879 .and_then(|t| t.sets.as_ref())
7880 .map(|s| {
7881 s.tags.as_deref()
7882 .map_or(false, |tgs| arrcontains(tgs, &args[1], true) != 0)
7883 })
7884 .unwrap_or(false);
7885 if hit { 0 } else { 1 } // c:3900
7886 }
7887 b'A' => { // c:3903
7888 let mut tab = comptags.lock().unwrap();
7889 let Some(t) = tab[lvl_idx].as_mut() else { return 1; };
7890 let Some(s) = t.sets.as_mut() else { return 1; };
7891 // c:3911 — refresh ptr if tag changed.
7892 if s.tag.as_deref() != Some(args[1].as_str()) {
7893 s.tag = Some(args[1].clone()); // c:3913
7894 s.ptr = 0; // c:3914
7895 }
7896 let tags_vec = s.tags.clone().unwrap_or_default();
7897 // c:3916-3925 — walk tags from ptr looking for a name match.
7898 let mut found: Option<(usize, String, String)> = None;
7899 for (i, q) in tags_vec.iter().enumerate().skip(s.ptr as usize) {
7900 if strpfx(&args[1], q) { // c:3917
7901 let l = args[1].len();
7902 let qb = q.as_bytes();
7903 if qb.len() == l { // c:3918
7904 found = Some((i, q.clone(), q.clone()));
7905 break;
7906 } else if qb.len() > l && qb[l] == b':' { // c:3921
7907 let v = String::from_utf8_lossy(&qb[l + 1..]).into_owned();
7908 found = Some((i, q.clone(), v));
7909 break;
7910 }
7911 }
7912 }
7913 let (q_idx, q_full, v) = match found {
7914 None => { // c:3927
7915 s.tag = None;
7916 return 1;
7917 }
7918 Some(t) => t,
7919 };
7920 s.ptr = (q_idx + 1) as i32; // c:3932
7921 // c:3933 — `setsparam(args[2], v == '-' ? dyncat(args[1], v) : v)`.
7922 let value = if v.starts_with('-') {
7923 crate::ported::string::dyncat(&args[1], &v)
7924 } else {
7925 v.clone()
7926 };
7927 setsparam(&args[2], &value);
7928 // c:3934 — optional 3rd arg gets the "name-up-to-`:`" of q.
7929 if args.len() > 3 {
7930 let pre_colon: String = q_full
7931 .splitn(2, ':').next().unwrap_or("").to_string();
7932 setsparam(&args[3], &pre_colon);
7933 }
7934 0 // c:3942
7935 }
7936 b'S' => { // c:3946
7937 let tab = comptags.lock().unwrap();
7938 if let Some(tags) = tab[lvl_idx].as_ref()
7939 .and_then(|t| t.sets.as_ref())
7940 .and_then(|s| s.tags.clone())
7941 {
7942 setaparam(&args[1], tags); // c:3951
7943 0
7944 } else {
7945 1 // c:3952
7946 }
7947 }
7948 _ => 0,
7949 }
7950}
7951
7952/// Direct port of `static int bin_comptry(char *nam, char **args,
7953/// UNUSED(Options ops),
7954/// UNUSED(int func))` from
7955/// `Src/Zle/computil.c:3961-4138`. Builds a new tag-set under the
7956/// active comptags[lasttaglevel] entry. Two forms:
7957/// - `comptry -m "pat1 pat2" [...]` — for each space-separated
7958/// tag pattern, glob-expand via braces/wildcards, match against
7959/// the registered `all` array, filter out tags already in any
7960/// existing set, then append the deduplicated matches as a new
7961/// ctset.
7962/// - `comptry [-s] tag1 tag2 ...` — filter args to keep only
7963/// registered tags not in any existing set; with `-s`, build one
7964/// ctset per arg, else one ctset for all of them.
7965pub fn bin_comptry(nam: &str, args: &[String], // c:3961
7966 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
7967 use crate::ported::glob::{hasbraces, tokenize, xpandbraces};
7968 use crate::ported::pattern::{patcompile, pattry};
7969 use crate::ported::ztype_h::{iblank, inblank};
7970 use crate::ported::zsh_h::{Comma, Inbrace, Outbrace};
7971 use std::sync::atomic::Ordering;
7972
7973 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:3963
7974 zwarnnam(nam, "can only be called from completion function");
7975 return 1;
7976 }
7977
7978 let lvl = lasttaglevel.load(Ordering::Relaxed);
7979 if lvl <= 0 { // c:3967 — !lasttaglevel
7980 zwarnnam(nam, "no tags registered");
7981 return 1;
7982 }
7983 let lvl_idx = lvl as usize;
7984 let registered = comptags.lock().ok()
7985 .map(|t| lvl_idx < t.len() && t[lvl_idx].is_some())
7986 .unwrap_or(false);
7987 if !registered {
7988 zwarnnam(nam, "no tags registered");
7989 return 1;
7990 }
7991
7992 if args.is_empty() { return 0; } // c:3971
7993
7994 // Helper: append a new ctset to comptags[lvl_idx].sets.
7995 let append_set = |tags: Vec<String>| {
7996 if let Ok(mut tab) = comptags.lock() {
7997 if let Some(t) = tab[lvl_idx].as_mut() {
7998 let new_set = Box::new(ctset {
7999 next: None,
8000 tags: Some(tags),
8001 tag: None,
8002 ptr: 0,
8003 });
8004 // c:4082 — walk to tail of existing sets.
8005 if let Some(head) = t.sets.as_mut() {
8006 let mut cur = head.as_mut();
8007 while cur.next.is_some() {
8008 cur = cur.next.as_mut().unwrap();
8009 }
8010 cur.next = Some(new_set);
8011 } else {
8012 t.sets = Some(new_set);
8013 }
8014 }
8015 }
8016 };
8017
8018 if args[0] == "-m" { // c:3972
8019 // c:3973-4090 — pattern-match mode.
8020 for arg in &args[1..] { // c:3978
8021 let mut s = arg.as_bytes().to_vec();
8022 let mut list: Vec<String> = Vec::new();
8023 let mut num = 0i32;
8024 let mut i = 0usize;
8025
8026 while i < s.len() {
8027 // c:3980 — skip leading blanks.
8028 while i < s.len() && iblank(s[i]) { i += 1; }
8029 if i >= s.len() { break; }
8030 // c:3982 — accumulate the token, watching for `\X` escape
8031 // and tracking the first unescaped ':' separator.
8032 let p_start = i;
8033 let mut p_pos = i;
8034 let mut colon_at: Option<usize> = None;
8035 while i < s.len() && !inblank(s[i]) {
8036 if colon_at.is_none() && s[i] == b':' {
8037 colon_at = Some(p_pos);
8038 }
8039 if s[i] == b'\\' && i + 1 < s.len() {
8040 i += 1;
8041 }
8042 s[p_pos] = s[i];
8043 p_pos += 1;
8044 i += 1;
8045 }
8046 // Skip the trailing blank.
8047 if i < s.len() { i += 1; }
8048
8049 let token_full = String::from_utf8_lossy(&s[p_start..p_pos]).into_owned();
8050 if token_full.is_empty() { continue; }
8051
8052 // c:3997 — split at colon: q = head, c = trailing.
8053 let (q, c_opt): (String, Option<String>) = match colon_at {
8054 Some(c_idx) => {
8055 let head = String::from_utf8_lossy(
8056 &s[p_start..c_idx]).into_owned();
8057 let tail = String::from_utf8_lossy(
8058 &s[c_idx + 1..p_pos]).into_owned();
8059 (head, Some(tail))
8060 }
8061 None => (token_full.clone(), None),
8062 };
8063 if q.is_empty() { continue; }
8064
8065 // c:4001-4012 — convert `{` / `}` / `,` to Inbrace / Outbrace
8066 // / Comma tokens for glob processing.
8067 let mut qq: String = q.chars().map(|ch| match ch {
8068 '\\' => ch, // keep; handled below
8069 '{' => Inbrace,
8070 '}' => Outbrace,
8071 ',' => Comma,
8072 other => other,
8073 }).collect();
8074 // Handle `\X` — keep both bytes literal by re-walking. The
8075 // C does this inline in the same loop; we just leave them.
8076 tokenize(&mut qq);
8077
8078 // c:4013 — if hasbraces/haswilds, glob-expand.
8079 let has_meta = hasbraces(&qq, false)
8080 || crate::ported::pattern::haswilds(&qq);
8081 let all_arr: Vec<String> = comptags.lock().ok()
8082 .and_then(|t| t[lvl_idx].as_ref().and_then(|c| c.all.clone()))
8083 .unwrap_or_default();
8084 let sets_clone: Vec<Vec<String>> = comptags.lock().ok()
8085 .map(|t| {
8086 let mut out = Vec::new();
8087 if let Some(c) = t[lvl_idx].as_ref() {
8088 let mut p = c.sets.as_deref();
8089 while let Some(set) = p {
8090 if let Some(ts) = set.tags.as_ref() {
8091 out.push(ts.clone());
8092 }
8093 p = set.next.as_deref();
8094 }
8095 }
8096 out
8097 })
8098 .unwrap_or_default();
8099
8100 if has_meta {
8101 // c:4015-4022 — expand braces, then compile each as a Patprog.
8102 let mut blist: Vec<String> = vec![qq.clone()];
8103 let mut bi = 0usize;
8104 while bi < blist.len() {
8105 if hasbraces(&blist[bi], false) {
8106 let expanded = xpandbraces(&blist[bi], false);
8107 blist.remove(bi);
8108 for e in expanded {
8109 blist.insert(bi, e);
8110 bi += 1;
8111 }
8112 } else {
8113 bi += 1;
8114 }
8115 }
8116 for bb in &blist { // c:4023
8117 if let Some(prog) = patcompile(bb, 0, None::<&mut String>) {
8118 for a in &all_arr { // c:4029
8119 // Skip if `a:c` (or just a) already in list.
8120 let already = list.iter().any(|item| {
8121 let item_head = item.split(':').next().unwrap_or(item);
8122 item_head == a.as_str()
8123 });
8124 if already { continue; }
8125 if pattry(&prog, a) { // c:4043
8126 let entry = match &c_opt {
8127 Some(c) => format!("{}:{}", a, c),
8128 None => a.clone(),
8129 };
8130 list.push(entry);
8131 num += 1;
8132 }
8133 }
8134 }
8135 }
8136 } else if arrcontains(&all_arr, &q, false) != 0 { // c:4056
8137 // c:4057-4064 — literal token: include if not in any set.
8138 let in_set = sets_clone.iter().any(|s|
8139 arrcontains(s, &q, false) != 0);
8140 if !in_set {
8141 list.push(q.clone());
8142 num += 1;
8143 }
8144 }
8145 }
8146
8147 if num > 0 { // c:4072
8148 append_set(list);
8149 }
8150 }
8151 } else { // c:4091 — plain mode
8152 let mut idx = 0usize;
8153 let sep = args[idx] == "-s"; // c:4095
8154 if sep { idx += 1; }
8155 let all_arr: Vec<String> = comptags.lock().ok()
8156 .and_then(|t| t[lvl_idx].as_ref().and_then(|c| c.all.clone()))
8157 .unwrap_or_default();
8158 let sets_clone: Vec<Vec<String>> = comptags.lock().ok()
8159 .map(|t| {
8160 let mut out = Vec::new();
8161 if let Some(c) = t[lvl_idx].as_ref() {
8162 let mut p = c.sets.as_deref();
8163 while let Some(set) = p {
8164 if let Some(ts) = set.tags.as_ref() {
8165 out.push(ts.clone());
8166 }
8167 p = set.next.as_deref();
8168 }
8169 }
8170 out
8171 })
8172 .unwrap_or_default();
8173
8174 // c:4098-4108 — filter args, keep only registered tags not in any set.
8175 let filtered: Vec<String> = args[idx..].iter()
8176 .filter(|p| {
8177 arrcontains(&all_arr, p, true) != 0
8178 && !sets_clone.iter().any(|s|
8179 arrcontains(s, p, false) != 0)
8180 })
8181 .cloned()
8182 .collect();
8183
8184 if filtered.is_empty() { return 0; }
8185
8186 // c:4114-4134 — push as one set, or split (one per arg) with -s.
8187 if sep {
8188 for t in &filtered {
8189 append_set(vec![t.clone()]);
8190 }
8191 } else {
8192 append_set(filtered);
8193 }
8194 }
8195 0 // c:4138
8196}
8197
8198/// Direct port of `static int bin_compvalues(char *nam, char **args,
8199/// UNUSED(Options ops),
8200/// UNUSED(int func))` from
8201/// `Src/Zle/computil.c:3475-3658`. Full subcommand dispatch for
8202/// `compvalues -i/-D/-C/-V/-s/-S/-d/-L/-v`. Each branch consumes
8203/// `cv_laststate` populated by `cv_parse_word`.
8204pub fn bin_compvalues(nam: &str, args: &[String], // c:3475
8205 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
8206 use crate::ported::params::{setsparam, setaparam, sethparam};
8207 use std::sync::atomic::Ordering;
8208
8209 if INCOMPFUNC.load(Ordering::Relaxed) != 1 { // c:3479
8210 zwarnnam(nam, "can only be called from completion function");
8211 return 1;
8212 }
8213 if args.is_empty() { return 1; }
8214 let a0 = args[0].as_bytes();
8215 if a0.len() != 2 || a0[0] != b'-' { // c:3483
8216 zwarnnam(nam, &format!("invalid argument: {}", args[0]));
8217 return 1;
8218 }
8219 let sub = a0[1];
8220
8221 if sub != b'i' && cv_parsed.load(Ordering::Relaxed) == 0 { // c:3487
8222 zwarnnam(nam, "no parsed state");
8223 return 1;
8224 }
8225
8226 let (min, max): (i32, i32) = match sub { // c:3491
8227 b'i' => (2, -1),
8228 b'D' => (2, 2),
8229 b'C' => (1, 1),
8230 b'V' => (3, 3),
8231 b's' => (1, 1),
8232 b'S' => (1, 1),
8233 b'd' => (1, 1),
8234 b'L' => (3, 4),
8235 b'v' => (1, 1),
8236 _ => {
8237 zwarnnam(nam, &format!("invalid option: {}", args[0]));
8238 return 1;
8239 }
8240 };
8241 let n = (args.len() as i32) - 1;
8242 if n < min { zwarnnam(nam, "not enough arguments"); return 1; }
8243 if max >= 0 && n > max { zwarnnam(nam, "too many arguments"); return 1; }
8244
8245 match sub {
8246 b'i' => { // c:3514
8247 let spec = &args[1..];
8248 let _ = get_cvdef(nam, spec);
8249 let mut cached: Option<Box<cvdef>> = {
8250 let cache = cvdef_cache.lock().ok();
8251 cache.and_then(|c| {
8252 c.iter().find_map(|slot| {
8253 slot.as_ref().filter(|e| {
8254 e.ndefs == spec.len() as i32
8255 && e.defs.as_deref().map_or(false, |d| {
8256 d.len() == spec.len()
8257 && d.iter().zip(spec.iter()).all(|(a, b)| a == b)
8258 })
8259 }).cloned()
8260 })
8261 })
8262 };
8263 let Some(ref mut def) = cached else { return 1; };
8264 cv_parsed.store(0, Ordering::Relaxed); // c:3521
8265 cv_parse_word(def); // c:3527
8266 cv_parsed.store(1, Ordering::Relaxed); // c:3528
8267 0
8268 }
8269
8270 b'D' => { // c:3533
8271 let arg = cv_laststate.lock().ok().and_then(|s| s.def.clone());
8272 if let Some(a) = arg {
8273 setsparam(&args[1], a.descr.as_deref().unwrap_or("")); // c:3541
8274 setsparam(&args[2], a.action.as_deref().unwrap_or("")); // c:3542
8275 0
8276 } else {
8277 1 // c:3546
8278 }
8279 }
8280
8281 b'C' => { // c:3548
8282 let arg = cv_laststate.lock().ok().and_then(|s| s.def.clone());
8283 if let Some(a) = arg {
8284 setsparam(&args[1], a.opt.as_deref().unwrap_or("")); // c:3555
8285 0
8286 } else {
8287 1 // c:3559
8288 }
8289 }
8290
8291 b'V' => { // c:3561
8292 let mut noarg: Vec<String> = Vec::new();
8293 let mut arg_l: Vec<String> = Vec::new();
8294 let mut opt_l: Vec<String> = Vec::new();
8295 if let Ok(ls) = cv_laststate.lock() {
8296 if let Some(d) = ls.d.as_ref() {
8297 let mut p = d.vals.as_deref();
8298 while let Some(v) = p {
8299 if v.active != 0 { // c:3574
8300 let bucket: &mut Vec<String> = match v.r#type {
8301 t if t == CVV_NOARG => &mut noarg,
8302 t if t == CVV_ARG => &mut arg_l,
8303 _ => &mut opt_l,
8304 };
8305 let name = v.name.as_deref().unwrap_or("");
8306 let str_val = if let Some(d) = v.descr.as_deref() {
8307 format!("{}:{}", name, d)
8308 } else {
8309 name.to_string()
8310 };
8311 bucket.push(str_val); // c:3589
8312 }
8313 p = v.next.as_deref();
8314 }
8315 }
8316 }
8317 setaparam(&args[1], noarg);
8318 setaparam(&args[2], arg_l);
8319 setaparam(&args[3], opt_l);
8320 0 // c:3596
8321 }
8322
8323 b's' => { // c:3598
8324 let (hassep, sep) = cv_laststate.lock().ok()
8325 .and_then(|ls| ls.d.as_ref().map(|d| (d.hassep, d.sep)))
8326 .unwrap_or((0, 0));
8327 if hassep != 0 {
8328 let tmp = (sep as u8 as char).to_string();
8329 setsparam(&args[1], &tmp);
8330 0 // c:3608
8331 } else {
8332 1 // c:3610
8333 }
8334 }
8335
8336 b'S' => { // c:3611
8337 let argsep = cv_laststate.lock().ok()
8338 .and_then(|ls| ls.d.as_ref().map(|d| d.argsep))
8339 .unwrap_or(0);
8340 let tmp = (argsep as u8 as char).to_string();
8341 setsparam(&args[1], &tmp);
8342 0 // c:3620
8343 }
8344
8345 b'd' => { // c:3621
8346 let descr = cv_laststate.lock().ok()
8347 .and_then(|ls| ls.d.as_ref().and_then(|d| d.descr.clone()))
8348 .unwrap_or_default();
8349 setsparam(&args[1], &descr);
8350 0
8351 }
8352
8353 b'L' => { // c:3626
8354 let val = cv_laststate.lock().ok().and_then(|ls| {
8355 ls.d.as_ref().and_then(|d| cv_get_val(d, &args[1]))
8356 });
8357 if let Some(v) = val {
8358 if let Some(a) = v.arg.as_deref() { // c:3634
8359 setsparam(&args[2], a.descr.as_deref().unwrap_or(""));
8360 setsparam(&args[3], a.action.as_deref().unwrap_or(""));
8361 if args.len() > 4 { // c:3638
8362 setsparam(&args[4], v.name.as_deref().unwrap_or(""));
8363 }
8364 return 0;
8365 }
8366 }
8367 1 // c:3643
8368 }
8369
8370 b'v' => { // c:3645
8371 let vals = cv_laststate.lock().ok()
8372 .and_then(|ls| ls.vals.clone());
8373 if let Some(v) = vals {
8374 sethparam(&args[1], v);
8375 0
8376 } else {
8377 1 // c:3656
8378 }
8379 }
8380
8381 _ => 1, // c:3658
8382 }
8383}