Skip to main content

zsh/
exec.rs

1//! Shell command executor for zshrs
2//!
3//! Executes the parsed shell AST via fusevm bytecodes.
4//! Builtins are dispatched through fusevm's CallBuiltin mechanism,
5//! with handlers accessing executor state via thread-local.
6
7use crate::history::HistoryEngine;
8use crate::math::MathEval;
9use crate::pcre::PcreState;
10use crate::prompt::{expand_prompt, PromptContext};
11use crate::tcp::TcpSessions;
12use crate::zftp::Zftp;
13use crate::zprof::Profiler;
14use crate::zutil::StyleTable;
15use compsys::cache::CompsysCache;
16use compsys::CompInitResult;
17use parking_lot::Mutex;
18use std::collections::HashSet;
19
20/// AOP advice type — before, after, or around.
21#[derive(Debug, Clone)]
22pub enum AdviceKind {
23    /// Run code before the command executes.
24    Before,
25    /// Run code after the command executes. $? and INTERCEPT_MS available.
26    After,
27    /// Wrap the command. Code must call `intercept_proceed` to run original.
28    Around,
29}
30
31/// An intercept registration.
32#[derive(Debug, Clone)]
33pub struct Intercept {
34    /// Pattern to match command names. Supports glob: "git *", "_*", "*".
35    pub pattern: String,
36    /// What kind of advice.
37    pub kind: AdviceKind,
38    /// Shell code to execute as advice.
39    pub code: String,
40    /// Unique ID for removal.
41    pub id: u32,
42}
43
44/// Result from background compinit thread
45pub struct CompInitBgResult {
46    pub result: CompInitResult,
47    pub cache: CompsysCache,
48}
49use std::io::Write;
50use std::sync::LazyLock;
51
52/// State snapshot for plugin delta computation.
53struct PluginSnapshot {
54    functions: std::collections::HashSet<String>,
55    aliases: std::collections::HashSet<String>,
56    global_aliases: std::collections::HashSet<String>,
57    suffix_aliases: std::collections::HashSet<String>,
58    variables: HashMap<String, String>,
59    arrays: std::collections::HashSet<String>,
60    assoc_arrays: std::collections::HashSet<String>,
61    fpath: Vec<PathBuf>,
62    options: HashMap<String, bool>,
63    hooks: HashMap<String, Vec<String>>,
64    autoloads: std::collections::HashSet<String>,
65}
66
67/// Cached compiled regexes for hot paths
68static REGEX_CACHE: LazyLock<Mutex<std::collections::HashMap<String, regex::Regex>>> =
69    LazyLock::new(|| Mutex::new(std::collections::HashMap::with_capacity(64)));
70
71// ═══════════════════════════════════════════════════════════════════════════
72// Thread-local executor context for VM builtin dispatch
73// ═══════════════════════════════════════════════════════════════════════════
74
75use std::cell::RefCell;
76
77/// Thread-local pointer to the current ShellExecutor.
78/// Set before VM execution, cleared after. Used by builtin handlers.
79thread_local! {
80    static CURRENT_EXECUTOR: RefCell<Option<*mut ShellExecutor>> = const { RefCell::new(None) };
81}
82
83/// RAII guard that sets/clears the thread-local executor pointer.
84struct ExecutorContext;
85
86impl ExecutorContext {
87    fn enter(executor: &mut ShellExecutor) -> Self {
88        CURRENT_EXECUTOR.with(|cell| {
89            *cell.borrow_mut() = Some(executor as *mut ShellExecutor);
90        });
91        ExecutorContext
92    }
93}
94
95impl Drop for ExecutorContext {
96    fn drop(&mut self) {
97        CURRENT_EXECUTOR.with(|cell| {
98            *cell.borrow_mut() = None;
99        });
100    }
101}
102
103/// Access the current executor from a builtin handler.
104/// # Safety
105/// Only call this from within a VM execution context (after ExecutorContext::enter).
106#[inline]
107fn with_executor<F, R>(f: F) -> R
108where
109    F: FnOnce(&mut ShellExecutor) -> R,
110{
111    CURRENT_EXECUTOR.with(|cell| {
112        let ptr = cell
113            .borrow()
114            .expect("with_executor called outside VM context");
115        // SAFETY: The pointer is valid for the duration of VM execution,
116        // and we're single-threaded within the executor.
117        let executor = unsafe { &mut *ptr };
118        f(executor)
119    })
120}
121
122/// Register all zsh builtins with the VM.
123fn register_builtins(vm: &mut fusevm::VM) {
124    use fusevm::shell_builtins::*;
125    use fusevm::Value;
126
127    // Core builtins
128    vm.register_builtin(BUILTIN_CD, |vm, argc| {
129        let args = pop_args(vm, argc);
130        let status = with_executor(|exec| exec.builtin_cd(&args));
131        Value::Status(status)
132    });
133
134    vm.register_builtin(BUILTIN_PWD, |vm, argc| {
135        let _args = pop_args(vm, argc);
136        let status = with_executor(|exec| exec.builtin_pwd(&[]));
137        Value::Status(status)
138    });
139
140    vm.register_builtin(BUILTIN_ECHO, |vm, argc| {
141        let args = pop_args(vm, argc);
142        let status = with_executor(|exec| exec.builtin_echo(&args, &[]));
143        Value::Status(status)
144    });
145
146    vm.register_builtin(BUILTIN_PRINT, |vm, argc| {
147        let args = pop_args(vm, argc);
148        let status = with_executor(|exec| exec.builtin_print(&args));
149        Value::Status(status)
150    });
151
152    vm.register_builtin(BUILTIN_PRINTF, |vm, argc| {
153        let args = pop_args(vm, argc);
154        let status = with_executor(|exec| exec.builtin_printf(&args));
155        Value::Status(status)
156    });
157
158    vm.register_builtin(BUILTIN_EXPORT, |vm, argc| {
159        let args = pop_args(vm, argc);
160        let status = with_executor(|exec| exec.builtin_export(&args));
161        Value::Status(status)
162    });
163
164    vm.register_builtin(BUILTIN_UNSET, |vm, argc| {
165        let args = pop_args(vm, argc);
166        let status = with_executor(|exec| exec.builtin_unset(&args));
167        Value::Status(status)
168    });
169
170    vm.register_builtin(BUILTIN_SOURCE, |vm, argc| {
171        let args = pop_args(vm, argc);
172        let status = with_executor(|exec| exec.builtin_source(&args));
173        Value::Status(status)
174    });
175
176    vm.register_builtin(BUILTIN_EXIT, |vm, argc| {
177        let args = pop_args(vm, argc);
178        let status = with_executor(|exec| exec.builtin_exit(&args));
179        Value::Status(status)
180    });
181
182    vm.register_builtin(BUILTIN_RETURN, |vm, argc| {
183        let args = pop_args(vm, argc);
184        let status = with_executor(|exec| exec.builtin_return(&args));
185        Value::Status(status)
186    });
187
188    vm.register_builtin(BUILTIN_TRUE, |_vm, _argc| Value::Status(0));
189    vm.register_builtin(BUILTIN_FALSE, |_vm, _argc| Value::Status(1));
190    vm.register_builtin(BUILTIN_COLON, |_vm, _argc| Value::Status(0));
191
192    vm.register_builtin(BUILTIN_TEST, |vm, argc| {
193        let args = pop_args(vm, argc);
194        let status = with_executor(|exec| exec.builtin_test(&args));
195        Value::Status(status)
196    });
197
198    // Variable declaration
199    vm.register_builtin(BUILTIN_LOCAL, |vm, argc| {
200        let args = pop_args(vm, argc);
201        let status = with_executor(|exec| exec.builtin_local(&args));
202        Value::Status(status)
203    });
204
205    vm.register_builtin(BUILTIN_TYPESET, |vm, argc| {
206        let args = pop_args(vm, argc);
207        let status = with_executor(|exec| exec.builtin_declare(&args));
208        Value::Status(status)
209    });
210
211    vm.register_builtin(BUILTIN_READONLY, |vm, argc| {
212        let args = pop_args(vm, argc);
213        let status = with_executor(|exec| exec.builtin_readonly(&args));
214        Value::Status(status)
215    });
216
217    vm.register_builtin(BUILTIN_INTEGER, |vm, argc| {
218        let args = pop_args(vm, argc);
219        let status = with_executor(|exec| exec.builtin_integer(&args));
220        Value::Status(status)
221    });
222
223    vm.register_builtin(BUILTIN_FLOAT, |vm, argc| {
224        let args = pop_args(vm, argc);
225        let status = with_executor(|exec| exec.builtin_float(&args));
226        Value::Status(status)
227    });
228
229    // I/O
230    vm.register_builtin(BUILTIN_READ, |vm, argc| {
231        let args = pop_args(vm, argc);
232        let status = with_executor(|exec| exec.builtin_read(&args));
233        Value::Status(status)
234    });
235
236    // Control flow
237    vm.register_builtin(BUILTIN_BREAK, |vm, argc| {
238        let args = pop_args(vm, argc);
239        let status = with_executor(|exec| exec.builtin_break(&args));
240        Value::Status(status)
241    });
242
243    vm.register_builtin(BUILTIN_CONTINUE, |vm, argc| {
244        let args = pop_args(vm, argc);
245        let status = with_executor(|exec| exec.builtin_continue(&args));
246        Value::Status(status)
247    });
248
249    vm.register_builtin(BUILTIN_SHIFT, |vm, argc| {
250        let args = pop_args(vm, argc);
251        let status = with_executor(|exec| exec.builtin_shift(&args));
252        Value::Status(status)
253    });
254
255    vm.register_builtin(BUILTIN_EVAL, |vm, argc| {
256        let args = pop_args(vm, argc);
257        let status = with_executor(|exec| exec.builtin_eval(&args));
258        Value::Status(status)
259    });
260
261    vm.register_builtin(BUILTIN_EXEC, |vm, argc| {
262        let args = pop_args(vm, argc);
263        let status = with_executor(|exec| exec.builtin_exec(&args));
264        Value::Status(status)
265    });
266
267    vm.register_builtin(BUILTIN_COMMAND, |vm, argc| {
268        let args = pop_args(vm, argc);
269        let status = with_executor(|exec| exec.builtin_command(&args, &[]));
270        Value::Status(status)
271    });
272
273    vm.register_builtin(BUILTIN_BUILTIN, |vm, argc| {
274        let args = pop_args(vm, argc);
275        let status = with_executor(|exec| exec.builtin_builtin(&args, &[]));
276        Value::Status(status)
277    });
278
279    vm.register_builtin(BUILTIN_LET, |vm, argc| {
280        let args = pop_args(vm, argc);
281        let status = with_executor(|exec| exec.builtin_let(&args));
282        Value::Status(status)
283    });
284
285    // Job control
286    vm.register_builtin(BUILTIN_JOBS, |vm, argc| {
287        let args = pop_args(vm, argc);
288        let status = with_executor(|exec| exec.builtin_jobs(&args));
289        Value::Status(status)
290    });
291
292    vm.register_builtin(BUILTIN_FG, |vm, argc| {
293        let args = pop_args(vm, argc);
294        let status = with_executor(|exec| exec.builtin_fg(&args));
295        Value::Status(status)
296    });
297
298    vm.register_builtin(BUILTIN_BG, |vm, argc| {
299        let args = pop_args(vm, argc);
300        let status = with_executor(|exec| exec.builtin_bg(&args));
301        Value::Status(status)
302    });
303
304    vm.register_builtin(BUILTIN_KILL, |vm, argc| {
305        let args = pop_args(vm, argc);
306        let status = with_executor(|exec| exec.builtin_kill(&args));
307        Value::Status(status)
308    });
309
310    vm.register_builtin(BUILTIN_DISOWN, |vm, argc| {
311        let args = pop_args(vm, argc);
312        let status = with_executor(|exec| exec.builtin_disown(&args));
313        Value::Status(status)
314    });
315
316    vm.register_builtin(BUILTIN_WAIT, |vm, argc| {
317        let args = pop_args(vm, argc);
318        let status = with_executor(|exec| exec.builtin_wait(&args));
319        Value::Status(status)
320    });
321
322    vm.register_builtin(BUILTIN_SUSPEND, |vm, argc| {
323        let args = pop_args(vm, argc);
324        let status = with_executor(|exec| exec.builtin_suspend(&args));
325        Value::Status(status)
326    });
327
328    // History
329    vm.register_builtin(BUILTIN_HISTORY, |vm, argc| {
330        let args = pop_args(vm, argc);
331        let status = with_executor(|exec| exec.builtin_history(&args));
332        Value::Status(status)
333    });
334
335    vm.register_builtin(BUILTIN_FC, |vm, argc| {
336        let args = pop_args(vm, argc);
337        let status = with_executor(|exec| exec.builtin_fc(&args));
338        Value::Status(status)
339    });
340
341    vm.register_builtin(BUILTIN_R, |vm, argc| {
342        let args = pop_args(vm, argc);
343        let status = with_executor(|exec| exec.builtin_r(&args));
344        Value::Status(status)
345    });
346
347    // Aliases
348    vm.register_builtin(BUILTIN_ALIAS, |vm, argc| {
349        let args = pop_args(vm, argc);
350        let status = with_executor(|exec| exec.builtin_alias(&args));
351        Value::Status(status)
352    });
353
354    vm.register_builtin(BUILTIN_UNALIAS, |vm, argc| {
355        let args = pop_args(vm, argc);
356        let status = with_executor(|exec| exec.builtin_unalias(&args));
357        Value::Status(status)
358    });
359
360    // Options
361    vm.register_builtin(BUILTIN_SET, |vm, argc| {
362        let args = pop_args(vm, argc);
363        let status = with_executor(|exec| exec.builtin_set(&args));
364        Value::Status(status)
365    });
366
367    vm.register_builtin(BUILTIN_SETOPT, |vm, argc| {
368        let args = pop_args(vm, argc);
369        let status = with_executor(|exec| exec.builtin_setopt(&args));
370        Value::Status(status)
371    });
372
373    vm.register_builtin(BUILTIN_UNSETOPT, |vm, argc| {
374        let args = pop_args(vm, argc);
375        let status = with_executor(|exec| exec.builtin_unsetopt(&args));
376        Value::Status(status)
377    });
378
379    vm.register_builtin(BUILTIN_SHOPT, |vm, argc| {
380        let args = pop_args(vm, argc);
381        let status = with_executor(|exec| exec.builtin_shopt(&args));
382        Value::Status(status)
383    });
384
385    vm.register_builtin(BUILTIN_EMULATE, |vm, argc| {
386        let args = pop_args(vm, argc);
387        let status = with_executor(|exec| exec.builtin_emulate(&args));
388        Value::Status(status)
389    });
390
391    vm.register_builtin(BUILTIN_GETOPTS, |vm, argc| {
392        let args = pop_args(vm, argc);
393        let status = with_executor(|exec| exec.builtin_getopts(&args));
394        Value::Status(status)
395    });
396
397    // Functions / Autoload
398    vm.register_builtin(BUILTIN_AUTOLOAD, |vm, argc| {
399        let args = pop_args(vm, argc);
400        let status = with_executor(|exec| exec.builtin_autoload(&args));
401        Value::Status(status)
402    });
403
404    vm.register_builtin(BUILTIN_FUNCTIONS, |vm, argc| {
405        let args = pop_args(vm, argc);
406        let status = with_executor(|exec| exec.builtin_functions(&args));
407        Value::Status(status)
408    });
409
410    vm.register_builtin(BUILTIN_UNFUNCTION, |vm, argc| {
411        let args = pop_args(vm, argc);
412        let status = with_executor(|exec| exec.builtin_unfunction(&args));
413        Value::Status(status)
414    });
415
416    // Traps
417    vm.register_builtin(BUILTIN_TRAP, |vm, argc| {
418        let args = pop_args(vm, argc);
419        let status = with_executor(|exec| exec.builtin_trap(&args));
420        Value::Status(status)
421    });
422
423    // Directory stack
424    vm.register_builtin(BUILTIN_PUSHD, |vm, argc| {
425        let args = pop_args(vm, argc);
426        let status = with_executor(|exec| exec.builtin_pushd(&args));
427        Value::Status(status)
428    });
429
430    vm.register_builtin(BUILTIN_POPD, |vm, argc| {
431        let args = pop_args(vm, argc);
432        let status = with_executor(|exec| exec.builtin_popd(&args));
433        Value::Status(status)
434    });
435
436    vm.register_builtin(BUILTIN_DIRS, |vm, argc| {
437        let args = pop_args(vm, argc);
438        let status = with_executor(|exec| exec.builtin_dirs(&args));
439        Value::Status(status)
440    });
441
442    // Type / Which / Hash
443    vm.register_builtin(BUILTIN_TYPE, |vm, argc| {
444        let args = pop_args(vm, argc);
445        let status = with_executor(|exec| exec.builtin_type(&args));
446        Value::Status(status)
447    });
448
449    vm.register_builtin(BUILTIN_WHENCE, |vm, argc| {
450        let args = pop_args(vm, argc);
451        let status = with_executor(|exec| exec.builtin_whence(&args));
452        Value::Status(status)
453    });
454
455    vm.register_builtin(BUILTIN_WHERE, |vm, argc| {
456        let args = pop_args(vm, argc);
457        let status = with_executor(|exec| exec.builtin_where(&args));
458        Value::Status(status)
459    });
460
461    vm.register_builtin(BUILTIN_WHICH, |vm, argc| {
462        let args = pop_args(vm, argc);
463        let status = with_executor(|exec| exec.builtin_which(&args));
464        Value::Status(status)
465    });
466
467    vm.register_builtin(BUILTIN_HASH, |vm, argc| {
468        let args = pop_args(vm, argc);
469        let status = with_executor(|exec| exec.builtin_hash(&args));
470        Value::Status(status)
471    });
472
473    vm.register_builtin(BUILTIN_REHASH, |vm, argc| {
474        let args = pop_args(vm, argc);
475        let status = with_executor(|exec| exec.builtin_rehash(&args));
476        Value::Status(status)
477    });
478
479    vm.register_builtin(BUILTIN_UNHASH, |vm, argc| {
480        let args = pop_args(vm, argc);
481        let status = with_executor(|exec| exec.builtin_unhash(&args));
482        Value::Status(status)
483    });
484
485    // Completion
486    vm.register_builtin(BUILTIN_COMPGEN, |vm, argc| {
487        let args = pop_args(vm, argc);
488        let status = with_executor(|exec| exec.builtin_compgen(&args));
489        Value::Status(status)
490    });
491
492    vm.register_builtin(BUILTIN_COMPLETE, |vm, argc| {
493        let args = pop_args(vm, argc);
494        let status = with_executor(|exec| exec.builtin_complete(&args));
495        Value::Status(status)
496    });
497
498    vm.register_builtin(BUILTIN_COMPOPT, |vm, argc| {
499        let args = pop_args(vm, argc);
500        let status = with_executor(|exec| exec.builtin_compopt(&args));
501        Value::Status(status)
502    });
503
504    vm.register_builtin(BUILTIN_COMPADD, |vm, argc| {
505        let args = pop_args(vm, argc);
506        let status = with_executor(|exec| exec.builtin_compadd(&args));
507        Value::Status(status)
508    });
509
510    vm.register_builtin(BUILTIN_COMPSET, |vm, argc| {
511        let args = pop_args(vm, argc);
512        let status = with_executor(|exec| exec.builtin_compset(&args));
513        Value::Status(status)
514    });
515
516    vm.register_builtin(BUILTIN_COMPDEF, |vm, argc| {
517        let args = pop_args(vm, argc);
518        let status = with_executor(|exec| exec.builtin_compdef(&args));
519        Value::Status(status)
520    });
521
522    vm.register_builtin(BUILTIN_COMPINIT, |vm, argc| {
523        let args = pop_args(vm, argc);
524        let status = with_executor(|exec| exec.builtin_compinit(&args));
525        Value::Status(status)
526    });
527
528    vm.register_builtin(BUILTIN_CDREPLAY, |vm, argc| {
529        let args = pop_args(vm, argc);
530        let status = with_executor(|exec| exec.builtin_cdreplay(&args));
531        Value::Status(status)
532    });
533
534    // Zsh-specific
535    vm.register_builtin(BUILTIN_ZSTYLE, |vm, argc| {
536        let args = pop_args(vm, argc);
537        let status = with_executor(|exec| exec.builtin_zstyle(&args));
538        Value::Status(status)
539    });
540
541    vm.register_builtin(BUILTIN_ZMODLOAD, |vm, argc| {
542        let args = pop_args(vm, argc);
543        let status = with_executor(|exec| exec.builtin_zmodload(&args));
544        Value::Status(status)
545    });
546
547    vm.register_builtin(BUILTIN_BINDKEY, |vm, argc| {
548        let args = pop_args(vm, argc);
549        let status = with_executor(|exec| exec.builtin_bindkey(&args));
550        Value::Status(status)
551    });
552
553    vm.register_builtin(BUILTIN_ZLE, |vm, argc| {
554        let args = pop_args(vm, argc);
555        let status = with_executor(|exec| exec.builtin_zle(&args));
556        Value::Status(status)
557    });
558
559    vm.register_builtin(BUILTIN_VARED, |vm, argc| {
560        let args = pop_args(vm, argc);
561        let status = with_executor(|exec| exec.builtin_vared(&args));
562        Value::Status(status)
563    });
564
565    vm.register_builtin(BUILTIN_ZCOMPILE, |vm, argc| {
566        let args = pop_args(vm, argc);
567        let status = with_executor(|exec| exec.builtin_zcompile(&args));
568        Value::Status(status)
569    });
570
571    vm.register_builtin(BUILTIN_ZFORMAT, |vm, argc| {
572        let args = pop_args(vm, argc);
573        let status = with_executor(|exec| exec.builtin_zformat(&args));
574        Value::Status(status)
575    });
576
577    vm.register_builtin(BUILTIN_ZPARSEOPTS, |vm, argc| {
578        let args = pop_args(vm, argc);
579        let status = with_executor(|exec| exec.builtin_zparseopts(&args));
580        Value::Status(status)
581    });
582
583    vm.register_builtin(BUILTIN_ZREGEXPARSE, |vm, argc| {
584        let args = pop_args(vm, argc);
585        let status = with_executor(|exec| exec.builtin_zregexparse(&args));
586        Value::Status(status)
587    });
588
589    // Resource limits
590    vm.register_builtin(BUILTIN_ULIMIT, |vm, argc| {
591        let args = pop_args(vm, argc);
592        let status = with_executor(|exec| exec.builtin_ulimit(&args));
593        Value::Status(status)
594    });
595
596    vm.register_builtin(BUILTIN_LIMIT, |vm, argc| {
597        let args = pop_args(vm, argc);
598        let status = with_executor(|exec| exec.builtin_limit(&args));
599        Value::Status(status)
600    });
601
602    vm.register_builtin(BUILTIN_UNLIMIT, |vm, argc| {
603        let args = pop_args(vm, argc);
604        let status = with_executor(|exec| exec.builtin_unlimit(&args));
605        Value::Status(status)
606    });
607
608    vm.register_builtin(BUILTIN_UMASK, |vm, argc| {
609        let args = pop_args(vm, argc);
610        let status = with_executor(|exec| exec.builtin_umask(&args));
611        Value::Status(status)
612    });
613
614    // Misc
615    vm.register_builtin(BUILTIN_TIMES, |vm, argc| {
616        let args = pop_args(vm, argc);
617        let status = with_executor(|exec| exec.builtin_times(&args));
618        Value::Status(status)
619    });
620
621    vm.register_builtin(BUILTIN_CALLER, |vm, argc| {
622        let args = pop_args(vm, argc);
623        let status = with_executor(|exec| exec.builtin_caller(&args));
624        Value::Status(status)
625    });
626
627    vm.register_builtin(BUILTIN_HELP, |vm, argc| {
628        let args = pop_args(vm, argc);
629        let status = with_executor(|exec| exec.builtin_help(&args));
630        Value::Status(status)
631    });
632
633    vm.register_builtin(BUILTIN_ENABLE, |vm, argc| {
634        let args = pop_args(vm, argc);
635        let status = with_executor(|exec| exec.builtin_enable(&args));
636        Value::Status(status)
637    });
638
639    vm.register_builtin(BUILTIN_DISABLE, |vm, argc| {
640        let args = pop_args(vm, argc);
641        let status = with_executor(|exec| exec.builtin_disable(&args));
642        Value::Status(status)
643    });
644
645    vm.register_builtin(BUILTIN_NOGLOB, |vm, argc| {
646        let args = pop_args(vm, argc);
647        let status = with_executor(|exec| exec.builtin_noglob(&args, &[]));
648        Value::Status(status)
649    });
650
651    vm.register_builtin(BUILTIN_TTYCTL, |vm, argc| {
652        let args = pop_args(vm, argc);
653        let status = with_executor(|exec| exec.builtin_ttyctl(&args));
654        Value::Status(status)
655    });
656
657    vm.register_builtin(BUILTIN_SYNC, |vm, argc| {
658        let args = pop_args(vm, argc);
659        let status = with_executor(|exec| exec.builtin_sync(&args));
660        Value::Status(status)
661    });
662
663    vm.register_builtin(BUILTIN_MKDIR, |vm, argc| {
664        let args = pop_args(vm, argc);
665        let status = with_executor(|exec| exec.builtin_mkdir(&args));
666        Value::Status(status)
667    });
668
669    vm.register_builtin(BUILTIN_STRFTIME, |vm, argc| {
670        let args = pop_args(vm, argc);
671        let status = with_executor(|exec| exec.builtin_strftime(&args));
672        Value::Status(status)
673    });
674
675    vm.register_builtin(BUILTIN_ZSLEEP, |vm, argc| {
676        let args = pop_args(vm, argc);
677        let status = with_executor(|exec| exec.builtin_zsleep(&args));
678        Value::Status(status)
679    });
680
681    vm.register_builtin(BUILTIN_ZSYSTEM, |vm, argc| {
682        let args = pop_args(vm, argc);
683        let status = with_executor(|exec| exec.builtin_zsystem(&args));
684        Value::Status(status)
685    });
686
687    // PCRE
688    vm.register_builtin(BUILTIN_PCRE_COMPILE, |vm, argc| {
689        let args = pop_args(vm, argc);
690        let status = with_executor(|exec| exec.builtin_pcre_compile(&args));
691        Value::Status(status)
692    });
693
694    vm.register_builtin(BUILTIN_PCRE_MATCH, |vm, argc| {
695        let args = pop_args(vm, argc);
696        let status = with_executor(|exec| exec.builtin_pcre_match(&args));
697        Value::Status(status)
698    });
699
700    vm.register_builtin(BUILTIN_PCRE_STUDY, |vm, argc| {
701        let args = pop_args(vm, argc);
702        let status = with_executor(|exec| exec.builtin_pcre_study(&args));
703        Value::Status(status)
704    });
705
706    // Database (GDBM)
707    vm.register_builtin(BUILTIN_ZTIE, |vm, argc| {
708        let args = pop_args(vm, argc);
709        let status = with_executor(|exec| exec.builtin_ztie(&args));
710        Value::Status(status)
711    });
712
713    vm.register_builtin(BUILTIN_ZUNTIE, |vm, argc| {
714        let args = pop_args(vm, argc);
715        let status = with_executor(|exec| exec.builtin_zuntie(&args));
716        Value::Status(status)
717    });
718
719    vm.register_builtin(BUILTIN_ZGDBMPATH, |vm, argc| {
720        let args = pop_args(vm, argc);
721        let status = with_executor(|exec| exec.builtin_zgdbmpath(&args));
722        Value::Status(status)
723    });
724
725    // Prompt
726    vm.register_builtin(BUILTIN_PROMPTINIT, |vm, argc| {
727        let args = pop_args(vm, argc);
728        let status = with_executor(|exec| exec.builtin_promptinit(&args));
729        Value::Status(status)
730    });
731
732    vm.register_builtin(BUILTIN_PROMPT, |vm, argc| {
733        let args = pop_args(vm, argc);
734        let status = with_executor(|exec| exec.builtin_prompt(&args));
735        Value::Status(status)
736    });
737
738    // Async / Parallel (zshrs extensions)
739    vm.register_builtin(BUILTIN_ASYNC, |vm, argc| {
740        let args = pop_args(vm, argc);
741        let status = with_executor(|exec| exec.builtin_async(&args));
742        Value::Status(status)
743    });
744
745    vm.register_builtin(BUILTIN_AWAIT, |vm, argc| {
746        let args = pop_args(vm, argc);
747        let status = with_executor(|exec| exec.builtin_await(&args));
748        Value::Status(status)
749    });
750
751    vm.register_builtin(BUILTIN_PMAP, |vm, argc| {
752        let args = pop_args(vm, argc);
753        let status = with_executor(|exec| exec.builtin_pmap(&args));
754        Value::Status(status)
755    });
756
757    vm.register_builtin(BUILTIN_PGREP, |vm, argc| {
758        let args = pop_args(vm, argc);
759        let status = with_executor(|exec| exec.builtin_pgrep(&args));
760        Value::Status(status)
761    });
762
763    vm.register_builtin(BUILTIN_PEACH, |vm, argc| {
764        let args = pop_args(vm, argc);
765        let status = with_executor(|exec| exec.builtin_peach(&args));
766        Value::Status(status)
767    });
768
769    vm.register_builtin(BUILTIN_BARRIER, |vm, argc| {
770        let args = pop_args(vm, argc);
771        let status = with_executor(|exec| exec.builtin_barrier(&args));
772        Value::Status(status)
773    });
774
775    // Intercept (AOP)
776    vm.register_builtin(BUILTIN_INTERCEPT, |vm, argc| {
777        let args = pop_args(vm, argc);
778        let status = with_executor(|exec| exec.builtin_intercept(&args));
779        Value::Status(status)
780    });
781
782    vm.register_builtin(BUILTIN_INTERCEPT_PROCEED, |vm, argc| {
783        let args = pop_args(vm, argc);
784        let status = with_executor(|exec| exec.builtin_intercept_proceed(&args));
785        Value::Status(status)
786    });
787
788    // Debug / Profile
789    vm.register_builtin(BUILTIN_DOCTOR, |vm, argc| {
790        let args = pop_args(vm, argc);
791        let status = with_executor(|exec| exec.builtin_doctor(&args));
792        Value::Status(status)
793    });
794
795    vm.register_builtin(BUILTIN_DBVIEW, |vm, argc| {
796        let args = pop_args(vm, argc);
797        let status = with_executor(|exec| exec.builtin_dbview(&args));
798        Value::Status(status)
799    });
800
801    vm.register_builtin(BUILTIN_PROFILE, |vm, argc| {
802        let args = pop_args(vm, argc);
803        let status = with_executor(|exec| exec.builtin_profile(&args));
804        Value::Status(status)
805    });
806
807    vm.register_builtin(BUILTIN_ZPROF, |vm, argc| {
808        let args = pop_args(vm, argc);
809        let status = with_executor(|exec| exec.builtin_zprof(&args));
810        Value::Status(status)
811    });
812}
813
814/// Pop argc arguments from the VM stack into a Vec<String>.
815#[inline]
816fn pop_args(vm: &mut fusevm::VM, argc: u8) -> Vec<String> {
817    let mut args = Vec::with_capacity(argc as usize);
818    for _ in 0..argc {
819        args.push(vm.pop().to_str());
820    }
821    args.reverse(); // Stack is LIFO, args should be in order
822    args
823}
824
825/// Match an intercept pattern against a command name or full command string.
826/// Supports: exact match, glob ("git *", "_*", "*"), or "all".
827fn intercept_matches(pattern: &str, cmd_name: &str, full_cmd: &str) -> bool {
828    if pattern == "*" || pattern == "all" {
829        return true;
830    }
831    if pattern == cmd_name {
832        return true;
833    }
834    // Glob match against full command (e.g. "git *" matches "git push")
835    if pattern.contains('*') || pattern.contains('?') {
836        if let Ok(pat) = glob::Pattern::new(pattern) {
837            return pat.matches(cmd_name) || pat.matches(full_cmd);
838        }
839    }
840    false
841}
842
843/// Get or compile a regex, caching the result
844fn cached_regex(pattern: &str) -> Option<regex::Regex> {
845    let mut cache = REGEX_CACHE.lock();
846    if let Some(re) = cache.get(pattern) {
847        return Some(re.clone());
848    }
849    match regex::Regex::new(pattern) {
850        Ok(re) => {
851            cache.insert(pattern.to_string(), re.clone());
852            Some(re)
853        }
854        Err(_) => None,
855    }
856}
857
858/// HashSet of all zsh options for O(1) lookup
859static ZSH_OPTIONS_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
860    [
861        "aliases",
862        "allexport",
863        "alwayslastprompt",
864        "alwaystoend",
865        "appendcreate",
866        "appendhistory",
867        "autocd",
868        "autocontinue",
869        "autolist",
870        "automenu",
871        "autonamedirs",
872        "autoparamkeys",
873        "autoparamslash",
874        "autopushd",
875        "autoremoveslash",
876        "autoresume",
877        "badpattern",
878        "banghist",
879        "bareglobqual",
880        "bashautolist",
881        "bashrematch",
882        "beep",
883        "bgnice",
884        "braceccl",
885        "bsdecho",
886        "caseglob",
887        "casematch",
888        "cbases",
889        "cdablevars",
890        "cdsilent",
891        "chasedots",
892        "chaselinks",
893        "checkjobs",
894        "checkrunningjobs",
895        "clobber",
896        "combiningchars",
897        "completealiases",
898        "completeinword",
899        "continueonerror",
900        "correct",
901        "correctall",
902        "cprecedences",
903        "cshjunkiehistory",
904        "cshjunkieloops",
905        "cshjunkiequotes",
906        "cshnullcmd",
907        "cshnullglob",
908        "debugbeforecmd",
909        "dotglob",
910        "dvorak",
911        "emacs",
912        "equals",
913        "errexit",
914        "errreturn",
915        "evallineno",
916        "exec",
917        "extendedglob",
918        "extendedhistory",
919        "flowcontrol",
920        "forcefloat",
921        "functionargzero",
922        "glob",
923        "globassign",
924        "globcomplete",
925        "globdots",
926        "globstarshort",
927        "globsubst",
928        "globalexport",
929        "globalrcs",
930        "hashall",
931        "hashcmds",
932        "hashdirs",
933        "hashexecutablesonly",
934        "hashlistall",
935        "histallowclobber",
936        "histappend",
937        "histbeep",
938        "histexpand",
939        "histexpiredupsfirst",
940        "histfcntllock",
941        "histfindnodups",
942        "histignorealldups",
943        "histignoredups",
944        "histignorespace",
945        "histlexwords",
946        "histnofunctions",
947        "histnostore",
948        "histreduceblanks",
949        "histsavebycopy",
950        "histsavenodups",
951        "histsubstpattern",
952        "histverify",
953        "hup",
954        "ignorebraces",
955        "ignoreclosebraces",
956        "ignoreeof",
957        "incappendhistory",
958        "incappendhistorytime",
959        "interactive",
960        "interactivecomments",
961        "ksharrays",
962        "kshautoload",
963        "kshglob",
964        "kshoptionprint",
965        "kshtypeset",
966        "kshzerosubscript",
967        "listambiguous",
968        "listbeep",
969        "listpacked",
970        "listrowsfirst",
971        "listtypes",
972        "localloops",
973        "localoptions",
974        "localpatterns",
975        "localtraps",
976        "log",
977        "login",
978        "longlistjobs",
979        "magicequalsubst",
980        "mailwarn",
981        "mailwarning",
982        "markdirs",
983        "menucomplete",
984        "monitor",
985        "multibyte",
986        "multifuncdef",
987        "multios",
988        "nomatch",
989        "notify",
990        "nullglob",
991        "numericglobsort",
992        "octalzeroes",
993        "onecmd",
994        "overstrike",
995        "pathdirs",
996        "pathscript",
997        "physical",
998        "pipefail",
999        "posixaliases",
1000        "posixargzero",
1001        "posixbuiltins",
1002        "posixcd",
1003        "posixidentifiers",
1004        "posixjobs",
1005        "posixstrings",
1006        "posixtraps",
1007        "printeightbit",
1008        "printexitvalue",
1009        "privileged",
1010        "promptbang",
1011        "promptcr",
1012        "promptpercent",
1013        "promptsp",
1014        "promptsubst",
1015        "promptvars",
1016        "pushdignoredups",
1017        "pushdminus",
1018        "pushdsilent",
1019        "pushdtohome",
1020        "rcexpandparam",
1021        "rcquotes",
1022        "rcs",
1023        "recexact",
1024        "rematchpcre",
1025        "restricted",
1026        "rmstarsilent",
1027        "rmstarwait",
1028        "sharehistory",
1029        "shfileexpansion",
1030        "shglob",
1031        "shinstdin",
1032        "shnullcmd",
1033        "shoptionletters",
1034        "shortloops",
1035        "shortrepeat",
1036        "shwordsplit",
1037        "singlecommand",
1038        "singlelinezle",
1039        "sourcetrace",
1040        "stdin",
1041        "sunkeyboardhack",
1042        "trackall",
1043        "transientrprompt",
1044        "trapsasync",
1045        "typesetsilent",
1046        "typesettounset",
1047        "unset",
1048        "verbose",
1049        "vi",
1050        "warncreateglobal",
1051        "warnnestedvar",
1052        "xtrace",
1053        "zle",
1054    ]
1055    .into_iter()
1056    .collect()
1057});
1058
1059/// O(1) builtin lookup — replaces the 130+ arm matches! macro in is_builtin()
1060static BUILTIN_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
1061    [
1062        "cd",
1063        "chdir",
1064        "pwd",
1065        "echo",
1066        "export",
1067        "unset",
1068        "source",
1069        "exit",
1070        "return",
1071        "bye",
1072        "logout",
1073        "log",
1074        "true",
1075        "false",
1076        "test",
1077        "local",
1078        "declare",
1079        "typeset",
1080        "read",
1081        "shift",
1082        "eval",
1083        "jobs",
1084        "fg",
1085        "bg",
1086        "kill",
1087        "disown",
1088        "wait",
1089        "autoload",
1090        "history",
1091        "fc",
1092        "trap",
1093        "suspend",
1094        "alias",
1095        "unalias",
1096        "set",
1097        "shopt",
1098        "setopt",
1099        "unsetopt",
1100        "getopts",
1101        "type",
1102        "hash",
1103        "command",
1104        "builtin",
1105        "let",
1106        "pushd",
1107        "popd",
1108        "dirs",
1109        "printf",
1110        "break",
1111        "continue",
1112        "disable",
1113        "enable",
1114        "emulate",
1115        "exec",
1116        "float",
1117        "integer",
1118        "functions",
1119        "print",
1120        "whence",
1121        "where",
1122        "which",
1123        "ulimit",
1124        "limit",
1125        "unlimit",
1126        "umask",
1127        "rehash",
1128        "unhash",
1129        "times",
1130        "zmodload",
1131        "r",
1132        "ttyctl",
1133        "noglob",
1134        "zstat",
1135        "stat",
1136        "strftime",
1137        "zsleep",
1138        "zln",
1139        "zmv",
1140        "zcp",
1141        "coproc",
1142        "zparseopts",
1143        "readonly",
1144        "unfunction",
1145        "getln",
1146        "pushln",
1147        "bindkey",
1148        "zle",
1149        "sched",
1150        "zformat",
1151        "zcompile",
1152        "vared",
1153        "echotc",
1154        "echoti",
1155        "zpty",
1156        "zprof",
1157        "zsocket",
1158        "ztcp",
1159        "zregexparse",
1160        "clone",
1161        "comparguments",
1162        "compcall",
1163        "compctl",
1164        "compdef",
1165        "compdescribe",
1166        "compfiles",
1167        "compgroups",
1168        "compinit",
1169        "compquote",
1170        "comptags",
1171        "comptry",
1172        "compvalues",
1173        "cdreplay",
1174        "cap",
1175        "getcap",
1176        "setcap",
1177        "zftp",
1178        "zcurses",
1179        "sysread",
1180        "syswrite",
1181        "syserror",
1182        "sysopen",
1183        "sysseek",
1184        "private",
1185        "zgetattr",
1186        "zsetattr",
1187        "zdelattr",
1188        "zlistattr",
1189        "[",
1190        ".",
1191        ":",
1192        "compgen",
1193        "complete",
1194    ]
1195    .into_iter()
1196    .collect()
1197});
1198
1199/// Convert float to hex representation (%a/%A format)
1200fn float_to_hex(val: f64, uppercase: bool) -> String {
1201    if val.is_nan() {
1202        return if uppercase { "NAN" } else { "nan" }.to_string();
1203    }
1204    if val.is_infinite() {
1205        return if val > 0.0 {
1206            if uppercase {
1207                "INF"
1208            } else {
1209                "inf"
1210            }
1211        } else {
1212            if uppercase {
1213                "-INF"
1214            } else {
1215                "-inf"
1216            }
1217        }
1218        .to_string();
1219    }
1220    if val == 0.0 {
1221        let sign = if val.is_sign_negative() { "-" } else { "" };
1222        return if uppercase {
1223            format!("{}0X0P+0", sign)
1224        } else {
1225            format!("{}0x0p+0", sign)
1226        };
1227    }
1228
1229    let sign = if val < 0.0 { "-" } else { "" };
1230    let abs_val = val.abs();
1231    let bits = abs_val.to_bits();
1232    let exponent = ((bits >> 52) & 0x7ff) as i32 - 1023;
1233    let mantissa = bits & 0xfffffffffffff;
1234
1235    let hex_mantissa = format!("{:013x}", mantissa);
1236    let hex_mantissa = hex_mantissa.trim_end_matches('0');
1237    let hex_mantissa = if hex_mantissa.is_empty() {
1238        "0"
1239    } else {
1240        hex_mantissa
1241    };
1242
1243    if uppercase {
1244        format!("{}0X1.{}P{:+}", sign, hex_mantissa.to_uppercase(), exponent)
1245    } else {
1246        format!("{}0x1.{}p{:+}", sign, hex_mantissa, exponent)
1247    }
1248}
1249
1250/// Quote a string for shell output (like zsh's set output)
1251fn shell_quote(s: &str) -> String {
1252    if s.is_empty() {
1253        return "''".to_string();
1254    }
1255    // Check if quoting is needed
1256    let needs_quotes = s.chars().any(|c| {
1257        matches!(
1258            c,
1259            ' ' | '\t'
1260                | '\n'
1261                | '\''
1262                | '"'
1263                | '\\'
1264                | '$'
1265                | '`'
1266                | '!'
1267                | '*'
1268                | '?'
1269                | '['
1270                | ']'
1271                | '{'
1272                | '}'
1273                | '('
1274                | ')'
1275                | '<'
1276                | '>'
1277                | '|'
1278                | '&'
1279                | ';'
1280                | '#'
1281                | '~'
1282        )
1283    });
1284    if !needs_quotes {
1285        return s.to_string();
1286    }
1287    // Use single quotes, escaping single quotes as '\''
1288    format!("'{}'", s.replace('\'', "'\\''"))
1289}
1290
1291/// Quote a value for typeset -p output (re-executable code)
1292/// Uses single quoting only when the value contains special characters
1293fn shell_quote_value(s: &str) -> String {
1294    if s.is_empty() {
1295        return "''".to_string();
1296    }
1297    let needs_quotes = s.chars().any(|c| {
1298        matches!(
1299            c,
1300            ' ' | '\t'
1301                | '\n'
1302                | '\''
1303                | '"'
1304                | '\\'
1305                | '$'
1306                | '`'
1307                | '!'
1308                | '*'
1309                | '?'
1310                | '['
1311                | ']'
1312                | '{'
1313                | '}'
1314                | '('
1315                | ')'
1316                | '<'
1317                | '>'
1318                | '|'
1319                | '&'
1320                | ';'
1321                | '#'
1322                | '~'
1323                | '^'
1324        )
1325    });
1326    if !needs_quotes {
1327        return s.to_string();
1328    }
1329    format!("'{}'", s.replace('\'', "'\\''"))
1330}
1331
1332use crate::jobs::{continue_job, wait_for_child, wait_for_job, JobState, JobTable};
1333use crate::parser::{
1334    CaseTerminator, CompoundCommand, CondExpr, ListOp, Redirect, RedirectOp, ShellCommand,
1335    ShellParser, ShellWord, SimpleCommand, VarModifier, ZshParamFlag,
1336};
1337use crate::zwc::ZwcFile;
1338use std::collections::HashMap;
1339use std::env;
1340use std::fs::{self, File, OpenOptions};
1341use std::io;
1342use std::path::{Path, PathBuf};
1343use std::process::{Child, Command, Stdio};
1344
1345/// A completion specification for the `complete` builtin
1346#[derive(Debug, Clone, Default)]
1347pub struct CompSpec {
1348    pub actions: Vec<String>,     // -a, -b, -c, etc.
1349    pub wordlist: Option<String>, // -W wordlist
1350    pub function: Option<String>, // -F function
1351    pub command: Option<String>,  // -C command
1352    pub globpat: Option<String>,  // -G glob
1353    pub prefix: Option<String>,   // -P prefix
1354    pub suffix: Option<String>,   // -S suffix
1355}
1356
1357/// A single completion match for zsh-style completion
1358#[derive(Debug, Clone)]
1359pub struct CompMatch {
1360    pub word: String,                   // The actual completion word
1361    pub display: Option<String>,        // Display string (-d)
1362    pub prefix: Option<String>,         // -P prefix (inserted but not part of match)
1363    pub suffix: Option<String>,         // -S suffix (inserted but not part of match)
1364    pub hidden_prefix: Option<String>,  // -p hidden prefix
1365    pub hidden_suffix: Option<String>,  // -s hidden suffix
1366    pub ignored_prefix: Option<String>, // -i ignored prefix
1367    pub ignored_suffix: Option<String>, // -I ignored suffix
1368    pub group: Option<String>,          // -J/-V group name
1369    pub description: Option<String>,    // -X explanation
1370    pub remove_suffix: Option<String>,  // -r remove chars
1371    pub file_match: bool,               // -f flag
1372    pub quote_match: bool,              // -q flag
1373}
1374
1375impl Default for CompMatch {
1376    fn default() -> Self {
1377        Self {
1378            word: String::new(),
1379            display: None,
1380            prefix: None,
1381            suffix: None,
1382            hidden_prefix: None,
1383            hidden_suffix: None,
1384            ignored_prefix: None,
1385            ignored_suffix: None,
1386            group: None,
1387            description: None,
1388            remove_suffix: None,
1389            file_match: false,
1390            quote_match: false,
1391        }
1392    }
1393}
1394
1395/// Completion group for organizing matches
1396#[derive(Debug, Clone, Default)]
1397pub struct CompGroup {
1398    pub name: String,
1399    pub matches: Vec<CompMatch>,
1400    pub explanation: Option<String>,
1401    pub sorted: bool,
1402}
1403
1404/// zsh completion state (compstate associative array)
1405#[derive(Debug, Clone, Default)]
1406pub struct CompState {
1407    pub context: String,               // completion context
1408    pub exact: String,                 // exact match handling
1409    pub exact_string: String,          // the exact string if matched
1410    pub ignored: i32,                  // number of ignored matches
1411    pub insert: String,                // what to insert
1412    pub insert_positions: String,      // cursor positions after insert
1413    pub last_prompt: String,           // whether to return to last prompt
1414    pub list: String,                  // listing style
1415    pub list_lines: i32,               // number of lines for listing
1416    pub list_max: i32,                 // max matches to list
1417    pub nmatches: i32,                 // number of matches
1418    pub old_insert: String,            // previous insert value
1419    pub old_list: String,              // previous list value
1420    pub parameter: String,             // parameter being completed
1421    pub pattern_insert: String,        // pattern insert mode
1422    pub pattern_match: String,         // pattern matching mode
1423    pub quote: String,                 // quoting type
1424    pub quoting: String,               // current quoting
1425    pub redirect: String,              // redirection type
1426    pub restore: String,               // restore mode
1427    pub to_end: String,                // move to end mode
1428    pub unambiguous: String,           // unambiguous prefix
1429    pub unambiguous_cursor: i32,       // cursor pos in unambiguous
1430    pub unambiguous_positions: String, // positions in unambiguous
1431    pub vared: String,                 // vared context
1432}
1433
1434/// zstyle entry for completion configuration
1435#[derive(Debug, Clone)]
1436pub struct ZStyle {
1437    pub pattern: String,
1438    pub style: String,
1439    pub values: Vec<String>,
1440}
1441
1442bitflags::bitflags! {
1443    /// Flags for autoloaded functions
1444    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1445    pub struct AutoloadFlags: u32 {
1446        const NO_ALIAS = 0b00000001;      // -U: don't expand aliases
1447        const ZSH_STYLE = 0b00000010;     // -z: zsh-style autoload
1448        const KSH_STYLE = 0b00000100;     // -k: ksh-style autoload
1449        const TRACE = 0b00001000;         // -t: trace execution
1450        const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
1451        const LOADED = 0b00100000;        // function has been loaded
1452    }
1453}
1454
1455/// State for a zpty pseudo-terminal
1456pub struct ZptyState {
1457    pub pid: u32,
1458    pub cmd: String,
1459    pub stdin: Option<std::process::ChildStdin>,
1460    pub stdout: Option<std::process::ChildStdout>,
1461    pub child: Option<std::process::Child>,
1462}
1463
1464/// Scheduled command for sched builtin
1465pub struct ScheduledCommand {
1466    pub id: u32,
1467    pub run_at: std::time::SystemTime,
1468    pub command: String,
1469}
1470
1471/// Profiling entry for zprof
1472#[derive(Clone, Default)]
1473pub struct ProfileEntry {
1474    pub calls: u64,
1475    pub total_time_us: u64,
1476    pub self_time_us: u64,
1477}
1478
1479/// Unix domain socket state
1480pub struct UnixSocketState {
1481    pub path: Option<PathBuf>,
1482    pub listening: bool,
1483    pub stream: Option<std::os::unix::net::UnixStream>,
1484    pub listener: Option<std::os::unix::net::UnixListener>,
1485}
1486
1487pub struct ShellExecutor {
1488    pub functions: HashMap<String, ShellCommand>,
1489    pub aliases: HashMap<String, String>,
1490    pub global_aliases: HashMap<String, String>, // alias -g: expand anywhere
1491    pub suffix_aliases: HashMap<String, String>, // alias -s: expand by file extension
1492    pub last_status: i32,
1493    pub variables: HashMap<String, String>,
1494    pub arrays: HashMap<String, Vec<String>>,
1495    pub assoc_arrays: HashMap<String, HashMap<String, String>>, // zsh associative arrays
1496    pub jobs: JobTable,
1497    pub fpath: Vec<PathBuf>,
1498    pub zwc_cache: HashMap<PathBuf, ZwcFile>,
1499    pub positional_params: Vec<String>,
1500    pub history: Option<HistoryEngine>,
1501    process_sub_counter: u32,
1502    pub traps: HashMap<String, String>,
1503    pub options: HashMap<String, bool>,
1504    pub completions: HashMap<String, CompSpec>, // command -> completion spec
1505    pub dir_stack: Vec<PathBuf>,
1506    // zsh completion system state
1507    pub comp_matches: Vec<CompMatch>, // Current completion matches
1508    pub comp_groups: Vec<CompGroup>,  // Completion groups
1509    pub comp_state: CompState,        // compstate associative array
1510    pub zstyles: Vec<ZStyle>,         // zstyle configurations
1511    pub comp_words: Vec<String>,      // words on command line
1512    pub comp_current: i32,            // current word index (1-based)
1513    pub comp_prefix: String,          // PREFIX parameter
1514    pub comp_suffix: String,          // SUFFIX parameter
1515    pub comp_iprefix: String,         // IPREFIX parameter
1516    pub comp_isuffix: String,         // ISUFFIX parameter
1517    pub readonly_vars: std::collections::HashSet<String>, // Read-only variables
1518    /// Stack for `local` variable save/restore (name, old_value).
1519    pub local_save_stack: Vec<(String, Option<String>)>,
1520    /// Current function scope depth for `local` tracking.
1521    pub local_scope_depth: usize,
1522    pub autoload_pending: HashMap<String, AutoloadFlags>, // Functions marked for autoload
1523    // zsh hooks (precmd, preexec, chpwd, etc.)
1524    pub hook_functions: HashMap<String, Vec<String>>, // hook_name -> [function_names]
1525    // Named directories (hash -d)
1526    pub named_dirs: HashMap<String, PathBuf>, // name -> path
1527    // zpty - pseudo-terminal management
1528    pub zptys: HashMap<String, ZptyState>,
1529    // sysopen - file descriptor management
1530    pub open_fds: HashMap<i32, std::fs::File>,
1531    pub next_fd: i32,
1532    // sched - scheduled commands
1533    pub scheduled_commands: Vec<ScheduledCommand>,
1534    // zprof - profiling data
1535    pub profile_data: HashMap<String, ProfileEntry>,
1536    pub profiling_enabled: bool,
1537    // zsocket - Unix domain sockets
1538    pub unix_sockets: HashMap<i32, UnixSocketState>,
1539    // compsys - completion system cache
1540    pub compsys_cache: Option<CompsysCache>,
1541    // Background compinit — receiver for async fpath scan result
1542    pub compinit_pending: Option<(
1543        std::sync::mpsc::Receiver<CompInitBgResult>,
1544        std::time::Instant,
1545    )>,
1546    // Plugin source cache — stores side effects of source/. in SQLite
1547    pub plugin_cache: Option<crate::plugin_cache::PluginCache>,
1548    // cdreplay - deferred compdef calls for zinit turbo mode
1549    pub deferred_compdefs: Vec<Vec<String>>,
1550    // command hash table (hash builtin)
1551    pub command_hash: HashMap<String, String>,
1552    // Control flow signals
1553    returning: Option<i32>, // Set by return builtin, cleared after function returns
1554    breaking: i32,          // break level (0 = not breaking, N = break N levels)
1555    continuing: i32,        // continue level
1556    // New module state
1557    pub pcre_state: PcreState,
1558    pub tcp_sessions: TcpSessions,
1559    pub zftp: Zftp,
1560    pub profiler: Profiler,
1561    pub style_table: StyleTable,
1562    /// zsh compatibility mode - use .zcompdump, fpath scanning, etc.
1563    pub zsh_compat: bool,
1564    /// POSIX sh strict mode — no SQLite, no worker pool, no zsh extensions
1565    pub posix_mode: bool,
1566    /// Worker thread pool for background tasks (compinit, process subs, etc.)
1567    pub worker_pool: std::sync::Arc<crate::worker::WorkerPool>,
1568    /// AOP intercept table: command/function name → advice chain.
1569    /// Glob patterns supported (e.g. "git *", "*").
1570    pub intercepts: Vec<Intercept>,
1571    /// Async job handles: id → receiver for (status, stdout)
1572    pub async_jobs: HashMap<u32, crossbeam_channel::Receiver<(i32, String)>>,
1573    /// Next async job ID
1574    pub next_async_id: u32,
1575    /// Defer stack: commands to run on scope exit (LIFO).
1576    pub defer_stack: Vec<Vec<String>>,
1577}
1578
1579impl ShellExecutor {
1580    pub fn new() -> Self {
1581        tracing::debug!("ShellExecutor::new() initializing");
1582        // Initialize fpath from FPATH env var or use defaults
1583        let fpath = env::var("FPATH")
1584            .unwrap_or_default()
1585            .split(':')
1586            .filter(|s| !s.is_empty())
1587            .map(PathBuf::from)
1588            .collect();
1589
1590        let history = HistoryEngine::new().ok();
1591
1592        // Initialize standard zsh variables
1593        let mut variables = HashMap::new();
1594        variables.insert("ZSH_VERSION".to_string(), "5.9".to_string());
1595        variables.insert(
1596            "ZSH_PATCHLEVEL".to_string(),
1597            "zsh-5.9-0-g73d3173".to_string(),
1598        );
1599        variables.insert("ZSH_NAME".to_string(), "zsh".to_string());
1600        variables.insert(
1601            "SHLVL".to_string(),
1602            env::var("SHLVL")
1603                .map(|v| {
1604                    v.parse::<i32>()
1605                        .map(|n| (n + 1).to_string())
1606                        .unwrap_or_else(|_| "1".to_string())
1607                })
1608                .unwrap_or_else(|_| "1".to_string()),
1609        );
1610
1611        Self {
1612            functions: HashMap::new(),
1613            aliases: HashMap::new(),
1614            global_aliases: HashMap::new(),
1615            suffix_aliases: HashMap::new(),
1616            last_status: 0,
1617            variables,
1618            arrays: {
1619                let mut a = HashMap::new();
1620                // $path mirrors $PATH (tied array)
1621                let path_dirs: Vec<String> = env::var("PATH")
1622                    .unwrap_or_default()
1623                    .split(':')
1624                    .map(|s| s.to_string())
1625                    .collect();
1626                a.insert("path".to_string(), path_dirs);
1627                a
1628            },
1629            assoc_arrays: HashMap::new(),
1630            jobs: JobTable::new(),
1631            fpath,
1632            zwc_cache: HashMap::new(),
1633            positional_params: Vec::new(),
1634            history,
1635            completions: HashMap::new(),
1636            dir_stack: Vec::new(),
1637            process_sub_counter: 0,
1638            traps: HashMap::new(),
1639            options: Self::default_options(),
1640            // zsh completion system
1641            comp_matches: Vec::new(),
1642            comp_groups: Vec::new(),
1643            comp_state: CompState::default(),
1644            zstyles: Vec::new(),
1645            comp_words: Vec::new(),
1646            comp_current: 0,
1647            comp_prefix: String::new(),
1648            comp_suffix: String::new(),
1649            comp_iprefix: String::new(),
1650            comp_isuffix: String::new(),
1651            readonly_vars: std::collections::HashSet::new(),
1652            local_save_stack: Vec::new(),
1653            local_scope_depth: 0,
1654            autoload_pending: HashMap::new(),
1655            hook_functions: HashMap::new(),
1656            named_dirs: HashMap::new(),
1657            zptys: HashMap::new(),
1658            open_fds: HashMap::new(),
1659            next_fd: 10,
1660            scheduled_commands: Vec::new(),
1661            profile_data: HashMap::new(),
1662            profiling_enabled: false,
1663            unix_sockets: HashMap::new(),
1664            compsys_cache: {
1665                let cache_path = compsys::cache::default_cache_path();
1666                if cache_path.exists() {
1667                    let db_size = std::fs::metadata(&cache_path).map(|m| m.len()).unwrap_or(0);
1668                    match CompsysCache::open(&cache_path) {
1669                        Ok(c) => {
1670                            tracing::info!(
1671                                db_bytes = db_size,
1672                                path = %cache_path.display(),
1673                                "compsys: sqlite cache opened"
1674                            );
1675                            Some(c)
1676                        }
1677                        Err(e) => {
1678                            tracing::warn!(error = %e, "compsys: failed to open cache");
1679                            None
1680                        }
1681                    }
1682                } else {
1683                    tracing::debug!("compsys: no cache at {}", cache_path.display());
1684                    None
1685                }
1686            },
1687            compinit_pending: None, // (receiver, start_time)
1688            plugin_cache: {
1689                let pc_path = crate::plugin_cache::default_cache_path();
1690                if let Some(parent) = pc_path.parent() {
1691                    let _ = std::fs::create_dir_all(parent);
1692                }
1693                match crate::plugin_cache::PluginCache::open(&pc_path) {
1694                    Ok(pc) => {
1695                        let (plugins, functions) = pc.stats();
1696                        tracing::info!(
1697                            plugins,
1698                            cached_functions = functions,
1699                            path = %pc_path.display(),
1700                            "plugin_cache: sqlite opened"
1701                        );
1702                        Some(pc)
1703                    }
1704                    Err(e) => {
1705                        tracing::warn!(error = %e, "plugin_cache: failed to open");
1706                        None
1707                    }
1708                }
1709            },
1710            deferred_compdefs: Vec::new(),
1711            command_hash: HashMap::new(),
1712            returning: None,
1713            breaking: 0,
1714            continuing: 0,
1715            pcre_state: PcreState::new(),
1716            tcp_sessions: TcpSessions::new(),
1717            zftp: Zftp::new(),
1718            profiler: Profiler::new(),
1719            style_table: StyleTable::new(),
1720            zsh_compat: false,
1721            posix_mode: false,
1722            worker_pool: {
1723                let config = crate::config::load();
1724                let pool_size = crate::config::resolve_pool_size(&config.worker_pool);
1725                std::sync::Arc::new(crate::worker::WorkerPool::new(pool_size))
1726            },
1727            intercepts: Vec::new(),
1728            async_jobs: HashMap::new(),
1729            next_async_id: 1,
1730            defer_stack: Vec::new(),
1731        }
1732    }
1733
1734    /// Enter POSIX strict mode — drop all SQLite caches, shrink worker pool to minimum.
1735    /// No zsh extensions, no caching, no threads beyond the bare minimum. Dinosaur mode.
1736    pub fn enter_posix_mode(&mut self) {
1737        self.posix_mode = true;
1738        self.plugin_cache = None;
1739        self.compsys_cache = None;
1740        self.compinit_pending = None;
1741        // Worker pool stays at size 1 — we can't drop it entirely because
1742        // some code paths use it unconditionally, but with 1 thread it's
1743        // effectively serial.
1744        self.worker_pool = std::sync::Arc::new(crate::worker::WorkerPool::new(1));
1745        tracing::info!("POSIX strict mode: SQLite caches dropped, worker pool shrunk to 1");
1746    }
1747
1748    /// Run hook functions (precmd, preexec, chpwd, etc.)
1749    pub fn run_hooks(&mut self, hook_name: &str) {
1750        if let Some(funcs) = self.hook_functions.get(hook_name).cloned() {
1751            for func_name in funcs {
1752                if self.functions.contains_key(&func_name) {
1753                    let _ = self.execute_script(&format!("{}", func_name));
1754                }
1755            }
1756        }
1757        // Also check for hook arrays (e.g., precmd_functions)
1758        let array_name = format!("{}_functions", hook_name);
1759        if let Some(funcs) = self.arrays.get(&array_name).cloned() {
1760            for func_name in funcs {
1761                if self.functions.contains_key(&func_name) {
1762                    let _ = self.execute_script(&format!("{}", func_name));
1763                }
1764            }
1765        }
1766    }
1767
1768    /// Add a function to a hook
1769    pub fn add_hook(&mut self, hook_name: &str, func_name: &str) {
1770        self.hook_functions
1771            .entry(hook_name.to_string())
1772            .or_default()
1773            .push(func_name.to_string());
1774    }
1775
1776    /// Add a named directory (hash -d name=path)
1777    pub fn add_named_dir(&mut self, name: &str, path: &str) {
1778        self.named_dirs
1779            .insert(name.to_string(), PathBuf::from(path));
1780    }
1781
1782    /// Expand ~ with named directories
1783    pub fn expand_tilde_named(&self, path: &str) -> String {
1784        if path.starts_with('~') {
1785            let rest = &path[1..];
1786            // Check for ~name or ~name/...
1787            let (name, suffix) = if let Some(slash_pos) = rest.find('/') {
1788                (&rest[..slash_pos], &rest[slash_pos..])
1789            } else {
1790                (rest, "")
1791            };
1792
1793            if name.is_empty() {
1794                // Regular ~ expansion
1795                if let Ok(home) = std::env::var("HOME") {
1796                    return format!("{}{}", home, suffix);
1797                }
1798            } else if let Some(dir) = self.named_dirs.get(name) {
1799                return format!("{}{}", dir.display(), suffix);
1800            }
1801        }
1802        path.to_string()
1803    }
1804
1805    fn all_zsh_options() -> &'static [&'static str] {
1806        &[
1807            "aliases",
1808            "aliasfuncdef",
1809            "allexport",
1810            "alwayslastprompt",
1811            "alwaystoend",
1812            "appendcreate",
1813            "appendhistory",
1814            "autocd",
1815            "autocontinue",
1816            "autolist",
1817            "automenu",
1818            "autonamedirs",
1819            "autoparamkeys",
1820            "autoparamslash",
1821            "autopushd",
1822            "autoremoveslash",
1823            "autoresume",
1824            "badpattern",
1825            "banghist",
1826            "bareglobqual",
1827            "bashautolist",
1828            "bashrematch",
1829            "beep",
1830            "bgnice",
1831            "braceccl",
1832            "braceexpand",
1833            "bsdecho",
1834            "caseglob",
1835            "casematch",
1836            "casepaths",
1837            "cbases",
1838            "cdablevars",
1839            "cdsilent",
1840            "chasedots",
1841            "chaselinks",
1842            "checkjobs",
1843            "checkrunningjobs",
1844            "clobber",
1845            "clobberempty",
1846            "combiningchars",
1847            "completealiases",
1848            "completeinword",
1849            "continueonerror",
1850            "correct",
1851            "correctall",
1852            "cprecedences",
1853            "cshjunkiehistory",
1854            "cshjunkieloops",
1855            "cshjunkiequotes",
1856            "cshnullcmd",
1857            "cshnullglob",
1858            "debugbeforecmd",
1859            "dotglob",
1860            "dvorak",
1861            "emacs",
1862            "equals",
1863            "errexit",
1864            "errreturn",
1865            "evallineno",
1866            "exec",
1867            "extendedglob",
1868            "extendedhistory",
1869            "flowcontrol",
1870            "forcefloat",
1871            "functionargzero",
1872            "glob",
1873            "globassign",
1874            "globcomplete",
1875            "globdots",
1876            "globstarshort",
1877            "globsubst",
1878            "globalexport",
1879            "globalrcs",
1880            "hashall",
1881            "hashcmds",
1882            "hashdirs",
1883            "hashexecutablesonly",
1884            "hashlistall",
1885            "histallowclobber",
1886            "histappend",
1887            "histbeep",
1888            "histexpand",
1889            "histexpiredupsfirst",
1890            "histfcntllock",
1891            "histfindnodups",
1892            "histignorealldups",
1893            "histignoredups",
1894            "histignorespace",
1895            "histlexwords",
1896            "histnofunctions",
1897            "histnostore",
1898            "histreduceblanks",
1899            "histsavebycopy",
1900            "histsavenodups",
1901            "histsubstpattern",
1902            "histverify",
1903            "hup",
1904            "ignorebraces",
1905            "ignoreclosebraces",
1906            "ignoreeof",
1907            "incappendhistory",
1908            "incappendhistorytime",
1909            "interactive",
1910            "interactivecomments",
1911            "ksharrays",
1912            "kshautoload",
1913            "kshglob",
1914            "kshoptionprint",
1915            "kshtypeset",
1916            "kshzerosubscript",
1917            "listambiguous",
1918            "listbeep",
1919            "listpacked",
1920            "listrowsfirst",
1921            "listtypes",
1922            "localloops",
1923            "localoptions",
1924            "localpatterns",
1925            "localtraps",
1926            "log",
1927            "login",
1928            "longlistjobs",
1929            "magicequalsubst",
1930            "mailwarn",
1931            "mailwarning",
1932            "markdirs",
1933            "menucomplete",
1934            "monitor",
1935            "multibyte",
1936            "multifuncdef",
1937            "multios",
1938            "nomatch",
1939            "notify",
1940            "nullglob",
1941            "numericglobsort",
1942            "octalzeroes",
1943            "onecmd",
1944            "overstrike",
1945            "pathdirs",
1946            "pathscript",
1947            "physical",
1948            "pipefail",
1949            "posixaliases",
1950            "posixargzero",
1951            "posixbuiltins",
1952            "posixcd",
1953            "posixidentifiers",
1954            "posixjobs",
1955            "posixstrings",
1956            "posixtraps",
1957            "printeightbit",
1958            "printexitvalue",
1959            "privileged",
1960            "promptbang",
1961            "promptcr",
1962            "promptpercent",
1963            "promptsp",
1964            "promptsubst",
1965            "promptvars",
1966            "pushdignoredups",
1967            "pushdminus",
1968            "pushdsilent",
1969            "pushdtohome",
1970            "rcexpandparam",
1971            "rcquotes",
1972            "rcs",
1973            "recexact",
1974            "rematchpcre",
1975            "restricted",
1976            "rmstarsilent",
1977            "rmstarwait",
1978            "sharehistory",
1979            "shfileexpansion",
1980            "shglob",
1981            "shinstdin",
1982            "shnullcmd",
1983            "shoptionletters",
1984            "shortloops",
1985            "shortrepeat",
1986            "shwordsplit",
1987            "singlecommand",
1988            "singlelinezle",
1989            "sourcetrace",
1990            "stdin",
1991            "sunkeyboardhack",
1992            "trackall",
1993            "transientrprompt",
1994            "trapsasync",
1995            "typesetsilent",
1996            "typesettounset",
1997            "unset",
1998            "verbose",
1999            "vi",
2000            "warncreateglobal",
2001            "warnnestedvar",
2002            "xtrace",
2003            "zle",
2004        ]
2005    }
2006
2007    fn default_options() -> HashMap<String, bool> {
2008        let mut opts = HashMap::new();
2009        // Initialize all options to false first
2010        for opt in Self::all_zsh_options() {
2011            opts.insert(opt.to_string(), false);
2012        }
2013        // Set zsh defaults (options marked with <D> or <Z> in zshoptions man page)
2014        let defaults_on = [
2015            "aliases",
2016            "alwayslastprompt",
2017            "appendhistory",
2018            "autolist",
2019            "automenu",
2020            "autoparamkeys",
2021            "autoparamslash",
2022            "autoremoveslash",
2023            "badpattern",
2024            "banghist",
2025            "bareglobqual",
2026            "beep",
2027            "bgnice",
2028            "caseglob",
2029            "casematch",
2030            "checkjobs",
2031            "checkrunningjobs",
2032            "clobber",
2033            "debugbeforecmd",
2034            "equals",
2035            "evallineno",
2036            "exec",
2037            "flowcontrol",
2038            "functionargzero",
2039            "glob",
2040            "globalexport",
2041            "globalrcs",
2042            "hashcmds",
2043            "hashdirs",
2044            "hashlistall",
2045            "histbeep",
2046            "histsavebycopy",
2047            "hup",
2048            "interactive",
2049            "listambiguous",
2050            "listbeep",
2051            "listtypes",
2052            "monitor",
2053            "multibyte",
2054            "multifuncdef",
2055            "multios",
2056            "nomatch",
2057            "notify",
2058            "promptcr",
2059            "promptpercent",
2060            "promptsp",
2061            "rcs",
2062            "shinstdin",
2063            "shortloops",
2064            "unset",
2065            "zle",
2066        ];
2067        for opt in defaults_on {
2068            opts.insert(opt.to_string(), true);
2069        }
2070        opts
2071    }
2072
2073    /// Normalize option name: lowercase, remove underscores/hyphens, handle "no" prefix
2074    fn normalize_option_name(name: &str) -> (String, bool) {
2075        let normalized = name.to_lowercase().replace(['-', '_'], "");
2076        if let Some(stripped) = normalized.strip_prefix("no") {
2077            // O(1) lookup in HashSet instead of linear scan
2078            if ZSH_OPTIONS_SET.contains(stripped) {
2079                return (stripped.to_string(), false);
2080            }
2081        }
2082        (normalized, true)
2083    }
2084
2085    /// Check if option name matches a pattern (for -m flag)
2086    fn option_matches_pattern(opt: &str, pattern: &str) -> bool {
2087        let pat = pattern.to_lowercase().replace(['-', '_'], "");
2088        let opt_lower = opt.to_lowercase();
2089
2090        if pat.contains('*') || pat.contains('?') || pat.contains('[') {
2091            let regex_pat = pat.replace('.', "\\.").replace('*', ".*").replace('?', ".");
2092            let full_pattern = format!("^{}$", regex_pat);
2093            cached_regex(&full_pattern)
2094                .map(|re| re.is_match(&opt_lower))
2095                .unwrap_or(false)
2096        } else {
2097            opt_lower == pat
2098        }
2099    }
2100
2101    /// Try to load a function from ZWC files in fpath
2102    pub fn autoload_function(&mut self, name: &str) -> Option<ShellCommand> {
2103        // First check if already loaded
2104        if let Some(func) = self.functions.get(name) {
2105            return Some(func.clone());
2106        }
2107
2108        // Search fpath for the function - use index to avoid borrow issues
2109        for i in 0..self.fpath.len() {
2110            let dir = self.fpath[i].clone();
2111            // Try directory.zwc first
2112            let zwc_path = dir.with_extension("zwc");
2113            if zwc_path.exists() {
2114                if let Some(func) = self.load_function_from_zwc(&zwc_path, name) {
2115                    return Some(func);
2116                }
2117            }
2118
2119            // Try individual function.zwc
2120            let func_zwc = dir.join(format!("{}.zwc", name));
2121            if func_zwc.exists() {
2122                if let Some(func) = self.load_function_from_zwc(&func_zwc, name) {
2123                    return Some(func);
2124                }
2125            }
2126
2127            // Look for directory/*.zwc files containing this function
2128            if dir.is_dir() {
2129                if let Ok(entries) = fs::read_dir(&dir) {
2130                    for entry in entries.flatten() {
2131                        let path = entry.path();
2132                        if path.extension().map_or(false, |e| e == "zwc") {
2133                            if let Some(func) = self.load_function_from_zwc(&path, name) {
2134                                return Some(func);
2135                            }
2136                        }
2137                    }
2138                }
2139            }
2140        }
2141
2142        None
2143    }
2144
2145    /// Load a specific function from a ZWC file
2146    fn load_function_from_zwc(&mut self, path: &Path, name: &str) -> Option<ShellCommand> {
2147        // Check cache
2148        let zwc = if let Some(cached) = self.zwc_cache.get(path) {
2149            cached
2150        } else {
2151            // Load and cache the ZWC file
2152            let zwc = ZwcFile::load(path).ok()?;
2153            self.zwc_cache.insert(path.to_path_buf(), zwc);
2154            self.zwc_cache.get(path)?
2155        };
2156
2157        // Find the function
2158        let func = zwc.get_function(name)?;
2159        let decoded = zwc.decode_function(func)?;
2160
2161        // Convert to shell command and cache
2162        let shell_func = decoded.to_shell_function()?;
2163
2164        // Register the function
2165        if let ShellCommand::FunctionDef(fname, body) = &shell_func {
2166            self.functions.insert(fname.clone(), (**body).clone());
2167        }
2168
2169        Some(shell_func)
2170    }
2171
2172    /// Add a directory to fpath
2173    pub fn add_fpath(&mut self, path: PathBuf) {
2174        if !self.fpath.contains(&path) {
2175            self.fpath.insert(0, path);
2176        }
2177    }
2178
2179    /// Match a string against a shell glob pattern
2180    fn glob_match(&self, s: &str, pattern: &str) -> bool {
2181        // Convert shell glob to regex
2182        let mut regex_pattern = String::from("^");
2183        let mut chars = pattern.chars().peekable();
2184
2185        while let Some(c) = chars.next() {
2186            match c {
2187                '*' => regex_pattern.push_str(".*"),
2188                '?' => regex_pattern.push('.'),
2189                '[' => {
2190                    regex_pattern.push('[');
2191                    // Handle character class
2192                    while let Some(cc) = chars.next() {
2193                        if cc == ']' {
2194                            regex_pattern.push(']');
2195                            break;
2196                        }
2197                        regex_pattern.push(cc);
2198                    }
2199                }
2200                '(' => {
2201                    // Handle alternation (a|b|c) -> (a|b|c)
2202                    regex_pattern.push('(');
2203                }
2204                ')' => regex_pattern.push(')'),
2205                '|' => regex_pattern.push('|'),
2206                '.' | '+' | '^' | '$' | '\\' | '{' | '}' => {
2207                    regex_pattern.push('\\');
2208                    regex_pattern.push(c);
2209                }
2210                _ => regex_pattern.push(c),
2211            }
2212        }
2213        regex_pattern.push('$');
2214
2215        regex::Regex::new(&regex_pattern)
2216            .map(|re| re.is_match(s))
2217            .unwrap_or(false)
2218    }
2219
2220    /// Static glob match — same logic as glob_match but callable without &self,
2221    /// needed for Rayon parallel iterators that can't capture &self.
2222    pub fn glob_match_static(s: &str, pattern: &str) -> bool {
2223        let mut regex_pattern = String::from("^");
2224        let mut chars = pattern.chars().peekable();
2225        while let Some(c) = chars.next() {
2226            match c {
2227                '*' => regex_pattern.push_str(".*"),
2228                '?' => regex_pattern.push('.'),
2229                '[' => {
2230                    regex_pattern.push('[');
2231                    while let Some(cc) = chars.next() {
2232                        if cc == ']' {
2233                            regex_pattern.push(']');
2234                            break;
2235                        }
2236                        regex_pattern.push(cc);
2237                    }
2238                }
2239                '(' => regex_pattern.push('('),
2240                ')' => regex_pattern.push(')'),
2241                '|' => regex_pattern.push('|'),
2242                '.' | '+' | '^' | '$' | '\\' | '{' | '}' => {
2243                    regex_pattern.push('\\');
2244                    regex_pattern.push(c);
2245                }
2246                _ => regex_pattern.push(c),
2247            }
2248        }
2249        regex_pattern.push('$');
2250        regex::Regex::new(&regex_pattern)
2251            .map(|re| re.is_match(s))
2252            .unwrap_or(false)
2253    }
2254
2255    /// Execute a script file with bytecode caching — skips lex+parse+compile on cache hit.
2256    /// The AST is stored in SQLite keyed by (path, mtime).
2257    pub fn execute_script_file(&mut self, file_path: &str) -> Result<i32, String> {
2258        // Read file and delegate to execute_script (which uses VM)
2259        let content =
2260            std::fs::read_to_string(file_path).map_err(|e| format!("{}: {}", file_path, e))?;
2261        self.execute_script(&content)
2262    }
2263
2264    #[tracing::instrument(skip(self, script), fields(len = script.len()))]
2265    pub fn execute_script(&mut self, script: &str) -> Result<i32, String> {
2266        // Expand history references before parsing
2267        let expanded = self.expand_history(script);
2268
2269        let mut parser = ShellParser::new(&expanded);
2270        let commands = parser.parse_script()?;
2271        tracing::trace!(cmds = commands.len(), "execute_script: parsed");
2272
2273        // Compile to fusevm bytecodes and execute on the VM.
2274        // All execution goes through the VM — no tree-walker fallback.
2275        // Builtins dispatch through CallBuiltin, accessing executor state via thread-local.
2276        let compiler = crate::shell_compiler::ShellCompiler::new();
2277        let chunk = compiler.compile(&commands);
2278
2279        if !chunk.ops.is_empty() {
2280            if std::env::var("ZSHRS_DEBUG_OPS").is_ok() {
2281                eprintln!("[DEBUG] Compiled {} ops:", chunk.ops.len());
2282                for (i, op) in chunk.ops.iter().enumerate() {
2283                    eprintln!("  {:3}: {:?}", i, op);
2284                }
2285            }
2286            let mut vm = fusevm::VM::new(chunk);
2287            register_builtins(&mut vm);
2288
2289            // Set executor context for builtin handlers
2290            let _ctx = ExecutorContext::enter(self);
2291
2292            match vm.run() {
2293                fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
2294                    self.last_status = vm.last_status;
2295                }
2296                fusevm::VMResult::Error(e) => {
2297                    return Err(format!("VM error: {}", e));
2298                }
2299            }
2300        }
2301
2302        // Fire EXIT trap if set (matches zsh's zshexit behavior).
2303        // Remove it first to prevent infinite recursion.
2304        if let Some(action) = self.traps.remove("EXIT") {
2305            tracing::debug!("firing EXIT trap");
2306            let _ = self.execute_script(&action);
2307        }
2308
2309        Ok(self.last_status)
2310    }
2311
2312    /// Expand history references: !!, !n, !-n, !string, !?string?
2313    fn expand_history(&self, input: &str) -> String {
2314        let Some(ref engine) = self.history else {
2315            return input.to_string();
2316        };
2317
2318        // Quick check: nothing to expand
2319        if !input.contains('!') && !input.starts_with('^') {
2320            return input.to_string();
2321        }
2322
2323        let history_count = engine.count().unwrap_or(0) as usize;
2324        if history_count == 0 {
2325            return input.to_string();
2326        }
2327
2328        let chars: Vec<char> = input.chars().collect();
2329
2330        // ^foo^bar quick substitution (only at start of input)
2331        if chars.first() == Some(&'^') {
2332            if let Some(expanded) = self.history_quick_subst(&chars, engine) {
2333                return expanded;
2334            }
2335        }
2336
2337        let mut result = String::new();
2338        let mut i = 0;
2339        let mut in_single_quote = false;
2340        let mut in_brace = 0; // Track ${...} nesting
2341        let mut last_subst: Option<(String, String)> = None; // for :& modifier
2342
2343        while i < chars.len() {
2344            // Track single quotes — no history expansion inside them
2345            if chars[i] == '\'' && in_brace == 0 {
2346                in_single_quote = !in_single_quote;
2347                result.push(chars[i]);
2348                i += 1;
2349                continue;
2350            }
2351            if in_single_quote {
2352                result.push(chars[i]);
2353                i += 1;
2354                continue;
2355            }
2356
2357            // Track ${...} nesting
2358            if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' {
2359                in_brace += 1;
2360                result.push(chars[i]);
2361                i += 1;
2362                result.push(chars[i]);
2363                i += 1;
2364                continue;
2365            }
2366            if chars[i] == '}' && in_brace > 0 {
2367                in_brace -= 1;
2368                result.push(chars[i]);
2369                i += 1;
2370                continue;
2371            }
2372
2373            // Backslash-escaped ! is literal
2374            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '!' {
2375                result.push('!');
2376                i += 2;
2377                continue;
2378            }
2379
2380            if chars[i] == '!' && in_brace == 0 {
2381                if i + 1 >= chars.len() {
2382                    // Trailing ! — literal
2383                    result.push('!');
2384                    i += 1;
2385                    continue;
2386                }
2387
2388                let next = chars[i + 1];
2389                // ! followed by space, =, ( — literal (zsh rule)
2390                if next == ' ' || next == '\t' || next == '=' || next == '(' || next == '\n' {
2391                    result.push('!');
2392                    i += 1;
2393                    continue;
2394                }
2395
2396                // Resolve the event string
2397                let (event_str, new_i) = self.history_resolve_event(&chars, i, engine, &result);
2398                if let Some(ev) = event_str {
2399                    // Check for word designators and modifiers
2400                    let (final_str, final_i) = self.history_apply_designators_and_modifiers(
2401                        &chars,
2402                        new_i,
2403                        &ev,
2404                        &mut last_subst,
2405                    );
2406                    result.push_str(&final_str);
2407                    i = final_i;
2408                } else {
2409                    // Could not resolve — keep the ! literal
2410                    result.push('!');
2411                    i += 1;
2412                }
2413                continue;
2414            }
2415            result.push(chars[i]);
2416            i += 1;
2417        }
2418
2419        result
2420    }
2421
2422    /// ^foo^bar quick substitution — replace first occurrence of foo with bar
2423    /// in the previous command.
2424    fn history_quick_subst(
2425        &self,
2426        chars: &[char],
2427        engine: &crate::history::HistoryEngine,
2428    ) -> Option<String> {
2429        let mut i = 1; // skip leading ^
2430        let mut old = String::new();
2431        while i < chars.len() && chars[i] != '^' {
2432            old.push(chars[i]);
2433            i += 1;
2434        }
2435        if i >= chars.len() {
2436            return None;
2437        }
2438        i += 1; // skip middle ^
2439        let mut new = String::new();
2440        while i < chars.len() && chars[i] != '^' && chars[i] != '\n' {
2441            new.push(chars[i]);
2442            i += 1;
2443        }
2444        let prev = engine.get_by_offset(0).ok()??;
2445        Some(prev.command.replacen(&old, &new, 1))
2446    }
2447
2448    /// Resolve which history event ! refers to.  Returns (Some(full_command), index_after_event)
2449    /// or (None, original_index) if we can't resolve.
2450    fn history_resolve_event(
2451        &self,
2452        chars: &[char],
2453        bang_pos: usize,
2454        engine: &crate::history::HistoryEngine,
2455        current_line: &str,
2456    ) -> (Option<String>, usize) {
2457        let mut i = bang_pos + 1; // past the !
2458
2459        // !{...} brace-wrapped event
2460        let in_brace = i < chars.len() && chars[i] == '{';
2461        if in_brace {
2462            i += 1;
2463        }
2464
2465        let c = if i < chars.len() {
2466            chars[i]
2467        } else {
2468            return (None, bang_pos);
2469        };
2470
2471        let (event, new_i) = match c {
2472            '!' => {
2473                // !! — previous command
2474                let entry = engine.get_by_offset(0).ok().flatten();
2475                (entry.map(|e| e.command), i + 1)
2476            }
2477            '#' => {
2478                // !# — current command line so far
2479                (Some(current_line.to_string()), i + 1)
2480            }
2481            '-' => {
2482                // !-n — nth previous command
2483                i += 1;
2484                let start = i;
2485                while i < chars.len() && chars[i].is_ascii_digit() {
2486                    i += 1;
2487                }
2488                if i > start {
2489                    let n: usize = chars[start..i]
2490                        .iter()
2491                        .collect::<String>()
2492                        .parse()
2493                        .unwrap_or(0);
2494                    if n > 0 {
2495                        let entry = engine.get_by_offset(n - 1).ok().flatten();
2496                        (entry.map(|e| e.command), i)
2497                    } else {
2498                        (None, bang_pos)
2499                    }
2500                } else {
2501                    (None, bang_pos)
2502                }
2503            }
2504            '?' => {
2505                // !?string? — contains search
2506                i += 1;
2507                let start = i;
2508                while i < chars.len() && chars[i] != '?' && chars[i] != '\n' {
2509                    i += 1;
2510                }
2511                let search: String = chars[start..i].iter().collect();
2512                if i < chars.len() && chars[i] == '?' {
2513                    i += 1;
2514                }
2515                let entry = engine
2516                    .search(&search, 1)
2517                    .ok()
2518                    .and_then(|v| v.into_iter().next());
2519                (entry.map(|e| e.command), i)
2520            }
2521            c if c.is_ascii_digit() => {
2522                // !n — command by absolute number
2523                let start = i;
2524                while i < chars.len() && chars[i].is_ascii_digit() {
2525                    i += 1;
2526                }
2527                let n: i64 = chars[start..i]
2528                    .iter()
2529                    .collect::<String>()
2530                    .parse()
2531                    .unwrap_or(0);
2532                if n > 0 {
2533                    let entry = engine.get_by_number(n).ok().flatten();
2534                    (entry.map(|e| e.command), i)
2535                } else {
2536                    (None, bang_pos)
2537                }
2538            }
2539            '$' => {
2540                // !$ — last word of previous command (shorthand for !!:$)
2541                let entry = engine.get_by_offset(0).ok().flatten();
2542                let word =
2543                    entry.and_then(|e| Self::history_split_words(&e.command).last().cloned());
2544                // Return the word directly — skip designator parsing
2545                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
2546                    i + 2
2547                } else {
2548                    i + 1
2549                };
2550                return (word, final_i);
2551            }
2552            '^' => {
2553                // !^ — first arg of previous command (shorthand for !!:1)
2554                let entry = engine.get_by_offset(0).ok().flatten();
2555                let word = entry.and_then(|e| {
2556                    let words = Self::history_split_words(&e.command);
2557                    words.get(1).cloned()
2558                });
2559                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
2560                    i + 2
2561                } else {
2562                    i + 1
2563                };
2564                return (word, final_i);
2565            }
2566            '*' => {
2567                // !* — all args of previous command (shorthand for !!:*)
2568                let entry = engine.get_by_offset(0).ok().flatten();
2569                let word = entry.map(|e| {
2570                    let words = Self::history_split_words(&e.command);
2571                    if words.len() > 1 {
2572                        words[1..].join(" ")
2573                    } else {
2574                        String::new()
2575                    }
2576                });
2577                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
2578                    i + 2
2579                } else {
2580                    i + 1
2581                };
2582                return (word, final_i);
2583            }
2584            c if c.is_alphabetic() || c == '_' || c == '/' || c == '.' => {
2585                // !string — prefix search
2586                let start = i;
2587                while i < chars.len()
2588                    && !chars[i].is_whitespace()
2589                    && chars[i] != ':'
2590                    && chars[i] != '!'
2591                    && chars[i] != '}'
2592                {
2593                    i += 1;
2594                }
2595                let prefix: String = chars[start..i].iter().collect();
2596                let entry = engine
2597                    .search_prefix(&prefix, 1)
2598                    .ok()
2599                    .and_then(|v| v.into_iter().next());
2600                (entry.map(|e| e.command), i)
2601            }
2602            _ => (None, bang_pos),
2603        };
2604
2605        // Skip closing brace
2606        let final_i = if in_brace && new_i < chars.len() && chars[new_i] == '}' {
2607            new_i + 1
2608        } else {
2609            new_i
2610        };
2611
2612        (event, final_i)
2613    }
2614
2615    /// Split a command string into words for word designators, respecting quotes.
2616    fn history_split_words(cmd: &str) -> Vec<String> {
2617        let mut words = Vec::new();
2618        let mut current = String::new();
2619        let mut in_sq = false;
2620        let mut in_dq = false;
2621        let mut escaped = false;
2622
2623        for c in cmd.chars() {
2624            if escaped {
2625                current.push(c);
2626                escaped = false;
2627                continue;
2628            }
2629            if c == '\\' {
2630                current.push(c);
2631                escaped = true;
2632                continue;
2633            }
2634            if c == '\'' && !in_dq {
2635                in_sq = !in_sq;
2636                current.push(c);
2637                continue;
2638            }
2639            if c == '"' && !in_sq {
2640                in_dq = !in_dq;
2641                current.push(c);
2642                continue;
2643            }
2644            if c.is_whitespace() && !in_sq && !in_dq {
2645                if !current.is_empty() {
2646                    words.push(std::mem::take(&mut current));
2647                }
2648                continue;
2649            }
2650            current.push(c);
2651        }
2652        if !current.is_empty() {
2653            words.push(current);
2654        }
2655        words
2656    }
2657
2658    /// Apply word designators (:0, :n, :^, :$, :*, :n-m) and modifiers
2659    /// (:h, :t, :r, :e, :s/old/new/, :gs/old/new/, :p, :l, :u, :q, :Q, :a, :A)
2660    /// to an already-resolved event string.
2661    fn history_apply_designators_and_modifiers(
2662        &self,
2663        chars: &[char],
2664        mut i: usize,
2665        event: &str,
2666        last_subst: &mut Option<(String, String)>,
2667    ) -> (String, usize) {
2668        let words = Self::history_split_words(event);
2669        let argc = words.len().saturating_sub(1); // last word index
2670
2671        // Check for word designator — either :N or bare :^ :$ :*
2672        let mut sline = event.to_string();
2673
2674        if i < chars.len() && chars[i] == ':' {
2675            i += 1;
2676            if i < chars.len() {
2677                // Parse word designator
2678                let (farg, larg, new_i) = self.history_parse_word_range(chars, i, argc);
2679                i = new_i;
2680                if farg.is_some() || larg.is_some() {
2681                    let f = farg.unwrap_or(0);
2682                    let l = larg.unwrap_or(argc);
2683                    let selected: Vec<&String> = words
2684                        .iter()
2685                        .enumerate()
2686                        .filter(|(idx, _)| *idx >= f && *idx <= l)
2687                        .map(|(_, w)| w)
2688                        .collect();
2689                    sline = selected
2690                        .iter()
2691                        .map(|s| s.as_str())
2692                        .collect::<Vec<_>>()
2693                        .join(" ");
2694                }
2695            }
2696        } else if i < chars.len() && chars[i] == '*' {
2697            // !!* shorthand for !!:1-$
2698            i += 1;
2699            if words.len() > 1 {
2700                sline = words[1..].join(" ");
2701            } else {
2702                sline = String::new();
2703            }
2704        }
2705
2706        // Apply modifiers (:h :t :r :e :s :gs :p :l :u :q :Q :a :A)
2707        while i < chars.len() && chars[i] == ':' {
2708            i += 1;
2709            if i >= chars.len() {
2710                break;
2711            }
2712            let mut global = false;
2713            if chars[i] == 'g' && i + 1 < chars.len() {
2714                global = true;
2715                i += 1;
2716            }
2717            match chars[i] {
2718                'h' => {
2719                    // Head — remove trailing path component
2720                    i += 1;
2721                    if let Some(pos) = sline.rfind('/') {
2722                        if pos > 0 {
2723                            sline = sline[..pos].to_string();
2724                        } else {
2725                            sline = "/".to_string();
2726                        }
2727                    }
2728                }
2729                't' => {
2730                    // Tail — remove leading path components
2731                    i += 1;
2732                    if let Some(pos) = sline.rfind('/') {
2733                        sline = sline[pos + 1..].to_string();
2734                    }
2735                }
2736                'r' => {
2737                    // Remove extension
2738                    i += 1;
2739                    if let Some(pos) = sline.rfind('.') {
2740                        if pos > 0 && sline[..pos].rfind('/').map_or(true, |sp| sp < pos) {
2741                            sline = sline[..pos].to_string();
2742                        }
2743                    }
2744                }
2745                'e' => {
2746                    // Extension only
2747                    i += 1;
2748                    if let Some(pos) = sline.rfind('.') {
2749                        sline = sline[pos + 1..].to_string();
2750                    } else {
2751                        sline = String::new();
2752                    }
2753                }
2754                'l' => {
2755                    // Lowercase
2756                    i += 1;
2757                    sline = sline.to_lowercase();
2758                }
2759                'u' => {
2760                    // Uppercase
2761                    i += 1;
2762                    sline = sline.to_uppercase();
2763                }
2764                'p' => {
2765                    // Print only, don't execute (we just expand — caller handles this)
2766                    i += 1;
2767                    // For now, just expand — :p suppression would need upstream support
2768                }
2769                'q' => {
2770                    // Quote — single-quote the result
2771                    i += 1;
2772                    sline = format!("'{}'", sline.replace('\'', "'\\''"));
2773                }
2774                'Q' => {
2775                    // Unquote — strip one level of quotes
2776                    i += 1;
2777                    sline = sline.replace('\'', "").replace('"', "");
2778                }
2779                'a' => {
2780                    // Absolute path
2781                    i += 1;
2782                    if !sline.starts_with('/') {
2783                        if let Ok(cwd) = std::env::current_dir() {
2784                            sline = format!("{}/{}", cwd.display(), sline);
2785                        }
2786                    }
2787                }
2788                'A' => {
2789                    // Realpath
2790                    i += 1;
2791                    if let Ok(real) = std::fs::canonicalize(&sline) {
2792                        sline = real.to_string_lossy().to_string();
2793                    }
2794                }
2795                's' | 'S' => {
2796                    // :s/old/new/ or :gs/old/new/
2797                    i += 1;
2798                    if i < chars.len() {
2799                        let delim = chars[i];
2800                        i += 1;
2801                        let mut old_s = String::new();
2802                        while i < chars.len() && chars[i] != delim {
2803                            old_s.push(chars[i]);
2804                            i += 1;
2805                        }
2806                        if i < chars.len() {
2807                            i += 1;
2808                        } // skip delimiter
2809                        let mut new_s = String::new();
2810                        while i < chars.len()
2811                            && chars[i] != delim
2812                            && chars[i] != ':'
2813                            && chars[i] != ' '
2814                        {
2815                            new_s.push(chars[i]);
2816                            i += 1;
2817                        }
2818                        if i < chars.len() && chars[i] == delim {
2819                            i += 1;
2820                        } // skip trailing delimiter
2821                        *last_subst = Some((old_s.clone(), new_s.clone()));
2822                        if global {
2823                            sline = sline.replace(&old_s, &new_s);
2824                        } else {
2825                            sline = sline.replacen(&old_s, &new_s, 1);
2826                        }
2827                    }
2828                }
2829                '&' => {
2830                    // Repeat last substitution
2831                    i += 1;
2832                    if let Some((ref old_s, ref new_s)) = last_subst {
2833                        if global {
2834                            sline = sline.replace(old_s.as_str(), new_s.as_str());
2835                        } else {
2836                            sline = sline.replacen(old_s.as_str(), new_s.as_str(), 1);
2837                        }
2838                    }
2839                }
2840                _ => {
2841                    if global {
2842                        // 'g' was consumed but next char isn't s/S/& — put back
2843                        // by not advancing i further
2844                    }
2845                    break;
2846                }
2847            }
2848        }
2849
2850        (sline, i)
2851    }
2852
2853    /// Parse a word range like 0, 1, ^, $, *, n-m, n-
2854    fn history_parse_word_range(
2855        &self,
2856        chars: &[char],
2857        mut i: usize,
2858        argc: usize,
2859    ) -> (Option<usize>, Option<usize>, usize) {
2860        if i >= chars.len() {
2861            return (None, None, i);
2862        }
2863
2864        // Check for modifiers that aren't word designators
2865        match chars[i] {
2866            'h' | 't' | 'r' | 'e' | 's' | 'S' | 'g' | 'p' | 'q' | 'Q' | 'l' | 'u' | 'a' | 'A'
2867            | '&' => {
2868                // This is a modifier, not a word designator — back up
2869                return (None, None, i - 1); // -1 to re-read the ':'
2870            }
2871            _ => {}
2872        }
2873
2874        let farg = if chars[i] == '^' {
2875            i += 1;
2876            Some(1usize)
2877        } else if chars[i] == '$' {
2878            i += 1;
2879            return (Some(argc), Some(argc), i);
2880        } else if chars[i] == '*' {
2881            i += 1;
2882            return (Some(1), Some(argc), i);
2883        } else if chars[i].is_ascii_digit() {
2884            let start = i;
2885            while i < chars.len() && chars[i].is_ascii_digit() {
2886                i += 1;
2887            }
2888            let n: usize = chars[start..i]
2889                .iter()
2890                .collect::<String>()
2891                .parse()
2892                .unwrap_or(0);
2893            Some(n)
2894        } else {
2895            None
2896        };
2897
2898        // Check for range: n-m or n-
2899        if i < chars.len() && chars[i] == '-' {
2900            i += 1;
2901            if i < chars.len() && chars[i] == '$' {
2902                i += 1;
2903                return (farg, Some(argc), i);
2904            } else if i < chars.len() && chars[i].is_ascii_digit() {
2905                let start = i;
2906                while i < chars.len() && chars[i].is_ascii_digit() {
2907                    i += 1;
2908                }
2909                let m: usize = chars[start..i]
2910                    .iter()
2911                    .collect::<String>()
2912                    .parse()
2913                    .unwrap_or(0);
2914                return (farg, Some(m), i);
2915            } else {
2916                // n- means n to argc-1
2917                return (farg, Some(argc.saturating_sub(1)), i);
2918            }
2919        }
2920
2921        if farg.is_some() {
2922            (farg, farg, i)
2923        } else {
2924            (None, None, i)
2925        }
2926    }
2927
2928    #[tracing::instrument(level = "trace", skip_all)]
2929    pub fn execute_command(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
2930        match cmd {
2931            ShellCommand::Simple(simple) => self.execute_simple(simple),
2932            ShellCommand::Pipeline(cmds, negated) => {
2933                let status = self.execute_pipeline(cmds)?;
2934                if *negated {
2935                    self.last_status = if status == 0 { 1 } else { 0 };
2936                } else {
2937                    self.last_status = status;
2938                }
2939                Ok(self.last_status)
2940            }
2941            ShellCommand::List(items) => self.execute_list(items),
2942            ShellCommand::Compound(compound) => self.execute_compound(compound),
2943            ShellCommand::FunctionDef(name, body) => {
2944                if name.is_empty() {
2945                    // Anonymous function - execute immediately
2946                    let result = self.execute_command(body);
2947                    // Clear returning flag since the anonymous function has completed
2948                    if let Some(ret) = self.returning.take() {
2949                        self.last_status = ret;
2950                        return Ok(ret);
2951                    }
2952                    result
2953                } else {
2954                    // Named function - just define it
2955                    self.functions.insert(name.clone(), (**body).clone());
2956                    self.last_status = 0;
2957                    Ok(0)
2958                }
2959            }
2960        }
2961    }
2962
2963    #[tracing::instrument(level = "trace", skip_all)]
2964    fn execute_simple(&mut self, cmd: &SimpleCommand) -> Result<i32, String> {
2965        // Handle assignments
2966        for (var, val, is_append) in &cmd.assignments {
2967            match val {
2968                ShellWord::ArrayLiteral(elements) => {
2969                    // Array assignment: arr=(a b c) or arr+=(a b c)
2970                    // For associative arrays: assoc=(k1 v1 k2 v2)
2971                    // Use expand_word_split so $(cmd) and $var undergo
2972                    // word splitting into separate array elements (C zsh behavior).
2973                    let new_elements: Vec<String> = elements
2974                        .iter()
2975                        .flat_map(|e| self.expand_word_split(e))
2976                        .collect();
2977
2978                    // Check if this is an associative array
2979                    if self.assoc_arrays.contains_key(var) {
2980                        // Associative array: treat pairs as key-value
2981                        if *is_append {
2982                            let assoc = self.assoc_arrays.get_mut(var).unwrap();
2983                            let mut iter = new_elements.iter();
2984                            while let Some(key) = iter.next() {
2985                                if let Some(val) = iter.next() {
2986                                    assoc.insert(key.clone(), val.clone());
2987                                }
2988                            }
2989                        } else {
2990                            let mut assoc = HashMap::new();
2991                            let mut iter = new_elements.iter();
2992                            while let Some(key) = iter.next() {
2993                                if let Some(val) = iter.next() {
2994                                    assoc.insert(key.clone(), val.clone());
2995                                }
2996                            }
2997                            self.assoc_arrays.insert(var.clone(), assoc);
2998                        }
2999                    } else if *is_append {
3000                        // Append to existing indexed array
3001                        let arr = self.arrays.entry(var.clone()).or_insert_with(Vec::new);
3002                        arr.extend(new_elements);
3003                    } else {
3004                        self.arrays.insert(var.clone(), new_elements);
3005                    }
3006                }
3007                _ => {
3008                    let expanded = self.expand_word(val);
3009
3010                    // Check for array element assignment: arr[idx]=value or assoc[key]=value
3011                    if let Some(bracket_pos) = var.find('[') {
3012                        if var.ends_with(']') {
3013                            let array_name = &var[..bracket_pos];
3014                            let key = &var[bracket_pos + 1..var.len() - 1];
3015                            let key = self.expand_string(key); // Expand the key/index
3016
3017                            // Check if it's an associative array
3018                            if self.assoc_arrays.contains_key(array_name) {
3019                                let assoc = self.assoc_arrays.get_mut(array_name).unwrap();
3020                                if *is_append {
3021                                    let existing = assoc.get(&key).cloned().unwrap_or_default();
3022                                    assoc.insert(key, existing + &expanded);
3023                                } else {
3024                                    assoc.insert(key, expanded);
3025                                }
3026                            } else if let Ok(idx) = key.parse::<i64>() {
3027                                // Regular indexed array
3028                                let idx = if idx < 0 { 0 } else { (idx - 1) as usize }; // zsh is 1-indexed
3029                                let arr = self
3030                                    .arrays
3031                                    .entry(array_name.to_string())
3032                                    .or_insert_with(Vec::new);
3033                                while arr.len() <= idx {
3034                                    arr.push(String::new());
3035                                }
3036                                if *is_append {
3037                                    arr[idx].push_str(&expanded);
3038                                } else {
3039                                    arr[idx] = expanded;
3040                                }
3041                            } else {
3042                                // Non-numeric key on non-assoc array - treat as assoc
3043                                let assoc = self
3044                                    .assoc_arrays
3045                                    .entry(array_name.to_string())
3046                                    .or_insert_with(HashMap::new);
3047                                if *is_append {
3048                                    let existing = assoc.get(&key).cloned().unwrap_or_default();
3049                                    assoc.insert(key, existing + &expanded);
3050                                } else {
3051                                    assoc.insert(key, expanded);
3052                                }
3053                            }
3054                            continue;
3055                        }
3056                    }
3057
3058                    // Regular variable assignment or append
3059                    let final_value = if *is_append {
3060                        let existing = self.variables.get(var).cloned().unwrap_or_default();
3061                        existing + &expanded
3062                    } else {
3063                        expanded
3064                    };
3065
3066                    if self.readonly_vars.contains(var) {
3067                        eprintln!("zshrs: read-only variable: {}", var);
3068                        self.last_status = 1;
3069                        return Ok(1);
3070                    }
3071                    if cmd.words.is_empty() {
3072                        // Just assignment, set in environment
3073                        env::set_var(var, &final_value);
3074                    }
3075                    self.variables.insert(var.clone(), final_value);
3076                }
3077            }
3078        }
3079
3080        if cmd.words.is_empty() {
3081            self.last_status = 0;
3082            return Ok(0);
3083        }
3084
3085        // Check if this is a noglob precommand — suppress glob expansion
3086        let is_noglob = cmd
3087            .words
3088            .first()
3089            .map(|w| self.expand_word(w) == "noglob")
3090            .unwrap_or(false);
3091        let saved_noglob = if is_noglob {
3092            let saved = self.options.get("noglob").copied();
3093            self.options.insert("noglob".to_string(), true);
3094            saved
3095        } else {
3096            None
3097        };
3098
3099        // Pre-launch external command substitutions in parallel before expanding words.
3100        // Each external $(cmd) gets spawned on the worker pool immediately.
3101        // When we reach that word during sequential expansion, we collect the result.
3102        let preflight = self.preflight_command_subs(&cmd.words);
3103
3104        let mut words: Vec<String> = cmd
3105            .words
3106            .iter()
3107            .enumerate()
3108            .flat_map(|(i, w)| {
3109                if let Some(rx) = &preflight[i] {
3110                    // Pre-launched external command sub — collect result
3111                    vec![rx.recv().unwrap_or_default()]
3112                } else {
3113                    self.expand_word_glob(w)
3114                }
3115            })
3116            .collect();
3117
3118        // Restore noglob after expansion
3119        if is_noglob {
3120            match saved_noglob {
3121                Some(v) => {
3122                    self.options.insert("noglob".to_string(), v);
3123                }
3124                None => {
3125                    self.options.remove("noglob");
3126                }
3127            }
3128        }
3129        if words.is_empty() {
3130            self.last_status = 0;
3131            return Ok(0);
3132        }
3133
3134        // Expand global aliases (alias -g) in all word positions
3135        if !self.global_aliases.is_empty() {
3136            let global_aliases = self.global_aliases.clone();
3137            words = words
3138                .into_iter()
3139                .map(|w| global_aliases.get(&w).cloned().unwrap_or(w))
3140                .collect();
3141        }
3142
3143        // xtrace: print expanded command to stderr (zsh -x / set -x)
3144        if self.options.get("xtrace").copied().unwrap_or(false) {
3145            let ps4 = self
3146                .variables
3147                .get("PS4")
3148                .cloned()
3149                .unwrap_or_else(|| "+".to_string());
3150            eprintln!("{}{}", ps4, words.join(" "));
3151        }
3152
3153        // Check for regular alias expansion (alias > builtin > function > command)
3154        let cmd_name = &words[0];
3155        if let Some(alias_value) = self.aliases.get(cmd_name).cloned() {
3156            // Expand the alias: replace cmd_name with alias value, keep remaining args
3157            let expanded_cmd = if words.len() > 1 {
3158                format!("{} {}", alias_value, words[1..].join(" "))
3159            } else {
3160                alias_value
3161            };
3162            // Re-execute the expanded command
3163            return self.execute_script(&expanded_cmd);
3164        }
3165
3166        // Check for suffix alias expansion (alias -s) when command is a file path
3167        if !self.suffix_aliases.is_empty() {
3168            let cmd_path = std::path::Path::new(cmd_name);
3169            if let Some(ext) = cmd_path.extension().and_then(|e| e.to_str()) {
3170                if let Some(handler) = self.suffix_aliases.get(ext).cloned() {
3171                    // Suffix alias: "alias -s txt=vim" makes "foo.txt" run "vim foo.txt"
3172                    let expanded_cmd = format!("{} {}", handler, words.join(" "));
3173                    return self.execute_script(&expanded_cmd);
3174                }
3175            }
3176        }
3177
3178        let args = &words[1..];
3179
3180        // Check if this is `exec` with only redirects (no command args)
3181        // For exec, redirects with {varname} allocate FDs; redirects are permanent
3182        let is_exec_with_redirects_only =
3183            cmd_name == "exec" && args.is_empty() && !cmd.redirects.is_empty();
3184
3185        // Apply redirects for builtins
3186        let mut saved_fds: Vec<(i32, i32)> = Vec::new();
3187        for redirect in &cmd.redirects {
3188            let target = self.expand_word(&redirect.target);
3189
3190            // Handle {varname}>file syntax - allocate new FD and store in variable
3191            if let Some(ref var_name) = redirect.fd_var {
3192                use std::os::unix::io::IntoRawFd;
3193                let file_result = match redirect.op {
3194                    RedirectOp::Write | RedirectOp::Clobber => std::fs::File::create(&target),
3195                    RedirectOp::Append => std::fs::OpenOptions::new()
3196                        .create(true)
3197                        .append(true)
3198                        .open(&target),
3199                    RedirectOp::Read => std::fs::File::open(&target),
3200                    _ => continue,
3201                };
3202                match file_result {
3203                    Ok(file) => {
3204                        let new_fd = file.into_raw_fd();
3205                        self.variables.insert(var_name.clone(), new_fd.to_string());
3206                        // Store allocated FD for potential cleanup (not for exec)
3207                        if !is_exec_with_redirects_only {
3208                            // For non-exec, we might want to track these
3209                        }
3210                    }
3211                    Err(e) => {
3212                        eprintln!("{}: {}: {}", cmd_name, target, e);
3213                        return Ok(1);
3214                    }
3215                }
3216                continue;
3217            }
3218
3219            let fd = redirect.fd.unwrap_or(match redirect.op {
3220                RedirectOp::Read
3221                | RedirectOp::HereDoc
3222                | RedirectOp::HereString
3223                | RedirectOp::ReadWrite => 0,
3224                _ => 1,
3225            });
3226
3227            match redirect.op {
3228                RedirectOp::Write | RedirectOp::Clobber => {
3229                    use std::os::unix::io::IntoRawFd;
3230                    if !is_exec_with_redirects_only {
3231                        let saved = unsafe { libc::dup(fd) };
3232                        if saved >= 0 {
3233                            saved_fds.push((fd, saved));
3234                        }
3235                    }
3236                    if let Ok(file) = std::fs::File::create(&target) {
3237                        let new_fd = file.into_raw_fd();
3238                        unsafe {
3239                            libc::dup2(new_fd, fd);
3240                        }
3241                        unsafe {
3242                            libc::close(new_fd);
3243                        }
3244                    }
3245                }
3246                RedirectOp::Append => {
3247                    use std::os::unix::io::IntoRawFd;
3248                    if !is_exec_with_redirects_only {
3249                        let saved = unsafe { libc::dup(fd) };
3250                        if saved >= 0 {
3251                            saved_fds.push((fd, saved));
3252                        }
3253                    }
3254                    if let Ok(file) = std::fs::OpenOptions::new()
3255                        .create(true)
3256                        .append(true)
3257                        .open(&target)
3258                    {
3259                        let new_fd = file.into_raw_fd();
3260                        unsafe {
3261                            libc::dup2(new_fd, fd);
3262                        }
3263                        unsafe {
3264                            libc::close(new_fd);
3265                        }
3266                    }
3267                }
3268                RedirectOp::Read => {
3269                    use std::os::unix::io::IntoRawFd;
3270                    if !is_exec_with_redirects_only {
3271                        let saved = unsafe { libc::dup(fd) };
3272                        if saved >= 0 {
3273                            saved_fds.push((fd, saved));
3274                        }
3275                    }
3276                    if let Ok(file) = std::fs::File::open(&target) {
3277                        let new_fd = file.into_raw_fd();
3278                        unsafe {
3279                            libc::dup2(new_fd, fd);
3280                        }
3281                        unsafe {
3282                            libc::close(new_fd);
3283                        }
3284                    }
3285                }
3286                RedirectOp::DupWrite | RedirectOp::DupRead => {
3287                    if let Ok(target_fd) = target.parse::<i32>() {
3288                        if !is_exec_with_redirects_only {
3289                            let saved = unsafe { libc::dup(fd) };
3290                            if saved >= 0 {
3291                                saved_fds.push((fd, saved));
3292                            }
3293                        }
3294                        unsafe {
3295                            libc::dup2(target_fd, fd);
3296                        }
3297                    }
3298                }
3299                _ => {}
3300            }
3301        }
3302
3303        // For exec with only redirects, we're done - redirects are applied permanently
3304        if is_exec_with_redirects_only {
3305            self.last_status = 0;
3306            return Ok(0);
3307        }
3308
3309        // Check for shell builtins
3310        let status = match cmd_name.as_str() {
3311            "cd" => self.builtin_cd(args),
3312            "pwd" => self.builtin_pwd(&cmd.redirects),
3313            "echo" => self.builtin_echo(args, &cmd.redirects),
3314            "export" => self.builtin_export(args),
3315            "unset" => self.builtin_unset(args),
3316            "source" | "." => self.builtin_source(args),
3317            "exit" | "bye" | "logout" => self.builtin_exit(args),
3318            "return" => self.builtin_return(args),
3319            "true" => 0,
3320            "false" => 1,
3321            ":" => 0,
3322            "chdir" => self.builtin_cd(args),
3323            "test" | "[" => self.builtin_test(args),
3324            "local" => self.builtin_local(args),
3325            "declare" | "typeset" => self.builtin_declare(args),
3326            "read" => self.builtin_read(args),
3327            "shift" => self.builtin_shift(args),
3328            "eval" => self.builtin_eval(args),
3329            "jobs" => self.builtin_jobs(args),
3330            "fg" => self.builtin_fg(args),
3331            "bg" => self.builtin_bg(args),
3332            "kill" => self.builtin_kill(args),
3333            "disown" => self.builtin_disown(args),
3334            "wait" => self.builtin_wait(args),
3335            "autoload" => self.builtin_autoload(args),
3336            "history" => self.builtin_history(args),
3337            "fc" => self.builtin_fc(args),
3338            "trap" => self.builtin_trap(args),
3339            "suspend" => self.builtin_suspend(args),
3340            "alias" => self.builtin_alias(args),
3341            "unalias" => self.builtin_unalias(args),
3342            "set" => self.builtin_set(args),
3343            "shopt" => self.builtin_shopt(args),
3344            // Bash compatibility
3345            "bind" => self.builtin_bindkey(args),
3346            "caller" => self.builtin_caller(args),
3347            "help" => self.builtin_help(args),
3348            "doctor" => self.builtin_doctor(args),
3349            "dbview" => self.builtin_dbview(args),
3350            "profile" => self.builtin_profile(args),
3351            "intercept" => self.builtin_intercept(args),
3352            "intercept_proceed" => self.builtin_intercept_proceed(args),
3353            // ── Concurrent primitives ──
3354            "async" => self.builtin_async(args),
3355            "await" => self.builtin_await(args),
3356            "pmap" => self.builtin_pmap(args),
3357            "pgrep" => self.builtin_pgrep(args),
3358            "peach" => self.builtin_peach(args),
3359            "barrier" => self.builtin_barrier(args),
3360            "readarray" | "mapfile" => self.builtin_readarray(args),
3361            "setopt" => self.builtin_setopt(args),
3362            "unsetopt" => self.builtin_unsetopt(args),
3363            "getopts" => self.builtin_getopts(args),
3364            "type" => self.builtin_type(args),
3365            "hash" => self.builtin_hash(args),
3366            "add-zsh-hook" => self.builtin_add_zsh_hook(args),
3367            "command" => self.builtin_command(args, &cmd.redirects),
3368            "builtin" => self.builtin_builtin(args, &cmd.redirects),
3369            "let" => self.builtin_let(args),
3370            "compgen" => self.builtin_compgen(args),
3371            "complete" => self.builtin_complete(args),
3372            "compopt" => self.builtin_compopt(args),
3373            "compadd" => self.builtin_compadd(args),
3374            "compset" => self.builtin_compset(args),
3375            "compdef" => self.builtin_compdef(args),
3376            "compinit" => self.builtin_compinit(args),
3377            "cdreplay" => self.builtin_cdreplay(args),
3378            "zstyle" => self.builtin_zstyle(args),
3379            // GDBM database bindings
3380            "ztie" => self.builtin_ztie(args),
3381            "zuntie" => self.builtin_zuntie(args),
3382            "zgdbmpath" => self.builtin_zgdbmpath(args),
3383            "pushd" => self.builtin_pushd(args),
3384            "popd" => self.builtin_popd(args),
3385            "dirs" => self.builtin_dirs(args),
3386            "printf" => self.builtin_printf(args),
3387            // Control flow
3388            "break" => self.builtin_break(args),
3389            "continue" => self.builtin_continue(args),
3390            // Enable/disable builtins
3391            "disable" => self.builtin_disable(args),
3392            "enable" => self.builtin_enable(args),
3393            // Emulation
3394            "emulate" => self.builtin_emulate(args),
3395            // Prompt themes
3396            "promptinit" => self.builtin_promptinit(args),
3397            "prompt" => self.builtin_prompt(args),
3398            // PCRE
3399            "pcre_compile" => self.builtin_pcre_compile(args),
3400            "pcre_match" => self.builtin_pcre_match(args),
3401            "pcre_study" => self.builtin_pcre_study(args),
3402            // Exec
3403            "exec" => self.builtin_exec(args),
3404            // Typed variables
3405            "float" => self.builtin_float(args),
3406            "integer" => self.builtin_integer(args),
3407            // Functions
3408            "functions" => self.builtin_functions(args),
3409            // Print (zsh style)
3410            "print" => self.builtin_print(args),
3411            // Command lookup
3412            "whence" => self.builtin_whence(args),
3413            "where" => self.builtin_where(args),
3414            "which" => self.builtin_which(args),
3415            // Resource limits
3416            "ulimit" => self.builtin_ulimit(args),
3417            "limit" => self.builtin_limit(args),
3418            "unlimit" => self.builtin_unlimit(args),
3419            // File mask
3420            "umask" => self.builtin_umask(args),
3421            // Hash table
3422            "rehash" => self.builtin_rehash(args),
3423            "unhash" => self.builtin_unhash(args),
3424            // Times
3425            "times" => self.builtin_times(args),
3426            // Module loading (stub)
3427            "zmodload" => self.builtin_zmodload(args),
3428            // Redo
3429            "r" => self.builtin_r(args),
3430            // TTY control
3431            "ttyctl" => self.builtin_ttyctl(args),
3432            // Noglob
3433            "noglob" => self.builtin_noglob(args, &cmd.redirects),
3434            // zsh/stat module
3435            "zstat" | "stat" => self.builtin_zstat(args),
3436            // zsh/datetime module
3437            "strftime" => self.builtin_strftime(args),
3438            // sleep with fractional seconds
3439            "zsleep" => self.builtin_zsleep(args),
3440            // zsh/system module - ported from Src/Modules/system.c
3441            "zsystem" => self.builtin_zsystem(args),
3442            // zsh/files module - ported from Src/Modules/files.c
3443            "sync" => self.builtin_sync(args),
3444            "mkdir" => self.builtin_mkdir(args),
3445            "rmdir" => self.builtin_rmdir(args),
3446            "ln" => self.builtin_ln(args),
3447            "mv" => self.builtin_mv(args),
3448            "cp" => self.builtin_cp(args),
3449            "rm" => self.builtin_rm(args),
3450            "chown" => self.builtin_chown(args),
3451            "chmod" => self.builtin_chmod(args),
3452            "zln" | "zmv" | "zcp" => self.builtin_zfiles(cmd_name, args),
3453            // coproc management
3454            "coproc" => self.builtin_coproc(args),
3455            // zparseopts - option parsing
3456            "zparseopts" => self.builtin_zparseopts(args),
3457            // readonly/unfunction
3458            "readonly" => self.builtin_readonly(args),
3459            "unfunction" => self.builtin_unfunction(args),
3460            // getln/pushln
3461            "getln" => self.builtin_getln(args),
3462            "pushln" => self.builtin_pushln(args),
3463            // bindkey stub
3464            "bindkey" => self.builtin_bindkey(args),
3465            // zle stub
3466            "zle" => self.builtin_zle(args),
3467            // sched
3468            "sched" => self.builtin_sched(args),
3469            // zformat
3470            "zformat" => self.builtin_zformat(args),
3471            // zcompile
3472            "zcompile" => self.builtin_zcompile(args),
3473            // vared - visual edit
3474            "vared" => self.builtin_vared(args),
3475            // terminal capabilities
3476            "echotc" => self.builtin_echotc(args),
3477            "echoti" => self.builtin_echoti(args),
3478            // PTY and socket operations
3479            "zpty" => self.builtin_zpty(args),
3480            "zprof" => self.builtin_zprof(args),
3481            "zsocket" => self.builtin_zsocket(args),
3482            "ztcp" => self.builtin_ztcp(args),
3483            "zregexparse" => self.builtin_zregexparse(args),
3484            "clone" => self.builtin_clone(args),
3485            "log" => self.builtin_log(args),
3486            // Completion system builtins
3487            "comparguments" => self.builtin_comparguments(args),
3488            "compcall" => self.builtin_compcall(args),
3489            "compctl" => self.builtin_compctl(args),
3490            "compdescribe" => self.builtin_compdescribe(args),
3491            "compfiles" => self.builtin_compfiles(args),
3492            "compgroups" => self.builtin_compgroups(args),
3493            "compquote" => self.builtin_compquote(args),
3494            "comptags" => self.builtin_comptags(args),
3495            "comptry" => self.builtin_comptry(args),
3496            "compvalues" => self.builtin_compvalues(args),
3497            // Capabilities (Linux-specific, stubs on macOS)
3498            "cap" | "getcap" | "setcap" => self.builtin_cap(args),
3499            // FTP client
3500            "zftp" => self.builtin_zftp(args),
3501            // zsh/curses module
3502            "zcurses" => self.builtin_zcurses(args),
3503            // zsh/system module
3504            "sysread" => self.builtin_sysread(args),
3505            "syswrite" => self.builtin_syswrite(args),
3506            "syserror" => self.builtin_syserror(args),
3507            "sysopen" => self.builtin_sysopen(args),
3508            "sysseek" => self.builtin_sysseek(args),
3509            // zsh/mapfile module
3510            "mapfile" => 0, // mapfile is a special parameter, not a command
3511            // zsh/param/private
3512            "private" => self.builtin_private(args),
3513            // zsh/attr (extended attributes)
3514            "zgetattr" | "zsetattr" | "zdelattr" | "zlistattr" => {
3515                self.builtin_zattr(cmd_name, args)
3516            }
3517            // Completion helper functions (now implemented in Rust compsys crate)
3518            // These are stubs that return success during non-completion execution
3519            "_arguments" | "_describe" | "_description" | "_message" | "_tags" | "_requested"
3520            | "_all_labels" | "_next_label" | "_files" | "_path_files" | "_directories" | "_cd"
3521            | "_default" | "_dispatch" | "_complete" | "_main_complete" | "_normal"
3522            | "_approximate" | "_correct" | "_expand" | "_history" | "_match" | "_menu"
3523            | "_oldlist" | "_list" | "_prefix" | "_generic" | "_wanted" | "_alternative"
3524            | "_values" | "_sequence" | "_sep_parts" | "_multi_parts" | "_combination"
3525            | "_parameters" | "_command" | "_command_names" | "_commands" | "_functions"
3526            | "_aliases" | "_builtins" | "_jobs" | "_pids" | "_process_names" | "_signals"
3527            | "_users" | "_groups" | "_hosts" | "_domains" | "_urls" | "_email_addresses"
3528            | "_options" | "_contexts" | "_set_options" | "_unset_options" | "_vars"
3529            | "_env_variables" | "_shell_variables" | "_arrays" | "_globflags" | "_globquals"
3530            | "_globqual_delims" | "_subscript" | "_history_modifiers" | "_brace_parameter"
3531            | "_tilde" | "_style" | "_cache_invalid" | "_store_cache" | "_retrieve_cache"
3532            | "_call_function" | "_call_program" | "_pick_variant" | "_setup"
3533            | "_comp_priv_prefix" | "_regex_arguments" | "_regex_words" | "_guard"
3534            | "_gnu_generic" | "_long_options" | "_x_arguments" | "_sub_commands"
3535            | "_cmdstring" | "_cmdambivalent" | "_first" | "_precommand" | "_user_at_host"
3536            | "_user_expand" | "_path_commands" | "_globbed_files" | "_have_glob_qual" => {
3537                // Return success - these functions are for completion context only
3538                // The actual completion logic is in the compsys Rust crate
3539                0
3540            }
3541            _ => {
3542                // ── AOP intercept dispatch ──
3543                // Check if any intercepts match this command name.
3544                // Fast path: skip if no intercepts registered.
3545                if !self.intercepts.is_empty() {
3546                    let full_cmd = if args.is_empty() {
3547                        cmd_name.to_string()
3548                    } else {
3549                        format!("{} {}", cmd_name, args.join(" "))
3550                    };
3551                    if let Some(result) = self.run_intercepts(cmd_name, &full_cmd, args) {
3552                        return result;
3553                    }
3554                }
3555
3556                // Check for function
3557                if let Some(func) = self.functions.get(cmd_name).cloned() {
3558                    return self.call_function(&func, args);
3559                }
3560
3561                // Try autoloading from pending autoload list
3562                if self.maybe_autoload(cmd_name) {
3563                    if let Some(func) = self.functions.get(cmd_name).cloned() {
3564                        return self.call_function(&func, args);
3565                    }
3566                }
3567
3568                // Try autoloading from ZWC
3569                if self.autoload_function(cmd_name).is_some() {
3570                    if let Some(func) = self.functions.get(cmd_name).cloned() {
3571                        return self.call_function(&func, args);
3572                    }
3573                }
3574
3575                // External command
3576                self.execute_external(cmd_name, args, &cmd.redirects)?
3577            }
3578        };
3579
3580        // Restore saved fds
3581        for (fd, saved) in saved_fds.into_iter().rev() {
3582            unsafe {
3583                libc::dup2(saved, fd);
3584                libc::close(saved);
3585            }
3586        }
3587
3588        self.last_status = status;
3589        Ok(status)
3590    }
3591
3592    /// Call a function with positional parameters
3593    #[tracing::instrument(level = "debug", skip_all)]
3594    fn call_function(&mut self, func: &ShellCommand, args: &[String]) -> Result<i32, String> {
3595        // Save current positional params
3596        let saved_params = std::mem::take(&mut self.positional_params);
3597
3598        // Save local variable scope — any `local` declarations during this
3599        // function will be reversed on exit (matches zsh's startparamscope/endparamscope).
3600        let saved_local_vars = self.local_save_stack.len();
3601        self.local_scope_depth += 1;
3602
3603        // Set new positional params
3604        self.positional_params = args.to_vec();
3605
3606        // Execute the function
3607        let result = self.execute_command(func);
3608
3609        // Handle return - clear the flag and use its value
3610        let final_result = if let Some(ret) = self.returning.take() {
3611            self.last_status = ret;
3612            Ok(ret)
3613        } else {
3614            result
3615        };
3616
3617        // Restore local variables (endparamscope)
3618        self.local_scope_depth -= 1;
3619        while self.local_save_stack.len() > saved_local_vars {
3620            if let Some((name, old_val)) = self.local_save_stack.pop() {
3621                match old_val {
3622                    Some(v) => {
3623                        self.variables.insert(name, v);
3624                    }
3625                    None => {
3626                        self.variables.remove(&name);
3627                    }
3628                }
3629            }
3630        }
3631
3632        // Restore positional params
3633        self.positional_params = saved_params;
3634
3635        final_result
3636    }
3637
3638    fn execute_external(
3639        &mut self,
3640        cmd: &str,
3641        args: &[String],
3642        redirects: &[Redirect],
3643    ) -> Result<i32, String> {
3644        self.execute_external_bg(cmd, args, redirects, false)
3645    }
3646
3647    fn execute_external_bg(
3648        &mut self,
3649        cmd: &str,
3650        args: &[String],
3651        redirects: &[Redirect],
3652        background: bool,
3653    ) -> Result<i32, String> {
3654        tracing::trace!(cmd, bg = background, "exec external");
3655        let mut command = Command::new(cmd);
3656        command.args(args);
3657
3658        // Apply redirections
3659        for redir in redirects {
3660            let target = self.expand_word(&redir.target);
3661            match redir.op {
3662                RedirectOp::Read => match File::open(&target) {
3663                    Ok(f) => {
3664                        command.stdin(Stdio::from(f));
3665                    }
3666                    Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3667                },
3668                RedirectOp::Write => match File::create(&target) {
3669                    Ok(f) => {
3670                        command.stdout(Stdio::from(f));
3671                    }
3672                    Err(e) => return Err(format!("Cannot create {}: {}", target, e)),
3673                },
3674                RedirectOp::Append => {
3675                    match OpenOptions::new().create(true).append(true).open(&target) {
3676                        Ok(f) => {
3677                            command.stdout(Stdio::from(f));
3678                        }
3679                        Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3680                    }
3681                }
3682                RedirectOp::WriteBoth => match File::create(&target) {
3683                    Ok(f) => {
3684                        let f2 = f
3685                            .try_clone()
3686                            .map_err(|e| format!("Cannot clone fd: {}", e))?;
3687                        command.stdout(Stdio::from(f));
3688                        command.stderr(Stdio::from(f2));
3689                    }
3690                    Err(e) => return Err(format!("Cannot create {}: {}", target, e)),
3691                },
3692                RedirectOp::AppendBoth => {
3693                    match OpenOptions::new().create(true).append(true).open(&target) {
3694                        Ok(f) => {
3695                            let f2 = f
3696                                .try_clone()
3697                                .map_err(|e| format!("Cannot clone fd: {}", e))?;
3698                            command.stdout(Stdio::from(f));
3699                            command.stderr(Stdio::from(f2));
3700                        }
3701                        Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3702                    }
3703                }
3704                RedirectOp::HereDoc => {
3705                    // Here-document - provide content as stdin
3706                    if let Some(ref content) = redir.heredoc_content {
3707                        // Expand variables in content (unless delimiter was quoted)
3708                        let expanded = self.expand_string(content);
3709                        command.stdin(Stdio::piped());
3710                        // Store the content to write after spawn
3711                        // For now, create a temp file
3712                        use std::io::Write;
3713                        let mut temp_file = tempfile::NamedTempFile::new()
3714                            .map_err(|e| format!("Cannot create temp file: {}", e))?;
3715                        temp_file
3716                            .write_all(expanded.as_bytes())
3717                            .map_err(|e| format!("Cannot write to temp file: {}", e))?;
3718                        let temp_path = temp_file.into_temp_path();
3719                        let f = File::open(&temp_path)
3720                            .map_err(|e| format!("Cannot open temp file: {}", e))?;
3721                        command.stdin(Stdio::from(f));
3722                    }
3723                }
3724                RedirectOp::HereString => {
3725                    // Here-string - provide target as stdin
3726                    use std::io::Write;
3727                    let content = format!("{}\n", target);
3728                    let mut temp_file = tempfile::NamedTempFile::new()
3729                        .map_err(|e| format!("Cannot create temp file: {}", e))?;
3730                    temp_file
3731                        .write_all(content.as_bytes())
3732                        .map_err(|e| format!("Cannot write to temp file: {}", e))?;
3733                    let temp_path = temp_file.into_temp_path();
3734                    let f = File::open(&temp_path)
3735                        .map_err(|e| format!("Cannot open temp file: {}", e))?;
3736                    command.stdin(Stdio::from(f));
3737                }
3738                _ => {
3739                    // Other redirections handled simply
3740                }
3741            }
3742
3743            // Handle {varname}>file syntax - store FD in variable
3744            if let Some(ref var_name) = redir.fd_var {
3745                // For {varname}>file, we open the file and store the fd number
3746                // This is typically used with exec, but we'll handle it for commands too
3747                #[cfg(unix)]
3748                {
3749                    use std::os::unix::io::AsRawFd;
3750                    let fd = match redir.op {
3751                        RedirectOp::Write | RedirectOp::Append => {
3752                            let f = if redir.op == RedirectOp::Write {
3753                                File::create(&target)
3754                            } else {
3755                                OpenOptions::new().create(true).append(true).open(&target)
3756                            };
3757                            match f {
3758                                Ok(file) => {
3759                                    let raw_fd = file.as_raw_fd();
3760                                    std::mem::forget(file); // Don't close the file
3761                                    raw_fd
3762                                }
3763                                Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3764                            }
3765                        }
3766                        RedirectOp::Read => match File::open(&target) {
3767                            Ok(file) => {
3768                                let raw_fd = file.as_raw_fd();
3769                                std::mem::forget(file);
3770                                raw_fd
3771                            }
3772                            Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3773                        },
3774                        _ => continue,
3775                    };
3776                    self.variables.insert(var_name.clone(), fd.to_string());
3777                }
3778            }
3779        }
3780
3781        if background {
3782            match command.spawn() {
3783                Ok(child) => {
3784                    let pid = child.id();
3785                    let cmd_str = format!("{} {}", cmd, args.join(" "));
3786                    let job_id = self.jobs.add_job(child, cmd_str, JobState::Running);
3787                    println!("[{}] {}", job_id, pid);
3788                    Ok(0)
3789                }
3790                Err(e) => {
3791                    if e.kind() == io::ErrorKind::NotFound {
3792                        eprintln!("zshrs: command not found: {}", cmd);
3793                        Ok(127)
3794                    } else {
3795                        Err(format!("zshrs: {}: {}", cmd, e))
3796                    }
3797                }
3798            }
3799        } else {
3800            match command.status() {
3801                Ok(status) => Ok(status.code().unwrap_or(1)),
3802                Err(e) => {
3803                    if e.kind() == io::ErrorKind::NotFound {
3804                        eprintln!("zshrs: command not found: {}", cmd);
3805                        Ok(127)
3806                    } else {
3807                        Err(format!("zshrs: {}: {}", cmd, e))
3808                    }
3809                }
3810            }
3811        }
3812    }
3813
3814    #[tracing::instrument(level = "trace", skip_all, fields(stages = cmds.len()))]
3815    fn execute_pipeline(&mut self, cmds: &[ShellCommand]) -> Result<i32, String> {
3816        if cmds.len() == 1 {
3817            return self.execute_command(&cmds[0]);
3818        }
3819
3820        let mut children: Vec<Child> = Vec::new();
3821        let mut prev_stdout: Option<std::process::ChildStdout> = None;
3822
3823        for (i, cmd) in cmds.iter().enumerate() {
3824            if let ShellCommand::Simple(simple) = cmd {
3825                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
3826                if words.is_empty() {
3827                    continue;
3828                }
3829
3830                let mut command = Command::new(&words[0]);
3831                command.args(&words[1..]);
3832
3833                if let Some(stdout) = prev_stdout.take() {
3834                    command.stdin(Stdio::from(stdout));
3835                }
3836
3837                if i < cmds.len() - 1 {
3838                    command.stdout(Stdio::piped());
3839                }
3840
3841                match command.spawn() {
3842                    Ok(mut child) => {
3843                        prev_stdout = child.stdout.take();
3844                        children.push(child);
3845                    }
3846                    Err(e) => {
3847                        eprintln!("zshrs: {}: {}", words[0], e);
3848                        return Ok(127);
3849                    }
3850                }
3851            }
3852        }
3853
3854        // Wait for all children
3855        let mut last_status = 0;
3856        for mut child in children {
3857            if let Ok(status) = child.wait() {
3858                last_status = status.code().unwrap_or(1);
3859            }
3860        }
3861
3862        Ok(last_status)
3863    }
3864
3865    fn execute_list(&mut self, items: &[(ShellCommand, ListOp)]) -> Result<i32, String> {
3866        for (cmd, op) in items {
3867            // Check if this command should run in background
3868            let background = matches!(op, ListOp::Amp);
3869
3870            let status = if background {
3871                self.execute_command_bg(cmd)?
3872            } else {
3873                self.execute_command(cmd)?
3874            };
3875
3876            // Check for control flow
3877            if self.returning.is_some() || self.breaking > 0 || self.continuing > 0 {
3878                return Ok(status);
3879            }
3880
3881            match op {
3882                ListOp::And => {
3883                    if status != 0 {
3884                        return Ok(status);
3885                    }
3886                }
3887                ListOp::Or => {
3888                    if status == 0 {
3889                        return Ok(0);
3890                    }
3891                }
3892                ListOp::Amp => {
3893                    // Already backgrounded above, continue
3894                }
3895                ListOp::Semi | ListOp::Newline => {
3896                    // Sequential, continue
3897                }
3898            }
3899        }
3900
3901        Ok(self.last_status)
3902    }
3903
3904    fn execute_command_bg(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
3905        // For simple commands, run in background
3906        if let ShellCommand::Simple(simple) = cmd {
3907            if simple.words.is_empty() {
3908                return Ok(0);
3909            }
3910            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
3911            let cmd_name = &words[0];
3912            let args: Vec<String> = words[1..].to_vec();
3913            return self.execute_external_bg(cmd_name, &args, &simple.redirects, true);
3914        }
3915        // For complex commands, just execute normally (could fork in future)
3916        self.execute_command(cmd)
3917    }
3918
3919    #[tracing::instrument(level = "trace", skip_all)]
3920    fn execute_compound(&mut self, compound: &CompoundCommand) -> Result<i32, String> {
3921        match compound {
3922            CompoundCommand::BraceGroup(cmds) => {
3923                for cmd in cmds {
3924                    self.execute_command(cmd)?;
3925                    if self.returning.is_some() {
3926                        break;
3927                    }
3928                }
3929                Ok(self.last_status)
3930            }
3931            CompoundCommand::Subshell(cmds) => {
3932                // Subshell isolates variable changes — save/restore all state.
3933                // In real zsh this forks; we simulate by cloning variables.
3934                let saved_vars = self.variables.clone();
3935                let saved_arrays = self.arrays.clone();
3936                let saved_assoc = self.assoc_arrays.clone();
3937                let saved_params = self.positional_params.clone();
3938
3939                for cmd in cmds {
3940                    self.execute_command(cmd)?;
3941                    if self.returning.is_some() {
3942                        break;
3943                    }
3944                }
3945                let status = self.last_status;
3946
3947                // Restore state — subshell changes are discarded
3948                self.variables = saved_vars;
3949                self.arrays = saved_arrays;
3950                self.assoc_arrays = saved_assoc;
3951                self.positional_params = saved_params;
3952                self.last_status = status;
3953
3954                Ok(status)
3955            }
3956
3957            CompoundCommand::If {
3958                conditions,
3959                else_part,
3960            } => {
3961                for (cond, body) in conditions {
3962                    // Execute condition
3963                    for cmd in cond {
3964                        self.execute_command(cmd)?;
3965                    }
3966
3967                    if self.last_status == 0 {
3968                        // Condition true, execute body
3969                        for cmd in body {
3970                            self.execute_command(cmd)?;
3971                        }
3972                        return Ok(self.last_status);
3973                    }
3974                }
3975
3976                // All conditions false, execute else
3977                if let Some(else_cmds) = else_part {
3978                    for cmd in else_cmds {
3979                        self.execute_command(cmd)?;
3980                    }
3981                }
3982
3983                Ok(self.last_status)
3984            }
3985
3986            CompoundCommand::For { var, words, body } => {
3987                let items: Vec<String> = if let Some(words) = words {
3988                    words
3989                        .iter()
3990                        .flat_map(|w| self.expand_word_split(w))
3991                        .collect()
3992                } else {
3993                    // Iterate over positional parameters
3994                    self.positional_params.clone()
3995                };
3996
3997                for item in items {
3998                    env::set_var(var, &item);
3999                    self.variables.insert(var.clone(), item);
4000
4001                    for cmd in body {
4002                        self.execute_command(cmd)?;
4003                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4004                            break;
4005                        }
4006                    }
4007
4008                    if self.continuing > 0 {
4009                        self.continuing -= 1;
4010                        if self.continuing > 0 {
4011                            break;
4012                        }
4013                        continue;
4014                    }
4015                    if self.breaking > 0 {
4016                        self.breaking -= 1;
4017                        break;
4018                    }
4019                    if self.returning.is_some() {
4020                        break;
4021                    }
4022                }
4023
4024                Ok(self.last_status)
4025            }
4026
4027            CompoundCommand::ForArith {
4028                init,
4029                cond,
4030                step,
4031                body,
4032            } => {
4033                // C-style for loop: for ((init; cond; step))
4034                // Execute init expression (use evaluate_arithmetic_expr for assignment support)
4035                if !init.is_empty() {
4036                    self.evaluate_arithmetic_expr(init);
4037                }
4038
4039                // Loop while condition is true
4040                loop {
4041                    // Evaluate condition (use eval_arith_expr for comparison result)
4042                    if !cond.is_empty() {
4043                        let cond_result = self.eval_arith_expr(cond);
4044                        if cond_result == 0 {
4045                            break;
4046                        }
4047                    }
4048
4049                    // Execute body
4050                    for cmd in body {
4051                        self.execute_command(cmd)?;
4052                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4053                            break;
4054                        }
4055                    }
4056
4057                    if self.continuing > 0 {
4058                        self.continuing -= 1;
4059                        if self.continuing > 0 {
4060                            break;
4061                        }
4062                        continue;
4063                    }
4064                    if self.breaking > 0 {
4065                        self.breaking -= 1;
4066                        break;
4067                    }
4068                    if self.returning.is_some() {
4069                        break;
4070                    }
4071
4072                    // Execute step (use evaluate_arithmetic_expr for assignment support like i++)
4073                    if !step.is_empty() {
4074                        self.evaluate_arithmetic_expr(step);
4075                    }
4076                }
4077                Ok(self.last_status)
4078            }
4079
4080            CompoundCommand::While { condition, body } => {
4081                loop {
4082                    for cmd in condition {
4083                        self.execute_command(cmd)?;
4084                        if self.breaking > 0 || self.returning.is_some() {
4085                            break;
4086                        }
4087                    }
4088
4089                    if self.last_status != 0 || self.breaking > 0 || self.returning.is_some() {
4090                        break;
4091                    }
4092
4093                    for cmd in body {
4094                        self.execute_command(cmd)?;
4095                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4096                            break;
4097                        }
4098                    }
4099
4100                    if self.continuing > 0 {
4101                        self.continuing -= 1;
4102                        if self.continuing > 0 {
4103                            break;
4104                        }
4105                        continue;
4106                    }
4107                    if self.breaking > 0 {
4108                        self.breaking -= 1;
4109                        break;
4110                    }
4111                }
4112                Ok(self.last_status)
4113            }
4114
4115            CompoundCommand::Until { condition, body } => {
4116                loop {
4117                    for cmd in condition {
4118                        self.execute_command(cmd)?;
4119                        if self.breaking > 0 || self.returning.is_some() {
4120                            break;
4121                        }
4122                    }
4123
4124                    if self.last_status == 0 || self.breaking > 0 || self.returning.is_some() {
4125                        break;
4126                    }
4127
4128                    for cmd in body {
4129                        self.execute_command(cmd)?;
4130                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4131                            break;
4132                        }
4133                    }
4134
4135                    if self.continuing > 0 {
4136                        self.continuing -= 1;
4137                        if self.continuing > 0 {
4138                            break;
4139                        }
4140                        continue;
4141                    }
4142                    if self.breaking > 0 {
4143                        self.breaking -= 1;
4144                        break;
4145                    }
4146                }
4147                Ok(self.last_status)
4148            }
4149
4150            CompoundCommand::Case { word, cases } => {
4151                let value = self.expand_word(word);
4152
4153                for (patterns, body, term) in cases {
4154                    for pattern in patterns {
4155                        let pat = self.expand_word(pattern);
4156                        if self.matches_pattern(&value, &pat) {
4157                            for cmd in body {
4158                                self.execute_command(cmd)?;
4159                            }
4160
4161                            match term {
4162                                CaseTerminator::Break => return Ok(self.last_status),
4163                                CaseTerminator::Fallthrough => {
4164                                    // Continue to next case body
4165                                }
4166                                CaseTerminator::Continue => {
4167                                    // Continue pattern matching
4168                                    break;
4169                                }
4170                            }
4171                        }
4172                    }
4173                }
4174
4175                Ok(self.last_status)
4176            }
4177
4178            CompoundCommand::Select { var, words, body } => {
4179                // Simplified: just use first word
4180                if let Some(words) = words {
4181                    if let Some(first) = words.first() {
4182                        let val = self.expand_word(first);
4183                        env::set_var(var, &val);
4184                        for cmd in body {
4185                            self.execute_command(cmd)?;
4186                        }
4187                    }
4188                }
4189                Ok(self.last_status)
4190            }
4191
4192            CompoundCommand::Repeat { count, body } => {
4193                let n: i64 = self
4194                    .expand_word(&ShellWord::Literal(count.clone()))
4195                    .parse()
4196                    .unwrap_or(0);
4197
4198                for _ in 0..n {
4199                    for cmd in body {
4200                        self.execute_command(cmd)?;
4201                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4202                            break;
4203                        }
4204                    }
4205
4206                    if self.continuing > 0 {
4207                        self.continuing -= 1;
4208                        if self.continuing > 0 {
4209                            break;
4210                        }
4211                        continue;
4212                    }
4213                    if self.breaking > 0 {
4214                        self.breaking -= 1;
4215                        break;
4216                    }
4217                    if self.returning.is_some() {
4218                        break;
4219                    }
4220                }
4221
4222                Ok(self.last_status)
4223            }
4224
4225            CompoundCommand::Try {
4226                try_body,
4227                always_body,
4228            } => {
4229                // Port of exectry() from Src/loop.c
4230                // The :try clause
4231                for cmd in try_body {
4232                    if let Err(_e) = self.execute_command(cmd) {
4233                        break;
4234                    }
4235                    if self.returning.is_some() {
4236                        break;
4237                    }
4238                }
4239
4240                // endval = lastval ? lastval : errflag
4241                let endval = self.last_status;
4242
4243                // Save and reset control flow flags for the always clause
4244                let save_returning = self.returning.take();
4245                let save_breaking = self.breaking;
4246                let save_continuing = self.continuing;
4247                self.breaking = 0;
4248                self.continuing = 0;
4249
4250                // The always clause — executes unconditionally
4251                for cmd in always_body {
4252                    let _ = self.execute_command(cmd);
4253                }
4254
4255                // Restore control flow: C uses "if (!retflag) retflag = save"
4256                // i.e. always block's flags take precedence if set
4257                if self.returning.is_none() {
4258                    self.returning = save_returning;
4259                }
4260                if self.breaking == 0 {
4261                    self.breaking = save_breaking;
4262                }
4263                if self.continuing == 0 {
4264                    self.continuing = save_continuing;
4265                }
4266
4267                self.last_status = endval;
4268                Ok(endval)
4269            }
4270
4271            CompoundCommand::Cond(expr) => {
4272                let result = self.eval_cond_expr(expr);
4273                self.last_status = if result { 0 } else { 1 };
4274                Ok(self.last_status)
4275            }
4276
4277            CompoundCommand::Arith(expr) => {
4278                // Evaluate arithmetic expression and set variables
4279                let result = self.evaluate_arithmetic_expr(expr);
4280                // (( )) returns 0 if result is non-zero, 1 if result is zero
4281                self.last_status = if result != 0 { 0 } else { 1 };
4282                Ok(self.last_status)
4283            }
4284
4285            CompoundCommand::Coproc { name, body } => {
4286                // Create pipes for stdin and stdout
4287                let (stdin_read, stdin_write) =
4288                    os_pipe::pipe().map_err(|e| format!("Cannot create pipe: {}", e))?;
4289                let (stdout_read, stdout_write) =
4290                    os_pipe::pipe().map_err(|e| format!("Cannot create pipe: {}", e))?;
4291
4292                // Get the command to run
4293                let cmd_str = match body.as_ref() {
4294                    ShellCommand::Simple(simple) => simple
4295                        .words
4296                        .iter()
4297                        .map(|w| self.expand_word(w))
4298                        .collect::<Vec<_>>()
4299                        .join(" "),
4300                    ShellCommand::Compound(CompoundCommand::BraceGroup(_cmds)) => {
4301                        // Just run as a subshell with the commands
4302                        // For simplicity, we'll use bash -c
4303                        "bash -c 'true'".to_string()
4304                    }
4305                    _ => "true".to_string(),
4306                };
4307
4308                // Fork and run the command in background with redirected stdin/stdout
4309                let parts: Vec<&str> = cmd_str.split_whitespace().collect();
4310                if parts.is_empty() {
4311                    return Ok(0);
4312                }
4313
4314                let mut command = Command::new(parts[0]);
4315                if parts.len() > 1 {
4316                    command.args(&parts[1..]);
4317                }
4318
4319                use std::os::unix::io::{FromRawFd, IntoRawFd};
4320
4321                command.stdin(unsafe { Stdio::from_raw_fd(stdin_read.into_raw_fd()) });
4322                command.stdout(unsafe { Stdio::from_raw_fd(stdout_write.into_raw_fd()) });
4323
4324                match command.spawn() {
4325                    Ok(child) => {
4326                        let pid = child.id();
4327                        let coproc_name = name.clone().unwrap_or_else(|| "COPROC".to_string());
4328
4329                        // Store file descriptors in environment-like variables
4330                        // COPROC[0] = read from coproc (stdout_read)
4331                        // COPROC[1] = write to coproc (stdin_write)
4332                        let read_fd = stdout_read.into_raw_fd();
4333                        let write_fd = stdin_write.into_raw_fd();
4334
4335                        self.arrays.insert(
4336                            coproc_name.clone(),
4337                            vec![read_fd.to_string(), write_fd.to_string()],
4338                        );
4339
4340                        // Also store PID
4341                        self.variables
4342                            .insert(format!("{}_PID", coproc_name), pid.to_string());
4343
4344                        let cmd_str_clone = cmd_str.clone();
4345                        self.jobs.add_job(child, cmd_str_clone, JobState::Running);
4346
4347                        Ok(0)
4348                    }
4349                    Err(e) => {
4350                        if e.kind() == io::ErrorKind::NotFound {
4351                            eprintln!("zshrs: command not found: {}", parts[0]);
4352                            Ok(127)
4353                        } else {
4354                            Err(format!("zshrs: coproc: {}: {}", parts[0], e))
4355                        }
4356                    }
4357                }
4358            }
4359
4360            CompoundCommand::WithRedirects(cmd, redirects) => {
4361                // Execute the command with redirects applied
4362                let mut saved_fds: Vec<(i32, i32)> = Vec::new();
4363
4364                // Set up redirects
4365                for redirect in redirects {
4366                    let fd = redirect.fd.unwrap_or(match redirect.op {
4367                        RedirectOp::Read
4368                        | RedirectOp::HereDoc
4369                        | RedirectOp::HereString
4370                        | RedirectOp::ReadWrite => 0,
4371                        _ => 1,
4372                    });
4373
4374                    let target = self.expand_word(&redirect.target);
4375
4376                    match redirect.op {
4377                        RedirectOp::Write | RedirectOp::Clobber => {
4378                            use std::os::unix::io::IntoRawFd;
4379                            let saved = unsafe { libc::dup(fd) };
4380                            if saved >= 0 {
4381                                saved_fds.push((fd, saved));
4382                            }
4383                            if let Ok(file) = std::fs::File::create(&target) {
4384                                let new_fd = file.into_raw_fd();
4385                                unsafe {
4386                                    libc::dup2(new_fd, fd);
4387                                }
4388                                unsafe {
4389                                    libc::close(new_fd);
4390                                }
4391                            }
4392                        }
4393                        RedirectOp::Append => {
4394                            use std::os::unix::io::IntoRawFd;
4395                            let saved = unsafe { libc::dup(fd) };
4396                            if saved >= 0 {
4397                                saved_fds.push((fd, saved));
4398                            }
4399                            if let Ok(file) = std::fs::OpenOptions::new()
4400                                .create(true)
4401                                .append(true)
4402                                .open(&target)
4403                            {
4404                                let new_fd = file.into_raw_fd();
4405                                unsafe {
4406                                    libc::dup2(new_fd, fd);
4407                                }
4408                                unsafe {
4409                                    libc::close(new_fd);
4410                                }
4411                            }
4412                        }
4413                        RedirectOp::Read => {
4414                            use std::os::unix::io::IntoRawFd;
4415                            let saved = unsafe { libc::dup(fd) };
4416                            if saved >= 0 {
4417                                saved_fds.push((fd, saved));
4418                            }
4419                            if let Ok(file) = std::fs::File::open(&target) {
4420                                let new_fd = file.into_raw_fd();
4421                                unsafe {
4422                                    libc::dup2(new_fd, fd);
4423                                }
4424                                unsafe {
4425                                    libc::close(new_fd);
4426                                }
4427                            }
4428                        }
4429                        RedirectOp::DupWrite | RedirectOp::DupRead => {
4430                            if let Ok(target_fd) = target.parse::<i32>() {
4431                                let saved = unsafe { libc::dup(fd) };
4432                                if saved >= 0 {
4433                                    saved_fds.push((fd, saved));
4434                                }
4435                                unsafe {
4436                                    libc::dup2(target_fd, fd);
4437                                }
4438                            }
4439                        }
4440                        _ => {}
4441                    }
4442                }
4443
4444                // Execute the inner command
4445                let result = self.execute_command(cmd);
4446
4447                // Restore saved fds
4448                for (fd, saved) in saved_fds.into_iter().rev() {
4449                    unsafe {
4450                        libc::dup2(saved, fd);
4451                        libc::close(saved);
4452                    }
4453                }
4454
4455                result
4456            }
4457        }
4458    }
4459
4460    /// Expand a word with brace and glob expansion (for command arguments)
4461    #[tracing::instrument(level = "trace", skip_all)]
4462    fn expand_word_glob(&mut self, word: &ShellWord) -> Vec<String> {
4463        match word {
4464            ShellWord::SingleQuoted(s) => vec![s.clone()],
4465            ShellWord::DoubleQuoted(parts) => {
4466                // Double quotes prevent glob and brace expansion
4467                vec![parts.iter().map(|p| self.expand_word(p)).collect()]
4468            }
4469            _ => {
4470                let expanded = self.expand_word(word);
4471
4472                // First do brace expansion
4473                let brace_expanded = self.expand_braces(&expanded);
4474
4475                // Then glob expansion on each result (unless noglob is set)
4476                let noglob = self.options.get("noglob").copied().unwrap_or(false)
4477                    || self.options.get("GLOB").map(|v| !v).unwrap_or(false);
4478                brace_expanded
4479                    .into_iter()
4480                    .flat_map(|s| {
4481                        if !noglob
4482                            && (s.contains('*')
4483                                || s.contains('?')
4484                                || s.contains('[')
4485                                || self.has_extglob_pattern(&s))
4486                        {
4487                            self.expand_glob(&s)
4488                        } else {
4489                            vec![s]
4490                        }
4491                    })
4492                    .collect()
4493            }
4494        }
4495    }
4496
4497    /// Expand brace patterns like {a,b,c} and {1..10}
4498    fn expand_braces(&self, s: &str) -> Vec<String> {
4499        // Find a brace pattern
4500        let mut depth = 0;
4501        let mut brace_start = None;
4502
4503        for (i, c) in s.char_indices() {
4504            match c {
4505                '{' => {
4506                    if depth == 0 {
4507                        brace_start = Some(i);
4508                    }
4509                    depth += 1;
4510                }
4511                '}' => {
4512                    depth -= 1;
4513                    if depth == 0 {
4514                        if let Some(start) = brace_start {
4515                            let prefix = &s[..start];
4516                            let content = &s[start + 1..i];
4517                            let suffix = &s[i + 1..];
4518
4519                            // Check if this is a sequence {a..b} or a list {a,b,c}
4520                            let expansions = if content.contains("..") {
4521                                self.expand_brace_sequence(content)
4522                            } else if content.contains(',') {
4523                                self.expand_brace_list(content)
4524                            } else {
4525                                // Not a valid brace expansion, return as-is
4526                                return vec![s.to_string()];
4527                            };
4528
4529                            // Combine prefix, expansions, and suffix
4530                            let mut results = Vec::new();
4531                            for exp in expansions {
4532                                let combined = format!("{}{}{}", prefix, exp, suffix);
4533                                // Recursively expand any remaining braces
4534                                results.extend(self.expand_braces(&combined));
4535                            }
4536                            return results;
4537                        }
4538                    }
4539                }
4540                _ => {}
4541            }
4542        }
4543
4544        // No brace expansion found
4545        vec![s.to_string()]
4546    }
4547
4548    /// Expand comma-separated brace list like {a,b,c}
4549    fn expand_brace_list(&self, content: &str) -> Vec<String> {
4550        // Split by comma, but respect nested braces
4551        let mut parts = Vec::new();
4552        let mut current = String::new();
4553        let mut depth = 0;
4554
4555        for c in content.chars() {
4556            match c {
4557                '{' => {
4558                    depth += 1;
4559                    current.push(c);
4560                }
4561                '}' => {
4562                    depth -= 1;
4563                    current.push(c);
4564                }
4565                ',' if depth == 0 => {
4566                    parts.push(current.clone());
4567                    current.clear();
4568                }
4569                _ => current.push(c),
4570            }
4571        }
4572        parts.push(current);
4573
4574        parts
4575    }
4576
4577    /// Expand sequence brace pattern like {1..10} or {a..z}
4578    fn expand_brace_sequence(&self, content: &str) -> Vec<String> {
4579        let parts: Vec<&str> = content.splitn(3, "..").collect();
4580        if parts.len() < 2 {
4581            return vec![content.to_string()];
4582        }
4583
4584        let start = parts[0];
4585        let end = parts[1];
4586        let step: i64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
4587
4588        // Try numeric sequence
4589        if let (Ok(start_num), Ok(end_num)) = (start.parse::<i64>(), end.parse::<i64>()) {
4590            let mut results = Vec::new();
4591            if start_num <= end_num {
4592                let mut i = start_num;
4593                while i <= end_num {
4594                    results.push(i.to_string());
4595                    i += step;
4596                }
4597            } else {
4598                let mut i = start_num;
4599                while i >= end_num {
4600                    results.push(i.to_string());
4601                    i -= step;
4602                }
4603            }
4604            return results;
4605        }
4606
4607        // Try character sequence
4608        if start.len() == 1 && end.len() == 1 {
4609            let start_char = start.chars().next().unwrap();
4610            let end_char = end.chars().next().unwrap();
4611            let mut results = Vec::new();
4612
4613            if start_char <= end_char {
4614                let mut c = start_char;
4615                while c <= end_char {
4616                    results.push(c.to_string());
4617                    c = (c as u8 + step as u8) as char;
4618                    if c as u8 > end_char as u8 {
4619                        break;
4620                    }
4621                }
4622            } else {
4623                let mut c = start_char;
4624                while c >= end_char {
4625                    results.push(c.to_string());
4626                    if (c as u8) < step as u8 {
4627                        break;
4628                    }
4629                    c = (c as u8 - step as u8) as char;
4630                }
4631            }
4632            return results;
4633        }
4634
4635        vec![content.to_string()]
4636    }
4637
4638    /// Expand glob pattern to matching files
4639    fn expand_glob(&self, pattern: &str) -> Vec<String> {
4640        // Check for zsh glob qualifiers at end: *(.) *(/) *(@) etc.
4641        let (glob_pattern, qualifiers) = self.parse_glob_qualifiers(pattern);
4642
4643        // Check for extended glob patterns: ?(pat), *(pat), +(pat), @(pat), !(pat)
4644        if self.has_extglob_pattern(&glob_pattern) {
4645            let expanded = self.expand_extglob(&glob_pattern);
4646            return self.filter_by_qualifiers(expanded, &qualifiers);
4647        }
4648
4649        let nullglob = self.options.get("nullglob").copied().unwrap_or(false);
4650        let dotglob = self.options.get("dotglob").copied().unwrap_or(false);
4651        let nocaseglob = self.options.get("nocaseglob").copied().unwrap_or(false);
4652
4653        // Parallel recursive glob: when pattern contains **/ we split the
4654        // directory walk across worker pool threads — one thread per top-level
4655        // subdirectory.  zsh does this single-threaded via fork+exec which is
4656        // why `echo **/*.rs` is painfully slow on large trees.
4657        let expanded = if glob_pattern.contains("**/") {
4658            self.expand_glob_parallel(&glob_pattern, dotglob, nocaseglob)
4659        } else {
4660            let options = glob::MatchOptions {
4661                case_sensitive: !nocaseglob,
4662                require_literal_separator: false,
4663                require_literal_leading_dot: !dotglob,
4664            };
4665            match glob::glob_with(&glob_pattern, options) {
4666                Ok(paths) => paths
4667                    .filter_map(|p| p.ok())
4668                    .map(|p| p.to_string_lossy().to_string())
4669                    .collect(),
4670                Err(_) => vec![],
4671            }
4672        };
4673
4674        let mut expanded = self.filter_by_qualifiers(expanded, &qualifiers);
4675        expanded.sort();
4676
4677        if expanded.is_empty() {
4678            if nullglob {
4679                vec![]
4680            } else {
4681                vec![pattern.to_string()]
4682            }
4683        } else {
4684            expanded
4685        }
4686    }
4687
4688    /// Parallel recursive glob using the worker pool.
4689    ///
4690    /// Splits `base/**/file_pattern` into per-subdirectory walks, each
4691    /// running on a pool thread via walkdir.  Results merge via channel.
4692    /// This is why `echo **/*.rs` will be 5-10x faster than zsh.
4693    fn expand_glob_parallel(&self, pattern: &str, dotglob: bool, nocaseglob: bool) -> Vec<String> {
4694        use walkdir::WalkDir;
4695
4696        // Split pattern at the first **/ into (base_dir, file_glob)
4697        // e.g. "src/**/*.rs" → ("src", "*.rs")
4698        //      "**/*.rs"     → (".", "*.rs")
4699        let (base, file_glob) = if let Some(pos) = pattern.find("**/") {
4700            let base = if pos == 0 {
4701                "."
4702            } else {
4703                &pattern[..pos.saturating_sub(1)]
4704            };
4705            let rest = &pattern[pos + 3..]; // skip "**/", get "*.rs" or "foo/**/*.rs"
4706            (base.to_string(), rest.to_string())
4707        } else {
4708            return vec![];
4709        };
4710
4711        // If file_glob itself contains **/, fall back to single-threaded glob
4712        // (nested recursive patterns are rare, not worth the complexity)
4713        if file_glob.contains("**/") {
4714            let options = glob::MatchOptions {
4715                case_sensitive: !nocaseglob,
4716                require_literal_separator: false,
4717                require_literal_leading_dot: !dotglob,
4718            };
4719            return match glob::glob_with(pattern, options) {
4720                Ok(paths) => paths
4721                    .filter_map(|p| p.ok())
4722                    .map(|p| p.to_string_lossy().to_string())
4723                    .collect(),
4724                Err(_) => vec![],
4725            };
4726        }
4727
4728        // Build the glob::Pattern for matching filenames
4729        let match_opts = glob::MatchOptions {
4730            case_sensitive: !nocaseglob,
4731            require_literal_separator: false,
4732            require_literal_leading_dot: !dotglob,
4733        };
4734        let file_pat = match glob::Pattern::new(&file_glob) {
4735            Ok(p) => p,
4736            Err(_) => return vec![],
4737        };
4738
4739        // Enumerate top-level entries in base dir to fan out across workers
4740        let top_entries: Vec<std::path::PathBuf> = match std::fs::read_dir(&base) {
4741            Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.path()).collect(),
4742            Err(_) => return vec![],
4743        };
4744
4745        // Also check files directly in base (not in subdirs)
4746        let mut results: Vec<String> = Vec::new();
4747        for entry in &top_entries {
4748            if entry.is_file() || entry.is_symlink() {
4749                if let Some(name) = entry.file_name().and_then(|n| n.to_str()) {
4750                    if file_pat.matches_with(name, match_opts) {
4751                        results.push(entry.to_string_lossy().to_string());
4752                    }
4753                }
4754            }
4755        }
4756
4757        // Fan out subdirectory walks to worker pool
4758        let subdirs: Vec<std::path::PathBuf> = top_entries
4759            .into_iter()
4760            .filter(|p| p.is_dir())
4761            .filter(|p| {
4762                dotglob
4763                    || !p
4764                        .file_name()
4765                        .and_then(|n| n.to_str())
4766                        .map(|n| n.starts_with('.'))
4767                        .unwrap_or(false)
4768            })
4769            .collect();
4770
4771        if subdirs.is_empty() {
4772            return results;
4773        }
4774
4775        let (tx, rx) = std::sync::mpsc::channel::<Vec<String>>();
4776
4777        for subdir in &subdirs {
4778            let tx = tx.clone();
4779            let subdir = subdir.clone();
4780            let file_pat = file_pat.clone();
4781            let skip_dot = !dotglob;
4782            self.worker_pool.submit(move || {
4783                let mut matches = Vec::new();
4784                let walker = WalkDir::new(&subdir)
4785                    .follow_links(false)
4786                    .into_iter()
4787                    .filter_entry(move |e| {
4788                        // Skip hidden dirs if !dotglob
4789                        if skip_dot {
4790                            if let Some(name) = e.file_name().to_str() {
4791                                if name.starts_with('.') && e.depth() > 0 {
4792                                    return false;
4793                                }
4794                            }
4795                        }
4796                        true
4797                    });
4798                for entry in walker.filter_map(|e| e.ok()) {
4799                    if entry.file_type().is_file() || entry.file_type().is_symlink() {
4800                        if let Some(name) = entry.file_name().to_str() {
4801                            if file_pat.matches_with(name, match_opts) {
4802                                matches.push(entry.path().to_string_lossy().to_string());
4803                            }
4804                        }
4805                    }
4806                }
4807                let _ = tx.send(matches);
4808            });
4809        }
4810
4811        // Drop our sender so rx knows when all workers are done
4812        drop(tx);
4813
4814        // Collect results from all workers
4815        for batch in rx {
4816            results.extend(batch);
4817        }
4818
4819        results
4820    }
4821
4822    /// Parse zsh glob qualifiers from the end of a pattern
4823    /// Returns (pattern_without_qualifiers, qualifiers_string)
4824    fn parse_glob_qualifiers(&self, pattern: &str) -> (String, String) {
4825        // Check if pattern ends with (...) that looks like qualifiers
4826        // Qualifiers are single chars like . / @ * % or combinations
4827        if !pattern.ends_with(')') {
4828            return (pattern.to_string(), String::new());
4829        }
4830
4831        // Find matching opening paren
4832        let chars: Vec<char> = pattern.chars().collect();
4833        let mut depth = 0;
4834        let mut qual_start = None;
4835
4836        for i in (0..chars.len()).rev() {
4837            match chars[i] {
4838                ')' => depth += 1,
4839                '(' => {
4840                    depth -= 1;
4841                    if depth == 0 {
4842                        qual_start = Some(i);
4843                        break;
4844                    }
4845                }
4846                _ => {}
4847            }
4848        }
4849
4850        if let Some(start) = qual_start {
4851            let qual_content: String = chars[start + 1..chars.len() - 1].iter().collect();
4852
4853            // Check if this looks like glob qualifiers (not extglob)
4854            // Qualifiers are things like: . / @ * % r w x ^ - etc.
4855            // Extglob would have | inside
4856            if !qual_content.contains('|') && self.looks_like_glob_qualifiers(&qual_content) {
4857                let base_pattern: String = chars[..start].iter().collect();
4858                return (base_pattern, qual_content);
4859            }
4860        }
4861
4862        (pattern.to_string(), String::new())
4863    }
4864
4865    /// Check if string looks like glob qualifiers
4866    fn looks_like_glob_qualifiers(&self, s: &str) -> bool {
4867        if s.is_empty() {
4868            return false;
4869        }
4870        // Valid qualifier chars: . / @ = p * % r w x A I E R W X s S t ^ - + :
4871        // Also numbers for depth limits, and things like [1,5] for ranges
4872        let valid_chars = "./@=p*%brwxAIERWXsStfedDLNnMmcaou^-+:0123456789,[]FT";
4873        s.chars()
4874            .all(|c| valid_chars.contains(c) || c.is_whitespace())
4875    }
4876
4877    /// Filter file list by glob qualifiers
4878    /// Prefetch file metadata in parallel across the worker pool.
4879    /// Returns a map from path → (metadata, symlink_metadata).
4880    /// Each batch of files is stat'd on a pool thread.
4881    fn prefetch_metadata(
4882        &self,
4883        files: &[String],
4884    ) -> HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)> {
4885        if files.len() < 32 {
4886            // Small list — serial stat is faster than channel overhead
4887            return files
4888                .iter()
4889                .map(|f| {
4890                    let meta = std::fs::metadata(f).ok();
4891                    let symlink_meta = std::fs::symlink_metadata(f).ok();
4892                    (f.clone(), (meta, symlink_meta))
4893                })
4894                .collect();
4895        }
4896
4897        let pool_size = self.worker_pool.size();
4898        let chunk_size = (files.len() + pool_size - 1) / pool_size;
4899        let (tx, rx) = std::sync::mpsc::channel();
4900
4901        for chunk in files.chunks(chunk_size) {
4902            let tx = tx.clone();
4903            let chunk: Vec<String> = chunk.to_vec();
4904            self.worker_pool.submit(move || {
4905                let batch: Vec<(
4906                    String,
4907                    (Option<std::fs::Metadata>, Option<std::fs::Metadata>),
4908                )> = chunk
4909                    .into_iter()
4910                    .map(|f| {
4911                        let meta = std::fs::metadata(&f).ok();
4912                        let symlink_meta = std::fs::symlink_metadata(&f).ok();
4913                        (f, (meta, symlink_meta))
4914                    })
4915                    .collect();
4916                let _ = tx.send(batch);
4917            });
4918        }
4919        drop(tx);
4920
4921        let mut map = HashMap::with_capacity(files.len());
4922        for batch in rx {
4923            for (path, metas) in batch {
4924                map.insert(path, metas);
4925            }
4926        }
4927        map
4928    }
4929
4930    fn filter_by_qualifiers(&self, files: Vec<String>, qualifiers: &str) -> Vec<String> {
4931        if qualifiers.is_empty() {
4932            return files;
4933        }
4934
4935        // Parallel metadata prefetch — all stat syscalls happen on pool threads,
4936        // then filter/sort uses cached metadata with zero syscalls.
4937        let meta_cache = self.prefetch_metadata(&files);
4938
4939        let mut result = files;
4940        let mut negate = false;
4941        let mut chars = qualifiers.chars().peekable();
4942
4943        while let Some(c) = chars.next() {
4944            match c {
4945                // Negation
4946                '^' => negate = !negate,
4947
4948                // File types — all use prefetched metadata cache
4949                '.' => {
4950                    result = result
4951                        .into_iter()
4952                        .filter(|f| {
4953                            let is_file = meta_cache
4954                                .get(f)
4955                                .and_then(|(m, _)| m.as_ref())
4956                                .map(|m| m.is_file())
4957                                .unwrap_or(false);
4958                            if negate {
4959                                !is_file
4960                            } else {
4961                                is_file
4962                            }
4963                        })
4964                        .collect();
4965                    negate = false;
4966                }
4967                '/' => {
4968                    result = result
4969                        .into_iter()
4970                        .filter(|f| {
4971                            let is_dir = meta_cache
4972                                .get(f)
4973                                .and_then(|(m, _)| m.as_ref())
4974                                .map(|m| m.is_dir())
4975                                .unwrap_or(false);
4976                            if negate {
4977                                !is_dir
4978                            } else {
4979                                is_dir
4980                            }
4981                        })
4982                        .collect();
4983                    negate = false;
4984                }
4985                '@' => {
4986                    result = result
4987                        .into_iter()
4988                        .filter(|f| {
4989                            let is_link = meta_cache
4990                                .get(f)
4991                                .and_then(|(_, sm)| sm.as_ref())
4992                                .map(|m| m.file_type().is_symlink())
4993                                .unwrap_or(false);
4994                            if negate {
4995                                !is_link
4996                            } else {
4997                                is_link
4998                            }
4999                        })
5000                        .collect();
5001                    negate = false;
5002                }
5003                '=' => {
5004                    // Sockets
5005                    use std::os::unix::fs::FileTypeExt;
5006                    result = result
5007                        .into_iter()
5008                        .filter(|f| {
5009                            let is_socket = meta_cache
5010                                .get(f)
5011                                .and_then(|(_, sm)| sm.as_ref())
5012                                .map(|m| m.file_type().is_socket())
5013                                .unwrap_or(false);
5014                            if negate {
5015                                !is_socket
5016                            } else {
5017                                is_socket
5018                            }
5019                        })
5020                        .collect();
5021                    negate = false;
5022                }
5023                'p' => {
5024                    // Named pipes (FIFOs)
5025                    use std::os::unix::fs::FileTypeExt;
5026                    result = result
5027                        .into_iter()
5028                        .filter(|f| {
5029                            let is_fifo = meta_cache
5030                                .get(f)
5031                                .and_then(|(_, sm)| sm.as_ref())
5032                                .map(|m| m.file_type().is_fifo())
5033                                .unwrap_or(false);
5034                            if negate {
5035                                !is_fifo
5036                            } else {
5037                                is_fifo
5038                            }
5039                        })
5040                        .collect();
5041                    negate = false;
5042                }
5043                '*' => {
5044                    // Executable files
5045                    use std::os::unix::fs::PermissionsExt;
5046                    result = result
5047                        .into_iter()
5048                        .filter(|f| {
5049                            let is_exec = meta_cache
5050                                .get(f)
5051                                .and_then(|(m, _)| m.as_ref())
5052                                .map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
5053                                .unwrap_or(false);
5054                            if negate {
5055                                !is_exec
5056                            } else {
5057                                is_exec
5058                            }
5059                        })
5060                        .collect();
5061                    negate = false;
5062                }
5063                '%' => {
5064                    // Device files
5065                    use std::os::unix::fs::FileTypeExt;
5066                    let next = chars.peek().copied();
5067                    result = result
5068                        .into_iter()
5069                        .filter(|f| {
5070                            let is_device = meta_cache
5071                                .get(f)
5072                                .and_then(|(_, sm)| sm.as_ref())
5073                                .map(|m| match next {
5074                                    Some('b') => m.file_type().is_block_device(),
5075                                    Some('c') => m.file_type().is_char_device(),
5076                                    _ => {
5077                                        m.file_type().is_block_device()
5078                                            || m.file_type().is_char_device()
5079                                    }
5080                                })
5081                                .unwrap_or(false);
5082                            if negate {
5083                                !is_device
5084                            } else {
5085                                is_device
5086                            }
5087                        })
5088                        .collect();
5089                    if next == Some('b') || next == Some('c') {
5090                        chars.next();
5091                    }
5092                    negate = false;
5093                }
5094
5095                // Permission qualifiers — all use prefetched metadata cache
5096                'r' => {
5097                    result = self.filter_by_permission(result, 0o400, negate, &meta_cache);
5098                    negate = false;
5099                }
5100                'w' => {
5101                    result = self.filter_by_permission(result, 0o200, negate, &meta_cache);
5102                    negate = false;
5103                }
5104                'x' => {
5105                    result = self.filter_by_permission(result, 0o100, negate, &meta_cache);
5106                    negate = false;
5107                }
5108                'A' => {
5109                    result = self.filter_by_permission(result, 0o040, negate, &meta_cache);
5110                    negate = false;
5111                }
5112                'I' => {
5113                    result = self.filter_by_permission(result, 0o020, negate, &meta_cache);
5114                    negate = false;
5115                }
5116                'E' => {
5117                    result = self.filter_by_permission(result, 0o010, negate, &meta_cache);
5118                    negate = false;
5119                }
5120                'R' => {
5121                    result = self.filter_by_permission(result, 0o004, negate, &meta_cache);
5122                    negate = false;
5123                }
5124                'W' => {
5125                    result = self.filter_by_permission(result, 0o002, negate, &meta_cache);
5126                    negate = false;
5127                }
5128                'X' => {
5129                    result = self.filter_by_permission(result, 0o001, negate, &meta_cache);
5130                    negate = false;
5131                }
5132                's' => {
5133                    result = self.filter_by_permission(result, 0o4000, negate, &meta_cache);
5134                    negate = false;
5135                }
5136                'S' => {
5137                    result = self.filter_by_permission(result, 0o2000, negate, &meta_cache);
5138                    negate = false;
5139                }
5140                't' => {
5141                    result = self.filter_by_permission(result, 0o1000, negate, &meta_cache);
5142                    negate = false;
5143                }
5144
5145                // Full/empty directories
5146                'F' => {
5147                    // Non-empty directories
5148                    result = result
5149                        .into_iter()
5150                        .filter(|f| {
5151                            let path = std::path::Path::new(f);
5152                            let is_nonempty = path.is_dir()
5153                                && std::fs::read_dir(path)
5154                                    .map(|mut d| d.next().is_some())
5155                                    .unwrap_or(false);
5156                            if negate {
5157                                !is_nonempty
5158                            } else {
5159                                is_nonempty
5160                            }
5161                        })
5162                        .collect();
5163                    negate = false;
5164                }
5165
5166                // Ownership — uses prefetched metadata cache
5167                'U' => {
5168                    // Owned by effective UID
5169                    let euid = unsafe { libc::geteuid() };
5170                    result = result
5171                        .into_iter()
5172                        .filter(|f| {
5173                            use std::os::unix::fs::MetadataExt;
5174                            let is_owned = meta_cache
5175                                .get(f)
5176                                .and_then(|(m, _)| m.as_ref())
5177                                .map(|m| m.uid() == euid)
5178                                .unwrap_or(false);
5179                            if negate {
5180                                !is_owned
5181                            } else {
5182                                is_owned
5183                            }
5184                        })
5185                        .collect();
5186                    negate = false;
5187                }
5188                'G' => {
5189                    // Owned by effective GID
5190                    let egid = unsafe { libc::getegid() };
5191                    result = result
5192                        .into_iter()
5193                        .filter(|f| {
5194                            use std::os::unix::fs::MetadataExt;
5195                            let is_owned = meta_cache
5196                                .get(f)
5197                                .and_then(|(m, _)| m.as_ref())
5198                                .map(|m| m.gid() == egid)
5199                                .unwrap_or(false);
5200                            if negate {
5201                                !is_owned
5202                            } else {
5203                                is_owned
5204                            }
5205                        })
5206                        .collect();
5207                    negate = false;
5208                }
5209
5210                // Sorting modifiers
5211                'o' => {
5212                    // Sort by name (ascending) - already default
5213                    if chars.peek() == Some(&'n') {
5214                        chars.next();
5215                        // Sort by name
5216                        result.sort();
5217                    } else if chars.peek() == Some(&'L') {
5218                        chars.next();
5219                        // Sort by size — uses prefetched metadata
5220                        result.sort_by_key(|f| {
5221                            meta_cache
5222                                .get(f)
5223                                .and_then(|(m, _)| m.as_ref())
5224                                .map(|m| m.len())
5225                                .unwrap_or(0)
5226                        });
5227                    } else if chars.peek() == Some(&'m') {
5228                        chars.next();
5229                        // Sort by modification time — uses prefetched metadata
5230                        result.sort_by_key(|f| {
5231                            meta_cache
5232                                .get(f)
5233                                .and_then(|(m, _)| m.as_ref())
5234                                .and_then(|m| m.modified().ok())
5235                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5236                        });
5237                    } else if chars.peek() == Some(&'a') {
5238                        chars.next();
5239                        // Sort by access time — uses prefetched metadata
5240                        result.sort_by_key(|f| {
5241                            meta_cache
5242                                .get(f)
5243                                .and_then(|(m, _)| m.as_ref())
5244                                .and_then(|m| m.accessed().ok())
5245                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5246                        });
5247                    }
5248                }
5249                'O' => {
5250                    // Reverse sort — uses prefetched metadata
5251                    if chars.peek() == Some(&'n') {
5252                        chars.next();
5253                        result.sort();
5254                        result.reverse();
5255                    } else if chars.peek() == Some(&'L') {
5256                        chars.next();
5257                        result.sort_by_key(|f| {
5258                            meta_cache
5259                                .get(f)
5260                                .and_then(|(m, _)| m.as_ref())
5261                                .map(|m| m.len())
5262                                .unwrap_or(0)
5263                        });
5264                        result.reverse();
5265                    } else if chars.peek() == Some(&'m') {
5266                        chars.next();
5267                        result.sort_by_key(|f| {
5268                            meta_cache
5269                                .get(f)
5270                                .and_then(|(m, _)| m.as_ref())
5271                                .and_then(|m| m.modified().ok())
5272                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5273                        });
5274                        result.reverse();
5275                    } else {
5276                        // Just reverse current order
5277                        result.reverse();
5278                    }
5279                }
5280
5281                // Subscript range [n] or [n,m]
5282                '[' => {
5283                    let mut range_str = String::new();
5284                    while let Some(&ch) = chars.peek() {
5285                        if ch == ']' {
5286                            chars.next();
5287                            break;
5288                        }
5289                        range_str.push(chars.next().unwrap());
5290                    }
5291
5292                    if let Some((start, end)) = self.parse_subscript_range(&range_str, result.len())
5293                    {
5294                        result = result.into_iter().skip(start).take(end - start).collect();
5295                    }
5296                }
5297
5298                // Depth limit (for **/)
5299                'D' => {
5300                    // Include dotfiles (handled by dotglob)
5301                }
5302                'N' => {
5303                    // Nullglob for this pattern
5304                }
5305
5306                // Unknown qualifier - ignore
5307                _ => {}
5308            }
5309        }
5310
5311        result
5312    }
5313
5314    /// Filter files by permission bits — uses prefetched metadata cache
5315    fn filter_by_permission(
5316        &self,
5317        files: Vec<String>,
5318        mode: u32,
5319        negate: bool,
5320        meta_cache: &HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)>,
5321    ) -> Vec<String> {
5322        use std::os::unix::fs::PermissionsExt;
5323        files
5324            .into_iter()
5325            .filter(|f| {
5326                let has_perm = meta_cache
5327                    .get(f)
5328                    .and_then(|(m, _)| m.as_ref())
5329                    .map(|m| (m.permissions().mode() & mode) != 0)
5330                    .unwrap_or(false);
5331                if negate {
5332                    !has_perm
5333                } else {
5334                    has_perm
5335                }
5336            })
5337            .collect()
5338    }
5339
5340    /// Parse subscript range like "1" or "1,5" or "-1" or "1,-1"
5341    fn parse_subscript_range(&self, s: &str, len: usize) -> Option<(usize, usize)> {
5342        if s.is_empty() || len == 0 {
5343            return None;
5344        }
5345
5346        let parts: Vec<&str> = s.split(',').collect();
5347
5348        let parse_idx = |idx_str: &str| -> Option<usize> {
5349            let idx: i64 = idx_str.trim().parse().ok()?;
5350            if idx < 0 {
5351                // Negative index from end
5352                let abs = (-idx) as usize;
5353                if abs > len {
5354                    None
5355                } else {
5356                    Some(len - abs)
5357                }
5358            } else if idx == 0 {
5359                Some(0)
5360            } else {
5361                // 1-indexed
5362                Some((idx as usize).saturating_sub(1).min(len))
5363            }
5364        };
5365
5366        match parts.len() {
5367            1 => {
5368                // Single element [n]
5369                let idx = parse_idx(parts[0])?;
5370                Some((idx, idx + 1))
5371            }
5372            2 => {
5373                // Range [n,m]
5374                let start = parse_idx(parts[0])?;
5375                let end = parse_idx(parts[1])?.saturating_add(1);
5376                Some((start.min(end), start.max(end)))
5377            }
5378            _ => None,
5379        }
5380    }
5381
5382    /// Check if pattern contains extended glob syntax
5383    fn has_extglob_pattern(&self, pattern: &str) -> bool {
5384        let chars: Vec<char> = pattern.chars().collect();
5385        for i in 0..chars.len().saturating_sub(1) {
5386            if (chars[i] == '?'
5387                || chars[i] == '*'
5388                || chars[i] == '+'
5389                || chars[i] == '@'
5390                || chars[i] == '!')
5391                && chars[i + 1] == '('
5392            {
5393                return true;
5394            }
5395        }
5396        false
5397    }
5398
5399    /// Convert extended glob pattern to regex
5400    fn extglob_to_regex(&self, pattern: &str) -> String {
5401        let mut regex = String::from("^");
5402        let chars: Vec<char> = pattern.chars().collect();
5403        let mut i = 0;
5404
5405        while i < chars.len() {
5406            let c = chars[i];
5407
5408            // Check for extglob patterns
5409            if i + 1 < chars.len() && chars[i + 1] == '(' {
5410                match c {
5411                    '?' => {
5412                        // ?(pattern) - zero or one occurrence
5413                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5414                        let inner_regex = self.extglob_inner_to_regex(&inner);
5415                        regex.push_str(&format!("({})?", inner_regex));
5416                        i = end + 1;
5417                        continue;
5418                    }
5419                    '*' => {
5420                        // *(pattern) - zero or more occurrences
5421                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5422                        let inner_regex = self.extglob_inner_to_regex(&inner);
5423                        regex.push_str(&format!("({})*", inner_regex));
5424                        i = end + 1;
5425                        continue;
5426                    }
5427                    '+' => {
5428                        // +(pattern) - one or more occurrences
5429                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5430                        let inner_regex = self.extglob_inner_to_regex(&inner);
5431                        regex.push_str(&format!("({})+", inner_regex));
5432                        i = end + 1;
5433                        continue;
5434                    }
5435                    '@' => {
5436                        // @(pattern) - exactly one occurrence
5437                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5438                        let inner_regex = self.extglob_inner_to_regex(&inner);
5439                        regex.push_str(&format!("({})", inner_regex));
5440                        i = end + 1;
5441                        continue;
5442                    }
5443                    '!' => {
5444                        // !(pattern) - handled specially in expand_extglob
5445                        // Just skip this extglob for regex, will do manual filtering
5446                        let (_, end) = self.extract_extglob_inner(&chars, i + 2);
5447                        regex.push_str(".*"); // Match anything, we filter later
5448                        i = end + 1;
5449                        continue;
5450                    }
5451                    _ => {}
5452                }
5453            }
5454
5455            // Handle regular glob characters
5456            match c {
5457                '*' => regex.push_str(".*"),
5458                '?' => regex.push('.'),
5459                '.' => regex.push_str("\\."),
5460                '[' => {
5461                    regex.push('[');
5462                    i += 1;
5463                    while i < chars.len() && chars[i] != ']' {
5464                        if chars[i] == '!' && regex.ends_with('[') {
5465                            regex.push('^');
5466                        } else {
5467                            regex.push(chars[i]);
5468                        }
5469                        i += 1;
5470                    }
5471                    regex.push(']');
5472                }
5473                '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
5474                    regex.push('\\');
5475                    regex.push(c);
5476                }
5477                _ => regex.push(c),
5478            }
5479            i += 1;
5480        }
5481
5482        regex.push('$');
5483        regex
5484    }
5485
5486    /// Extract the inner part of an extglob pattern (until closing paren)
5487    fn extract_extglob_inner(&self, chars: &[char], start: usize) -> (String, usize) {
5488        let mut inner = String::new();
5489        let mut depth = 1;
5490        let mut i = start;
5491
5492        while i < chars.len() && depth > 0 {
5493            if chars[i] == '(' {
5494                depth += 1;
5495            } else if chars[i] == ')' {
5496                depth -= 1;
5497                if depth == 0 {
5498                    return (inner, i);
5499                }
5500            }
5501            inner.push(chars[i]);
5502            i += 1;
5503        }
5504
5505        (inner, i)
5506    }
5507
5508    /// Convert the inner part of extglob (handles | for alternation)
5509    fn extglob_inner_to_regex(&self, inner: &str) -> String {
5510        // Split by | and convert each alternative
5511        let alternatives: Vec<String> = inner
5512            .split('|')
5513            .map(|alt| {
5514                let mut result = String::new();
5515                for c in alt.chars() {
5516                    match c {
5517                        '*' => result.push_str(".*"),
5518                        '?' => result.push('.'),
5519                        '.' => result.push_str("\\."),
5520                        '^' | '$' | '(' | ')' | '{' | '}' | '\\' => {
5521                            result.push('\\');
5522                            result.push(c);
5523                        }
5524                        _ => result.push(c),
5525                    }
5526                }
5527                result
5528            })
5529            .collect();
5530
5531        alternatives.join("|")
5532    }
5533
5534    /// Expand extended glob pattern
5535    fn expand_extglob(&self, pattern: &str) -> Vec<String> {
5536        // Determine directory to search
5537        let (search_dir, file_pattern) = if let Some(last_slash) = pattern.rfind('/') {
5538            (&pattern[..last_slash], &pattern[last_slash + 1..])
5539        } else {
5540            (".", pattern)
5541        };
5542
5543        // Check for !(pattern) - negative matching
5544        if let Some((neg_pat, suffix)) = self.extract_neg_extglob(file_pattern) {
5545            return self.expand_neg_extglob(search_dir, &neg_pat, &suffix, pattern);
5546        }
5547
5548        // Convert file pattern to regex for positive extglob
5549        let regex_str = self.extglob_to_regex(file_pattern);
5550
5551        let re = match cached_regex(&regex_str) {
5552            Some(r) => r,
5553            None => return vec![pattern.to_string()],
5554        };
5555
5556        let mut results = Vec::new();
5557
5558        if let Ok(entries) = std::fs::read_dir(search_dir) {
5559            for entry in entries.flatten() {
5560                let name = entry.file_name().to_string_lossy().to_string();
5561                // Skip hidden files unless pattern starts with .
5562                if name.starts_with('.') && !file_pattern.starts_with('.') {
5563                    continue;
5564                }
5565
5566                if re.is_match(&name) {
5567                    let full_path = if search_dir == "." {
5568                        name
5569                    } else {
5570                        format!("{}/{}", search_dir, name)
5571                    };
5572                    results.push(full_path);
5573                }
5574            }
5575        }
5576
5577        if results.is_empty() {
5578            vec![pattern.to_string()]
5579        } else {
5580            results.sort();
5581            results
5582        }
5583    }
5584
5585    /// Handle !(pattern) negative extglob expansion
5586    fn expand_neg_extglob(
5587        &self,
5588        search_dir: &str,
5589        neg_pat: &str,
5590        suffix: &str,
5591        original_pattern: &str,
5592    ) -> Vec<String> {
5593        let mut results = Vec::new();
5594
5595        if let Ok(entries) = std::fs::read_dir(search_dir) {
5596            for entry in entries.flatten() {
5597                let name = entry.file_name().to_string_lossy().to_string();
5598                // Skip hidden files
5599                if name.starts_with('.') {
5600                    continue;
5601                }
5602
5603                // File must end with suffix
5604                if !name.ends_with(suffix) {
5605                    continue;
5606                }
5607
5608                let basename = &name[..name.len() - suffix.len()];
5609                // Check if basename matches any negated alternative
5610                let alts: Vec<&str> = neg_pat.split('|').collect();
5611                let matches_neg = alts.iter().any(|alt| {
5612                    if alt.contains('*') || alt.contains('?') {
5613                        let alt_re = self.extglob_inner_to_regex(alt);
5614                        let full_pattern = format!("^{}$", alt_re);
5615                        if let Some(r) = cached_regex(&full_pattern) {
5616                            r.is_match(basename)
5617                        } else {
5618                            *alt == basename
5619                        }
5620                    } else {
5621                        *alt == basename
5622                    }
5623                });
5624
5625                if !matches_neg {
5626                    let full_path = if search_dir == "." {
5627                        name
5628                    } else {
5629                        format!("{}/{}", search_dir, name)
5630                    };
5631                    results.push(full_path);
5632                }
5633            }
5634        }
5635
5636        if results.is_empty() {
5637            vec![original_pattern.to_string()]
5638        } else {
5639            results.sort();
5640            results
5641        }
5642    }
5643
5644    /// Extract !(pattern) info from file pattern, returns (inner_pattern, suffix)
5645    fn extract_neg_extglob(&self, pattern: &str) -> Option<(String, String)> {
5646        let chars: Vec<char> = pattern.chars().collect();
5647        if chars.len() >= 3 && chars[0] == '!' && chars[1] == '(' {
5648            let mut depth = 1;
5649            let mut i = 2;
5650            while i < chars.len() && depth > 0 {
5651                if chars[i] == '(' {
5652                    depth += 1;
5653                } else if chars[i] == ')' {
5654                    depth -= 1;
5655                }
5656                i += 1;
5657            }
5658            if depth == 0 {
5659                let inner: String = chars[2..i - 1].iter().collect();
5660                let suffix: String = chars[i..].iter().collect();
5661                return Some((inner, suffix));
5662            }
5663        }
5664        None
5665    }
5666
5667    /// Expand a word with word splitting (for contexts like `for x in $words`)
5668    fn expand_word_split(&mut self, word: &ShellWord) -> Vec<String> {
5669        match word {
5670            ShellWord::Literal(s) => {
5671                // First do brace expansion, then variable expansion on each result
5672                let brace_expanded = self.expand_braces(s);
5673                brace_expanded
5674                    .into_iter()
5675                    .flat_map(|item| self.expand_string_split(&item))
5676                    .collect()
5677            }
5678            ShellWord::SingleQuoted(s) => vec![s.clone()],
5679            ShellWord::DoubleQuoted(parts) => {
5680                // Double quotes prevent word splitting
5681                vec![parts.iter().map(|p| self.expand_word(p)).collect()]
5682            }
5683            ShellWord::Variable(name) => {
5684                let val = env::var(name).unwrap_or_default();
5685                self.split_words(&val)
5686            }
5687            ShellWord::VariableBraced(name, modifier) => {
5688                let val = env::var(name).ok();
5689                let expanded = self.apply_var_modifier(name, val, modifier.as_deref());
5690                self.split_words(&expanded)
5691            }
5692            ShellWord::ArrayVar(name, index) => {
5693                let idx_str = self.expand_word(index);
5694                if idx_str == "@" || idx_str == "*" {
5695                    // ${arr[@]} returns each element as separate word
5696                    self.arrays.get(name).cloned().unwrap_or_default()
5697                } else {
5698                    vec![self.expand_array_access(name, index)]
5699                }
5700            }
5701            ShellWord::Glob(pattern) => match glob::glob(pattern) {
5702                Ok(paths) => {
5703                    let expanded: Vec<String> = paths
5704                        .filter_map(|p| p.ok())
5705                        .map(|p| p.to_string_lossy().to_string())
5706                        .collect();
5707                    if expanded.is_empty() {
5708                        vec![pattern.clone()]
5709                    } else {
5710                        expanded
5711                    }
5712                }
5713                Err(_) => vec![pattern.clone()],
5714            },
5715            ShellWord::CommandSub(_) => {
5716                // Command substitution results must be word-split for array context
5717                let val = self.expand_word(word);
5718                self.split_words(&val)
5719            }
5720            ShellWord::Concat(parts) => {
5721                // Concat in split context — expand and split the result
5722                let val = self.expand_concat_parallel(parts);
5723                self.split_words(&val)
5724            }
5725            _ => vec![self.expand_word(word)],
5726        }
5727    }
5728
5729    /// Expand string with word splitting - returns Vec for array expansions
5730    fn expand_string_split(&mut self, s: &str) -> Vec<String> {
5731        let mut results: Vec<String> = Vec::new();
5732        let mut current = String::new();
5733        let mut chars = s.chars().peekable();
5734
5735        while let Some(c) = chars.next() {
5736            if c == '$' {
5737                if chars.peek() == Some(&'{') {
5738                    chars.next(); // consume '{'
5739                    let mut brace_content = String::new();
5740                    let mut depth = 1;
5741                    while let Some(ch) = chars.next() {
5742                        if ch == '{' {
5743                            depth += 1;
5744                            brace_content.push(ch);
5745                        } else if ch == '}' {
5746                            depth -= 1;
5747                            if depth == 0 {
5748                                break;
5749                            }
5750                            brace_content.push(ch);
5751                        } else {
5752                            brace_content.push(ch);
5753                        }
5754                    }
5755
5756                    // Check if this is an array expansion ${arr[@]} or ${arr[*]}
5757                    if let Some(bracket_start) = brace_content.find('[') {
5758                        let var_name = &brace_content[..bracket_start];
5759                        let bracket_content = &brace_content[bracket_start + 1..];
5760                        if let Some(bracket_end) = bracket_content.find(']') {
5761                            let index = &bracket_content[..bracket_end];
5762                            if (index == "@" || index == "*")
5763                                && bracket_end + 1 == bracket_content.len()
5764                            {
5765                                // This is ${arr[@]} - expand to separate elements
5766                                if !current.is_empty() {
5767                                    results.push(current.clone());
5768                                    current.clear();
5769                                }
5770                                if let Some(arr) = self.arrays.get(var_name) {
5771                                    results.extend(arr.clone());
5772                                }
5773                                continue;
5774                            }
5775                        }
5776                    }
5777
5778                    // Not an array expansion, use normal expansion
5779                    current.push_str(&self.expand_braced_variable(&brace_content));
5780                } else {
5781                    // Simple variable like $var
5782                    let mut var_name = String::new();
5783                    while let Some(&ch) = chars.peek() {
5784                        if ch.is_alphanumeric() || ch == '_' {
5785                            var_name.push(chars.next().unwrap());
5786                        } else {
5787                            break;
5788                        }
5789                    }
5790                    let val = self.get_variable(&var_name);
5791                    // Split this variable's value
5792                    if !current.is_empty() {
5793                        results.push(current.clone());
5794                        current.clear();
5795                    }
5796                    results.extend(self.split_words(&val));
5797                }
5798            } else {
5799                current.push(c);
5800            }
5801        }
5802
5803        if !current.is_empty() {
5804            results.push(current);
5805        }
5806
5807        if results.is_empty() {
5808            results.push(String::new());
5809        }
5810
5811        results
5812    }
5813
5814    /// Split a string into words based on IFS
5815    fn split_words(&self, s: &str) -> Vec<String> {
5816        let ifs = self
5817            .variables
5818            .get("IFS")
5819            .cloned()
5820            .or_else(|| env::var("IFS").ok())
5821            .unwrap_or_else(|| " \t\n".to_string());
5822
5823        if ifs.is_empty() {
5824            return vec![s.to_string()];
5825        }
5826
5827        s.split(|c: char| ifs.contains(c))
5828            .filter(|s| !s.is_empty())
5829            .map(|s| s.to_string())
5830            .collect()
5831    }
5832
5833    #[tracing::instrument(level = "trace", skip_all)]
5834    fn expand_word(&mut self, word: &ShellWord) -> String {
5835        match word {
5836            ShellWord::Literal(s) => {
5837                let expanded = self.expand_string(s);
5838                // Don't glob-expand here, that's done in expand_word_glob
5839                expanded
5840            }
5841            ShellWord::SingleQuoted(s) => s.clone(),
5842            ShellWord::DoubleQuoted(parts) => parts.iter().map(|p| self.expand_word(p)).collect(),
5843            ShellWord::Variable(name) => self.get_variable(name),
5844            ShellWord::VariableBraced(name, modifier) => {
5845                let val = env::var(name).ok();
5846                self.apply_var_modifier(name, val, modifier.as_deref())
5847            }
5848            ShellWord::Tilde(user) => {
5849                if let Some(u) = user {
5850                    // ~user expansion (simplified)
5851                    format!("/home/{}", u)
5852                } else {
5853                    dirs::home_dir()
5854                        .map(|p| p.to_string_lossy().to_string())
5855                        .unwrap_or_else(|| "~".to_string())
5856                }
5857            }
5858            ShellWord::Glob(pattern) => {
5859                // Expand glob
5860                match glob::glob(pattern) {
5861                    Ok(paths) => {
5862                        let expanded: Vec<String> = paths
5863                            .filter_map(|p| p.ok())
5864                            .map(|p| p.to_string_lossy().to_string())
5865                            .collect();
5866                        if expanded.is_empty() {
5867                            pattern.clone()
5868                        } else {
5869                            expanded.join(" ")
5870                        }
5871                    }
5872                    Err(_) => pattern.clone(),
5873                }
5874            }
5875            ShellWord::Concat(parts) => self.expand_concat_parallel(parts),
5876            ShellWord::CommandSub(cmd) => self.execute_command_substitution(cmd),
5877            ShellWord::ProcessSubIn(cmd) => self.execute_process_sub_in(cmd),
5878            ShellWord::ProcessSubOut(cmd) => self.execute_process_sub_out(cmd),
5879            ShellWord::ArithSub(expr) => self.evaluate_arithmetic(expr),
5880            ShellWord::ArrayVar(name, index) => self.expand_array_access(name, index),
5881            ShellWord::ArrayLiteral(elements) => elements
5882                .iter()
5883                .map(|e| self.expand_word(e))
5884                .collect::<Vec<_>>()
5885                .join(" "),
5886        }
5887    }
5888
5889    /// Pre-launch external command substitutions from a word list onto the worker pool.
5890    /// Returns a Vec aligned with `words` — Some(receiver) for pre-launched externals, None otherwise.
5891    fn preflight_command_subs(
5892        &mut self,
5893        words: &[ShellWord],
5894    ) -> Vec<Option<crossbeam_channel::Receiver<String>>> {
5895        use crate::parser::ShellWord;
5896        use std::process::{Command, Stdio};
5897
5898        let mut receivers = Vec::with_capacity(words.len());
5899
5900        // Count external command subs — don't bother with pool overhead for just one
5901        let external_count = words
5902            .iter()
5903            .filter(|w| {
5904                if let ShellWord::CommandSub(cmd) = w {
5905                    if let ShellCommand::Simple(simple) = cmd.as_ref() {
5906                        if let Some(first) = simple.words.first() {
5907                            let name = self.expand_word(first);
5908                            return !self.functions.contains_key(&name) && !self.is_builtin(&name);
5909                        }
5910                    }
5911                }
5912                false
5913            })
5914            .count();
5915
5916        if external_count < 2 {
5917            // Not worth parallelizing — fall through to sequential
5918            return vec![None; words.len()];
5919        }
5920
5921        for word in words {
5922            if let ShellWord::CommandSub(cmd) = word {
5923                if let ShellCommand::Simple(simple) = cmd.as_ref() {
5924                    let first = simple.words.first().map(|w| self.expand_word(w));
5925                    if let Some(ref name) = first {
5926                        if !self.functions.contains_key(name) && !self.is_builtin(name) {
5927                            let expanded: Vec<String> =
5928                                simple.words.iter().map(|w| self.expand_word(w)).collect();
5929                            let rx = self.worker_pool.submit_with_result(move || {
5930                                let output = Command::new(&expanded[0])
5931                                    .args(&expanded[1..])
5932                                    .stdout(Stdio::piped())
5933                                    .stderr(Stdio::inherit())
5934                                    .output();
5935                                match output {
5936                                    Ok(out) => String::from_utf8_lossy(&out.stdout)
5937                                        .trim_end_matches('\n')
5938                                        .to_string(),
5939                                    Err(_) => String::new(),
5940                                }
5941                            });
5942                            receivers.push(Some(rx));
5943                            continue;
5944                        }
5945                    }
5946                }
5947            }
5948            receivers.push(None);
5949        }
5950
5951        receivers
5952    }
5953
5954    /// Expand a Concat word list, launching external command substitutions in parallel.
5955    /// Internal subs (builtins/functions) still run sequentially on the main thread.
5956    fn expand_concat_parallel(&mut self, parts: &[ShellWord]) -> String {
5957        use crate::parser::ShellWord;
5958        use std::process::{Command, Stdio};
5959
5960        // Phase 1: identify external command subs and pre-launch them
5961        let mut preflight: Vec<Option<crossbeam_channel::Receiver<String>>> =
5962            Vec::with_capacity(parts.len());
5963
5964        for part in parts {
5965            if let ShellWord::CommandSub(cmd) = part {
5966                if let ShellCommand::Simple(simple) = cmd.as_ref() {
5967                    let first = simple.words.first().map(|w| self.expand_word(w));
5968                    if let Some(ref name) = first {
5969                        if !self.functions.contains_key(name) && !self.is_builtin(name) {
5970                            // External command — pre-launch on background thread
5971                            let words: Vec<String> =
5972                                simple.words.iter().map(|w| self.expand_word(w)).collect();
5973                            let rx = self.worker_pool.submit_with_result(move || {
5974                                let output = Command::new(&words[0])
5975                                    .args(&words[1..])
5976                                    .stdout(Stdio::piped())
5977                                    .stderr(Stdio::inherit())
5978                                    .output();
5979                                match output {
5980                                    Ok(out) => String::from_utf8_lossy(&out.stdout)
5981                                        .trim_end_matches('\n')
5982                                        .to_string(),
5983                                    Err(_) => String::new(),
5984                                }
5985                            });
5986                            preflight.push(Some(rx));
5987                            continue;
5988                        }
5989                    }
5990                }
5991            }
5992            preflight.push(None); // not pre-launched
5993        }
5994
5995        // Phase 2: collect results in order, using pre-launched receivers where available
5996        let mut result = String::new();
5997        for (i, part) in parts.iter().enumerate() {
5998            if let Some(rx) = preflight[i].take() {
5999                // Pre-launched external command sub — collect result
6000                result.push_str(&rx.recv().unwrap_or_default());
6001            } else {
6002                // Everything else — expand sequentially (may be internal sub, variable, literal)
6003                result.push_str(&self.expand_word(part));
6004            }
6005        }
6006        result
6007    }
6008
6009    fn expand_braced_variable(&mut self, content: &str) -> String {
6010        // Handle nested expansion: ${${inner}[subscript]} or ${${inner}modifier}
6011        if content.starts_with("${") {
6012            // Find matching closing brace for inner expansion
6013            let mut depth = 0;
6014            let mut inner_end = 0;
6015            for (i, c) in content.char_indices() {
6016                match c {
6017                    '{' => depth += 1,
6018                    '}' => {
6019                        depth -= 1;
6020                        if depth == 0 {
6021                            inner_end = i;
6022                            break;
6023                        }
6024                    }
6025                    _ => {}
6026                }
6027            }
6028
6029            if inner_end > 0 {
6030                // Expand the inner ${...}
6031                let inner_content = &content[2..inner_end];
6032                let inner_result = self.expand_braced_variable(inner_content);
6033
6034                // Check for subscript or modifier after the inner expansion
6035                let rest = &content[inner_end + 1..];
6036                if rest.starts_with('[') {
6037                    // Apply subscript to result: ${${...}[idx]}
6038                    if let Some(bracket_end) = rest.find(']') {
6039                        let index = &rest[1..bracket_end];
6040                        if let Ok(idx) = index.parse::<i64>() {
6041                            let chars: Vec<char> = inner_result.chars().collect();
6042                            let actual_idx = if idx < 0 {
6043                                (chars.len() as i64 + idx).max(0) as usize
6044                            } else if idx > 0 {
6045                                (idx - 1) as usize
6046                            } else {
6047                                0
6048                            };
6049                            return chars
6050                                .get(actual_idx)
6051                                .map(|c| c.to_string())
6052                                .unwrap_or_default();
6053                        }
6054                    }
6055                }
6056
6057                return inner_result;
6058            }
6059        }
6060
6061        // Handle zsh-style parameter expansion flags ${(flags)var}
6062        if content.starts_with('(') {
6063            if let Some(close_paren) = content.find(')') {
6064                let flags_str = &content[1..close_paren];
6065                let rest = &content[close_paren + 1..];
6066                let flags = self.parse_zsh_flags(flags_str);
6067
6068                // Check for (M) match flag
6069                let has_match_flag = flags.iter().any(|f| matches!(f, ZshParamFlag::Match));
6070
6071                // Handle ${(M)var:#pattern} - pattern filter with flags
6072                if let Some(filter_pos) = rest.find(":#") {
6073                    let var_name = &rest[..filter_pos];
6074                    let pattern = &rest[filter_pos + 2..];
6075
6076                    // Array path: filter each element against pattern
6077                    if let Some(arr) = self.arrays.get(var_name).cloned() {
6078                        let filtered: Vec<String> = if arr.len() >= 1000 {
6079                            tracing::trace!(
6080                                count = arr.len(),
6081                                pattern,
6082                                "using parallel filter (rayon) for large array"
6083                            );
6084                            use rayon::prelude::*;
6085                            let pattern = pattern.to_string();
6086                            arr.into_par_iter()
6087                                .filter(|elem| {
6088                                    let m = Self::glob_match_static(elem, &pattern);
6089                                    if has_match_flag {
6090                                        m
6091                                    } else {
6092                                        !m
6093                                    }
6094                                })
6095                                .collect()
6096                        } else {
6097                            arr.into_iter()
6098                                .filter(|elem| {
6099                                    let m = self.glob_match(elem, pattern);
6100                                    if has_match_flag {
6101                                        m
6102                                    } else {
6103                                        !m
6104                                    }
6105                                })
6106                                .collect()
6107                        };
6108                        return filtered.join(" ");
6109                    }
6110
6111                    // Scalar path: original behavior
6112                    let val = self.get_variable(var_name);
6113                    let matches = self.glob_match(&val, pattern);
6114
6115                    return if has_match_flag {
6116                        if matches {
6117                            val
6118                        } else {
6119                            String::new()
6120                        }
6121                    } else {
6122                        if matches {
6123                            String::new()
6124                        } else {
6125                            val
6126                        }
6127                    };
6128                }
6129
6130                // Handle ${(%):-%n} style - empty var with default after flags
6131                // rest could be ":-%n" or ":-default" or "var:-default" or just "var"
6132                let (var_name, default_val) = if rest.starts_with(":-") {
6133                    // Empty variable name with default: ${(%):-default}
6134                    ("", Some(&rest[2..]))
6135                } else if let Some(pos) = rest.find(":-") {
6136                    // Variable with default: ${(%)var:-default}
6137                    (&rest[..pos], Some(&rest[pos + 2..]))
6138                } else if rest.starts_with(':') {
6139                    // Just ":" means empty var name, no default
6140                    ("", None)
6141                } else {
6142                    // Normal variable reference
6143                    let vn = rest
6144                        .split(|c: char| !c.is_alphanumeric() && c != '_')
6145                        .next()
6146                        .unwrap_or("");
6147                    (vn, None)
6148                };
6149
6150                let mut val = self.get_variable(var_name);
6151
6152                // Use default if variable is empty
6153                if val.is_empty() {
6154                    if let Some(def) = default_val {
6155                        // Expand the default value (handles $var and other expansions)
6156                        val = self.expand_string(def);
6157                    }
6158                }
6159
6160                // Apply flags in order
6161                for flag in &flags {
6162                    val = self.apply_zsh_param_flag(&val, var_name, flag);
6163                }
6164                return val;
6165            }
6166        }
6167
6168        // Handle ${#arr[@]} - array length
6169        if content.starts_with('#') {
6170            let rest = &content[1..];
6171            if let Some(bracket_start) = rest.find('[') {
6172                let var_name = &rest[..bracket_start];
6173                let bracket_content = &rest[bracket_start + 1..];
6174                if let Some(bracket_end) = bracket_content.find(']') {
6175                    let index = &bracket_content[..bracket_end];
6176                    if index == "@" || index == "*" {
6177                        // ${#arr[@]} - return array length
6178                        return self
6179                            .arrays
6180                            .get(var_name)
6181                            .map(|arr| arr.len().to_string())
6182                            .unwrap_or_else(|| "0".to_string());
6183                    }
6184                }
6185            }
6186            // ${#arr} - if rest is an array name, return array length
6187            if self.arrays.contains_key(rest) {
6188                return self
6189                    .arrays
6190                    .get(rest)
6191                    .map(|arr| arr.len().to_string())
6192                    .unwrap_or_else(|| "0".to_string());
6193            }
6194            // ${#assoc} - if rest is an assoc array name, return assoc length
6195            if self.assoc_arrays.contains_key(rest) {
6196                return self
6197                    .assoc_arrays
6198                    .get(rest)
6199                    .map(|h| h.len().to_string())
6200                    .unwrap_or_else(|| "0".to_string());
6201            }
6202            // ${#var} - string length
6203            let val = self.get_variable(rest);
6204            return val.len().to_string();
6205        }
6206
6207        // Handle ${+var} and ${+arr[key]} - test if variable/element is set (returns 1 if set, 0 if not)
6208        if content.starts_with('+') {
6209            let rest = &content[1..];
6210
6211            // Check for array/assoc access: ${+arr[key]}
6212            if let Some(bracket_start) = rest.find('[') {
6213                let var_name = &rest[..bracket_start];
6214                let bracket_content = &rest[bracket_start + 1..];
6215                if let Some(bracket_end) = bracket_content.find(']') {
6216                    let key = &bracket_content[..bracket_end];
6217
6218                    // Check special arrays first
6219                    if let Some(val) = self.get_special_array_value(var_name, key) {
6220                        return if val.is_empty() {
6221                            "0".to_string()
6222                        } else {
6223                            "1".to_string()
6224                        };
6225                    }
6226
6227                    // Check user assoc arrays
6228                    if self.assoc_arrays.contains_key(var_name) {
6229                        let expanded_key = self.expand_string(key);
6230                        let has_key = self
6231                            .assoc_arrays
6232                            .get(var_name)
6233                            .map(|a| a.contains_key(&expanded_key))
6234                            .unwrap_or(false);
6235                        return if has_key {
6236                            "1".to_string()
6237                        } else {
6238                            "0".to_string()
6239                        };
6240                    }
6241
6242                    // Check regular arrays
6243                    if let Some(arr) = self.arrays.get(var_name) {
6244                        if let Ok(idx) = key.parse::<usize>() {
6245                            let actual_idx = if idx > 0 { idx - 1 } else { 0 };
6246                            return if arr.get(actual_idx).is_some() {
6247                                "1".to_string()
6248                            } else {
6249                                "0".to_string()
6250                            };
6251                        }
6252                    }
6253
6254                    return "0".to_string();
6255                }
6256            }
6257
6258            // Simple variable: ${+var}
6259            let is_set = self.variables.contains_key(rest)
6260                || self.arrays.contains_key(rest)
6261                || self.assoc_arrays.contains_key(rest)
6262                || std::env::var(rest).is_ok()
6263                || self.functions.contains_key(rest);
6264            return if is_set {
6265                "1".to_string()
6266            } else {
6267                "0".to_string()
6268            };
6269        }
6270
6271        // Handle ${arr[idx]} or ${assoc[key]}
6272        if let Some(bracket_start) = content.find('[') {
6273            let var_name = &content[..bracket_start];
6274            let bracket_content = &content[bracket_start + 1..];
6275            if let Some(bracket_end) = bracket_content.find(']') {
6276                let index = &bracket_content[..bracket_end];
6277
6278                // Check for zsh/parameter special associative arrays (options, commands, etc.)
6279                if let Some(val) = self.get_special_array_value(var_name, index) {
6280                    return val;
6281                }
6282
6283                // Check if it's a user-defined associative array
6284                if self.assoc_arrays.contains_key(var_name) {
6285                    if index == "@" || index == "*" {
6286                        // ${assoc[@]} - return all values
6287                        return self
6288                            .assoc_arrays
6289                            .get(var_name)
6290                            .map(|a| a.values().cloned().collect::<Vec<_>>().join(" "))
6291                            .unwrap_or_default();
6292                    } else {
6293                        // ${assoc[key]} - return value for key
6294                        let key = self.expand_string(index);
6295                        return self
6296                            .assoc_arrays
6297                            .get(var_name)
6298                            .and_then(|a| a.get(&key).cloned())
6299                            .unwrap_or_default();
6300                    }
6301                }
6302
6303                // Regular indexed array
6304                if index == "@" || index == "*" {
6305                    // ${arr[@]} - return all elements
6306                    return self
6307                        .arrays
6308                        .get(var_name)
6309                        .map(|arr| arr.join(" "))
6310                        .unwrap_or_default();
6311                }
6312
6313                // Use the ported subscript module for comprehensive index parsing
6314                use crate::subscript::{
6315                    get_array_by_subscript, get_array_element_by_subscript, getindex,
6316                };
6317                let ksh_arrays = self.options.get("ksh_arrays").copied().unwrap_or(false);
6318
6319                if let Ok(v) = getindex(index, false, ksh_arrays) {
6320                    // Check if it's an array first
6321                    if let Some(arr) = self.arrays.get(var_name) {
6322                        if v.is_all() {
6323                            return arr.join(" ");
6324                        }
6325                        // Check if this is a range (comma in subscript) vs single element
6326                        // For a single element, v.end == v.start + 1 after adjustment
6327                        // But for negative single indices, we need to handle specially
6328                        let is_range = index.contains(',');
6329                        if is_range {
6330                            // Range: ${arr[2,4]} returns elements 2 through 4
6331                            return get_array_by_subscript(arr, &v, ksh_arrays).join(" ");
6332                        } else {
6333                            // Single element (including negative indices like -1)
6334                            return get_array_element_by_subscript(arr, &v, ksh_arrays)
6335                                .unwrap_or_default();
6336                        }
6337                    }
6338
6339                    // Not an array - treat as string subscripting
6340                    let val = self.get_variable(var_name);
6341                    if !val.is_empty() {
6342                        let chars: Vec<char> = val.chars().collect();
6343                        let idx = v.start;
6344                        let actual_idx = if idx < 0 {
6345                            (chars.len() as i64 + idx).max(0) as usize
6346                        } else if idx > 0 {
6347                            (idx - 1) as usize // zsh is 1-indexed
6348                        } else {
6349                            0
6350                        };
6351
6352                        if v.end > v.start + 1 {
6353                            // String range
6354                            let end_idx = if v.end < 0 {
6355                                (chars.len() as i64 + v.end + 1).max(0) as usize
6356                            } else {
6357                                v.end as usize
6358                            };
6359                            let end_idx = end_idx.min(chars.len());
6360                            return chars[actual_idx..end_idx].iter().collect();
6361                        } else {
6362                            return chars
6363                                .get(actual_idx)
6364                                .map(|c| c.to_string())
6365                                .unwrap_or_default();
6366                        }
6367                    }
6368                    return String::new();
6369                }
6370
6371                // Non-numeric index on non-assoc - return empty
6372                return String::new();
6373            }
6374        }
6375
6376        // Handle ${var:-default}, ${var:=default}, ${var:?error}, ${var:+alternate}
6377        if let Some(colon_pos) = content.find(':') {
6378            let var_name = &content[..colon_pos];
6379            let rest = &content[colon_pos + 1..];
6380            let val = self.get_variable(var_name);
6381            let val_opt = if val.is_empty() {
6382                None
6383            } else {
6384                Some(val.clone())
6385            };
6386
6387            if rest.starts_with('-') {
6388                // ${var:-default}
6389                return match val_opt {
6390                    Some(v) if !v.is_empty() => v,
6391                    _ => self.expand_string(&rest[1..]),
6392                };
6393            } else if rest.starts_with('=') {
6394                // ${var:=default}
6395                return match val_opt {
6396                    Some(v) if !v.is_empty() => v,
6397                    _ => {
6398                        let default = self.expand_string(&rest[1..]);
6399                        self.variables.insert(var_name.to_string(), default.clone());
6400                        default
6401                    }
6402                };
6403            } else if rest.starts_with('?') {
6404                // ${var:?error}
6405                return match val_opt {
6406                    Some(v) if !v.is_empty() => v,
6407                    _ => {
6408                        let msg = self.expand_string(&rest[1..]);
6409                        eprintln!("zshrs: {}: {}", var_name, msg);
6410                        String::new()
6411                    }
6412                };
6413            } else if rest.starts_with('+') {
6414                // ${var:+alternate}
6415                return match val_opt {
6416                    Some(v) if !v.is_empty() => self.expand_string(&rest[1..]),
6417                    _ => String::new(),
6418                };
6419            } else if rest.starts_with('#') {
6420                // ${var:#pattern} - filter: remove elements matching pattern
6421                // With (M) flag, keep only matching elements
6422                let pattern = &rest[1..];
6423                // For scalars, return empty if matches, value if not
6424                if self.glob_match(&val, pattern) {
6425                    return String::new();
6426                } else {
6427                    return val;
6428                }
6429            } else if self.is_history_modifier(rest) {
6430                // Handle history-style modifiers: :A, :h, :t, :r, :e, :l, :u, :q, :Q
6431                // These can be chained: ${var:A:h:h}
6432                return self.apply_history_modifiers(&val, rest);
6433            } else if rest
6434                .chars()
6435                .next()
6436                .map(|c| c.is_ascii_digit() || c == '-')
6437                .unwrap_or(false)
6438            {
6439                // ${var:offset} or ${var:offset:length}
6440                let parts: Vec<&str> = rest.splitn(2, ':').collect();
6441                let offset: i64 = parts[0].parse().unwrap_or(0);
6442                let length: Option<usize> = parts.get(1).and_then(|s| s.parse().ok());
6443
6444                let start = if offset < 0 {
6445                    (val.len() as i64 + offset).max(0) as usize
6446                } else {
6447                    (offset as usize).min(val.len())
6448                };
6449
6450                return if let Some(len) = length {
6451                    val.chars().skip(start).take(len).collect()
6452                } else {
6453                    val.chars().skip(start).collect()
6454                };
6455            }
6456        }
6457
6458        // Handle ${var/pattern/replacement} and ${var//pattern/replacement}
6459        // Only if the part before / is a valid variable name
6460        if let Some(slash_pos) = content.find('/') {
6461            let var_name = &content[..slash_pos];
6462            // Variable names must start with letter/underscore and contain only alnum/_
6463            if !var_name.is_empty()
6464                && var_name
6465                    .chars()
6466                    .next()
6467                    .map(|c| c.is_alphabetic() || c == '_')
6468                    .unwrap_or(false)
6469                && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
6470            {
6471                let rest = &content[slash_pos + 1..];
6472                let val = self.get_variable(var_name);
6473
6474                let replace_all = rest.starts_with('/');
6475                let rest = if replace_all { &rest[1..] } else { rest };
6476
6477                let parts: Vec<&str> = rest.splitn(2, '/').collect();
6478                let pattern = parts.get(0).unwrap_or(&"");
6479                let replacement = parts.get(1).unwrap_or(&"");
6480
6481                return if replace_all {
6482                    val.replace(pattern, replacement)
6483                } else {
6484                    val.replacen(pattern, replacement, 1)
6485                };
6486            }
6487        }
6488
6489        // Handle ${var#pattern} and ${var##pattern} - remove prefix
6490        // But only if the # is not at the start (which would be length)
6491        if let Some(hash_pos) = content.find('#') {
6492            if hash_pos > 0 {
6493                let var_name = &content[..hash_pos];
6494                // Make sure var_name looks like a valid variable name
6495                if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
6496                    let rest = &content[hash_pos + 1..];
6497                    let val = self.get_variable(var_name);
6498
6499                    let long = rest.starts_with('#');
6500                    let pattern = if long { &rest[1..] } else { rest };
6501
6502                    // Convert shell glob pattern to regex-style for matching prefixes
6503                    let pattern_regex = regex::escape(pattern)
6504                        .replace(r"\*", ".*")
6505                        .replace(r"\?", ".");
6506                    let full_pattern = format!("^{}", pattern_regex);
6507
6508                    if let Some(re) = cached_regex(&full_pattern) {
6509                        if long {
6510                            // Remove longest prefix match - find all matches and use the longest
6511                            let mut longest_end = 0;
6512                            for m in re.find_iter(&val) {
6513                                if m.end() > longest_end {
6514                                    longest_end = m.end();
6515                                }
6516                            }
6517                            if longest_end > 0 {
6518                                return val[longest_end..].to_string();
6519                            }
6520                        } else {
6521                            // Remove shortest prefix match
6522                            if let Some(m) = re.find(&val) {
6523                                return val[m.end()..].to_string();
6524                            }
6525                        }
6526                    }
6527                    return val;
6528                }
6529            }
6530        }
6531
6532        // Handle ${var%pattern} and ${var%%pattern} - remove suffix
6533        if let Some(pct_pos) = content.find('%') {
6534            if pct_pos > 0 {
6535                let var_name = &content[..pct_pos];
6536                if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
6537                    let rest = &content[pct_pos + 1..];
6538                    let val = self.get_variable(var_name);
6539
6540                    let long = rest.starts_with('%');
6541                    let pattern = if long { &rest[1..] } else { rest };
6542
6543                    // Use glob pattern matching for suffix removal
6544                    if let Ok(glob) = glob::Pattern::new(pattern) {
6545                        if long {
6546                            // Remove longest suffix match - find earliest matching position
6547                            for i in 0..=val.len() {
6548                                if glob.matches(&val[i..]) {
6549                                    return val[..i].to_string();
6550                                }
6551                            }
6552                        } else {
6553                            // Remove shortest suffix match - find latest matching position
6554                            for i in (0..=val.len()).rev() {
6555                                if glob.matches(&val[i..]) {
6556                                    return val[..i].to_string();
6557                                }
6558                            }
6559                        }
6560                    }
6561                    return val;
6562                }
6563            }
6564        }
6565
6566        // Handle ${var^} and ${var^^} - uppercase
6567        if let Some(caret_pos) = content.find('^') {
6568            let var_name = &content[..caret_pos];
6569            let val = self.get_variable(var_name);
6570            let all = content[caret_pos + 1..].starts_with('^');
6571
6572            return if all {
6573                val.to_uppercase()
6574            } else {
6575                let mut chars = val.chars();
6576                match chars.next() {
6577                    Some(first) => first.to_uppercase().to_string() + chars.as_str(),
6578                    None => String::new(),
6579                }
6580            };
6581        }
6582
6583        // Handle ${var,} and ${var,,} - lowercase
6584        if let Some(comma_pos) = content.find(',') {
6585            let var_name = &content[..comma_pos];
6586            let val = self.get_variable(var_name);
6587            let all = content[comma_pos + 1..].starts_with(',');
6588
6589            return if all {
6590                val.to_lowercase()
6591            } else {
6592                let mut chars = val.chars();
6593                match chars.next() {
6594                    Some(first) => first.to_lowercase().to_string() + chars.as_str(),
6595                    None => String::new(),
6596                }
6597            };
6598        }
6599
6600        // Handle ${!prefix*} and ${!prefix@} - expand to variable names with prefix
6601        if content.starts_with('!') {
6602            let rest = &content[1..];
6603            if rest.ends_with('*') || rest.ends_with('@') {
6604                let prefix = &rest[..rest.len() - 1];
6605                let mut matches: Vec<String> = self
6606                    .variables
6607                    .keys()
6608                    .filter(|k| k.starts_with(prefix))
6609                    .cloned()
6610                    .collect();
6611                // Also check arrays
6612                for k in self.arrays.keys() {
6613                    if k.starts_with(prefix) && !matches.contains(k) {
6614                        matches.push(k.clone());
6615                    }
6616                }
6617                matches.sort();
6618                return matches.join(" ");
6619            }
6620
6621            // ${!var} - indirect expansion
6622            let var_name = self.get_variable(rest);
6623            return self.get_variable(&var_name);
6624        }
6625
6626        // Default: just get the variable
6627        self.get_variable(content)
6628    }
6629
6630    fn expand_array_access(&mut self, name: &str, index: &ShellWord) -> String {
6631        use crate::subscript::{get_array_by_subscript, get_array_element_by_subscript, getindex};
6632
6633        let idx_str = self.expand_word(index);
6634        let ksh_arrays = self.options.get("ksh_arrays").copied().unwrap_or(false);
6635
6636        // Use the ported subscript module for index parsing
6637        match getindex(&idx_str, false, ksh_arrays) {
6638            Ok(v) => {
6639                if let Some(arr) = self.arrays.get(name) {
6640                    if v.is_all() {
6641                        arr.join(" ")
6642                    } else if v.start == v.end - 1 {
6643                        // Single element
6644                        get_array_element_by_subscript(arr, &v, ksh_arrays).unwrap_or_default()
6645                    } else {
6646                        // Range
6647                        get_array_by_subscript(arr, &v, ksh_arrays).join(" ")
6648                    }
6649                } else {
6650                    String::new()
6651                }
6652            }
6653            Err(_) => String::new(),
6654        }
6655    }
6656
6657    #[tracing::instrument(level = "trace", skip_all)]
6658    fn expand_string(&mut self, s: &str) -> String {
6659        let mut result = String::new();
6660        let mut chars = s.chars().peekable();
6661
6662        while let Some(c) = chars.next() {
6663            // \x00 prefix marks chars from single quotes - keep them literal
6664            if c == '\x00' {
6665                if let Some(literal_char) = chars.next() {
6666                    result.push(literal_char);
6667                }
6668                continue;
6669            }
6670            if c == '$' {
6671                if chars.peek() == Some(&'(') {
6672                    chars.next(); // consume '('
6673
6674                    // Check for $(( )) arithmetic
6675                    if chars.peek() == Some(&'(') {
6676                        chars.next(); // consume second '('
6677                        let expr = Self::collect_until_double_paren(&mut chars);
6678                        result.push_str(&self.evaluate_arithmetic(&expr));
6679                    } else {
6680                        // Command substitution $(...)
6681                        let cmd_str = Self::collect_until_paren(&mut chars);
6682                        result.push_str(&self.run_command_substitution(&cmd_str));
6683                    }
6684                } else if chars.peek() == Some(&'{') {
6685                    chars.next();
6686                    // Collect the full braced expression including brackets
6687                    let mut brace_content = String::new();
6688                    let mut depth = 1;
6689                    while let Some(c) = chars.next() {
6690                        if c == '{' {
6691                            depth += 1;
6692                            brace_content.push(c);
6693                        } else if c == '}' {
6694                            depth -= 1;
6695                            if depth == 0 {
6696                                break;
6697                            }
6698                            brace_content.push(c);
6699                        } else {
6700                            brace_content.push(c);
6701                        }
6702                    }
6703                    result.push_str(&self.expand_braced_variable(&brace_content));
6704                } else {
6705                    // Check for single-char special vars first: $$, $!, $-
6706                    if matches!(chars.peek(), Some(&'$') | Some(&'!') | Some(&'-')) {
6707                        let sc = chars.next().unwrap();
6708                        result.push_str(&self.get_variable(&sc.to_string()));
6709                        continue;
6710                    }
6711                    // $#name → ${#name} (string/array length)
6712                    if chars.peek() == Some(&'#') {
6713                        let mut peek_iter = chars.clone();
6714                        peek_iter.next(); // skip #
6715                        if peek_iter
6716                            .peek()
6717                            .map(|c| c.is_alphabetic() || *c == '_')
6718                            .unwrap_or(false)
6719                        {
6720                            chars.next(); // consume #
6721                            let mut name = String::new();
6722                            while let Some(&c) = chars.peek() {
6723                                if c.is_alphanumeric() || c == '_' {
6724                                    name.push(chars.next().unwrap());
6725                                } else {
6726                                    break;
6727                                }
6728                            }
6729                            // Return length of variable or array
6730                            let len = if let Some(arr) = self.arrays.get(&name) {
6731                                arr.len()
6732                            } else {
6733                                self.get_variable(&name).len()
6734                            };
6735                            result.push_str(&len.to_string());
6736                            continue;
6737                        }
6738                    }
6739                    let mut var_name = String::new();
6740                    while let Some(&c) = chars.peek() {
6741                        if c.is_alphanumeric()
6742                            || c == '_'
6743                            || c == '@'
6744                            || c == '*'
6745                            || c == '#'
6746                            || c == '?'
6747                        {
6748                            var_name.push(chars.next().unwrap());
6749                            // Handle single-char special vars
6750                            if matches!(
6751                                var_name.as_str(),
6752                                "@" | "*"
6753                                    | "#"
6754                                    | "?"
6755                                    | "$"
6756                                    | "!"
6757                                    | "-"
6758                                    | "0"
6759                                    | "1"
6760                                    | "2"
6761                                    | "3"
6762                                    | "4"
6763                                    | "5"
6764                                    | "6"
6765                                    | "7"
6766                                    | "8"
6767                                    | "9"
6768                            ) {
6769                                break;
6770                            }
6771                        } else {
6772                            break;
6773                        }
6774                    }
6775                    result.push_str(&self.get_variable(&var_name));
6776                }
6777            } else if c == '`' {
6778                // Backtick command substitution
6779                let cmd_str: String = chars.by_ref().take_while(|&c| c != '`').collect();
6780                result.push_str(&self.run_command_substitution(&cmd_str));
6781            } else if c == '<' && chars.peek() == Some(&'(') {
6782                // Process substitution <(cmd)
6783                chars.next(); // consume '('
6784                let cmd_str = Self::collect_until_paren(&mut chars);
6785                result.push_str(&self.run_process_sub_in(&cmd_str));
6786            } else if c == '>' && chars.peek() == Some(&'(') {
6787                // Process substitution >(cmd)
6788                chars.next(); // consume '('
6789                let cmd_str = Self::collect_until_paren(&mut chars);
6790                result.push_str(&self.run_process_sub_out(&cmd_str));
6791            } else if c == '~' && result.is_empty() {
6792                if let Some(home) = dirs::home_dir() {
6793                    result.push_str(&home.to_string_lossy());
6794                } else {
6795                    result.push(c);
6796                }
6797            } else {
6798                result.push(c);
6799            }
6800        }
6801
6802        result
6803    }
6804
6805    fn collect_until_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
6806        let mut result = String::new();
6807        let mut depth = 1;
6808
6809        while let Some(c) = chars.next() {
6810            if c == '(' {
6811                depth += 1;
6812                result.push(c);
6813            } else if c == ')' {
6814                depth -= 1;
6815                if depth == 0 {
6816                    break;
6817                }
6818                result.push(c);
6819            } else {
6820                result.push(c);
6821            }
6822        }
6823
6824        result
6825    }
6826
6827    fn collect_until_double_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
6828        let mut result = String::new();
6829        let mut arith_depth = 1; // Tracks $(( ... )) nesting
6830        let mut paren_depth = 0; // Tracks ( ... ) nesting within expression
6831
6832        while let Some(c) = chars.next() {
6833            if c == '(' {
6834                if paren_depth == 0 && chars.peek() == Some(&'(') {
6835                    // Nested $(( - but we need to see if it's really another arithmetic
6836                    // For simplicity, track inner parens
6837                    paren_depth += 1;
6838                    result.push(c);
6839                } else {
6840                    paren_depth += 1;
6841                    result.push(c);
6842                }
6843            } else if c == ')' {
6844                if paren_depth > 0 {
6845                    // Inside nested parens, just close one level
6846                    paren_depth -= 1;
6847                    result.push(c);
6848                } else if chars.peek() == Some(&')') {
6849                    // At top level and seeing )) - this closes our arithmetic
6850                    chars.next();
6851                    arith_depth -= 1;
6852                    if arith_depth == 0 {
6853                        break;
6854                    }
6855                    result.push_str("))");
6856                } else {
6857                    // Single ) at top level - shouldn't happen in valid expression
6858                    result.push(c);
6859                }
6860            } else {
6861                result.push(c);
6862            }
6863        }
6864
6865        result
6866    }
6867
6868    fn run_process_sub_in(&mut self, cmd_str: &str) -> String {
6869        use std::fs;
6870        use std::process::Stdio;
6871
6872        // Parse the command
6873        let mut parser = ShellParser::new(cmd_str);
6874        let commands = match parser.parse_script() {
6875            Ok(cmds) => cmds,
6876            Err(_) => return String::new(),
6877        };
6878
6879        // Create a unique FIFO in temp directory
6880        let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
6881        let fifo_counter = self.process_sub_counter;
6882        self.process_sub_counter += 1;
6883        let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
6884
6885        // Remove if exists, then create FIFO
6886        let _ = fs::remove_file(&fifo_path);
6887        if let Err(_) = nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU) {
6888            return String::new();
6889        }
6890
6891        // Spawn command that writes to the FIFO
6892        let fifo_clone = fifo_path.clone();
6893        if let Some(cmd) = commands.first() {
6894            if let ShellCommand::Simple(simple) = cmd {
6895                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
6896                if !words.is_empty() {
6897                    let cmd_name = words[0].clone();
6898                    let args: Vec<String> = words[1..].to_vec();
6899
6900                    self.worker_pool.submit(move || {
6901                        // Open FIFO for writing (will block until reader connects)
6902                        if let Ok(fifo) = fs::OpenOptions::new().write(true).open(&fifo_clone) {
6903                            let _ = Command::new(&cmd_name)
6904                                .args(&args)
6905                                .stdout(fifo)
6906                                .stderr(Stdio::inherit())
6907                                .status();
6908                        }
6909                        // Clean up FIFO after command completes
6910                        let _ = fs::remove_file(&fifo_clone);
6911                    });
6912                }
6913            }
6914        }
6915
6916        fifo_path
6917    }
6918
6919    fn run_process_sub_out(&mut self, cmd_str: &str) -> String {
6920        use std::fs;
6921        use std::process::Stdio;
6922
6923        // Parse the command
6924        let mut parser = ShellParser::new(cmd_str);
6925        let commands = match parser.parse_script() {
6926            Ok(cmds) => cmds,
6927            Err(_) => return String::new(),
6928        };
6929
6930        // Create a unique FIFO in temp directory
6931        let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
6932        let fifo_counter = self.process_sub_counter;
6933        self.process_sub_counter += 1;
6934        let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
6935
6936        // Remove if exists, then create FIFO
6937        let _ = fs::remove_file(&fifo_path);
6938        if let Err(_) = nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU) {
6939            return String::new();
6940        }
6941
6942        // Spawn command that reads from the FIFO
6943        let fifo_clone = fifo_path.clone();
6944        if let Some(cmd) = commands.first() {
6945            if let ShellCommand::Simple(simple) = cmd {
6946                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
6947                if !words.is_empty() {
6948                    let cmd_name = words[0].clone();
6949                    let args: Vec<String> = words[1..].to_vec();
6950
6951                    self.worker_pool.submit(move || {
6952                        // Open FIFO for reading (will block until writer connects)
6953                        if let Ok(fifo) = fs::File::open(&fifo_clone) {
6954                            let _ = Command::new(&cmd_name)
6955                                .args(&args)
6956                                .stdin(fifo)
6957                                .stdout(Stdio::inherit())
6958                                .stderr(Stdio::inherit())
6959                                .status();
6960                        }
6961                        // Clean up FIFO after command completes
6962                        let _ = fs::remove_file(&fifo_clone);
6963                    });
6964                }
6965            }
6966        }
6967
6968        fifo_path
6969    }
6970
6971    fn run_command_substitution(&mut self, cmd_str: &str) -> String {
6972        use std::process::Stdio;
6973
6974        // Port of getoutput() from Src/exec.c:
6975        // C zsh forks, redirects stdout to a pipe, executes via execode(),
6976        // and the parent reads back the output.  We achieve the same by
6977        // capturing stdout through an in-process pipe.
6978
6979        let mut parser = ShellParser::new(cmd_str);
6980        let commands = match parser.parse_script() {
6981            Ok(cmds) => cmds,
6982            Err(_) => return String::new(),
6983        };
6984
6985        if commands.is_empty() {
6986            return String::new();
6987        }
6988
6989        // Check if this is a simple external-only command (no builtins/functions)
6990        // so we can use the fast path of spawning a child process.
6991        let is_internal = if let ShellCommand::Simple(simple) = &commands[0] {
6992            let first = simple.words.first().map(|w| self.expand_word(w));
6993            if let Some(ref name) = first {
6994                self.functions.contains_key(name) || self.is_builtin(name)
6995            } else {
6996                true
6997            }
6998        } else {
6999            true // compound commands are always internal
7000        };
7001
7002        if is_internal {
7003            // Internal execution: capture stdout via a pipe
7004            let (read_fd, write_fd) = {
7005                let mut fds = [0i32; 2];
7006                if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
7007                    return String::new();
7008                }
7009                (fds[0], fds[1])
7010            };
7011
7012            // Save original stdout and redirect to our pipe
7013            let saved_stdout = unsafe { libc::dup(1) };
7014            unsafe {
7015                libc::dup2(write_fd, 1);
7016            }
7017            unsafe {
7018                libc::close(write_fd);
7019            }
7020
7021            // Execute all commands
7022            for cmd in &commands {
7023                let _ = self.execute_command(cmd);
7024            }
7025
7026            // Flush stdout so buffered output goes to pipe
7027            use std::io::Write;
7028            let _ = io::stdout().flush();
7029
7030            // Restore stdout
7031            unsafe {
7032                libc::dup2(saved_stdout, 1);
7033            }
7034            unsafe {
7035                libc::close(saved_stdout);
7036            }
7037
7038            // Read captured output
7039            use std::os::unix::io::FromRawFd;
7040            let mut output = String::new();
7041            let read_file = unsafe { std::fs::File::from_raw_fd(read_fd) };
7042            use std::io::Read;
7043            let _ = std::io::BufReader::new(read_file).read_to_string(&mut output);
7044
7045            output.trim_end_matches('\n').to_string()
7046        } else {
7047            // External command: spawn child and capture stdout
7048            if let ShellCommand::Simple(simple) = &commands[0] {
7049                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7050                if words.is_empty() {
7051                    return String::new();
7052                }
7053
7054                let output = Command::new(&words[0])
7055                    .args(&words[1..])
7056                    .stdout(Stdio::piped())
7057                    .stderr(Stdio::inherit())
7058                    .output();
7059
7060                match output {
7061                    Ok(out) => String::from_utf8_lossy(&out.stdout)
7062                        .trim_end_matches('\n')
7063                        .to_string(),
7064                    Err(_) => String::new(),
7065                }
7066            } else {
7067                String::new()
7068            }
7069        }
7070    }
7071
7072    /// Process substitution <(cmd) - returns FIFO path
7073    fn execute_process_sub_in(&mut self, cmd: &ShellCommand) -> String {
7074        if let ShellCommand::Simple(simple) = cmd {
7075            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7076            let cmd_str = words.join(" ");
7077            self.run_process_sub_in(&cmd_str)
7078        } else {
7079            String::new()
7080        }
7081    }
7082
7083    /// Process substitution >(cmd) - returns FIFO path
7084    fn execute_process_sub_out(&mut self, cmd: &ShellCommand) -> String {
7085        if let ShellCommand::Simple(simple) = cmd {
7086            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7087            let cmd_str = words.join(" ");
7088            self.run_process_sub_out(&cmd_str)
7089        } else {
7090            String::new()
7091        }
7092    }
7093
7094    /// Get value from zsh/parameter special arrays (options, commands, functions, etc.)
7095    /// Returns Some(value) if this is a special array access, None otherwise
7096    fn get_special_array_value(&self, array_name: &str, key: &str) -> Option<String> {
7097        match array_name {
7098            // === SHELL OPTIONS ===
7099            "options" => {
7100                if key == "@" || key == "*" {
7101                    // Return all options as "name=on/off" pairs
7102                    let opts: Vec<String> = self
7103                        .options
7104                        .iter()
7105                        .map(|(k, v)| format!("{}={}", k, if *v { "on" } else { "off" }))
7106                        .collect();
7107                    return Some(opts.join(" "));
7108                }
7109                let opt_name = key.to_lowercase().replace('_', "");
7110                let is_on = self.options.get(&opt_name).copied().unwrap_or(false);
7111                Some(if is_on {
7112                    "on".to_string()
7113                } else {
7114                    "off".to_string()
7115                })
7116            }
7117
7118            // === ALIASES ===
7119            "aliases" => {
7120                if key == "@" || key == "*" {
7121                    let vals: Vec<String> = self.aliases.values().cloned().collect();
7122                    return Some(vals.join(" "));
7123                }
7124                Some(self.aliases.get(key).cloned().unwrap_or_default())
7125            }
7126            "galiases" => {
7127                if key == "@" || key == "*" {
7128                    let vals: Vec<String> = self.global_aliases.values().cloned().collect();
7129                    return Some(vals.join(" "));
7130                }
7131                Some(self.global_aliases.get(key).cloned().unwrap_or_default())
7132            }
7133            "saliases" => {
7134                if key == "@" || key == "*" {
7135                    let vals: Vec<String> = self.suffix_aliases.values().cloned().collect();
7136                    return Some(vals.join(" "));
7137                }
7138                Some(self.suffix_aliases.get(key).cloned().unwrap_or_default())
7139            }
7140
7141            // === FUNCTIONS ===
7142            "functions" => {
7143                if key == "@" || key == "*" {
7144                    let names: Vec<String> = self.functions.keys().cloned().collect();
7145                    return Some(names.join(" "));
7146                }
7147                if let Some(body) = self.functions.get(key) {
7148                    Some(format!("{:?}", body))
7149                } else {
7150                    Some(String::new())
7151                }
7152            }
7153            "functions_source" => {
7154                // We don't track source locations, return empty
7155                Some(String::new())
7156            }
7157
7158            // === COMMANDS (command hash table) ===
7159            "commands" => {
7160                if key == "@" || key == "*" {
7161                    return Some(String::new()); // Would need to enumerate PATH
7162                }
7163                // Look up command in PATH
7164                if let Some(path) = self.find_in_path(key) {
7165                    Some(path)
7166                } else {
7167                    Some(String::new())
7168                }
7169            }
7170
7171            // === BUILTINS ===
7172            "builtins" => {
7173                let builtins = Self::get_builtin_names();
7174                if key == "@" || key == "*" {
7175                    return Some(builtins.join(" "));
7176                }
7177                if builtins.contains(&key) {
7178                    Some("defined".to_string())
7179                } else {
7180                    Some(String::new())
7181                }
7182            }
7183
7184            // === PARAMETERS ===
7185            "parameters" => {
7186                if key == "@" || key == "*" {
7187                    let mut names: Vec<String> = self.variables.keys().cloned().collect();
7188                    names.extend(self.arrays.keys().cloned());
7189                    names.extend(self.assoc_arrays.keys().cloned());
7190                    return Some(names.join(" "));
7191                }
7192                // Return type of parameter
7193                if self.assoc_arrays.contains_key(key) {
7194                    Some("association".to_string())
7195                } else if self.arrays.contains_key(key) {
7196                    Some("array".to_string())
7197                } else if self.variables.contains_key(key) || std::env::var(key).is_ok() {
7198                    Some("scalar".to_string())
7199                } else {
7200                    Some(String::new())
7201                }
7202            }
7203
7204            // === NAMED DIRECTORIES ===
7205            "nameddirs" => {
7206                if key == "@" || key == "*" {
7207                    let vals: Vec<String> = self
7208                        .named_dirs
7209                        .values()
7210                        .map(|p| p.display().to_string())
7211                        .collect();
7212                    return Some(vals.join(" "));
7213                }
7214                Some(
7215                    self.named_dirs
7216                        .get(key)
7217                        .map(|p| p.display().to_string())
7218                        .unwrap_or_default(),
7219                )
7220            }
7221
7222            // === USER DIRECTORIES ===
7223            "userdirs" => {
7224                if key == "@" || key == "*" {
7225                    return Some(String::new());
7226                }
7227                // Get home directory for user
7228                #[cfg(unix)]
7229                {
7230                    use std::ffi::CString;
7231                    if let Ok(name) = CString::new(key) {
7232                        unsafe {
7233                            let pwd = libc::getpwnam(name.as_ptr());
7234                            if !pwd.is_null() {
7235                                let dir = std::ffi::CStr::from_ptr((*pwd).pw_dir);
7236                                return Some(dir.to_string_lossy().to_string());
7237                            }
7238                        }
7239                    }
7240                }
7241                Some(String::new())
7242            }
7243
7244            // === USER GROUPS ===
7245            "usergroups" => {
7246                if key == "@" || key == "*" {
7247                    return Some(String::new());
7248                }
7249                // Get GID for group name
7250                #[cfg(unix)]
7251                {
7252                    use std::ffi::CString;
7253                    if let Ok(name) = CString::new(key) {
7254                        unsafe {
7255                            let grp = libc::getgrnam(name.as_ptr());
7256                            if !grp.is_null() {
7257                                return Some((*grp).gr_gid.to_string());
7258                            }
7259                        }
7260                    }
7261                }
7262                Some(String::new())
7263            }
7264
7265            // === DIRECTORY STACK ===
7266            "dirstack" => {
7267                if key == "@" || key == "*" {
7268                    let dirs: Vec<String> = self
7269                        .dir_stack
7270                        .iter()
7271                        .map(|p| p.display().to_string())
7272                        .collect();
7273                    return Some(dirs.join(" "));
7274                }
7275                if let Ok(idx) = key.parse::<usize>() {
7276                    Some(
7277                        self.dir_stack
7278                            .get(idx)
7279                            .map(|p| p.display().to_string())
7280                            .unwrap_or_default(),
7281                    )
7282                } else {
7283                    Some(String::new())
7284                }
7285            }
7286
7287            // === JOBS ===
7288            "jobstates" => {
7289                if key == "@" || key == "*" {
7290                    let states: Vec<String> = self
7291                        .jobs
7292                        .iter()
7293                        .map(|(id, job)| format!("{}:{:?}", id, job.state))
7294                        .collect();
7295                    return Some(states.join(" "));
7296                }
7297                if let Ok(id) = key.parse::<usize>() {
7298                    if let Some(job) = self.jobs.get(id) {
7299                        return Some(format!("{:?}", job.state));
7300                    }
7301                }
7302                Some(String::new())
7303            }
7304            "jobtexts" => {
7305                if key == "@" || key == "*" {
7306                    let texts: Vec<String> = self
7307                        .jobs
7308                        .iter()
7309                        .map(|(_, job)| job.command.clone())
7310                        .collect();
7311                    return Some(texts.join(" "));
7312                }
7313                if let Ok(id) = key.parse::<usize>() {
7314                    if let Some(job) = self.jobs.get(id) {
7315                        return Some(job.command.clone());
7316                    }
7317                }
7318                Some(String::new())
7319            }
7320            "jobdirs" => {
7321                // We don't track job directories separately - return current dir
7322                if key == "@" || key == "*" {
7323                    return Some(String::new());
7324                }
7325                Some(String::new())
7326            }
7327
7328            // === HISTORY ===
7329            "history" => {
7330                if key == "@" || key == "*" {
7331                    // Return recent history
7332                    if let Some(ref engine) = self.history {
7333                        if let Ok(entries) = engine.recent(100) {
7334                            let cmds: Vec<String> =
7335                                entries.iter().map(|e| e.command.clone()).collect();
7336                            return Some(cmds.join("\n"));
7337                        }
7338                    }
7339                    return Some(String::new());
7340                }
7341                if let Ok(num) = key.parse::<usize>() {
7342                    if let Some(ref engine) = self.history {
7343                        if let Ok(Some(entry)) = engine.get_by_offset(num.saturating_sub(1)) {
7344                            return Some(entry.command);
7345                        }
7346                    }
7347                }
7348                Some(String::new())
7349            }
7350            "historywords" => {
7351                // Array of words from history - simplified
7352                Some(String::new())
7353            }
7354
7355            // === MODULES ===
7356            "modules" => {
7357                // zshrs doesn't have loadable modules like zsh
7358                // Return empty or fake "loaded" for common modules
7359                if key == "@" || key == "*" {
7360                    return Some("zsh/parameter zsh/zutil".to_string());
7361                }
7362                match key {
7363                    "zsh/parameter" | "zsh/zutil" | "zsh/complete" | "zsh/complist" => {
7364                        Some("loaded".to_string())
7365                    }
7366                    _ => Some(String::new()),
7367                }
7368            }
7369
7370            // === RESERVED WORDS ===
7371            "reswords" => {
7372                let reswords = [
7373                    "do",
7374                    "done",
7375                    "esac",
7376                    "then",
7377                    "elif",
7378                    "else",
7379                    "fi",
7380                    "for",
7381                    "case",
7382                    "if",
7383                    "while",
7384                    "function",
7385                    "repeat",
7386                    "time",
7387                    "until",
7388                    "select",
7389                    "coproc",
7390                    "nocorrect",
7391                    "foreach",
7392                    "end",
7393                    "in",
7394                ];
7395                if key == "@" || key == "*" {
7396                    return Some(reswords.join(" "));
7397                }
7398                if let Ok(idx) = key.parse::<usize>() {
7399                    Some(reswords.get(idx).map(|s| s.to_string()).unwrap_or_default())
7400                } else {
7401                    Some(String::new())
7402                }
7403            }
7404
7405            // === PATCHARS (characters with special meaning in patterns) ===
7406            "patchars" => {
7407                let patchars = ["?", "*", "[", "]", "^", "#", "~", "(", ")", "|"];
7408                if key == "@" || key == "*" {
7409                    return Some(patchars.join(" "));
7410                }
7411                if let Ok(idx) = key.parse::<usize>() {
7412                    Some(patchars.get(idx).map(|s| s.to_string()).unwrap_or_default())
7413                } else {
7414                    Some(String::new())
7415                }
7416            }
7417
7418            // === FUNCTION CALL STACK ===
7419            "funcstack" | "functrace" | "funcfiletrace" | "funcsourcetrace" => {
7420                // Would need call stack tracking - return empty for now
7421                Some(String::new())
7422            }
7423
7424            // === DISABLED VARIANTS (dis_*) ===
7425            "dis_aliases"
7426            | "dis_galiases"
7427            | "dis_saliases"
7428            | "dis_functions"
7429            | "dis_functions_source"
7430            | "dis_builtins"
7431            | "dis_reswords"
7432            | "dis_patchars" => {
7433                // We don't track disabled items - return empty
7434                Some(String::new())
7435            }
7436
7437            // Not a special array
7438            _ => None,
7439        }
7440    }
7441
7442    /// Get list of all builtin command names
7443    fn get_builtin_names() -> Vec<&'static str> {
7444        vec![
7445            ".",
7446            ":",
7447            "[",
7448            "alias",
7449            "autoload",
7450            "bg",
7451            "bind",
7452            "bindkey",
7453            "break",
7454            "builtin",
7455            "bye",
7456            "caller",
7457            "cd",
7458            "cdreplay",
7459            "chdir",
7460            "clone",
7461            "command",
7462            "compadd",
7463            "comparguments",
7464            "compcall",
7465            "compctl",
7466            "compdef",
7467            "compdescribe",
7468            "compfiles",
7469            "compgen",
7470            "compgroups",
7471            "compinit",
7472            "complete",
7473            "compopt",
7474            "compquote",
7475            "compset",
7476            "comptags",
7477            "comptry",
7478            "compvalues",
7479            "continue",
7480            "coproc",
7481            "declare",
7482            "dirs",
7483            "disable",
7484            "disown",
7485            "echo",
7486            "echotc",
7487            "echoti",
7488            "emulate",
7489            "enable",
7490            "eval",
7491            "exec",
7492            "exit",
7493            "export",
7494            "false",
7495            "fc",
7496            "fg",
7497            "float",
7498            "functions",
7499            "getln",
7500            "getopts",
7501            "hash",
7502            "help",
7503            "history",
7504            "integer",
7505            "jobs",
7506            "kill",
7507            "let",
7508            "limit",
7509            "local",
7510            "log",
7511            "logout",
7512            "mapfile",
7513            "noglob",
7514            "popd",
7515            "print",
7516            "printf",
7517            "private",
7518            "prompt",
7519            "promptinit",
7520            "pushd",
7521            "pushln",
7522            "pwd",
7523            "r",
7524            "read",
7525            "readarray",
7526            "readonly",
7527            "rehash",
7528            "return",
7529            "sched",
7530            "set",
7531            "setopt",
7532            "shift",
7533            "shopt",
7534            "source",
7535            "stat",
7536            "strftime",
7537            "suspend",
7538            "test",
7539            "times",
7540            "trap",
7541            "true",
7542            "ttyctl",
7543            "type",
7544            "typeset",
7545            "ulimit",
7546            "umask",
7547            "unalias",
7548            "unfunction",
7549            "unhash",
7550            "unlimit",
7551            "unset",
7552            "unsetopt",
7553            "vared",
7554            "wait",
7555            "whence",
7556            "where",
7557            "which",
7558            "zcompile",
7559            "zcurses",
7560            "zformat",
7561            "zle",
7562            "zmodload",
7563            "zparseopts",
7564            "zprof",
7565            "zpty",
7566            "zregexparse",
7567            "zsocket",
7568            "zstyle",
7569            "ztcp",
7570            "add-zsh-hook",
7571        ]
7572    }
7573
7574    fn get_variable(&self, name: &str) -> String {
7575        // Handle special parameters
7576        match name {
7577            "" => String::new(), // Empty name returns empty
7578            "$" => std::process::id().to_string(),
7579            "@" | "*" => self.positional_params.join(" "),
7580            "#" => self.positional_params.len().to_string(),
7581            "?" => self.last_status.to_string(),
7582            "0" => self
7583                .variables
7584                .get("0")
7585                .cloned()
7586                .unwrap_or_else(|| env::args().next().unwrap_or_default()),
7587            n if !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) => {
7588                let idx: usize = n.parse().unwrap_or(0);
7589                if idx == 0 {
7590                    env::args().next().unwrap_or_default()
7591                } else {
7592                    self.positional_params
7593                        .get(idx - 1)
7594                        .cloned()
7595                        .unwrap_or_default()
7596                }
7597            }
7598            _ => {
7599                // Check local variables first, then arrays, then env
7600                self.variables
7601                    .get(name)
7602                    .cloned()
7603                    .or_else(|| {
7604                        // In zsh, $arr expands to space-joined array elements
7605                        self.arrays.get(name).map(|a| a.join(" "))
7606                    })
7607                    .or_else(|| env::var(name).ok())
7608                    .unwrap_or_default()
7609            }
7610        }
7611    }
7612
7613    fn apply_var_modifier(
7614        &mut self,
7615        name: &str,
7616        val: Option<String>,
7617        modifier: Option<&VarModifier>,
7618    ) -> String {
7619        match modifier {
7620            None => val.unwrap_or_default(),
7621
7622            // ${var:-word} - use default value
7623            Some(VarModifier::Default(word)) => match &val {
7624                Some(v) if !v.is_empty() => v.clone(),
7625                _ => self.expand_word(word),
7626            },
7627
7628            // ${var:=word} - assign default value
7629            Some(VarModifier::DefaultAssign(word)) => match &val {
7630                Some(v) if !v.is_empty() => v.clone(),
7631                _ => self.expand_word(word),
7632            },
7633
7634            // ${var:?word} - error if null or unset
7635            Some(VarModifier::Error(word)) => match &val {
7636                Some(v) if !v.is_empty() => v.clone(),
7637                _ => {
7638                    let msg = self.expand_word(word);
7639                    eprintln!("zshrs: {}", msg);
7640                    String::new()
7641                }
7642            },
7643
7644            // ${var:+word} - use alternate value
7645            Some(VarModifier::Alternate(word)) => match &val {
7646                Some(v) if !v.is_empty() => self.expand_word(word),
7647                _ => String::new(),
7648            },
7649
7650            // ${#var} - string length
7651            Some(VarModifier::Length) => val
7652                .map(|v| v.len().to_string())
7653                .unwrap_or_else(|| "0".to_string()),
7654
7655            // ${var:offset} or ${var:offset:length} - substring
7656            Some(VarModifier::Substring(offset, length)) => {
7657                let v = val.unwrap_or_default();
7658                let start = if *offset < 0 {
7659                    (v.len() as i64 + offset).max(0) as usize
7660                } else {
7661                    (*offset as usize).min(v.len())
7662                };
7663
7664                if let Some(len) = length {
7665                    let len = (*len as usize).min(v.len().saturating_sub(start));
7666                    v.chars().skip(start).take(len).collect()
7667                } else {
7668                    v.chars().skip(start).collect()
7669                }
7670            }
7671
7672            // ${var#pattern} - remove shortest prefix
7673            Some(VarModifier::RemovePrefix(pattern)) => {
7674                let v = val.unwrap_or_default();
7675                let pat = self.expand_word(pattern);
7676                if v.starts_with(&pat) {
7677                    v[pat.len()..].to_string()
7678                } else {
7679                    v
7680                }
7681            }
7682
7683            // ${var##pattern} - remove longest prefix
7684            Some(VarModifier::RemovePrefixLong(pattern)) => {
7685                let v = val.unwrap_or_default();
7686                let pat = self.expand_word(pattern);
7687                // For glob patterns, find longest match from start
7688                if let Ok(glob) = glob::Pattern::new(&pat) {
7689                    for i in (0..=v.len()).rev() {
7690                        if glob.matches(&v[..i]) {
7691                            return v[i..].to_string();
7692                        }
7693                    }
7694                }
7695                v
7696            }
7697
7698            // ${var%pattern} - remove shortest suffix
7699            Some(VarModifier::RemoveSuffix(pattern)) => {
7700                let v = val.unwrap_or_default();
7701                let pat = self.expand_word(pattern);
7702                // For glob patterns, find shortest match from end
7703                if let Ok(glob) = glob::Pattern::new(&pat) {
7704                    for i in (0..=v.len()).rev() {
7705                        if glob.matches(&v[i..]) {
7706                            return v[..i].to_string();
7707                        }
7708                    }
7709                } else if v.ends_with(&pat) {
7710                    return v[..v.len() - pat.len()].to_string();
7711                }
7712                v
7713            }
7714
7715            // ${var%%pattern} - remove longest suffix
7716            Some(VarModifier::RemoveSuffixLong(pattern)) => {
7717                let v = val.unwrap_or_default();
7718                let pat = self.expand_word(pattern);
7719                // For glob patterns, find longest match from end
7720                if let Ok(glob) = glob::Pattern::new(&pat) {
7721                    for i in 0..=v.len() {
7722                        if glob.matches(&v[i..]) {
7723                            return v[..i].to_string();
7724                        }
7725                    }
7726                }
7727                v
7728            }
7729
7730            // ${var/pattern/replacement} - replace first match
7731            Some(VarModifier::Replace(pattern, replacement)) => {
7732                let v = val.unwrap_or_default();
7733                let pat = self.expand_word(pattern);
7734                let repl = self.expand_word(replacement);
7735                v.replacen(&pat, &repl, 1)
7736            }
7737
7738            // ${var//pattern/replacement} - replace all matches
7739            Some(VarModifier::ReplaceAll(pattern, replacement)) => {
7740                let v = val.unwrap_or_default();
7741                let pat = self.expand_word(pattern);
7742                let repl = self.expand_word(replacement);
7743                v.replace(&pat, &repl)
7744            }
7745
7746            // ${var^} or ${var^^} - uppercase
7747            Some(VarModifier::Upper) => val.map(|v| v.to_uppercase()).unwrap_or_default(),
7748
7749            // ${var,} or ${var,,} - lowercase
7750            Some(VarModifier::Lower) => val.map(|v| v.to_lowercase()).unwrap_or_default(),
7751
7752            // ${(flags)var} - zsh parameter expansion flags
7753            Some(VarModifier::ZshFlags(flags)) => {
7754                let mut result = val.unwrap_or_default();
7755                for flag in flags {
7756                    result = self.apply_zsh_param_flag(&result, name, flag);
7757                }
7758                result
7759            }
7760
7761            // Array-related modifiers are handled elsewhere
7762            Some(VarModifier::ArrayLength)
7763            | Some(VarModifier::ArrayIndex(_))
7764            | Some(VarModifier::ArrayAll) => val.unwrap_or_default(),
7765        }
7766    }
7767
7768    /// Check if a string starts with history modifier characters
7769    fn is_history_modifier(&self, s: &str) -> bool {
7770        if s.is_empty() {
7771            return false;
7772        }
7773        let first = s.chars().next().unwrap();
7774        matches!(
7775            first,
7776            'A' | 'a' | 'h' | 't' | 'r' | 'e' | 'l' | 'u' | 'q' | 'Q' | 'P'
7777        )
7778    }
7779
7780    /// Apply zsh history-style modifiers to a value
7781    /// Modifiers can be chained: :A:h:h
7782    fn apply_history_modifiers(&self, val: &str, modifiers: &str) -> String {
7783        let mut result = val.to_string();
7784        let mut chars = modifiers.chars().peekable();
7785
7786        while let Some(c) = chars.next() {
7787            match c {
7788                ':' => continue,
7789                'A' => {
7790                    if let Ok(abs) = std::fs::canonicalize(&result) {
7791                        result = abs.to_string_lossy().to_string();
7792                    } else if !result.starts_with('/') {
7793                        if let Ok(cwd) = std::env::current_dir() {
7794                            result = cwd.join(&result).to_string_lossy().to_string();
7795                        }
7796                    }
7797                }
7798                'a' => {
7799                    if !result.starts_with('/') {
7800                        if let Ok(cwd) = std::env::current_dir() {
7801                            result = cwd.join(&result).to_string_lossy().to_string();
7802                        }
7803                    }
7804                }
7805                'h' => {
7806                    if let Some(pos) = result.rfind('/') {
7807                        if pos == 0 {
7808                            result = "/".to_string();
7809                        } else {
7810                            result = result[..pos].to_string();
7811                        }
7812                    } else {
7813                        result = ".".to_string();
7814                    }
7815                }
7816                't' => {
7817                    if let Some(pos) = result.rfind('/') {
7818                        result = result[pos + 1..].to_string();
7819                    }
7820                }
7821                'r' => {
7822                    if let Some(dot_pos) = result.rfind('.') {
7823                        let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
7824                        if dot_pos > slash_pos {
7825                            result = result[..dot_pos].to_string();
7826                        }
7827                    }
7828                }
7829                'e' => {
7830                    if let Some(dot_pos) = result.rfind('.') {
7831                        let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
7832                        if dot_pos > slash_pos {
7833                            result = result[dot_pos + 1..].to_string();
7834                        } else {
7835                            result = String::new();
7836                        }
7837                    } else {
7838                        result = String::new();
7839                    }
7840                }
7841                'l' => result = result.to_lowercase(),
7842                'u' => result = result.to_uppercase(),
7843                'q' => result = format!("'{}'", result.replace('\'', "'\\''")),
7844                'Q' => {
7845                    if result.starts_with('\'') && result.ends_with('\'') && result.len() >= 2 {
7846                        result = result[1..result.len() - 1].to_string();
7847                    } else if result.starts_with('"') && result.ends_with('"') && result.len() >= 2
7848                    {
7849                        result = result[1..result.len() - 1].to_string();
7850                    }
7851                }
7852                'P' => {
7853                    if let Ok(real) = std::fs::canonicalize(&result) {
7854                        result = real.to_string_lossy().to_string();
7855                    }
7856                }
7857                _ => break,
7858            }
7859        }
7860        result
7861    }
7862
7863    /// Parse zsh parameter expansion flags from a string like "L", "U", "j:,:"
7864    fn parse_zsh_flags(&self, s: &str) -> Vec<ZshParamFlag> {
7865        let mut flags = Vec::new();
7866        let mut chars = s.chars().peekable();
7867
7868        while let Some(c) = chars.next() {
7869            match c {
7870                'L' => flags.push(ZshParamFlag::Lower),
7871                'U' => flags.push(ZshParamFlag::Upper),
7872                'C' => flags.push(ZshParamFlag::Capitalize),
7873                'j' => {
7874                    // j<delim>sep<delim> — join with separator (delim can be any char)
7875                    if let Some(&delim) = chars.peek() {
7876                        chars.next(); // consume delimiter char
7877                        let mut sep = String::new();
7878                        while let Some(&ch) = chars.peek() {
7879                            if ch == delim {
7880                                chars.next();
7881                                break;
7882                            }
7883                            sep.push(chars.next().unwrap());
7884                        }
7885                        flags.push(ZshParamFlag::Join(sep));
7886                    }
7887                }
7888                'F' => flags.push(ZshParamFlag::JoinNewline),
7889                's' => {
7890                    // s:sep: - split on separator
7891                    if chars.peek() == Some(&':') {
7892                        chars.next();
7893                        let mut sep = String::new();
7894                        while let Some(&ch) = chars.peek() {
7895                            if ch == ':' {
7896                                chars.next();
7897                                break;
7898                            }
7899                            sep.push(chars.next().unwrap());
7900                        }
7901                        flags.push(ZshParamFlag::Split(sep));
7902                    }
7903                }
7904                'f' => flags.push(ZshParamFlag::SplitLines),
7905                'z' => flags.push(ZshParamFlag::SplitWords),
7906                't' => flags.push(ZshParamFlag::Type),
7907                'w' => flags.push(ZshParamFlag::Words),
7908                'b' => flags.push(ZshParamFlag::QuoteBackslash),
7909                'q' => {
7910                    if chars.peek() == Some(&'q') {
7911                        chars.next();
7912                        flags.push(ZshParamFlag::DoubleQuote);
7913                    } else {
7914                        flags.push(ZshParamFlag::Quote);
7915                    }
7916                }
7917                'u' => flags.push(ZshParamFlag::Unique),
7918                'O' => flags.push(ZshParamFlag::Reverse),
7919                'o' => flags.push(ZshParamFlag::Sort),
7920                'n' => flags.push(ZshParamFlag::NumericSort),
7921                'a' => flags.push(ZshParamFlag::IndexSort),
7922                'k' => flags.push(ZshParamFlag::Keys),
7923                'v' => flags.push(ZshParamFlag::Values),
7924                '#' => flags.push(ZshParamFlag::Length),
7925                'c' => flags.push(ZshParamFlag::CountChars),
7926                'e' => flags.push(ZshParamFlag::Expand),
7927                '%' => {
7928                    if chars.peek() == Some(&'%') {
7929                        chars.next();
7930                        flags.push(ZshParamFlag::PromptExpandFull);
7931                    } else {
7932                        flags.push(ZshParamFlag::PromptExpand);
7933                    }
7934                }
7935                'V' => flags.push(ZshParamFlag::Visible),
7936                'D' => flags.push(ZshParamFlag::Directory),
7937                'M' => flags.push(ZshParamFlag::Match),
7938                'R' => flags.push(ZshParamFlag::Remove),
7939                'S' => flags.push(ZshParamFlag::Subscript),
7940                'P' => flags.push(ZshParamFlag::Parameter),
7941                '~' => flags.push(ZshParamFlag::Glob),
7942                'l' => {
7943                    // l:len:fill: - pad left
7944                    if chars.peek() == Some(&':') {
7945                        chars.next();
7946                        let mut len_str = String::new();
7947                        while let Some(&ch) = chars.peek() {
7948                            if ch == ':' {
7949                                chars.next();
7950                                break;
7951                            }
7952                            len_str.push(chars.next().unwrap());
7953                        }
7954                        let mut fill = ' ';
7955                        if let Some(&ch) = chars.peek() {
7956                            if ch != ':' {
7957                                fill = chars.next().unwrap();
7958                                if chars.peek() == Some(&':') {
7959                                    chars.next();
7960                                }
7961                            }
7962                        }
7963                        if let Ok(len) = len_str.parse() {
7964                            flags.push(ZshParamFlag::PadLeft(len, fill));
7965                        }
7966                    }
7967                }
7968                'r' => {
7969                    // r:len:fill: - pad right
7970                    if chars.peek() == Some(&':') {
7971                        chars.next();
7972                        let mut len_str = String::new();
7973                        while let Some(&ch) = chars.peek() {
7974                            if ch == ':' {
7975                                chars.next();
7976                                break;
7977                            }
7978                            len_str.push(chars.next().unwrap());
7979                        }
7980                        let mut fill = ' ';
7981                        if let Some(&ch) = chars.peek() {
7982                            if ch != ':' {
7983                                fill = chars.next().unwrap();
7984                                if chars.peek() == Some(&':') {
7985                                    chars.next();
7986                                }
7987                            }
7988                        }
7989                        if let Ok(len) = len_str.parse() {
7990                            flags.push(ZshParamFlag::PadRight(len, fill));
7991                        }
7992                    }
7993                }
7994                'm' => {
7995                    // Width for padding - parse number if present
7996                    let mut width_str = String::new();
7997                    while let Some(&ch) = chars.peek() {
7998                        if ch.is_ascii_digit() {
7999                            width_str.push(chars.next().unwrap());
8000                        } else {
8001                            break;
8002                        }
8003                    }
8004                    if let Ok(w) = width_str.parse() {
8005                        flags.push(ZshParamFlag::Width(w));
8006                    }
8007                }
8008                _ => {}
8009            }
8010        }
8011        flags
8012    }
8013
8014    /// Apply a single zsh parameter expansion flag
8015    fn apply_zsh_param_flag(&self, val: &str, name: &str, flag: &ZshParamFlag) -> String {
8016        match flag {
8017            ZshParamFlag::Lower => val.to_lowercase(),
8018            ZshParamFlag::Upper => val.to_uppercase(),
8019            ZshParamFlag::Capitalize => val
8020                .split_whitespace()
8021                .map(|word| {
8022                    let mut c = word.chars();
8023                    match c.next() {
8024                        None => String::new(),
8025                        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
8026                    }
8027                })
8028                .collect::<Vec<_>>()
8029                .join(" "),
8030            ZshParamFlag::Join(sep) => {
8031                if let Some(arr) = self.arrays.get(name) {
8032                    arr.join(sep)
8033                } else {
8034                    val.to_string()
8035                }
8036            }
8037            ZshParamFlag::Split(sep) => val.split(sep).collect::<Vec<_>>().join(" "),
8038            ZshParamFlag::SplitLines => val.lines().collect::<Vec<_>>().join(" "),
8039            ZshParamFlag::Type => {
8040                if self.arrays.contains_key(name) {
8041                    "array".to_string()
8042                } else if self.assoc_arrays.contains_key(name) {
8043                    "association".to_string()
8044                } else if self.functions.contains_key(name) {
8045                    "function".to_string()
8046                } else if std::env::var(name).is_ok() || self.variables.contains_key(name) {
8047                    "scalar".to_string()
8048                } else {
8049                    "".to_string()
8050                }
8051            }
8052            ZshParamFlag::Words => val.split_whitespace().collect::<Vec<_>>().join(" "),
8053            ZshParamFlag::Quote => format!("'{}'", val.replace('\'', "'\\''")),
8054            ZshParamFlag::DoubleQuote => format!("\"{}\"", val.replace('"', "\\\"")),
8055            ZshParamFlag::Unique => {
8056                // Unique preserves first-occurrence order, so parallel doesn't help.
8057                // For 1000+ elements, pre-allocate the HashSet for less rehashing.
8058                let words: Vec<&str> = val.split_whitespace().collect();
8059                let mut seen = std::collections::HashSet::with_capacity(if words.len() >= 1000 {
8060                    words.len()
8061                } else {
8062                    0
8063                });
8064                if words.len() >= 1000 {
8065                    tracing::trace!(
8066                        count = words.len(),
8067                        "unique on large array ({} elements)",
8068                        words.len()
8069                    );
8070                }
8071                words
8072                    .into_iter()
8073                    .filter(|s| seen.insert(*s))
8074                    .collect::<Vec<_>>()
8075                    .join(" ")
8076            }
8077            ZshParamFlag::Reverse => {
8078                // (O) flag: reverse sort (sort descending)
8079                let mut words: Vec<&str> = val.split_whitespace().collect();
8080                if words.len() >= 1000 {
8081                    tracing::trace!(
8082                        count = words.len(),
8083                        "using parallel reverse sort (rayon) for large array"
8084                    );
8085                    use rayon::prelude::*;
8086                    words.par_sort_unstable_by(|a, b| b.cmp(a));
8087                } else {
8088                    words.sort_unstable_by(|a, b| b.cmp(a));
8089                }
8090                words.join(" ")
8091            }
8092            ZshParamFlag::Sort => {
8093                let mut words: Vec<&str> = val.split_whitespace().collect();
8094                if words.len() >= 1000 {
8095                    tracing::trace!(
8096                        count = words.len(),
8097                        "using parallel sort (rayon) for large array"
8098                    );
8099                    use rayon::prelude::*;
8100                    words.par_sort_unstable();
8101                } else {
8102                    words.sort_unstable();
8103                }
8104                words.join(" ")
8105            }
8106            ZshParamFlag::NumericSort => {
8107                let mut words: Vec<&str> = val.split_whitespace().collect();
8108                let cmp = |a: &&str, b: &&str| {
8109                    let na: i64 = a.parse().unwrap_or(0);
8110                    let nb: i64 = b.parse().unwrap_or(0);
8111                    na.cmp(&nb)
8112                };
8113                if words.len() >= 1000 {
8114                    tracing::trace!(
8115                        count = words.len(),
8116                        "using parallel numeric sort (rayon) for large array"
8117                    );
8118                    use rayon::prelude::*;
8119                    words.par_sort_unstable_by(cmp);
8120                } else {
8121                    words.sort_unstable_by(cmp);
8122                }
8123                words.join(" ")
8124            }
8125            ZshParamFlag::Keys => {
8126                if let Some(assoc) = self.assoc_arrays.get(name) {
8127                    assoc.keys().cloned().collect::<Vec<_>>().join(" ")
8128                } else {
8129                    String::new()
8130                }
8131            }
8132            ZshParamFlag::Values => {
8133                if let Some(assoc) = self.assoc_arrays.get(name) {
8134                    assoc.values().cloned().collect::<Vec<_>>().join(" ")
8135                } else {
8136                    val.to_string()
8137                }
8138            }
8139            ZshParamFlag::Length => val.len().to_string(),
8140            ZshParamFlag::Head(n) => val
8141                .split_whitespace()
8142                .take(*n)
8143                .collect::<Vec<_>>()
8144                .join(" "),
8145            ZshParamFlag::Tail(n) => {
8146                let words: Vec<&str> = val.split_whitespace().collect();
8147                if words.len() > *n {
8148                    words[words.len() - n..].join(" ")
8149                } else {
8150                    val.to_string()
8151                }
8152            }
8153            ZshParamFlag::JoinNewline => {
8154                if let Some(arr) = self.arrays.get(name) {
8155                    arr.join("\n")
8156                } else {
8157                    val.to_string()
8158                }
8159            }
8160            ZshParamFlag::SplitWords => {
8161                // Shell-style word splitting
8162                val.split_whitespace().collect::<Vec<_>>().join(" ")
8163            }
8164            ZshParamFlag::QuoteBackslash => {
8165                // Quote special pattern chars with backslashes
8166                let mut result = String::new();
8167                for c in val.chars() {
8168                    if "\\*?[]{}()".contains(c) {
8169                        result.push('\\');
8170                    }
8171                    result.push(c);
8172                }
8173                result
8174            }
8175            ZshParamFlag::IndexSort => {
8176                // Array index order - just return as-is (default)
8177                val.to_string()
8178            }
8179            ZshParamFlag::CountChars => {
8180                // Count total characters
8181                val.chars().count().to_string()
8182            }
8183            ZshParamFlag::Expand => {
8184                // Would need mutable self to do expansions
8185                val.to_string()
8186            }
8187            ZshParamFlag::PromptExpand => {
8188                // Expand prompt escapes
8189                self.expand_prompt_string(val)
8190            }
8191            ZshParamFlag::PromptExpandFull => {
8192                // Full prompt expansion
8193                self.expand_prompt_string(val)
8194            }
8195            ZshParamFlag::Visible => {
8196                // Make non-printable characters visible
8197                val.chars()
8198                    .map(|c| {
8199                        if c.is_control() {
8200                            format!("^{}", (c as u8 + 64) as char)
8201                        } else {
8202                            c.to_string()
8203                        }
8204                    })
8205                    .collect()
8206            }
8207            ZshParamFlag::Directory => {
8208                // Substitute leading directory with ~ if it's home
8209                if let Some(home) = dirs::home_dir() {
8210                    let home_str = home.to_string_lossy();
8211                    if val.starts_with(home_str.as_ref()) {
8212                        format!("~{}", &val[home_str.len()..])
8213                    } else {
8214                        val.to_string()
8215                    }
8216                } else {
8217                    val.to_string()
8218                }
8219            }
8220            ZshParamFlag::PadLeft(len, fill) => {
8221                if val.len() >= *len {
8222                    val.to_string()
8223                } else {
8224                    let padding: String = std::iter::repeat(*fill).take(len - val.len()).collect();
8225                    format!("{}{}", padding, val)
8226                }
8227            }
8228            ZshParamFlag::PadRight(len, fill) => {
8229                if val.len() >= *len {
8230                    val.to_string()
8231                } else {
8232                    let padding: String = std::iter::repeat(*fill).take(len - val.len()).collect();
8233                    format!("{}{}", val, padding)
8234                }
8235            }
8236            ZshParamFlag::Width(_) => {
8237                // Width modifier - used with padding, just return value
8238                val.to_string()
8239            }
8240            ZshParamFlag::Match => {
8241                // Match flag - used with pattern operations, just pass through
8242                // Actual matching is handled in the pattern operations below
8243                val.to_string()
8244            }
8245            ZshParamFlag::Remove => {
8246                // Remove flag - complement of Match
8247                val.to_string()
8248            }
8249            ZshParamFlag::Subscript => {
8250                // Subscript scanning
8251                val.to_string()
8252            }
8253            ZshParamFlag::Parameter => {
8254                // Parameter indirection - treat val as parameter name
8255                self.get_variable(val)
8256            }
8257            ZshParamFlag::Glob => {
8258                // Glob patterns in pattern matching
8259                val.to_string()
8260            }
8261        }
8262    }
8263
8264    /// Expand prompt escape sequences using the full prompt module
8265    fn expand_prompt_string(&self, s: &str) -> String {
8266        let ctx = self.build_prompt_context();
8267        expand_prompt(s, &ctx)
8268    }
8269
8270    /// Build a PromptContext from current executor state
8271    fn build_prompt_context(&self) -> PromptContext {
8272        let pwd = env::current_dir()
8273            .map(|p| p.to_string_lossy().to_string())
8274            .unwrap_or_else(|_| "/".to_string());
8275
8276        let home = env::var("HOME").unwrap_or_default();
8277
8278        let user = env::var("USER")
8279            .or_else(|_| env::var("LOGNAME"))
8280            .unwrap_or_else(|_| "user".to_string());
8281
8282        let host = hostname::get()
8283            .map(|h| h.to_string_lossy().to_string())
8284            .unwrap_or_else(|_| "localhost".to_string());
8285
8286        let host_short = host.split('.').next().unwrap_or(&host).to_string();
8287
8288        let shlvl = env::var("SHLVL")
8289            .ok()
8290            .and_then(|s| s.parse().ok())
8291            .unwrap_or(1);
8292
8293        PromptContext {
8294            pwd,
8295            home,
8296            user,
8297            host,
8298            host_short,
8299            tty: String::new(),
8300            lastval: self.last_status,
8301            histnum: self
8302                .history
8303                .as_ref()
8304                .and_then(|h| h.count().ok())
8305                .unwrap_or(1),
8306            shlvl,
8307            num_jobs: self.jobs.list().len() as i32,
8308            is_root: unsafe { libc::geteuid() } == 0,
8309            cmd_stack: Vec::new(),
8310            psvar: self.get_psvar(),
8311            term_width: self.get_term_width(),
8312            lineno: 1,
8313        }
8314    }
8315
8316    fn get_psvar(&self) -> Vec<String> {
8317        if let Some(arr) = self.arrays.get("psvar") {
8318            arr.clone()
8319        } else {
8320            Vec::new()
8321        }
8322    }
8323
8324    fn get_term_width(&self) -> usize {
8325        env::var("COLUMNS")
8326            .ok()
8327            .and_then(|s| s.parse().ok())
8328            .unwrap_or(80)
8329    }
8330
8331    /// Execute a command and capture its output (command substitution)
8332    fn execute_command_substitution(&mut self, cmd: &ShellCommand) -> String {
8333        match self.execute_command_capture(cmd) {
8334            Ok(output) => output.trim_end_matches('\n').to_string(),
8335            Err(_) => String::new(),
8336        }
8337    }
8338
8339    /// Execute a command and capture its stdout
8340    fn execute_command_capture(&mut self, cmd: &ShellCommand) -> Result<String, String> {
8341        // For simple commands, we can use Command directly
8342        if let ShellCommand::Simple(simple) = cmd {
8343            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
8344            if words.is_empty() {
8345                return Ok(String::new());
8346            }
8347
8348            let cmd_name = &words[0];
8349            let args = &words[1..];
8350
8351            // Handle some builtins that can return values
8352            match cmd_name.as_str() {
8353                "echo" => {
8354                    let output = args.join(" ");
8355                    return Ok(format!("{}\n", output));
8356                }
8357                "printf" => {
8358                    if !args.is_empty() {
8359                        // Simple printf - just format string with args
8360                        let format = &args[0];
8361                        let result = if args.len() > 1 {
8362                            // Very basic: just handle %s
8363                            let mut out = format.clone();
8364                            for (i, arg) in args[1..].iter().enumerate() {
8365                                out = out.replacen("%s", arg, 1);
8366                                out = out.replacen(&format!("${}", i + 1), arg, 1);
8367                            }
8368                            out
8369                        } else {
8370                            format.clone()
8371                        };
8372                        return Ok(result);
8373                    }
8374                    return Ok(String::new());
8375                }
8376                "pwd" => {
8377                    return Ok(env::current_dir()
8378                        .map(|p| format!("{}\n", p.display()))
8379                        .unwrap_or_default());
8380                }
8381                _ => {}
8382            }
8383
8384            // External command - capture its output
8385            let output = Command::new(cmd_name)
8386                .args(args)
8387                .stdout(Stdio::piped())
8388                .stderr(Stdio::inherit())
8389                .output();
8390
8391            match output {
8392                Ok(output) => {
8393                    self.last_status = output.status.code().unwrap_or(1);
8394                    Ok(String::from_utf8_lossy(&output.stdout).to_string())
8395                }
8396                Err(e) => {
8397                    self.last_status = 127;
8398                    Err(format!("{}: {}", cmd_name, e))
8399                }
8400            }
8401        } else if let ShellCommand::Pipeline(cmds, _negated) = cmd {
8402            // For pipelines, execute and capture output of the last command
8403            // This is simplified - proper implementation would pipe between all
8404            if let Some(last) = cmds.last() {
8405                return self.execute_command_capture(last);
8406            }
8407            Ok(String::new())
8408        } else {
8409            // For compound commands, execute them and return empty
8410            // (complex case - could be expanded later)
8411            let _ = self.execute_command(cmd);
8412            Ok(String::new())
8413        }
8414    }
8415
8416    /// Evaluate arithmetic expression using the full math module
8417    fn evaluate_arithmetic(&mut self, expr: &str) -> String {
8418        let expr = self.expand_string(expr);
8419        let force_float = self.options.get("forcefloat").copied().unwrap_or(false);
8420        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
8421        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
8422
8423        let mut evaluator = MathEval::new(&expr)
8424            .with_string_variables(&self.variables)
8425            .with_force_float(force_float)
8426            .with_c_precedences(c_prec)
8427            .with_octal_zeroes(octal);
8428
8429        match evaluator.evaluate() {
8430            Ok(result) => {
8431                for (k, v) in evaluator.extract_string_variables() {
8432                    self.variables.insert(k.clone(), v.clone());
8433                    env::set_var(&k, &v);
8434                }
8435                match result {
8436                    crate::math::MathNum::Integer(i) => i.to_string(),
8437                    crate::math::MathNum::Float(f) => {
8438                        if f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
8439                            (f as i64).to_string()
8440                        } else {
8441                            f.to_string()
8442                        }
8443                    }
8444                    crate::math::MathNum::Unset => "0".to_string(),
8445                }
8446            }
8447            Err(_) => "0".to_string(),
8448        }
8449    }
8450
8451    fn eval_arith_expr(&mut self, expr: &str) -> i64 {
8452        let expr_expanded = self.expand_string(expr);
8453        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
8454        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
8455
8456        let mut evaluator = MathEval::new(&expr_expanded)
8457            .with_string_variables(&self.variables)
8458            .with_c_precedences(c_prec)
8459            .with_octal_zeroes(octal);
8460
8461        match evaluator.evaluate() {
8462            Ok(result) => {
8463                for (k, v) in evaluator.extract_string_variables() {
8464                    self.variables.insert(k.clone(), v.clone());
8465                    env::set_var(&k, &v);
8466                }
8467                result.to_int()
8468            }
8469            Err(_) => 0,
8470        }
8471    }
8472
8473    fn eval_arith_expr_float(&mut self, expr: &str) -> f64 {
8474        let expr_expanded = self.expand_string(expr);
8475        let force_float = self.options.get("forcefloat").copied().unwrap_or(false);
8476        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
8477        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
8478
8479        let mut evaluator = MathEval::new(&expr_expanded)
8480            .with_string_variables(&self.variables)
8481            .with_force_float(force_float)
8482            .with_c_precedences(c_prec)
8483            .with_octal_zeroes(octal);
8484
8485        match evaluator.evaluate() {
8486            Ok(result) => {
8487                for (k, v) in evaluator.extract_string_variables() {
8488                    self.variables.insert(k.clone(), v.clone());
8489                    env::set_var(&k, &v);
8490                }
8491                result.to_float()
8492            }
8493            Err(_) => 0.0,
8494        }
8495    }
8496
8497    fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
8498        // Simple glob matching
8499        if pattern == "*" {
8500            return true;
8501        }
8502        if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
8503            // Use glob matching for wildcards and character classes
8504            glob::Pattern::new(pattern)
8505                .map(|p| p.matches(value))
8506                .unwrap_or(false)
8507        } else {
8508            value == pattern
8509        }
8510    }
8511
8512    fn eval_cond_expr(&mut self, expr: &CondExpr) -> bool {
8513        match expr {
8514            CondExpr::FileExists(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8515            CondExpr::FileRegular(w) => std::path::Path::new(&self.expand_word(w)).is_file(),
8516            CondExpr::FileDirectory(w) => std::path::Path::new(&self.expand_word(w)).is_dir(),
8517            CondExpr::FileSymlink(w) => std::path::Path::new(&self.expand_word(w)).is_symlink(),
8518            CondExpr::FileReadable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8519            CondExpr::FileWritable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8520            CondExpr::FileExecutable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8521            CondExpr::FileNonEmpty(w) => std::fs::metadata(&self.expand_word(w))
8522                .map(|m| m.len() > 0)
8523                .unwrap_or(false),
8524            CondExpr::StringEmpty(w) => self.expand_word(w).is_empty(),
8525            CondExpr::StringNonEmpty(w) => !self.expand_word(w).is_empty(),
8526            CondExpr::StringEqual(a, b) => {
8527                let left = self.expand_word(a);
8528                let right = self.expand_word(b);
8529                // In [[ ]], == does glob pattern matching on the right side
8530                if right.contains('*') || right.contains('?') || right.contains('[') {
8531                    crate::glob::pattern_match(&right, &left, true, true)
8532                } else {
8533                    left == right
8534                }
8535            }
8536            CondExpr::StringNotEqual(a, b) => {
8537                let left = self.expand_word(a);
8538                let right = self.expand_word(b);
8539                if right.contains('*') || right.contains('?') || right.contains('[') {
8540                    !crate::glob::pattern_match(&right, &left, true, true)
8541                } else {
8542                    left != right
8543                }
8544            }
8545            CondExpr::StringMatch(a, b) => {
8546                let val = self.expand_word(a);
8547                let pattern = self.expand_word(b);
8548                if let Some(re) = cached_regex(&pattern) {
8549                    if let Some(caps) = re.captures(&val) {
8550                        // Set $MATCH to the full match
8551                        if let Some(m) = caps.get(0) {
8552                            self.variables
8553                                .insert("MATCH".to_string(), m.as_str().to_string());
8554                        }
8555                        // Set $match array with capture groups
8556                        let mut match_arr = Vec::new();
8557                        for i in 1..caps.len() {
8558                            if let Some(g) = caps.get(i) {
8559                                match_arr.push(g.as_str().to_string());
8560                            }
8561                        }
8562                        if !match_arr.is_empty() {
8563                            self.arrays.insert("match".to_string(), match_arr);
8564                        }
8565                        true
8566                    } else {
8567                        self.variables.remove("MATCH");
8568                        self.arrays.remove("match");
8569                        false
8570                    }
8571                } else {
8572                    false
8573                }
8574            }
8575            CondExpr::StringLess(a, b) => self.expand_word(a) < self.expand_word(b),
8576            CondExpr::StringGreater(a, b) => self.expand_word(a) > self.expand_word(b),
8577            CondExpr::NumEqual(a, b) => {
8578                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8579                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8580                a_val == b_val
8581            }
8582            CondExpr::NumNotEqual(a, b) => {
8583                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8584                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8585                a_val != b_val
8586            }
8587            CondExpr::NumLess(a, b) => {
8588                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8589                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8590                a_val < b_val
8591            }
8592            CondExpr::NumLessEqual(a, b) => {
8593                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8594                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8595                a_val <= b_val
8596            }
8597            CondExpr::NumGreater(a, b) => {
8598                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8599                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8600                a_val > b_val
8601            }
8602            CondExpr::NumGreaterEqual(a, b) => {
8603                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8604                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8605                a_val >= b_val
8606            }
8607            CondExpr::Not(inner) => !self.eval_cond_expr(inner),
8608            CondExpr::And(a, b) => self.eval_cond_expr(a) && self.eval_cond_expr(b),
8609            CondExpr::Or(a, b) => self.eval_cond_expr(a) || self.eval_cond_expr(b),
8610        }
8611    }
8612
8613    // Builtins
8614    // Ported from zsh/Src/builtin.c
8615
8616    /// cd builtin - change directory
8617    /// Ported from zsh/Src/builtin.c bin_cd() lines 839-859, cd_get_dest() lines 864-957,
8618    /// cd_do_chdir() lines 967-1081, cd_try_chdir() lines 1116-1181
8619    fn builtin_cd(&mut self, args: &[String]) -> i32 {
8620        // cd [ -qsLP ] [ arg ]
8621        // cd [ -qsLP ] old new
8622        // cd [ -qsLP ] {+|-}n
8623        let mut quiet = false;
8624        let mut use_cdpath = false;
8625        let mut logical = true; // -L is default
8626        let mut positional_args: Vec<&str> = Vec::new();
8627
8628        for arg in args {
8629            if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
8630                // Check if it's a stack index like -2
8631                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
8632                    positional_args.push(arg);
8633                    continue;
8634                }
8635                for ch in arg[1..].chars() {
8636                    match ch {
8637                        'q' => quiet = true,
8638                        's' => use_cdpath = true,
8639                        'L' => logical = true,
8640                        'P' => logical = false,
8641                        _ => {
8642                            eprintln!("cd: bad option: -{}", ch);
8643                            return 1;
8644                        }
8645                    }
8646                }
8647            } else if arg.starts_with('+')
8648                && arg.len() > 1
8649                && arg[1..].chars().all(|c| c.is_ascii_digit())
8650            {
8651                // Stack index like +2
8652                positional_args.push(arg);
8653            } else {
8654                positional_args.push(arg);
8655            }
8656        }
8657
8658        // Handle cd old new (substitution)
8659        if positional_args.len() == 2 {
8660            if let Ok(cwd) = env::current_dir() {
8661                let cwd_str = cwd.to_string_lossy();
8662                let old = positional_args[0];
8663                let new = positional_args[1];
8664                if cwd_str.contains(old) {
8665                    let new_path = cwd_str.replace(old, new);
8666                    if !quiet {
8667                        println!("{}", new_path);
8668                    }
8669                    positional_args = vec![];
8670                    return self.do_cd(&new_path, quiet, use_cdpath, logical);
8671                }
8672            }
8673        }
8674
8675        let path_arg = positional_args.first().map(|s| *s).unwrap_or("~");
8676
8677        // Handle stack indices
8678        if path_arg.starts_with('+') || path_arg.starts_with('-') {
8679            if let Ok(n) = path_arg[1..].parse::<usize>() {
8680                let idx = if path_arg.starts_with('+') {
8681                    n
8682                } else {
8683                    self.dir_stack.len().saturating_sub(n)
8684                };
8685                if let Some(dir) = self.dir_stack.get(idx) {
8686                    let dir_path = dir.to_string_lossy().to_string();
8687                    return self.do_cd(&dir_path, quiet, use_cdpath, logical);
8688                } else {
8689                    eprintln!("cd: no such entry in dir stack");
8690                    return 1;
8691                }
8692            }
8693        }
8694
8695        self.do_cd(path_arg, quiet, use_cdpath, logical)
8696    }
8697
8698    fn do_cd(&mut self, path_arg: &str, quiet: bool, use_cdpath: bool, physical: bool) -> i32 {
8699        let path = if path_arg == "~" || path_arg.is_empty() {
8700            dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
8701        } else if path_arg.starts_with("~/") {
8702            dirs::home_dir()
8703                .unwrap_or_else(|| PathBuf::from("."))
8704                .join(&path_arg[2..])
8705        } else if path_arg == "-" {
8706            if let Ok(oldpwd) = env::var("OLDPWD") {
8707                if !quiet {
8708                    println!("{}", oldpwd);
8709                }
8710                PathBuf::from(oldpwd)
8711            } else {
8712                eprintln!("cd: OLDPWD not set");
8713                return 1;
8714            }
8715        } else if use_cdpath && !path_arg.starts_with('/') && !path_arg.starts_with('.') {
8716            // Search CDPATH
8717            let cdpath = env::var("CDPATH").unwrap_or_default();
8718            let mut found = None;
8719            for dir in cdpath.split(':') {
8720                let candidate = if dir.is_empty() {
8721                    PathBuf::from(path_arg)
8722                } else {
8723                    PathBuf::from(dir).join(path_arg)
8724                };
8725                if candidate.is_dir() {
8726                    found = Some(candidate);
8727                    break;
8728                }
8729            }
8730            found.unwrap_or_else(|| PathBuf::from(path_arg))
8731        } else {
8732            PathBuf::from(path_arg)
8733        };
8734
8735        if let Ok(cwd) = env::current_dir() {
8736            env::set_var("OLDPWD", &cwd);
8737        }
8738
8739        // Resolve symlinks if -P (physical)
8740        let target = if !physical {
8741            if let Ok(resolved) = path.canonicalize() {
8742                resolved
8743            } else {
8744                path.clone()
8745            }
8746        } else {
8747            path.clone()
8748        };
8749
8750        match env::set_current_dir(&target) {
8751            Ok(_) => {
8752                if let Ok(cwd) = env::current_dir() {
8753                    env::set_var("PWD", &cwd);
8754                    self.variables
8755                        .insert("PWD".to_string(), cwd.to_string_lossy().to_string());
8756                }
8757                0
8758            }
8759            Err(e) => {
8760                eprintln!("cd: {}: {}", path.display(), e);
8761                1
8762            }
8763        }
8764    }
8765
8766    fn builtin_pwd(&mut self, _redirects: &[Redirect]) -> i32 {
8767        match env::current_dir() {
8768            Ok(path) => {
8769                println!("{}", path.display());
8770                0
8771            }
8772            Err(e) => {
8773                eprintln!("pwd: {}", e);
8774                1
8775            }
8776        }
8777    }
8778
8779    fn builtin_echo(&mut self, args: &[String], _redirects: &[Redirect]) -> i32 {
8780        let mut newline = true;
8781        let mut interpret_escapes = false;
8782        let mut start = 0;
8783
8784        for (i, arg) in args.iter().enumerate() {
8785            match arg.as_str() {
8786                "-n" => {
8787                    newline = false;
8788                    start = i + 1;
8789                }
8790                "-e" => {
8791                    interpret_escapes = true;
8792                    start = i + 1;
8793                }
8794                "-E" => {
8795                    interpret_escapes = false;
8796                    start = i + 1;
8797                }
8798                _ => break,
8799            }
8800        }
8801
8802        let output = args[start..].join(" ");
8803        if interpret_escapes {
8804            print!("{}", output.replace("\\n", "\n").replace("\\t", "\t"));
8805        } else {
8806            print!("{}", output);
8807        }
8808
8809        if newline {
8810            println!();
8811        }
8812        0
8813    }
8814
8815    fn builtin_export(&mut self, args: &[String]) -> i32 {
8816        for arg in args {
8817            if let Some((key, value)) = arg.split_once('=') {
8818                self.variables.insert(key.to_string(), value.to_string());
8819                env::set_var(key, value);
8820            } else {
8821                // export VAR (no value) — mark existing var as exported
8822                let val = self.get_variable(arg);
8823                env::set_var(arg, &val);
8824            }
8825        }
8826        0
8827    }
8828
8829    fn builtin_unset(&mut self, args: &[String]) -> i32 {
8830        for arg in args {
8831            env::remove_var(arg);
8832            self.variables.remove(arg);
8833        }
8834        0
8835    }
8836
8837    fn builtin_source(&mut self, args: &[String]) -> i32 {
8838        if args.is_empty() {
8839            eprintln!("source: filename argument required");
8840            return 1;
8841        }
8842
8843        let path = &args[0];
8844
8845        // Resolve to absolute path
8846        let abs_path = if path.starts_with('/') {
8847            path.clone()
8848        } else if path.starts_with("~/") {
8849            if let Some(home) = dirs::home_dir() {
8850                home.join(&path[2..]).to_string_lossy().to_string()
8851            } else {
8852                path.clone()
8853            }
8854        } else {
8855            std::env::current_dir()
8856                .map(|cwd| cwd.join(path).to_string_lossy().to_string())
8857                .unwrap_or_else(|_| path.clone())
8858        };
8859
8860        // Save current $0 and set to the sourced file path
8861        let saved_zero = self.variables.get("0").cloned();
8862        self.variables.insert("0".to_string(), abs_path.clone());
8863
8864        let result;
8865
8866        if self.posix_mode {
8867            // --- POSIX mode: plain read + execute, no SQLite, no caching, no threads ---
8868            result = match std::fs::read_to_string(&abs_path) {
8869                Ok(content) => match self.execute_script(&content) {
8870                    Ok(status) => status,
8871                    Err(e) => {
8872                        eprintln!("source: {}: {}", path, e);
8873                        1
8874                    }
8875                },
8876                Err(e) => {
8877                    eprintln!("source: {}: {}", path, e);
8878                    1
8879                }
8880            };
8881        } else {
8882            // --- zshrs/zsh mode: plugin cache + AST cache + worker pool ---
8883            let file_path = std::path::Path::new(&abs_path);
8884
8885            // Check plugin cache for side-effect replay
8886            if let Some(ref cache) = self.plugin_cache {
8887                if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(file_path) {
8888                    if let Some(plugin_id) = cache.check(&abs_path, mt_s, mt_ns) {
8889                        if let Ok(delta) = cache.load(plugin_id) {
8890                            let t0 = std::time::Instant::now();
8891                            self.replay_plugin_delta(&delta);
8892                            tracing::info!(
8893                                path = %abs_path,
8894                                replay_us = t0.elapsed().as_micros() as u64,
8895                                funcs = delta.functions.len(),
8896                                aliases = delta.aliases.len(),
8897                                vars = delta.variables.len() + delta.exports.len(),
8898                                "source: cache hit, replayed"
8899                            );
8900                            // Restore $0
8901                            if let Some(z) = saved_zero {
8902                                self.variables.insert("0".to_string(), z);
8903                            } else {
8904                                self.variables.remove("0");
8905                            }
8906                            return 0;
8907                        }
8908                    }
8909                }
8910            }
8911
8912            // Cache miss — snapshot, execute via AST-cached path, diff, async store
8913            let snapshot = self.snapshot_state();
8914            let t0 = std::time::Instant::now();
8915            tracing::debug!(path = %abs_path, "source: cache miss, executing via AST-cached path");
8916            result = match self.execute_script_file(&abs_path) {
8917                Ok(status) => status,
8918                Err(e) => {
8919                    tracing::warn!(path = %abs_path, error = %e, "source: execution failed");
8920                    eprintln!("source: {}: {}", path, e);
8921                    1
8922                }
8923            };
8924            let source_ms = t0.elapsed().as_millis() as u64;
8925
8926            // Async-store delta to plugin cache on worker pool
8927            if result == 0 {
8928                if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(file_path) {
8929                    let delta = self.diff_state(&snapshot);
8930                    let store_path = abs_path.clone();
8931                    tracing::info!(
8932                        path = %abs_path, source_ms,
8933                        funcs = delta.functions.len(),
8934                        aliases = delta.aliases.len(),
8935                        vars = delta.variables.len() + delta.exports.len(),
8936                        "source: caching delta on worker"
8937                    );
8938                    let cache_db_path = crate::plugin_cache::default_cache_path();
8939                    self.worker_pool.submit(move || {
8940                        match crate::plugin_cache::PluginCache::open(&cache_db_path) {
8941                            Ok(cache) => {
8942                                if let Err(e) = cache.store(&store_path, mt_s, mt_ns, source_ms, &delta) {
8943                                    tracing::error!(path = %store_path, error = %e, "plugin_cache: store failed");
8944                                } else {
8945                                    tracing::debug!(path = %store_path, "plugin_cache: stored");
8946                                }
8947                            }
8948                            Err(e) => tracing::error!(error = %e, "plugin_cache: open for write failed"),
8949                        }
8950                    });
8951                }
8952            }
8953        }
8954
8955        // Handle return from sourced script
8956        let final_result = if let Some(ret) = self.returning.take() {
8957            ret
8958        } else {
8959            result
8960        };
8961
8962        // Restore $0
8963        if let Some(z) = saved_zero {
8964            self.variables.insert("0".to_string(), z);
8965        } else {
8966            self.variables.remove("0");
8967        }
8968
8969        final_result
8970    }
8971
8972    /// Snapshot executor state before sourcing a plugin (for delta computation).
8973    fn snapshot_state(&self) -> PluginSnapshot {
8974        PluginSnapshot {
8975            functions: self.functions.keys().cloned().collect(),
8976            aliases: self.aliases.keys().cloned().collect(),
8977            global_aliases: self.global_aliases.keys().cloned().collect(),
8978            suffix_aliases: self.suffix_aliases.keys().cloned().collect(),
8979            variables: self.variables.clone(),
8980            arrays: self.arrays.keys().cloned().collect(),
8981            assoc_arrays: self.assoc_arrays.keys().cloned().collect(),
8982            fpath: self.fpath.clone(),
8983            options: self.options.clone(),
8984            hooks: self.hook_functions.clone(),
8985            autoloads: self.autoload_pending.keys().cloned().collect(),
8986        }
8987    }
8988
8989    /// Compute the delta between current state and a previous snapshot.
8990    fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
8991        use crate::plugin_cache::{AliasKind, PluginDelta};
8992        let mut delta = PluginDelta::default();
8993
8994        // New functions — serialize AST to bincode for instant replay
8995        for (name, body) in &self.functions {
8996            if !snap.functions.contains(name) {
8997                if let Ok(bytes) = bincode::serialize(body) {
8998                    delta.functions.push((name.clone(), bytes));
8999                }
9000            }
9001        }
9002
9003        // New aliases
9004        for (name, value) in &self.aliases {
9005            if !snap.aliases.contains(name) {
9006                delta
9007                    .aliases
9008                    .push((name.clone(), value.clone(), AliasKind::Regular));
9009            }
9010        }
9011        for (name, value) in &self.global_aliases {
9012            if !snap.global_aliases.contains(name) {
9013                delta
9014                    .aliases
9015                    .push((name.clone(), value.clone(), AliasKind::Global));
9016            }
9017        }
9018        for (name, value) in &self.suffix_aliases {
9019            if !snap.suffix_aliases.contains(name) {
9020                delta
9021                    .aliases
9022                    .push((name.clone(), value.clone(), AliasKind::Suffix));
9023            }
9024        }
9025
9026        // New/changed variables
9027        for (name, value) in &self.variables {
9028            if name == "0" {
9029                continue;
9030            } // skip $0 (we set it ourselves)
9031            match snap.variables.get(name) {
9032                Some(old) if old == value => {} // unchanged
9033                _ => {
9034                    // Check if it's also exported
9035                    if env::var(name).ok().as_ref() == Some(value) {
9036                        delta.exports.push((name.clone(), value.clone()));
9037                    } else {
9038                        delta.variables.push((name.clone(), value.clone()));
9039                    }
9040                }
9041            }
9042        }
9043
9044        // New arrays
9045        for (name, values) in &self.arrays {
9046            if !snap.arrays.contains(name) {
9047                delta.arrays.push((name.clone(), values.clone()));
9048            }
9049        }
9050
9051        // New fpath entries
9052        for p in &self.fpath {
9053            if !snap.fpath.contains(p) {
9054                delta.fpath_additions.push(p.to_string_lossy().to_string());
9055            }
9056        }
9057
9058        // Changed options
9059        for (name, value) in &self.options {
9060            match snap.options.get(name) {
9061                Some(old) if old == value => {}
9062                _ => delta.options_changed.push((name.clone(), *value)),
9063            }
9064        }
9065
9066        // New hooks
9067        for (hook, funcs) in &self.hook_functions {
9068            let old_funcs = snap.hooks.get(hook);
9069            for f in funcs {
9070                let is_new = old_funcs.map_or(true, |old| !old.contains(f));
9071                if is_new {
9072                    delta.hooks.push((hook.clone(), f.clone()));
9073                }
9074            }
9075        }
9076
9077        // New autoloads
9078        for (name, flags) in &self.autoload_pending {
9079            if !snap.autoloads.contains(name) {
9080                delta.autoloads.push((name.clone(), format!("{:?}", flags)));
9081            }
9082        }
9083
9084        delta
9085    }
9086
9087    /// Replay a cached plugin delta into the executor state.
9088    fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
9089        use crate::plugin_cache::AliasKind;
9090
9091        // Aliases
9092        for (name, value, kind) in &delta.aliases {
9093            match kind {
9094                AliasKind::Regular => {
9095                    self.aliases.insert(name.clone(), value.clone());
9096                }
9097                AliasKind::Global => {
9098                    self.global_aliases.insert(name.clone(), value.clone());
9099                }
9100                AliasKind::Suffix => {
9101                    self.suffix_aliases.insert(name.clone(), value.clone());
9102                }
9103            }
9104        }
9105
9106        // Variables
9107        for (name, value) in &delta.variables {
9108            self.variables.insert(name.clone(), value.clone());
9109        }
9110
9111        // Exports (set in both variables and process env)
9112        for (name, value) in &delta.exports {
9113            self.variables.insert(name.clone(), value.clone());
9114            env::set_var(name, value);
9115        }
9116
9117        // Arrays
9118        for (name, values) in &delta.arrays {
9119            self.arrays.insert(name.clone(), values.clone());
9120        }
9121
9122        // Fpath additions
9123        for p in &delta.fpath_additions {
9124            let pb = PathBuf::from(p);
9125            if !self.fpath.contains(&pb) {
9126                self.fpath.push(pb);
9127            }
9128        }
9129
9130        // Completions
9131        for (cmd, func) in &delta.completions {
9132            if let Some(ref mut comps) = self.assoc_arrays.get_mut("_comps") {
9133                comps.insert(cmd.clone(), func.clone());
9134            }
9135        }
9136
9137        // Options
9138        for (name, enabled) in &delta.options_changed {
9139            self.options.insert(name.clone(), *enabled);
9140        }
9141
9142        // Hooks
9143        for (hook, func) in &delta.hooks {
9144            self.hook_functions
9145                .entry(hook.clone())
9146                .or_insert_with(Vec::new)
9147                .push(func.clone());
9148        }
9149
9150        // Functions — deserialize bincode bytecode blobs directly into self.functions
9151        for (name, bytes) in &delta.functions {
9152            if let Ok(ast) = bincode::deserialize::<crate::parser::ShellCommand>(bytes) {
9153                self.functions.insert(name.clone(), ast);
9154            }
9155        }
9156    }
9157
9158    fn builtin_exit(&mut self, args: &[String]) -> i32 {
9159        let code = args
9160            .first()
9161            .and_then(|s| s.parse::<i32>().ok())
9162            .unwrap_or(self.last_status);
9163        std::process::exit(code);
9164    }
9165
9166    fn builtin_return(&mut self, args: &[String]) -> i32 {
9167        let status = args
9168            .first()
9169            .and_then(|s| s.parse::<i32>().ok())
9170            .unwrap_or(self.last_status);
9171        self.returning = Some(status);
9172        status
9173    }
9174
9175    fn builtin_test(&mut self, args: &[String]) -> i32 {
9176        if args.is_empty() {
9177            return 1;
9178        }
9179
9180        // Strip trailing "]" when called as `[`
9181        let args: Vec<&str> = args
9182            .iter()
9183            .map(|s| s.as_str())
9184            .filter(|&s| s != "]")
9185            .collect();
9186
9187        // Prefetch metadata for all file paths in the expression — one stat() per unique path
9188        // instead of one stat() per test flag. Avoids 7 serial stat()s for -r -w -x -g -k -u -s.
9189        let mut meta_cache: HashMap<String, Option<std::fs::Metadata>> = HashMap::new();
9190        for arg in &args {
9191            if !arg.starts_with('-') && !arg.starts_with('!') && *arg != "(" && *arg != ")" {
9192                let path_str = arg.to_string();
9193                if !meta_cache.contains_key(&path_str) {
9194                    meta_cache.insert(path_str, std::fs::metadata(arg).ok());
9195                }
9196            }
9197        }
9198
9199        // Helper closure: get metadata from cache or fetch
9200        let get_meta = |path: &str| -> Option<std::fs::Metadata> {
9201            meta_cache
9202                .get(path)
9203                .cloned()
9204                .unwrap_or_else(|| std::fs::metadata(path).ok())
9205        };
9206
9207        match args.as_slice() {
9208            // String tests
9209            ["-z", s] => {
9210                if s.is_empty() {
9211                    0
9212                } else {
9213                    1
9214                }
9215            }
9216            ["-n", s] => {
9217                if !s.is_empty() {
9218                    0
9219                } else {
9220                    1
9221                }
9222            }
9223
9224            // File existence/type tests
9225            ["-a", path] | ["-e", path] => {
9226                if std::path::Path::new(path).exists() {
9227                    0
9228                } else {
9229                    1
9230                }
9231            }
9232            ["-f", path] => {
9233                if std::path::Path::new(path).is_file() {
9234                    0
9235                } else {
9236                    1
9237                }
9238            }
9239            ["-d", path] => {
9240                if std::path::Path::new(path).is_dir() {
9241                    0
9242                } else {
9243                    1
9244                }
9245            }
9246            ["-b", path] => {
9247                use std::os::unix::fs::FileTypeExt;
9248                if std::fs::symlink_metadata(path)
9249                    .map(|m| m.file_type().is_block_device())
9250                    .unwrap_or(false)
9251                {
9252                    0
9253                } else {
9254                    1
9255                }
9256            }
9257            ["-c", path] => {
9258                use std::os::unix::fs::FileTypeExt;
9259                if std::fs::symlink_metadata(path)
9260                    .map(|m| m.file_type().is_char_device())
9261                    .unwrap_or(false)
9262                {
9263                    0
9264                } else {
9265                    1
9266                }
9267            }
9268            ["-p", path] => {
9269                use std::os::unix::fs::FileTypeExt;
9270                if std::fs::symlink_metadata(path)
9271                    .map(|m| m.file_type().is_fifo())
9272                    .unwrap_or(false)
9273                {
9274                    0
9275                } else {
9276                    1
9277                }
9278            }
9279            ["-S", path] => {
9280                use std::os::unix::fs::FileTypeExt;
9281                if std::fs::symlink_metadata(path)
9282                    .map(|m| m.file_type().is_socket())
9283                    .unwrap_or(false)
9284                {
9285                    0
9286                } else {
9287                    1
9288                }
9289            }
9290            ["-h", path] | ["-L", path] => {
9291                if std::path::Path::new(path).is_symlink() {
9292                    0
9293                } else {
9294                    1
9295                }
9296            }
9297
9298            // File permission tests — all use prefetched metadata (one stat per path)
9299            ["-r", path] => {
9300                use std::os::unix::fs::MetadataExt;
9301                if let Some(meta) = get_meta(path) {
9302                    let mode = meta.mode();
9303                    let uid = unsafe { libc::geteuid() };
9304                    let gid = unsafe { libc::getegid() };
9305                    let readable = if meta.uid() == uid {
9306                        mode & 0o400 != 0
9307                    } else if meta.gid() == gid {
9308                        mode & 0o040 != 0
9309                    } else {
9310                        mode & 0o004 != 0
9311                    };
9312                    if readable {
9313                        0
9314                    } else {
9315                        1
9316                    }
9317                } else {
9318                    1
9319                }
9320            }
9321            ["-w", path] => {
9322                use std::os::unix::fs::MetadataExt;
9323                if let Some(meta) = get_meta(path) {
9324                    let mode = meta.mode();
9325                    let uid = unsafe { libc::geteuid() };
9326                    let gid = unsafe { libc::getegid() };
9327                    let writable = if meta.uid() == uid {
9328                        mode & 0o200 != 0
9329                    } else if meta.gid() == gid {
9330                        mode & 0o020 != 0
9331                    } else {
9332                        mode & 0o002 != 0
9333                    };
9334                    if writable {
9335                        0
9336                    } else {
9337                        1
9338                    }
9339                } else {
9340                    1
9341                }
9342            }
9343            ["-x", path] => {
9344                use std::os::unix::fs::MetadataExt;
9345                if let Some(meta) = get_meta(path) {
9346                    let mode = meta.mode();
9347                    let uid = unsafe { libc::geteuid() };
9348                    let gid = unsafe { libc::getegid() };
9349                    let executable = if meta.uid() == uid {
9350                        mode & 0o100 != 0
9351                    } else if meta.gid() == gid {
9352                        mode & 0o010 != 0
9353                    } else {
9354                        mode & 0o001 != 0
9355                    };
9356                    if executable {
9357                        0
9358                    } else {
9359                        1
9360                    }
9361                } else {
9362                    1
9363                }
9364            }
9365
9366            // Special permission bits — prefetched metadata
9367            ["-g", path] => {
9368                use std::os::unix::fs::MetadataExt;
9369                if get_meta(path)
9370                    .map(|m| m.mode() & 0o2000 != 0)
9371                    .unwrap_or(false)
9372                {
9373                    0
9374                } else {
9375                    1
9376                }
9377            }
9378            ["-k", path] => {
9379                use std::os::unix::fs::MetadataExt;
9380                if get_meta(path)
9381                    .map(|m| m.mode() & 0o1000 != 0)
9382                    .unwrap_or(false)
9383                {
9384                    0
9385                } else {
9386                    1
9387                }
9388            }
9389            ["-u", path] => {
9390                use std::os::unix::fs::MetadataExt;
9391                if get_meta(path)
9392                    .map(|m| m.mode() & 0o4000 != 0)
9393                    .unwrap_or(false)
9394                {
9395                    0
9396                } else {
9397                    1
9398                }
9399            }
9400
9401            // File size — prefetched metadata
9402            ["-s", path] => {
9403                if get_meta(path).map(|m| m.len() > 0).unwrap_or(false) {
9404                    0
9405                } else {
9406                    1
9407                }
9408            }
9409
9410            // Ownership — prefetched metadata
9411            ["-O", path] => {
9412                use std::os::unix::fs::MetadataExt;
9413                if get_meta(path)
9414                    .map(|m| m.uid() == unsafe { libc::geteuid() })
9415                    .unwrap_or(false)
9416                {
9417                    0
9418                } else {
9419                    1
9420                }
9421            }
9422            ["-G", path] => {
9423                use std::os::unix::fs::MetadataExt;
9424                if get_meta(path)
9425                    .map(|m| m.gid() == unsafe { libc::getegid() })
9426                    .unwrap_or(false)
9427                {
9428                    0
9429                } else {
9430                    1
9431                }
9432            }
9433
9434            // File times — prefetched metadata
9435            ["-N", path] => {
9436                use std::os::unix::fs::MetadataExt;
9437                if let Some(meta) = get_meta(path) {
9438                    if meta.mtime() > meta.atime() {
9439                        0
9440                    } else {
9441                        1
9442                    }
9443                } else {
9444                    1
9445                }
9446            }
9447
9448            // Terminal test
9449            ["-t", fd] => {
9450                if let Ok(fd_num) = fd.parse::<i32>() {
9451                    if unsafe { libc::isatty(fd_num) } == 1 {
9452                        0
9453                    } else {
9454                        1
9455                    }
9456                } else {
9457                    1
9458                }
9459            }
9460
9461            // Variable test
9462            ["-v", varname] => {
9463                if self.variables.contains_key(*varname) || std::env::var(varname).is_ok() {
9464                    0
9465                } else {
9466                    1
9467                }
9468            }
9469
9470            // Option test
9471            ["-o", opt] => {
9472                let (name, _) = Self::normalize_option_name(opt);
9473                if self.options.get(&name).copied().unwrap_or(false) {
9474                    0
9475                } else {
9476                    1
9477                }
9478            }
9479
9480            // String comparisons
9481            [a, "=", b] | [a, "==", b] => {
9482                if a == b {
9483                    0
9484                } else {
9485                    1
9486                }
9487            }
9488            [a, "!=", b] => {
9489                if a != b {
9490                    0
9491                } else {
9492                    1
9493                }
9494            }
9495            [a, "<", b] => {
9496                if *a < *b {
9497                    0
9498                } else {
9499                    1
9500                }
9501            }
9502            [a, ">", b] => {
9503                if *a > *b {
9504                    0
9505                } else {
9506                    1
9507                }
9508            }
9509
9510            // Numeric comparisons
9511            [a, "-eq", b] => {
9512                let a: i64 = a.parse().unwrap_or(0);
9513                let b: i64 = b.parse().unwrap_or(0);
9514                if a == b {
9515                    0
9516                } else {
9517                    1
9518                }
9519            }
9520            [a, "-ne", b] => {
9521                let a: i64 = a.parse().unwrap_or(0);
9522                let b: i64 = b.parse().unwrap_or(0);
9523                if a != b {
9524                    0
9525                } else {
9526                    1
9527                }
9528            }
9529            [a, "-lt", b] => {
9530                let a: i64 = a.parse().unwrap_or(0);
9531                let b: i64 = b.parse().unwrap_or(0);
9532                if a < b {
9533                    0
9534                } else {
9535                    1
9536                }
9537            }
9538            [a, "-le", b] => {
9539                let a: i64 = a.parse().unwrap_or(0);
9540                let b: i64 = b.parse().unwrap_or(0);
9541                if a <= b {
9542                    0
9543                } else {
9544                    1
9545                }
9546            }
9547            [a, "-gt", b] => {
9548                let a: i64 = a.parse().unwrap_or(0);
9549                let b: i64 = b.parse().unwrap_or(0);
9550                if a > b {
9551                    0
9552                } else {
9553                    1
9554                }
9555            }
9556            [a, "-ge", b] => {
9557                let a: i64 = a.parse().unwrap_or(0);
9558                let b: i64 = b.parse().unwrap_or(0);
9559                if a >= b {
9560                    0
9561                } else {
9562                    1
9563                }
9564            }
9565
9566            // File comparisons
9567            [f1, "-nt", f2] => {
9568                let m1 = std::fs::metadata(f1).and_then(|m| m.modified()).ok();
9569                let m2 = std::fs::metadata(f2).and_then(|m| m.modified()).ok();
9570                match (m1, m2) {
9571                    (Some(t1), Some(t2)) => {
9572                        if t1 > t2 {
9573                            0
9574                        } else {
9575                            1
9576                        }
9577                    }
9578                    (Some(_), None) => 0,
9579                    _ => 1,
9580                }
9581            }
9582            [f1, "-ot", f2] => {
9583                let m1 = std::fs::metadata(f1).and_then(|m| m.modified()).ok();
9584                let m2 = std::fs::metadata(f2).and_then(|m| m.modified()).ok();
9585                match (m1, m2) {
9586                    (Some(t1), Some(t2)) => {
9587                        if t1 < t2 {
9588                            0
9589                        } else {
9590                            1
9591                        }
9592                    }
9593                    (None, Some(_)) => 0,
9594                    _ => 1,
9595                }
9596            }
9597            [f1, "-ef", f2] => {
9598                use std::os::unix::fs::MetadataExt;
9599                let m1 = std::fs::metadata(f1).ok();
9600                let m2 = std::fs::metadata(f2).ok();
9601                match (m1, m2) {
9602                    (Some(a), Some(b)) => {
9603                        if a.dev() == b.dev() && a.ino() == b.ino() {
9604                            0
9605                        } else {
9606                            1
9607                        }
9608                    }
9609                    _ => 1,
9610                }
9611            }
9612
9613            // Single string test
9614            [s] => {
9615                if !s.is_empty() {
9616                    0
9617                } else {
9618                    1
9619                }
9620            }
9621
9622            _ => 1,
9623        }
9624    }
9625
9626    fn builtin_local(&mut self, args: &[String]) -> i32 {
9627        self.builtin_typeset(args)
9628    }
9629
9630    fn builtin_declare(&mut self, args: &[String]) -> i32 {
9631        self.builtin_typeset(args)
9632    }
9633
9634    fn builtin_typeset(&mut self, args: &[String]) -> i32 {
9635        // Save old values when inside a function scope (local variable support).
9636        // Restored by call_function on function exit.
9637        if self.local_scope_depth > 0 {
9638            for arg in args {
9639                if arg.starts_with('-') || arg.starts_with('+') {
9640                    continue;
9641                }
9642                let name = arg.split('=').next().unwrap_or(arg);
9643                if !name.is_empty() {
9644                    let old_val = self.variables.get(name).cloned();
9645                    self.local_save_stack.push((name.to_string(), old_val));
9646                }
9647            }
9648        }
9649
9650        // typeset [ {+|-}AHUaghlmrtux ] [ {+|-}EFLRZip [ n ] ]
9651        //         [ + ] [ name[=value] ... ]
9652        // typeset -T [ {+|-}Urux ] [ {+|-}LRZp [ n ] ] SCALAR[=value] array
9653        // typeset -f [ {+|-}TUkmtuz ] [ + ] [ name ... ]
9654
9655        let mut is_array = false; // -a
9656        let mut is_assoc = false; // -A
9657        let mut is_export = false; // -x
9658        let mut is_integer = false; // -i
9659        let mut is_readonly = false; // -r
9660        let mut is_lower = false; // -l
9661        let mut is_upper = false; // -u
9662        let mut is_left_pad = false; // -L
9663        let mut is_right_pad = false; // -R
9664        let mut is_zero_pad = false; // -Z
9665        let mut is_float = false; // -F
9666        let mut is_float_exp = false; // -E
9667        let mut is_function = false; // -f
9668        let mut is_global = false; // -g
9669        let mut is_tied = false; // -T
9670        let mut is_hidden = false; // -H
9671        let mut is_hide_val = false; // -h
9672        let mut is_trace = false; // -t
9673        let mut print_mode = false; // -p
9674        let mut pattern_match = false; // -m
9675        let mut list_mode = false; // no args: list all
9676        let mut plus_mode = false; // +x etc: remove attribute
9677        let mut width: Option<usize> = None;
9678        let mut precision: Option<usize> = None;
9679        let mut var_args: Vec<String> = Vec::new();
9680
9681        let mut i = 0;
9682        while i < args.len() {
9683            let arg = &args[i];
9684
9685            if arg == "--" {
9686                i += 1;
9687                while i < args.len() {
9688                    var_args.push(args[i].clone());
9689                    i += 1;
9690                }
9691                break;
9692            }
9693
9694            if arg == "+" {
9695                plus_mode = true;
9696                i += 1;
9697                continue;
9698            }
9699
9700            if arg.starts_with('+') && arg.len() > 1 {
9701                plus_mode = true;
9702                for c in arg[1..].chars() {
9703                    match c {
9704                        'a' => is_array = false,
9705                        'A' => is_assoc = false,
9706                        'x' => is_export = false,
9707                        'i' => is_integer = false,
9708                        'r' => is_readonly = false,
9709                        'l' => is_lower = false,
9710                        'u' => is_upper = false,
9711                        'L' => is_left_pad = false,
9712                        'R' => is_right_pad = false,
9713                        'Z' => is_zero_pad = false,
9714                        'F' => is_float = false,
9715                        'E' => is_float_exp = false,
9716                        'f' => is_function = false,
9717                        'g' => is_global = false,
9718                        'T' => is_tied = false,
9719                        'H' => is_hidden = false,
9720                        'h' => is_hide_val = false,
9721                        't' => is_trace = false,
9722                        'p' => print_mode = false,
9723                        'm' => pattern_match = false,
9724                        _ => {}
9725                    }
9726                }
9727            } else if arg.starts_with('-') && arg.len() > 1 {
9728                let mut chars = arg[1..].chars().peekable();
9729                while let Some(c) = chars.next() {
9730                    match c {
9731                        'a' => is_array = true,
9732                        'A' => is_assoc = true,
9733                        'x' => is_export = true,
9734                        'i' => is_integer = true,
9735                        'r' => is_readonly = true,
9736                        'l' => is_lower = true,
9737                        'u' => is_upper = true,
9738                        'L' => {
9739                            is_left_pad = true;
9740                            // Check for width
9741                            let rest: String = chars.clone().collect();
9742                            if !rest.is_empty()
9743                                && rest
9744                                    .chars()
9745                                    .next()
9746                                    .map(|c| c.is_ascii_digit())
9747                                    .unwrap_or(false)
9748                            {
9749                                let num: String =
9750                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9751                                width = num.parse().ok();
9752                            }
9753                        }
9754                        'R' => {
9755                            is_right_pad = true;
9756                            let rest: String = chars.clone().collect();
9757                            if !rest.is_empty()
9758                                && rest
9759                                    .chars()
9760                                    .next()
9761                                    .map(|c| c.is_ascii_digit())
9762                                    .unwrap_or(false)
9763                            {
9764                                let num: String =
9765                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9766                                width = num.parse().ok();
9767                            }
9768                        }
9769                        'Z' => {
9770                            is_zero_pad = true;
9771                            let rest: String = chars.clone().collect();
9772                            if !rest.is_empty()
9773                                && rest
9774                                    .chars()
9775                                    .next()
9776                                    .map(|c| c.is_ascii_digit())
9777                                    .unwrap_or(false)
9778                            {
9779                                let num: String =
9780                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9781                                width = num.parse().ok();
9782                            }
9783                        }
9784                        'F' => {
9785                            is_float = true;
9786                            let rest: String = chars.clone().collect();
9787                            if !rest.is_empty()
9788                                && rest
9789                                    .chars()
9790                                    .next()
9791                                    .map(|c| c.is_ascii_digit())
9792                                    .unwrap_or(false)
9793                            {
9794                                let num: String =
9795                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9796                                precision = num.parse().ok();
9797                            }
9798                        }
9799                        'E' => {
9800                            is_float_exp = true;
9801                            let rest: String = chars.clone().collect();
9802                            if !rest.is_empty()
9803                                && rest
9804                                    .chars()
9805                                    .next()
9806                                    .map(|c| c.is_ascii_digit())
9807                                    .unwrap_or(false)
9808                            {
9809                                let num: String =
9810                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9811                                precision = num.parse().ok();
9812                            }
9813                        }
9814                        'f' => is_function = true,
9815                        'g' => is_global = true,
9816                        'T' => is_tied = true,
9817                        'H' => is_hidden = true,
9818                        'h' => is_hide_val = true,
9819                        't' => is_trace = true,
9820                        'p' => print_mode = true,
9821                        'm' => pattern_match = true,
9822                        _ => {}
9823                    }
9824                }
9825            } else {
9826                var_args.push(arg.clone());
9827            }
9828            i += 1;
9829        }
9830
9831        let _ = is_global;
9832        let _ = is_tied;
9833        let _ = is_hidden;
9834        let _ = is_hide_val;
9835        let _ = is_trace;
9836        let _ = pattern_match;
9837        let _ = precision;
9838
9839        // If -f (function mode) with no args, list functions
9840        if is_function && var_args.is_empty() {
9841            let mut func_names: Vec<_> = self.functions.keys().cloned().collect();
9842            func_names.sort();
9843            for name in &func_names {
9844                if let Some(func) = self.functions.get(name) {
9845                    if print_mode {
9846                        let body = crate::text::getpermtext(func);
9847                        println!("{} () {{\n\t{}\n}}", name, body.trim());
9848                    } else {
9849                        let body = crate::text::getpermtext(func);
9850                        println!("{} () {{\n\t{}\n}}", name, body.trim());
9851                    }
9852                }
9853            }
9854            return 0;
9855        }
9856
9857        // If -f with args, just show those functions
9858        if is_function {
9859            for name in &var_args {
9860                if let Some(func) = self.functions.get(name) {
9861                    if print_mode {
9862                        let body = crate::text::getpermtext(func);
9863                        println!("{} () {{\n\t{}\n}}", name, body.trim());
9864                    } else {
9865                        let body = crate::text::getpermtext(func);
9866                        println!("{} () {{\n\t{}\n}}", name, body.trim());
9867                    }
9868                }
9869            }
9870            return 0;
9871        }
9872
9873        // No args: list all variables with attributes
9874        if var_args.is_empty() {
9875            list_mode = true;
9876        }
9877
9878        if list_mode {
9879            let mut sorted_names: Vec<_> = self.variables.keys().cloned().collect();
9880            sorted_names.sort();
9881            for name in &sorted_names {
9882                let val = self.variables.get(name).cloned().unwrap_or_default();
9883                let mut attrs = String::new();
9884                if is_export || env::var(name).is_ok() {
9885                    attrs.push('x');
9886                }
9887                let is_arr = self.arrays.contains_key(name);
9888                let is_hash = self.assoc_arrays.contains_key(name);
9889                if is_arr {
9890                    attrs.push('a');
9891                }
9892                if is_hash {
9893                    attrs.push('A');
9894                }
9895                if print_mode {
9896                    // typeset -p: output re-executable code with values
9897                    let prefix = if attrs.is_empty() {
9898                        "typeset".to_string()
9899                    } else {
9900                        format!("typeset -{}", attrs)
9901                    };
9902                    if is_hash {
9903                        if let Some(assoc) = self.assoc_arrays.get(name) {
9904                            let mut pairs: Vec<_> = assoc.iter().collect();
9905                            pairs.sort_by_key(|(k, _)| (*k).clone());
9906                            let formatted: Vec<String> = pairs
9907                                .iter()
9908                                .map(|(k, v)| {
9909                                    format!("[{}]={}", shell_quote_value(k), shell_quote_value(v))
9910                                })
9911                                .collect();
9912                            println!("{} {}=( {} )", prefix, name, formatted.join(" "));
9913                        }
9914                    } else if is_arr {
9915                        if let Some(arr) = self.arrays.get(name) {
9916                            let formatted: Vec<String> =
9917                                arr.iter().map(|v| shell_quote_value(v)).collect();
9918                            println!("{} {}=( {} )", prefix, name, formatted.join(" "));
9919                        }
9920                    } else {
9921                        println!("{} {}={}", prefix, name, shell_quote_value(&val));
9922                    }
9923                } else if is_hide_val {
9924                    println!("{}={}", name, "*".repeat(val.len().min(8)));
9925                } else {
9926                    println!("{}={}", name, val);
9927                }
9928            }
9929            return 0;
9930        }
9931
9932        // Process variable assignments
9933        for arg in var_args {
9934            // Check if this starts an array assignment: "name=(" or "name=(value"
9935            if let Some(eq_pos) = arg.find('=') {
9936                let name = &arg[..eq_pos];
9937                let rest = &arg[eq_pos + 1..];
9938
9939                if rest.starts_with('(') {
9940                    // Array assignment - collect all elements until we find ')'
9941                    let mut elements = Vec::new();
9942                    let current = rest[1..].to_string(); // skip '('
9943
9944                    // Check if closing ) is in this arg
9945                    if let Some(close_pos) = current.find(')') {
9946                        let content = &current[..close_pos];
9947                        if !content.is_empty() {
9948                            elements.extend(content.split_whitespace().map(|s| s.to_string()));
9949                        }
9950                    } else {
9951                        // Single arg with just elements
9952                        if !current.is_empty() {
9953                            let trimmed = current.trim_end_matches(')');
9954                            elements.extend(trimmed.split_whitespace().map(|s| s.to_string()));
9955                        }
9956                    }
9957
9958                    // Set array variable
9959                    if is_assoc {
9960                        let mut assoc = std::collections::HashMap::new();
9961                        let mut iter = elements.iter();
9962                        while let Some(key) = iter.next() {
9963                            if let Some(val) = iter.next() {
9964                                assoc.insert(key.clone(), val.clone());
9965                            }
9966                        }
9967                        self.assoc_arrays.insert(name.to_string(), assoc);
9968                    } else {
9969                        self.arrays.insert(name.to_string(), elements);
9970                    }
9971                    self.variables.insert(name.to_string(), String::new());
9972                } else {
9973                    // Regular assignment - apply transformations
9974                    let mut value = rest.to_string();
9975
9976                    if is_integer {
9977                        // Force integer evaluation
9978                        value = self.evaluate_arithmetic(&value).to_string();
9979                    }
9980                    if is_lower {
9981                        value = value.to_lowercase();
9982                    }
9983                    if is_upper {
9984                        value = value.to_uppercase();
9985                    }
9986                    if let Some(w) = width {
9987                        if is_left_pad {
9988                            value = format!("{:<width$}", value, width = w);
9989                            value.truncate(w);
9990                        } else if is_right_pad || is_zero_pad {
9991                            let pad_char = if is_zero_pad { '0' } else { ' ' };
9992                            if value.len() < w {
9993                                value = format!(
9994                                    "{}{}",
9995                                    pad_char.to_string().repeat(w - value.len()),
9996                                    value
9997                                );
9998                            }
9999                            if value.len() > w {
10000                                value = value[value.len() - w..].to_string();
10001                            }
10002                        }
10003                    }
10004                    if is_float || is_float_exp {
10005                        if let Ok(f) = value.parse::<f64>() {
10006                            let prec = precision.unwrap_or(10);
10007                            value = if is_float_exp {
10008                                format!("{:.prec$e}", f, prec = prec)
10009                            } else {
10010                                format!("{:.prec$}", f, prec = prec)
10011                            };
10012                        }
10013                    }
10014
10015                    self.variables.insert(name.to_string(), value.clone());
10016
10017                    if is_export {
10018                        env::set_var(name, &value);
10019                    }
10020                }
10021            } else if is_array || is_assoc {
10022                // Just declaring the variable
10023                if is_assoc {
10024                    self.assoc_arrays
10025                        .insert(arg.clone(), std::collections::HashMap::new());
10026                } else {
10027                    self.arrays.insert(arg.clone(), Vec::new());
10028                }
10029                self.variables.insert(arg.clone(), String::new());
10030            } else {
10031                self.variables.insert(arg.clone(), String::new());
10032                if is_export {
10033                    env::set_var(&arg, "");
10034                }
10035            }
10036
10037            // Apply readonly flag — must come after the variable is set
10038            if is_readonly {
10039                let name = if let Some(eq_pos) = arg.find('=') {
10040                    arg[..eq_pos].to_string()
10041                } else {
10042                    arg.clone()
10043                };
10044                self.readonly_vars.insert(name);
10045            }
10046        }
10047        0
10048    }
10049
10050    fn builtin_read(&mut self, args: &[String]) -> i32 {
10051        // read [ -rszpqAclneE ] [ -t timeout ] [ -d delim ] [ -k [ num ] ] [ -u fd ]
10052        //      [ name[?prompt] ] [ name ... ]
10053        use std::io::{BufRead, Read as IoRead};
10054
10055        let mut raw_mode = false; // -r: don't interpret backslash escapes
10056        let mut silent = false; // -s: don't echo input
10057        let mut to_history = false; // -z: read from history stack
10058        let mut prompt_str: Option<String> = None; // -p prompt
10059        let mut use_array = false; // -A: read into array
10060        let mut timeout: Option<u64> = None; // -t timeout in seconds
10061        let mut delimiter = '\n'; // -d delim
10062        let mut nchars: Option<usize> = None; // -k num: read exactly num chars
10063        let mut fd = 0; // -u fd: read from fd
10064        let mut quiet = false; // -q: test only, don't assign
10065        let mut var_names: Vec<String> = Vec::new();
10066
10067        let mut i = 0;
10068        while i < args.len() {
10069            let arg = &args[i];
10070
10071            if arg == "--" {
10072                i += 1;
10073                while i < args.len() {
10074                    var_names.push(args[i].clone());
10075                    i += 1;
10076                }
10077                break;
10078            }
10079
10080            if arg.starts_with('-') && arg.len() > 1 {
10081                let mut chars = arg[1..].chars().peekable();
10082                while let Some(ch) = chars.next() {
10083                    match ch {
10084                        'r' => raw_mode = true,
10085                        's' => silent = true,
10086                        'z' => to_history = true,
10087                        'A' => use_array = true,
10088                        'c' | 'l' | 'n' | 'e' | 'E' => {} // TODO
10089                        'q' => quiet = true,
10090                        't' => {
10091                            let rest: String = chars.collect();
10092                            if !rest.is_empty() {
10093                                timeout = rest.parse().ok();
10094                            } else {
10095                                i += 1;
10096                                if i < args.len() {
10097                                    timeout = args[i].parse().ok();
10098                                }
10099                            }
10100                            break;
10101                        }
10102                        'd' => {
10103                            let rest: String = chars.collect();
10104                            if !rest.is_empty() {
10105                                delimiter = rest.chars().next().unwrap_or('\n');
10106                            } else {
10107                                i += 1;
10108                                if i < args.len() {
10109                                    delimiter = args[i].chars().next().unwrap_or('\n');
10110                                }
10111                            }
10112                            break;
10113                        }
10114                        'k' => {
10115                            let rest: String = chars.collect();
10116                            if !rest.is_empty() {
10117                                nchars = Some(rest.parse().unwrap_or(1));
10118                            } else if i + 1 < args.len()
10119                                && args[i + 1].chars().all(|c| c.is_ascii_digit())
10120                            {
10121                                i += 1;
10122                                nchars = Some(args[i].parse().unwrap_or(1));
10123                            } else {
10124                                nchars = Some(1);
10125                            }
10126                            break;
10127                        }
10128                        'u' => {
10129                            let rest: String = chars.collect();
10130                            if !rest.is_empty() {
10131                                fd = rest.parse().unwrap_or(0);
10132                            } else {
10133                                i += 1;
10134                                if i < args.len() {
10135                                    fd = args[i].parse().unwrap_or(0);
10136                                }
10137                            }
10138                            break;
10139                        }
10140                        'p' => {
10141                            let rest: String = chars.collect();
10142                            if !rest.is_empty() {
10143                                prompt_str = Some(rest);
10144                            } else {
10145                                i += 1;
10146                                if i < args.len() {
10147                                    prompt_str = Some(args[i].clone());
10148                                }
10149                            }
10150                            break;
10151                        }
10152                        _ => {}
10153                    }
10154                }
10155            } else {
10156                if let Some(pos) = arg.find('?') {
10157                    var_names.push(arg[..pos].to_string());
10158                    prompt_str = Some(arg[pos + 1..].to_string());
10159                } else {
10160                    var_names.push(arg.clone());
10161                }
10162            }
10163            i += 1;
10164        }
10165
10166        if var_names.is_empty() {
10167            var_names.push("REPLY".to_string());
10168        }
10169
10170        if let Some(ref p) = prompt_str {
10171            eprint!("{}", p);
10172            let _ = std::io::stderr().flush();
10173        }
10174
10175        let _ = to_history;
10176        let _ = fd;
10177        let _ = silent;
10178
10179        let input = if let Some(n) = nchars {
10180            let mut buf = vec![0u8; n];
10181            let stdin = io::stdin();
10182            if let Some(_t) = timeout {
10183                // TODO: proper timeout
10184            }
10185            match stdin.lock().read_exact(&mut buf) {
10186                Ok(_) => String::from_utf8_lossy(&buf).to_string(),
10187                Err(_) => return 1,
10188            }
10189        } else {
10190            let stdin = io::stdin();
10191            let mut input = String::new();
10192            if delimiter == '\n' {
10193                match stdin.lock().read_line(&mut input) {
10194                    Ok(0) => return 1,
10195                    Ok(_) => {}
10196                    Err(_) => return 1,
10197                }
10198            } else {
10199                let mut byte = [0u8; 1];
10200                loop {
10201                    match stdin.lock().read_exact(&mut byte) {
10202                        Ok(_) => {
10203                            let c = byte[0] as char;
10204                            if c == delimiter {
10205                                break;
10206                            }
10207                            input.push(c);
10208                        }
10209                        Err(_) => break,
10210                    }
10211                }
10212            }
10213            input
10214                .trim_end_matches('\n')
10215                .trim_end_matches('\r')
10216                .to_string()
10217        };
10218
10219        let processed = if raw_mode {
10220            input
10221        } else {
10222            input.replace("\\\n", "")
10223        };
10224
10225        if quiet {
10226            return if processed.is_empty() { 1 } else { 0 };
10227        }
10228
10229        if use_array {
10230            let var = &var_names[0];
10231            let words: Vec<String> = processed.split_whitespace().map(String::from).collect();
10232            self.arrays.insert(var.clone(), words);
10233        } else if var_names.len() == 1 {
10234            let var = &var_names[0];
10235            env::set_var(var, &processed);
10236            self.variables.insert(var.clone(), processed);
10237        } else {
10238            let ifs = self
10239                .variables
10240                .get("IFS")
10241                .map(|s| s.as_str())
10242                .unwrap_or(" \t\n");
10243            let words: Vec<&str> = processed
10244                .split(|c| ifs.contains(c))
10245                .filter(|s| !s.is_empty())
10246                .collect();
10247
10248            for (j, var) in var_names.iter().enumerate() {
10249                if j < words.len() {
10250                    if j == var_names.len() - 1 && words.len() > var_names.len() {
10251                        let remaining = words[j..].join(" ");
10252                        env::set_var(var, &remaining);
10253                        self.variables.insert(var.clone(), remaining);
10254                    } else {
10255                        env::set_var(var, words[j]);
10256                        self.variables.insert(var.clone(), words[j].to_string());
10257                    }
10258                } else {
10259                    env::set_var(var, "");
10260                    self.variables.insert(var.clone(), String::new());
10261                }
10262            }
10263        }
10264
10265        0
10266    }
10267
10268    fn builtin_shift(&mut self, args: &[String]) -> i32 {
10269        // shift [ -p ] [ n ] [ name ... ]
10270        // -p: shift from end instead of beginning (pop)
10271        // n: number of elements to shift (default 1)
10272        // name: array names to shift (default: shift positional parameters)
10273
10274        let mut from_end = false;
10275        let mut count = 1usize;
10276        let mut array_names: Vec<String> = Vec::new();
10277
10278        let mut i = 0;
10279        while i < args.len() {
10280            let arg = &args[i];
10281            if arg == "-p" {
10282                from_end = true;
10283            } else if arg.chars().all(|c| c.is_ascii_digit()) {
10284                count = arg.parse().unwrap_or(1);
10285            } else {
10286                array_names.push(arg.clone());
10287            }
10288            i += 1;
10289        }
10290
10291        if array_names.is_empty() {
10292            // Shift positional parameters
10293            if from_end {
10294                for _ in 0..count {
10295                    if !self.positional_params.is_empty() {
10296                        self.positional_params.pop();
10297                    }
10298                }
10299            } else {
10300                for _ in 0..count.min(self.positional_params.len()) {
10301                    self.positional_params.remove(0);
10302                }
10303            }
10304        } else {
10305            // Shift specified arrays
10306            for name in array_names {
10307                if let Some(arr) = self.arrays.get_mut(&name) {
10308                    if from_end {
10309                        for _ in 0..count {
10310                            if !arr.is_empty() {
10311                                arr.pop();
10312                            }
10313                        }
10314                    } else {
10315                        for _ in 0..count {
10316                            if !arr.is_empty() {
10317                                arr.remove(0);
10318                            }
10319                        }
10320                    }
10321                }
10322            }
10323        }
10324
10325        0
10326    }
10327
10328    #[tracing::instrument(level = "debug", skip(self))]
10329    fn builtin_eval(&mut self, args: &[String]) -> i32 {
10330        let code = args.join(" ");
10331        match self.execute_script(&code) {
10332            Ok(status) => status,
10333            Err(e) => {
10334                eprintln!("eval: {}", e);
10335                1
10336            }
10337        }
10338    }
10339
10340    fn builtin_autoload(&mut self, args: &[String]) -> i32 {
10341        // Parse options like zsh: -U (no alias), -z (zsh style), -k (ksh style),
10342        // -X (execute now), -x (export), -r (resolve), -R (resolve recurse),
10343        // -t (trace), -T (trace local), -W (warn nested), -d (use calling dir)
10344        let mut functions = Vec::new();
10345        let mut no_alias = false; // -U
10346        let mut zsh_style = false; // -z
10347        let mut ksh_style = false; // -k
10348        let mut execute_now = false; // -X
10349        let mut resolve = false; // -r
10350        let mut trace = false; // -t
10351        let mut use_caller_dir = false; // -d
10352        let _list_mode = false;
10353
10354        let mut i = 0;
10355        while i < args.len() {
10356            let arg = &args[i];
10357
10358            if arg == "--" {
10359                i += 1;
10360                break;
10361            }
10362
10363            if arg.starts_with('+') {
10364                let flags = &arg[1..];
10365                for c in flags.chars() {
10366                    match c {
10367                        'U' => no_alias = false,
10368                        'z' => zsh_style = false,
10369                        'k' => ksh_style = false,
10370                        't' => trace = false,
10371                        'd' => use_caller_dir = false,
10372                        _ => {}
10373                    }
10374                }
10375            } else if arg.starts_with('-') {
10376                let flags = &arg[1..];
10377                if flags.is_empty() {
10378                    // Just "-" means end of options
10379                    i += 1;
10380                    break;
10381                }
10382                for c in flags.chars() {
10383                    match c {
10384                        'U' => no_alias = true,
10385                        'z' => zsh_style = true,
10386                        'k' => ksh_style = true,
10387                        'X' => execute_now = true,
10388                        'r' | 'R' => resolve = true,
10389                        't' => trace = true,
10390                        'T' => {} // trace local
10391                        'W' => {} // warn nested
10392                        'd' => use_caller_dir = true,
10393                        'w' => {} // wordcode
10394                        'm' => {} // pattern match
10395                        _ => {}
10396                    }
10397                }
10398            } else {
10399                functions.push(arg.clone());
10400            }
10401            i += 1;
10402        }
10403
10404        // Collect remaining args as function names
10405        while i < args.len() {
10406            functions.push(args[i].clone());
10407            i += 1;
10408        }
10409
10410        // If no functions specified, list autoloaded functions
10411        if functions.is_empty() && !execute_now {
10412            for (name, _) in &self.autoload_pending {
10413                if no_alias && zsh_style {
10414                    println!("autoload -Uz {}", name);
10415                } else if no_alias {
10416                    println!("autoload -U {}", name);
10417                } else {
10418                    println!("autoload {}", name);
10419                }
10420            }
10421            return 0;
10422        }
10423
10424        // Handle -X: load and execute function immediately (called from stub)
10425        // When a stub function calls `builtin autoload -Xz`, we load the real function
10426        // and then need to execute it with the original arguments
10427        if execute_now {
10428            for func_name in &functions {
10429                // Load the function from fpath
10430                if let Some(loaded) = self.load_autoload_function(func_name) {
10431                    // Extract body from FunctionDef
10432                    let body = match loaded {
10433                        ShellCommand::FunctionDef(_, body) => (*body).clone(),
10434                        other => other,
10435                    };
10436                    // Replace the stub with the real function
10437                    self.functions.insert(func_name.clone(), body);
10438                    // Remove from pending
10439                    self.autoload_pending.remove(func_name);
10440                } else {
10441                    eprintln!(
10442                        "autoload: {}: function definition file not found",
10443                        func_name
10444                    );
10445                    return 1;
10446                }
10447            }
10448            return 0;
10449        }
10450
10451        // Register functions for autoload - create stub functions
10452        for func_name in &functions {
10453            // Store autoload metadata
10454            let mut flags = AutoloadFlags::empty();
10455            if no_alias {
10456                flags |= AutoloadFlags::NO_ALIAS;
10457            }
10458            if zsh_style {
10459                flags |= AutoloadFlags::ZSH_STYLE;
10460            }
10461            if ksh_style {
10462                flags |= AutoloadFlags::KSH_STYLE;
10463            }
10464            if trace {
10465                flags |= AutoloadFlags::TRACE;
10466            }
10467            if use_caller_dir {
10468                flags |= AutoloadFlags::USE_CALLER_DIR;
10469            }
10470
10471            self.autoload_pending.insert(func_name.clone(), flags);
10472
10473            // Create a stub function: `builtin autoload -Xz funcname && funcname "$@"`
10474            // When called, this loads the real function and re-calls it
10475            let autoload_opts = if zsh_style && no_alias {
10476                "-XUz"
10477            } else if zsh_style {
10478                "-Xz"
10479            } else if no_alias {
10480                "-XU"
10481            } else {
10482                "-X"
10483            };
10484
10485            // The stub: builtin autoload -Xz funcname && funcname "$@"
10486            let stub = ShellCommand::List(vec![
10487                (
10488                    ShellCommand::Simple(SimpleCommand {
10489                        assignments: vec![],
10490                        words: vec![
10491                            ShellWord::Literal("builtin".to_string()),
10492                            ShellWord::Literal("autoload".to_string()),
10493                            ShellWord::Literal(autoload_opts.to_string()),
10494                            ShellWord::Literal(func_name.clone()),
10495                        ],
10496                        redirects: vec![],
10497                    }),
10498                    ListOp::And,
10499                ),
10500                (
10501                    ShellCommand::Simple(SimpleCommand {
10502                        assignments: vec![],
10503                        words: vec![
10504                            ShellWord::Literal(func_name.clone()),
10505                            ShellWord::DoubleQuoted(vec![ShellWord::Variable("@".to_string())]),
10506                        ],
10507                        redirects: vec![],
10508                    }),
10509                    ListOp::Semi,
10510                ),
10511            ]);
10512
10513            self.functions.insert(func_name.clone(), stub);
10514
10515            // If -r or -R, resolve the path now to verify it exists
10516            if resolve {
10517                if self.find_function_file(func_name).is_none() {
10518                    eprintln!(
10519                        "autoload: {}: function definition file not found",
10520                        func_name
10521                    );
10522                }
10523            }
10524        }
10525
10526        // Batch pre-resolution: when multiple autoloads are registered at once
10527        // (common during .zshrc processing), dispatch fpath lookups in parallel
10528        // across the worker pool to pre-read function files into the OS page cache.
10529        if functions.len() >= 4 && !resolve && !execute_now {
10530            let fpath_dirs: Vec<PathBuf> = self.fpath.clone();
10531            let names: Vec<String> = functions.clone();
10532            let pool = std::sync::Arc::clone(&self.worker_pool);
10533
10534            tracing::debug!(
10535                count = names.len(),
10536                fpath_dirs = fpath_dirs.len(),
10537                "batch autoload: pre-resolving fpath lookups on worker pool"
10538            );
10539
10540            // Submit resolution tasks — each worker scans fpath for a subset of names.
10541            // Results are cached in a shared map for later use by load_autoload_function.
10542            let resolved = std::sync::Arc::new(parking_lot::Mutex::new(
10543                HashMap::<String, PathBuf>::with_capacity(names.len()),
10544            ));
10545
10546            for name in names {
10547                let dirs = fpath_dirs.clone();
10548                let resolved = std::sync::Arc::clone(&resolved);
10549                pool.submit(move || {
10550                    for dir in &dirs {
10551                        let path = dir.join(&name);
10552                        if path.exists() && path.is_file() {
10553                            // Pre-read to warm OS page cache (the read result is discarded,
10554                            // but the pages stay in the kernel buffer cache)
10555                            let _ = std::fs::read(&path);
10556                            resolved.lock().insert(name.clone(), path);
10557                            tracing::trace!(func = %name, "autoload batch: pre-resolved");
10558                            break;
10559                        }
10560                    }
10561                });
10562            }
10563        }
10564
10565        0
10566    }
10567
10568    /// Find a function file in fpath
10569    fn find_function_file(&self, name: &str) -> Option<PathBuf> {
10570        for dir in &self.fpath {
10571            let path = dir.join(name);
10572            if path.exists() && path.is_file() {
10573                return Some(path);
10574            }
10575        }
10576        None
10577    }
10578
10579    /// Load an autoloaded function from fpath - reads file and parses it
10580    fn load_autoload_function(&mut self, name: &str) -> Option<ShellCommand> {
10581        // FAST PATH: Try SQLite cache first (no filesystem access)
10582        // Skip in zsh_compat mode - use traditional fpath scanning only
10583        if !self.zsh_compat {
10584            if let Some(ref cache) = self.compsys_cache {
10585                // FASTEST: try cached bytecodes (skip lex+parse+compile entirely)
10586                if let Ok(Some(bc_blob)) = cache.get_autoload_bytecode(name) {
10587                    // Try fusevm::Chunk first (new format — actual bytecodes)
10588                    if let Ok(chunk) = bincode::deserialize::<fusevm::Chunk>(&bc_blob) {
10589                        if !chunk.ops.is_empty() {
10590                            tracing::trace!(
10591                                name,
10592                                bytes = bc_blob.len(),
10593                                ops = chunk.ops.len(),
10594                                "autoload: bytecode cache hit → VM"
10595                            );
10596                            // Execute directly on fusevm — no parse, no compile
10597                            let mut vm = fusevm::VM::new(chunk);
10598                            let _ = vm.run();
10599                            self.last_status = vm.last_status;
10600                            // Return a no-op so the caller doesn't try to execute again
10601                            return Some(ShellCommand::Simple(crate::parser::SimpleCommand {
10602                                assignments: Vec::new(),
10603                                words: Vec::new(),
10604                                redirects: Vec::new(),
10605                            }));
10606                        }
10607                    }
10608                    // Fallback: try legacy Vec<ShellCommand> format (migration)
10609                    if let Ok(commands) = bincode::deserialize::<Vec<ShellCommand>>(&bc_blob) {
10610                        if !commands.is_empty() {
10611                            tracing::trace!(
10612                                name,
10613                                bytes = bc_blob.len(),
10614                                "autoload: legacy AST cache hit"
10615                            );
10616                            return Some(self.wrap_autoload_commands(name, commands));
10617                        }
10618                    }
10619                }
10620
10621                // FAST: cached source text — parse + compile (still no filesystem access)
10622                if let Ok(Some(body)) = cache.get_autoload_body(name) {
10623                    let mut parser = ShellParser::new(&body);
10624                    if let Ok(commands) = parser.parse_script() {
10625                        if !commands.is_empty() {
10626                            // Compile to bytecodes and cache for next time
10627                            let compiler = crate::shell_compiler::ShellCompiler::new();
10628                            let chunk = compiler.compile(&commands);
10629                            if let Ok(blob) = bincode::serialize(&chunk) {
10630                                let _ = cache.set_autoload_bytecode(name, &blob);
10631                                tracing::trace!(
10632                                    name,
10633                                    bytes = blob.len(),
10634                                    "autoload: bytecodes compiled and cached"
10635                                );
10636                            }
10637                            return Some(self.wrap_autoload_commands(name, commands));
10638                        }
10639                    }
10640                }
10641            }
10642        }
10643
10644        // SLOW PATH: Try ZWC cache (but skip if we're reloading an existing function)
10645        if !self.functions.contains_key(name) {
10646            // Try to load from ZWC files
10647            for dir in &self.fpath.clone() {
10648                // Try dir.zwc (e.g., /path/to/src.zwc for /path/to/src)
10649                let zwc_path = dir.with_extension("zwc");
10650                if zwc_path.exists() {
10651                    // Function name in directory ZWC includes path prefix
10652                    let prefixed_name = format!(
10653                        "{}/{}",
10654                        dir.file_name().and_then(|n| n.to_str()).unwrap_or(""),
10655                        name
10656                    );
10657                    if let Some(func) = self.load_function_from_zwc(&zwc_path, &prefixed_name) {
10658                        return Some(func);
10659                    }
10660                    // Also try without prefix
10661                    if let Some(func) = self.load_function_from_zwc(&zwc_path, name) {
10662                        return Some(func);
10663                    }
10664                }
10665                // Try individual function.zwc
10666                let func_zwc = dir.join(format!("{}.zwc", name));
10667                if func_zwc.exists() {
10668                    if let Some(func) = self.load_function_from_zwc(&func_zwc, name) {
10669                        return Some(func);
10670                    }
10671                }
10672            }
10673        }
10674
10675        // SLOWEST PATH: Find the function file in fpath
10676        let path = self.find_function_file(name)?;
10677
10678        // Read the file
10679        let content = std::fs::read_to_string(&path).ok()?;
10680
10681        // Parse the content
10682        let mut parser = ShellParser::new(&content);
10683
10684        if let Ok(commands) = parser.parse_script() {
10685            if commands.is_empty() {
10686                return None;
10687            }
10688
10689            // Check if it's a single function definition for this name (ksh style)
10690            if commands.len() == 1 {
10691                if let ShellCommand::FunctionDef(ref fn_name, _) = commands[0] {
10692                    if fn_name == name {
10693                        return Some(commands[0].clone());
10694                    }
10695                }
10696            }
10697
10698            // Otherwise, the file contents become the function body (zsh style)
10699            // Wrap all commands in a List
10700            let body = if commands.len() == 1 {
10701                commands.into_iter().next().unwrap()
10702            } else {
10703                // Convert to List with Semi separators
10704                let list_cmds: Vec<(ShellCommand, ListOp)> =
10705                    commands.into_iter().map(|c| (c, ListOp::Semi)).collect();
10706                ShellCommand::List(list_cmds)
10707            };
10708
10709            return Some(ShellCommand::FunctionDef(name.to_string(), Box::new(body)));
10710        }
10711
10712        None
10713    }
10714
10715    /// Convert parsed commands into a FunctionDef, handling ksh vs zsh style.
10716    fn wrap_autoload_commands(&self, name: &str, commands: Vec<ShellCommand>) -> ShellCommand {
10717        // ksh style: file contains a single function definition for this name
10718        if commands.len() == 1 {
10719            if let ShellCommand::FunctionDef(ref fn_name, _) = commands[0] {
10720                if fn_name == name {
10721                    return commands.into_iter().next().unwrap();
10722                }
10723            }
10724        }
10725        // zsh style: file body IS the function body
10726        let body = if commands.len() == 1 {
10727            commands.into_iter().next().unwrap()
10728        } else {
10729            let list_cmds: Vec<(ShellCommand, ListOp)> =
10730                commands.into_iter().map(|c| (c, ListOp::Semi)).collect();
10731            ShellCommand::List(list_cmds)
10732        };
10733        ShellCommand::FunctionDef(name.to_string(), Box::new(body))
10734    }
10735
10736    /// Check if a function is autoload pending and load it if so
10737    pub fn maybe_autoload(&mut self, name: &str) -> bool {
10738        if self.autoload_pending.contains_key(name) {
10739            if let Some(func) = self.load_autoload_function(name) {
10740                // For FunctionDef, extract the body and store it
10741                let to_store = match func {
10742                    ShellCommand::FunctionDef(_, body) => (*body).clone(),
10743                    other => other,
10744                };
10745                self.functions.insert(name.to_string(), to_store);
10746                self.autoload_pending.remove(name);
10747                return true;
10748            }
10749        }
10750        false
10751    }
10752
10753    fn builtin_jobs(&mut self, args: &[String]) -> i32 {
10754        // jobs [ -dlprsZ ] [ job ... ]
10755        // -l: long format (show PID)
10756        // -p: print process group IDs only
10757        // -d: show directory from which job was started
10758        // -r: show running jobs only
10759        // -s: show stopped jobs only
10760        // -Z: set process name (not relevant here)
10761
10762        let mut long_format = false;
10763        let mut pids_only = false;
10764        let mut show_dir = false;
10765        let mut running_only = false;
10766        let mut stopped_only = false;
10767        let mut job_ids: Vec<usize> = Vec::new();
10768
10769        for arg in args {
10770            if arg.starts_with('-') {
10771                for c in arg[1..].chars() {
10772                    match c {
10773                        'l' => long_format = true,
10774                        'p' => pids_only = true,
10775                        'd' => show_dir = true,
10776                        'r' => running_only = true,
10777                        's' => stopped_only = true,
10778                        'Z' => {} // ignore
10779                        _ => {}
10780                    }
10781                }
10782            } else if arg.starts_with('%') {
10783                if let Ok(id) = arg[1..].parse::<usize>() {
10784                    job_ids.push(id);
10785                }
10786            } else if let Ok(id) = arg.parse::<usize>() {
10787                job_ids.push(id);
10788            }
10789        }
10790
10791        // Reap finished jobs first
10792        for job in self.jobs.reap_finished() {
10793            if !running_only && !stopped_only {
10794                if pids_only {
10795                    println!("{}", job.pid);
10796                } else {
10797                    println!("[{}]  Done                    {}", job.id, job.command);
10798                }
10799            }
10800        }
10801
10802        // List jobs (optionally filtered)
10803        for job in self.jobs.list() {
10804            // Filter by specific job IDs if provided
10805            if !job_ids.is_empty() && !job_ids.contains(&job.id) {
10806                continue;
10807            }
10808
10809            // Filter by state
10810            if running_only && job.state != JobState::Running {
10811                continue;
10812            }
10813            if stopped_only && job.state != JobState::Stopped {
10814                continue;
10815            }
10816
10817            if pids_only {
10818                println!("{}", job.pid);
10819                continue;
10820            }
10821
10822            let marker = if job.is_current { "+" } else { "-" };
10823            let state = match job.state {
10824                JobState::Running => "running",
10825                JobState::Stopped => "suspended",
10826                JobState::Done => "done",
10827            };
10828
10829            if long_format {
10830                println!(
10831                    "[{}]{} {:6} {}  {}",
10832                    job.id, marker, job.pid, state, job.command
10833                );
10834            } else {
10835                println!("[{}]{} {}  {}", job.id, marker, state, job.command);
10836            }
10837
10838            if show_dir {
10839                if let Ok(cwd) = env::current_dir() {
10840                    println!("    (pwd: {})", cwd.display());
10841                }
10842            }
10843        }
10844        0
10845    }
10846
10847    fn builtin_fg(&mut self, args: &[String]) -> i32 {
10848        let job_id = if let Some(arg) = args.first() {
10849            // Parse %N or just N
10850            let s = arg.trim_start_matches('%');
10851            match s.parse::<usize>() {
10852                Ok(id) => Some(id),
10853                Err(_) => {
10854                    eprintln!("fg: {}: no such job", arg);
10855                    return 1;
10856                }
10857            }
10858        } else {
10859            self.jobs.current().map(|j| j.id)
10860        };
10861
10862        let Some(id) = job_id else {
10863            eprintln!("fg: no current job");
10864            return 1;
10865        };
10866
10867        let Some(job) = self.jobs.get(id) else {
10868            eprintln!("fg: %{}: no such job", id);
10869            return 1;
10870        };
10871
10872        let pid = job.pid;
10873        let cmd = job.command.clone();
10874        println!("{}", cmd);
10875
10876        // Continue the job
10877        if let Err(e) = continue_job(pid) {
10878            eprintln!("fg: {}", e);
10879            return 1;
10880        }
10881
10882        // Wait for it
10883        match wait_for_job(pid) {
10884            Ok(status) => {
10885                self.jobs.remove(id);
10886                status
10887            }
10888            Err(e) => {
10889                eprintln!("fg: {}", e);
10890                1
10891            }
10892        }
10893    }
10894
10895    fn builtin_bg(&mut self, args: &[String]) -> i32 {
10896        let job_id = if let Some(arg) = args.first() {
10897            let s = arg.trim_start_matches('%');
10898            match s.parse::<usize>() {
10899                Ok(id) => Some(id),
10900                Err(_) => {
10901                    eprintln!("bg: {}: no such job", arg);
10902                    return 1;
10903                }
10904            }
10905        } else {
10906            self.jobs.current().map(|j| j.id)
10907        };
10908
10909        let Some(id) = job_id else {
10910            eprintln!("bg: no current job");
10911            return 1;
10912        };
10913
10914        let Some(job) = self.jobs.get_mut(id) else {
10915            eprintln!("bg: %{}: no such job", id);
10916            return 1;
10917        };
10918
10919        let pid = job.pid;
10920        let cmd = job.command.clone();
10921
10922        if let Err(e) = continue_job(pid) {
10923            eprintln!("bg: {}", e);
10924            return 1;
10925        }
10926
10927        job.state = JobState::Running;
10928        println!("[{}] {} &", id, cmd);
10929        0
10930    }
10931
10932    fn builtin_kill(&mut self, args: &[String]) -> i32 {
10933        // kill [ -s signal_name | -n signal_number | -sig ] job ...
10934        // kill -l [ sig ... ]
10935        use crate::jobs::send_signal;
10936        use nix::sys::signal::Signal;
10937
10938        if args.is_empty() {
10939            eprintln!("kill: usage: kill [-s signal | -n num | -sig] pid ...");
10940            eprintln!("       kill -l [sig ...]");
10941            return 1;
10942        }
10943
10944        // Signal name/number mapping
10945        let signal_map: &[(&str, i32, Signal)] = &[
10946            ("HUP", 1, Signal::SIGHUP),
10947            ("INT", 2, Signal::SIGINT),
10948            ("QUIT", 3, Signal::SIGQUIT),
10949            ("ILL", 4, Signal::SIGILL),
10950            ("TRAP", 5, Signal::SIGTRAP),
10951            ("ABRT", 6, Signal::SIGABRT),
10952            ("BUS", 7, Signal::SIGBUS),
10953            ("FPE", 8, Signal::SIGFPE),
10954            ("KILL", 9, Signal::SIGKILL),
10955            ("USR1", 10, Signal::SIGUSR1),
10956            ("SEGV", 11, Signal::SIGSEGV),
10957            ("USR2", 12, Signal::SIGUSR2),
10958            ("PIPE", 13, Signal::SIGPIPE),
10959            ("ALRM", 14, Signal::SIGALRM),
10960            ("TERM", 15, Signal::SIGTERM),
10961            ("CHLD", 17, Signal::SIGCHLD),
10962            ("CONT", 18, Signal::SIGCONT),
10963            ("STOP", 19, Signal::SIGSTOP),
10964            ("TSTP", 20, Signal::SIGTSTP),
10965            ("TTIN", 21, Signal::SIGTTIN),
10966            ("TTOU", 22, Signal::SIGTTOU),
10967            ("URG", 23, Signal::SIGURG),
10968            ("XCPU", 24, Signal::SIGXCPU),
10969            ("XFSZ", 25, Signal::SIGXFSZ),
10970            ("VTALRM", 26, Signal::SIGVTALRM),
10971            ("PROF", 27, Signal::SIGPROF),
10972            ("WINCH", 28, Signal::SIGWINCH),
10973            ("IO", 29, Signal::SIGIO),
10974            ("SYS", 31, Signal::SIGSYS),
10975        ];
10976
10977        let mut sig = Signal::SIGTERM;
10978        let mut pids: Vec<String> = Vec::new();
10979        let mut list_mode = false;
10980        let mut list_args: Vec<String> = Vec::new();
10981
10982        let mut i = 0;
10983        while i < args.len() {
10984            let arg = &args[i];
10985
10986            if arg == "-l" || arg == "-L" {
10987                list_mode = true;
10988                // Remaining args are signal numbers to translate
10989                list_args = args[i + 1..].to_vec();
10990                break;
10991            } else if arg == "-s" {
10992                // -s signal_name
10993                i += 1;
10994                if i >= args.len() {
10995                    eprintln!("kill: -s requires an argument");
10996                    return 1;
10997                }
10998                let sig_name = args[i].to_uppercase();
10999                let sig_name = sig_name.strip_prefix("SIG").unwrap_or(&sig_name);
11000                if let Some((_, _, s)) = signal_map.iter().find(|(name, _, _)| *name == sig_name) {
11001                    sig = *s;
11002                } else {
11003                    eprintln!("kill: invalid signal: {}", args[i]);
11004                    return 1;
11005                }
11006            } else if arg == "-n" {
11007                // -n signal_number
11008                i += 1;
11009                if i >= args.len() {
11010                    eprintln!("kill: -n requires an argument");
11011                    return 1;
11012                }
11013                let num: i32 = match args[i].parse() {
11014                    Ok(n) => n,
11015                    Err(_) => {
11016                        eprintln!("kill: invalid signal number: {}", args[i]);
11017                        return 1;
11018                    }
11019                };
11020                if let Some((_, _, s)) = signal_map.iter().find(|(_, n, _)| *n == num) {
11021                    sig = *s;
11022                } else {
11023                    eprintln!("kill: invalid signal number: {}", num);
11024                    return 1;
11025                }
11026            } else if arg.starts_with('-') && arg.len() > 1 {
11027                // -SIGNAL or -NUM
11028                let sig_str = &arg[1..];
11029                let sig_upper = sig_str.to_uppercase();
11030                let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
11031
11032                // Try as number first
11033                if let Ok(num) = sig_str.parse::<i32>() {
11034                    if let Some((_, _, s)) = signal_map.iter().find(|(_, n, _)| *n == num) {
11035                        sig = *s;
11036                    } else {
11037                        eprintln!("kill: invalid signal: {}", arg);
11038                        return 1;
11039                    }
11040                } else if let Some((_, _, s)) =
11041                    signal_map.iter().find(|(name, _, _)| *name == sig_name)
11042                {
11043                    sig = *s;
11044                } else {
11045                    eprintln!("kill: invalid signal: {}", arg);
11046                    return 1;
11047                }
11048            } else {
11049                pids.push(arg.clone());
11050            }
11051            i += 1;
11052        }
11053
11054        // Handle -l (list signals)
11055        if list_mode {
11056            if list_args.is_empty() {
11057                // List all signals
11058                for (name, num, _) in signal_map {
11059                    println!("{:2}) SIG{}", num, name);
11060                }
11061            } else {
11062                // Translate signal numbers to names or vice versa
11063                for arg in &list_args {
11064                    if let Ok(num) = arg.parse::<i32>() {
11065                        // Number -> name
11066                        if let Some((name, _, _)) = signal_map.iter().find(|(_, n, _)| *n == num) {
11067                            println!("{}", name);
11068                        } else {
11069                            eprintln!("kill: unknown signal: {}", num);
11070                        }
11071                    } else {
11072                        // Name -> number
11073                        let sig_upper = arg.to_uppercase();
11074                        let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
11075                        if let Some((_, num, _)) =
11076                            signal_map.iter().find(|(name, _, _)| *name == sig_name)
11077                        {
11078                            println!("{}", num);
11079                        } else {
11080                            eprintln!("kill: unknown signal: {}", arg);
11081                        }
11082                    }
11083                }
11084            }
11085            return 0;
11086        }
11087
11088        if pids.is_empty() {
11089            eprintln!("kill: usage: kill [-s signal | -n num | -sig] pid ...");
11090            return 1;
11091        }
11092
11093        let mut status = 0;
11094        for arg in &pids {
11095            // Handle %job syntax
11096            if arg.starts_with('%') {
11097                let id: usize = match arg[1..].parse() {
11098                    Ok(id) => id,
11099                    Err(_) => {
11100                        eprintln!("kill: {}: no such job", arg);
11101                        status = 1;
11102                        continue;
11103                    }
11104                };
11105                if let Some(job) = self.jobs.get(id) {
11106                    if let Err(e) = send_signal(job.pid, sig) {
11107                        eprintln!("kill: {}", e);
11108                        status = 1;
11109                    }
11110                } else {
11111                    eprintln!("kill: {}: no such job", arg);
11112                    status = 1;
11113                }
11114            } else {
11115                // Direct PID
11116                let pid: u32 = match arg.parse() {
11117                    Ok(p) => p,
11118                    Err(_) => {
11119                        eprintln!("kill: {}: invalid pid", arg);
11120                        status = 1;
11121                        continue;
11122                    }
11123                };
11124                if let Err(e) = send_signal(pid as i32, sig) {
11125                    eprintln!("kill: {}", e);
11126                    status = 1;
11127                }
11128            }
11129        }
11130        status
11131    }
11132
11133    fn builtin_disown(&mut self, args: &[String]) -> i32 {
11134        if args.is_empty() {
11135            // Disown current job
11136            if let Some(job) = self.jobs.current() {
11137                let id = job.id;
11138                self.jobs.remove(id);
11139            }
11140            return 0;
11141        }
11142
11143        for arg in args {
11144            let s = arg.trim_start_matches('%');
11145            if let Ok(id) = s.parse::<usize>() {
11146                self.jobs.remove(id);
11147            } else {
11148                eprintln!("disown: {}: no such job", arg);
11149            }
11150        }
11151        0
11152    }
11153
11154    fn builtin_wait(&mut self, args: &[String]) -> i32 {
11155        if args.is_empty() {
11156            // Wait for all jobs
11157            let ids: Vec<usize> = self.jobs.list().iter().map(|j| j.id).collect();
11158            for id in ids {
11159                if let Some(mut job) = self.jobs.remove(id) {
11160                    if let Some(ref mut child) = job.child {
11161                        let _ = wait_for_child(child);
11162                    }
11163                }
11164            }
11165            return 0;
11166        }
11167
11168        let mut status = 0;
11169        for arg in args {
11170            if arg.starts_with('%') {
11171                let id: usize = match arg[1..].parse() {
11172                    Ok(id) => id,
11173                    Err(_) => {
11174                        eprintln!("wait: {}: no such job", arg);
11175                        status = 127;
11176                        continue;
11177                    }
11178                };
11179                if let Some(mut job) = self.jobs.remove(id) {
11180                    if let Some(ref mut child) = job.child {
11181                        match wait_for_child(child) {
11182                            Ok(s) => status = s,
11183                            Err(e) => {
11184                                eprintln!("wait: {}", e);
11185                                status = 127;
11186                            }
11187                        }
11188                    }
11189                } else {
11190                    eprintln!("wait: {}: no such job", arg);
11191                    status = 127;
11192                }
11193            } else {
11194                let pid: u32 = match arg.parse() {
11195                    Ok(p) => p,
11196                    Err(_) => {
11197                        eprintln!("wait: {}: invalid pid", arg);
11198                        status = 127;
11199                        continue;
11200                    }
11201                };
11202                match wait_for_job(pid as i32) {
11203                    Ok(s) => status = s,
11204                    Err(e) => {
11205                        eprintln!("wait: {}", e);
11206                        status = 127;
11207                    }
11208                }
11209            }
11210        }
11211        status
11212    }
11213
11214    fn builtin_suspend(&self, args: &[String]) -> i32 {
11215        let mut force = false;
11216        for arg in args {
11217            if arg == "-f" {
11218                force = true;
11219            }
11220        }
11221
11222        #[cfg(unix)]
11223        {
11224            use nix::sys::signal::{kill, Signal};
11225            use nix::unistd::getppid;
11226
11227            // Check if we're a login shell (parent is init/PID 1)
11228            let ppid = getppid();
11229            if !force && ppid == nix::unistd::Pid::from_raw(1) {
11230                eprintln!("suspend: cannot suspend a login shell");
11231                return 1;
11232            }
11233
11234            // Send SIGTSTP to ourselves
11235            let pid = nix::unistd::getpid();
11236            if let Err(e) = kill(pid, Signal::SIGTSTP) {
11237                eprintln!("suspend: {}", e);
11238                return 1;
11239            }
11240            0
11241        }
11242
11243        #[cfg(not(unix))]
11244        {
11245            eprintln!("suspend: not supported on this platform");
11246            1
11247        }
11248    }
11249}
11250
11251impl Default for ShellExecutor {
11252    fn default() -> Self {
11253        Self::new()
11254    }
11255}
11256
11257#[cfg(test)]
11258mod tests {
11259    use super::*;
11260
11261    #[test]
11262    fn test_simple_echo() {
11263        let mut exec = ShellExecutor::new();
11264        let status = exec.execute_script("true").unwrap();
11265        assert_eq!(status, 0);
11266    }
11267
11268    #[test]
11269    fn test_if_true() {
11270        let mut exec = ShellExecutor::new();
11271        let status = exec.execute_script("if true; then true; fi").unwrap();
11272        assert_eq!(status, 0);
11273    }
11274
11275    #[test]
11276    fn test_if_false() {
11277        let mut exec = ShellExecutor::new();
11278        let status = exec
11279            .execute_script("if false; then true; else false; fi")
11280            .unwrap();
11281        assert_eq!(status, 1);
11282    }
11283
11284    #[test]
11285    fn test_for_loop() {
11286        let mut exec = ShellExecutor::new();
11287        exec.execute_script("for i in a b c; do true; done")
11288            .unwrap();
11289        assert_eq!(exec.last_status, 0);
11290    }
11291
11292    #[test]
11293    fn test_and_list() {
11294        let mut exec = ShellExecutor::new();
11295        let status = exec.execute_script("true && true").unwrap();
11296        assert_eq!(status, 0);
11297
11298        let status = exec.execute_script("true && false").unwrap();
11299        assert_eq!(status, 1);
11300    }
11301
11302    #[test]
11303    fn test_or_list() {
11304        let mut exec = ShellExecutor::new();
11305        let status = exec.execute_script("false || true").unwrap();
11306        assert_eq!(status, 0);
11307    }
11308}
11309
11310impl ShellExecutor {
11311    fn builtin_history(&self, args: &[String]) -> i32 {
11312        let Some(ref engine) = self.history else {
11313            eprintln!("history: history engine not available");
11314            return 1;
11315        };
11316
11317        // Parse options
11318        let mut count = 20usize;
11319        let mut show_all = false;
11320        let mut search_query = None;
11321
11322        let mut i = 0;
11323        while i < args.len() {
11324            match args[i].as_str() {
11325                "-c" | "--clear" => {
11326                    // Clear history - need mutable access
11327                    eprintln!("history: clear not supported in this mode");
11328                    return 1;
11329                }
11330                "-a" | "--all" => show_all = true,
11331                "-n" => {
11332                    if i + 1 < args.len() {
11333                        i += 1;
11334                        count = args[i].parse().unwrap_or(20);
11335                    }
11336                }
11337                s if s.starts_with('-') && s[1..].chars().all(|c| c.is_ascii_digit()) => {
11338                    count = s[1..].parse().unwrap_or(20);
11339                }
11340                s if s.chars().all(|c| c.is_ascii_digit()) => {
11341                    count = s.parse().unwrap_or(20);
11342                }
11343                s => {
11344                    search_query = Some(s.to_string());
11345                }
11346            }
11347            i += 1;
11348        }
11349
11350        if show_all {
11351            count = 10000;
11352        }
11353
11354        let entries = if let Some(ref q) = search_query {
11355            engine.search(q, count)
11356        } else {
11357            engine.recent(count)
11358        };
11359
11360        match entries {
11361            Ok(entries) => {
11362                // Print in chronological order (reverse the results since recent() is newest-first)
11363                for entry in entries.into_iter().rev() {
11364                    println!("{:>6}  {}", entry.id, entry.command);
11365                }
11366                0
11367            }
11368            Err(e) => {
11369                eprintln!("history: {}", e);
11370                1
11371            }
11372        }
11373    }
11374
11375    /// fc builtin - fix command (history manipulation)
11376    /// Ported from zsh/Src/builtin.c bin_fc() lines 1426-1700
11377    /// Options: -l (list), -n (no numbers), -r (reverse), -d/-f/-E/-i/-t (time formats),
11378    /// -D (duration), -e editor, -m pattern, -R/-W/-A (read/write/append history file),
11379    /// -p/-P (push/pop history stack), -I (skip old), -L (local), -s (substitute)
11380    fn builtin_fc(&mut self, args: &[String]) -> i32 {
11381        let Some(ref engine) = self.history else {
11382            eprintln!("fc: history engine not available");
11383            return 1;
11384        };
11385
11386        // Parse options
11387        let mut list_mode = false;
11388        let mut no_numbers = false;
11389        let mut reverse = false;
11390        let mut show_time = false;
11391        let mut show_duration = false;
11392        let mut editor: Option<String> = None;
11393        let mut read_file = false;
11394        let mut write_file = false;
11395        let mut append_file = false;
11396        let mut substitute_mode = false;
11397        let mut positional: Vec<&str> = Vec::new();
11398        let mut substitutions: Vec<(String, String)> = Vec::new();
11399
11400        let mut i = 0;
11401        while i < args.len() {
11402            let arg = &args[i];
11403            if arg == "--" {
11404                i += 1;
11405                while i < args.len() {
11406                    positional.push(&args[i]);
11407                    i += 1;
11408                }
11409                break;
11410            }
11411            if arg.starts_with('-') && arg.len() > 1 {
11412                let chars: Vec<char> = arg[1..].chars().collect();
11413                let mut j = 0;
11414                while j < chars.len() {
11415                    match chars[j] {
11416                        'l' => list_mode = true,
11417                        'n' => no_numbers = true,
11418                        'r' => reverse = true,
11419                        'd' | 'f' | 'E' | 'i' => show_time = true,
11420                        'D' => show_duration = true,
11421                        'R' => read_file = true,
11422                        'W' => write_file = true,
11423                        'A' => append_file = true,
11424                        's' => substitute_mode = true,
11425                        'e' => {
11426                            if j + 1 < chars.len() {
11427                                editor = Some(chars[j + 1..].iter().collect());
11428                                break;
11429                            } else {
11430                                i += 1;
11431                                if i < args.len() {
11432                                    editor = Some(args[i].clone());
11433                                }
11434                            }
11435                        }
11436                        't' => {
11437                            show_time = true;
11438                            if j + 1 < chars.len() {
11439                                break;
11440                            } else {
11441                                i += 1;
11442                            }
11443                        }
11444                        'p' | 'P' | 'a' | 'I' | 'L' | 'm' => {} // Handled but no-op for now
11445                        _ => {
11446                            if chars[j].is_ascii_digit() {
11447                                positional.push(arg);
11448                                break;
11449                            }
11450                        }
11451                    }
11452                    j += 1;
11453                }
11454            } else if arg.contains('=') && !list_mode {
11455                if let Some((old, new)) = arg.split_once('=') {
11456                    substitutions.push((old.to_string(), new.to_string()));
11457                }
11458            } else {
11459                positional.push(arg);
11460            }
11461            i += 1;
11462        }
11463
11464        // Handle file operations (read/write/append)
11465        // Note: HistoryEngine uses SQLite, so file ops are simplified
11466        if read_file || write_file || append_file {
11467            let filename = positional.first().map(|s| *s).unwrap_or("~/.zsh_history");
11468            let path = if filename.starts_with("~/") {
11469                dirs::home_dir()
11470                    .map(|h| h.join(&filename[2..]))
11471                    .unwrap_or_else(|| std::path::PathBuf::from(filename))
11472            } else {
11473                std::path::PathBuf::from(filename)
11474            };
11475
11476            if read_file {
11477                // Read plain text history file and import
11478                if let Ok(contents) = std::fs::read_to_string(&path) {
11479                    for line in contents.lines() {
11480                        if !line.is_empty() && !line.starts_with('#') && !line.starts_with(':') {
11481                            let _ = engine.add(line, None);
11482                        }
11483                    }
11484                } else {
11485                    eprintln!("fc: cannot read {}", path.display());
11486                    return 1;
11487                }
11488            } else if write_file || append_file {
11489                // Export history to plain text file
11490                let mode = if append_file {
11491                    std::fs::OpenOptions::new()
11492                        .create(true)
11493                        .append(true)
11494                        .open(&path)
11495                } else {
11496                    std::fs::File::create(&path)
11497                };
11498                match mode {
11499                    Ok(mut file) => {
11500                        use std::io::Write;
11501                        if let Ok(entries) = engine.recent(10000) {
11502                            for entry in entries.iter().rev() {
11503                                let _ = writeln!(file, ": {}:0;{}", entry.timestamp, entry.command);
11504                            }
11505                        }
11506                    }
11507                    Err(e) => {
11508                        eprintln!("fc: cannot write {}: {}", path.display(), e);
11509                        return 1;
11510                    }
11511                }
11512            }
11513            return 0;
11514        }
11515
11516        // List mode (fc -l)
11517        if list_mode || args.is_empty() {
11518            let (first, last) = match positional.len() {
11519                0 => (-16i64, -1i64),
11520                1 => {
11521                    let n = positional[0].parse::<i64>().unwrap_or(-16);
11522                    (n, -1)
11523                }
11524                _ => {
11525                    let f = positional[0].parse::<i64>().unwrap_or(-16);
11526                    let l = positional[1].parse::<i64>().unwrap_or(-1);
11527                    (f, l)
11528                }
11529            };
11530
11531            let count = if first < 0 { (-first) as usize } else { 16 };
11532            match engine.recent(count.max(100)) {
11533                Ok(mut entries) => {
11534                    if reverse {
11535                        entries.reverse();
11536                    }
11537                    for entry in entries.iter().rev().take(count) {
11538                        if no_numbers {
11539                            println!("{}", entry.command);
11540                        } else if show_time {
11541                            println!(
11542                                "{:>6}  {:>10}  {}",
11543                                entry.id, entry.timestamp, entry.command
11544                            );
11545                        } else if show_duration {
11546                            println!(
11547                                "{:>6}  {:>5}  {}",
11548                                entry.id,
11549                                entry.duration_ms.unwrap_or(0),
11550                                entry.command
11551                            );
11552                        } else {
11553                            println!("{:>6}  {}", entry.id, entry.command);
11554                        }
11555                    }
11556                    0
11557                }
11558                Err(e) => {
11559                    eprintln!("fc: {}", e);
11560                    1
11561                }
11562            }
11563        } else if substitute_mode || !substitutions.is_empty() {
11564            // Substitution mode: fc -s old=new
11565            match engine.get_by_offset(0) {
11566                Ok(Some(entry)) => {
11567                    let mut cmd = entry.command.clone();
11568                    for (old, new) in &substitutions {
11569                        cmd = cmd.replace(old, new);
11570                    }
11571                    println!("{}", cmd);
11572                    self.execute_script(&cmd).unwrap_or(1)
11573                }
11574                Ok(None) => {
11575                    eprintln!("fc: no command to re-execute");
11576                    1
11577                }
11578                Err(e) => {
11579                    eprintln!("fc: {}", e);
11580                    1
11581                }
11582            }
11583        } else if editor.as_deref() == Some("-") {
11584            // fc -e -: re-execute last command without editor
11585            match engine.get_by_offset(0) {
11586                Ok(Some(entry)) => {
11587                    println!("{}", entry.command);
11588                    self.execute_script(&entry.command).unwrap_or(1)
11589                }
11590                Ok(None) => {
11591                    eprintln!("fc: no command to re-execute");
11592                    1
11593                }
11594                Err(e) => {
11595                    eprintln!("fc: {}", e);
11596                    1
11597                }
11598            }
11599        } else if let Some(arg) = positional.first() {
11600            if arg.starts_with('-') || arg.starts_with('+') {
11601                // fc -N or fc +N: re-execute Nth command
11602                let n: usize = arg[1..].parse().unwrap_or(1);
11603                let offset = if arg.starts_with('-') { n - 1 } else { n };
11604                match engine.get_by_offset(offset) {
11605                    Ok(Some(entry)) => {
11606                        println!("{}", entry.command);
11607                        self.execute_script(&entry.command).unwrap_or(1)
11608                    }
11609                    Ok(None) => {
11610                        eprintln!("fc: event not found");
11611                        1
11612                    }
11613                    Err(e) => {
11614                        eprintln!("fc: {}", e);
11615                        1
11616                    }
11617                }
11618            } else {
11619                // Try to find command by prefix
11620                match engine.search_prefix(arg, 1) {
11621                    Ok(entries) if !entries.is_empty() => {
11622                        println!("{}", entries[0].command);
11623                        self.execute_script(&entries[0].command).unwrap_or(1)
11624                    }
11625                    Ok(_) => {
11626                        eprintln!("fc: event not found: {}", arg);
11627                        1
11628                    }
11629                    Err(e) => {
11630                        eprintln!("fc: {}", e);
11631                        1
11632                    }
11633                }
11634            }
11635        } else {
11636            // Default: edit and execute last command
11637            match engine.get_by_offset(0) {
11638                Ok(Some(entry)) => {
11639                    println!("{}", entry.command);
11640                    self.execute_script(&entry.command).unwrap_or(1)
11641                }
11642                Ok(None) => {
11643                    eprintln!("fc: no command to re-execute");
11644                    1
11645                }
11646                Err(e) => {
11647                    eprintln!("fc: {}", e);
11648                    1
11649                }
11650            }
11651        }
11652    }
11653
11654    fn builtin_trap(&mut self, args: &[String]) -> i32 {
11655        if args.is_empty() {
11656            // List all traps
11657            for (sig, action) in &self.traps {
11658                println!("trap -- '{}' {}", action, sig);
11659            }
11660            return 0;
11661        }
11662
11663        // trap -l: list signal names
11664        if args.len() == 1 && args[0] == "-l" {
11665            let signals = [
11666                "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
11667                "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN",
11668                "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS",
11669            ];
11670            for (i, sig) in signals.iter().enumerate() {
11671                print!("{:2}) SIG{:<8}", i + 1, sig);
11672                if (i + 1) % 5 == 0 {
11673                    println!();
11674                }
11675            }
11676            println!();
11677            return 0;
11678        }
11679
11680        // trap -p [sigspec...]: print trap commands
11681        if args.len() >= 1 && args[0] == "-p" {
11682            let signals = if args.len() > 1 {
11683                &args[1..]
11684            } else {
11685                &[] as &[String]
11686            };
11687            if signals.is_empty() {
11688                for (sig, action) in &self.traps {
11689                    println!("trap -- '{}' {}", action, sig);
11690                }
11691            } else {
11692                for sig in signals {
11693                    if let Some(action) = self.traps.get(sig) {
11694                        println!("trap -- '{}' {}", action, sig);
11695                    }
11696                }
11697            }
11698            return 0;
11699        }
11700
11701        // trap '' signal: reset to default
11702        // trap action signal...: set trap
11703        // trap signal: print current action for signal
11704        if args.len() == 1 {
11705            // Print trap for this signal
11706            let sig = &args[0];
11707            if let Some(action) = self.traps.get(sig) {
11708                println!("trap -- '{}' {}", action, sig);
11709            }
11710            return 0;
11711        }
11712
11713        let action = &args[0];
11714        let signals = &args[1..];
11715
11716        for sig in signals {
11717            let sig_upper = sig.to_uppercase();
11718            let sig_name = if sig_upper.starts_with("SIG") {
11719                sig_upper[3..].to_string()
11720            } else {
11721                sig_upper.clone()
11722            };
11723
11724            if action.is_empty() || action == "-" {
11725                // Reset to default
11726                self.traps.remove(&sig_name);
11727            } else {
11728                self.traps.insert(sig_name, action.clone());
11729            }
11730        }
11731
11732        0
11733    }
11734
11735    /// Execute trap handlers for a signal
11736    pub fn run_trap(&mut self, signal: &str) {
11737        if let Some(action) = self.traps.get(signal).cloned() {
11738            let _ = self.execute_script(&action);
11739        }
11740    }
11741
11742    fn builtin_alias(&mut self, args: &[String]) -> i32 {
11743        // alias [ {+|-}gmrsL ] [ name[=value] ... ]
11744        // -g: global alias (expanded anywhere in command line)
11745        // -s: suffix alias (file.ext expands to "handler file.ext")
11746        // -r: regular alias (default)
11747        // -m: pattern match mode
11748        // -L: list in form suitable for reinput
11749        // +g/+s/+r: print aliases of that type
11750
11751        let mut is_global = false;
11752        let mut is_suffix = false;
11753        let mut list_form = false;
11754        let mut pattern_match = false;
11755        let mut print_global = false;
11756        let mut print_suffix = false;
11757        let mut print_regular = false;
11758        let mut positional_args = Vec::new();
11759
11760        let mut i = 0;
11761        while i < args.len() {
11762            let arg = &args[i];
11763            if arg.starts_with('+') && arg.len() > 1 {
11764                // +g, +s, +r: print aliases of that type
11765                for ch in arg[1..].chars() {
11766                    match ch {
11767                        'g' => print_global = true,
11768                        's' => print_suffix = true,
11769                        'r' => print_regular = true,
11770                        'L' => list_form = true,
11771                        'm' => pattern_match = true,
11772                        _ => {}
11773                    }
11774                }
11775            } else if arg.starts_with('-') && arg != "-" {
11776                for ch in arg[1..].chars() {
11777                    match ch {
11778                        'g' => is_global = true,
11779                        's' => is_suffix = true,
11780                        'L' => list_form = true,
11781                        'm' => pattern_match = true,
11782                        'r' => {} // regular alias (default)
11783                        _ => {
11784                            eprintln!("zshrs: alias: bad option: -{}", ch);
11785                            return 1;
11786                        }
11787                    }
11788                }
11789            } else {
11790                positional_args.push(arg.clone());
11791            }
11792            i += 1;
11793        }
11794
11795        // If +g/+s/+r used, list those types
11796        if print_global || print_suffix || print_regular {
11797            if print_regular {
11798                for (name, value) in &self.aliases {
11799                    if list_form {
11800                        println!("alias {}='{}'", name, value);
11801                    } else {
11802                        println!("{}='{}'", name, value);
11803                    }
11804                }
11805            }
11806            if print_global {
11807                for (name, value) in &self.global_aliases {
11808                    if list_form {
11809                        println!("alias -g {}='{}'", name, value);
11810                    } else {
11811                        println!("{}='{}'", name, value);
11812                    }
11813                }
11814            }
11815            if print_suffix {
11816                for (name, value) in &self.suffix_aliases {
11817                    if list_form {
11818                        println!("alias -s {}='{}'", name, value);
11819                    } else {
11820                        println!("{}='{}'", name, value);
11821                    }
11822                }
11823            }
11824            return 0;
11825        }
11826
11827        if positional_args.is_empty() {
11828            // List aliases
11829            let prefix = if is_suffix {
11830                "alias -s "
11831            } else if is_global {
11832                "alias -g "
11833            } else {
11834                "alias "
11835            };
11836            let alias_map: Vec<(String, String)> = if is_suffix {
11837                self.suffix_aliases
11838                    .iter()
11839                    .map(|(k, v)| (k.clone(), v.clone()))
11840                    .collect()
11841            } else if is_global {
11842                self.global_aliases
11843                    .iter()
11844                    .map(|(k, v)| (k.clone(), v.clone()))
11845                    .collect()
11846            } else {
11847                self.aliases
11848                    .iter()
11849                    .map(|(k, v)| (k.clone(), v.clone()))
11850                    .collect()
11851            };
11852            for (name, value) in alias_map {
11853                if list_form {
11854                    println!("{}{}='{}'", prefix, name, value);
11855                } else {
11856                    println!("{}='{}'", name, value);
11857                }
11858            }
11859            return 0;
11860        }
11861
11862        for arg in &positional_args {
11863            if let Some(eq_pos) = arg.find('=') {
11864                // Define alias: name=value
11865                let name = &arg[..eq_pos];
11866                let value = &arg[eq_pos + 1..];
11867                if is_suffix {
11868                    self.suffix_aliases
11869                        .insert(name.to_string(), value.to_string());
11870                } else if is_global {
11871                    self.global_aliases
11872                        .insert(name.to_string(), value.to_string());
11873                } else {
11874                    self.aliases.insert(name.to_string(), value.to_string());
11875                }
11876            } else if pattern_match {
11877                // -m: pattern match mode - list matching aliases
11878                let pattern = arg.replace("*", ".*").replace("?", ".");
11879                let re = regex::Regex::new(&format!("^{}$", pattern));
11880
11881                let alias_map: &HashMap<String, String> = if is_suffix {
11882                    &self.suffix_aliases
11883                } else if is_global {
11884                    &self.global_aliases
11885                } else {
11886                    &self.aliases
11887                };
11888
11889                let prefix = if is_suffix {
11890                    "alias -s "
11891                } else if is_global {
11892                    "alias -g "
11893                } else {
11894                    "alias "
11895                };
11896
11897                for (name, value) in alias_map {
11898                    let matches = if let Ok(ref r) = re {
11899                        r.is_match(name)
11900                    } else {
11901                        name.contains(arg.as_str())
11902                    };
11903                    if matches {
11904                        if list_form {
11905                            println!("{}{}='{}'", prefix, name, value);
11906                        } else {
11907                            println!("{}='{}'", name, value);
11908                        }
11909                    }
11910                }
11911            } else {
11912                // Print alias - look up directly without holding borrow
11913                let value = if is_suffix {
11914                    self.suffix_aliases.get(arg.as_str()).cloned()
11915                } else if is_global {
11916                    self.global_aliases.get(arg.as_str()).cloned()
11917                } else {
11918                    self.aliases.get(arg.as_str()).cloned()
11919                };
11920                if let Some(v) = value {
11921                    println!("{}='{}'", arg, v);
11922                } else {
11923                    eprintln!("zshrs: alias: {}: not found", arg);
11924                    return 1;
11925                }
11926            }
11927        }
11928        0
11929    }
11930
11931    fn builtin_unalias(&mut self, args: &[String]) -> i32 {
11932        if args.is_empty() {
11933            eprintln!("zshrs: unalias: usage: unalias [-agsm] name [name ...]");
11934            return 1;
11935        }
11936
11937        let mut is_global = false;
11938        let mut is_suffix = false;
11939        let mut remove_all = false;
11940        let mut positional_args = Vec::new();
11941
11942        for arg in args {
11943            if arg.starts_with('-') && arg != "-" {
11944                for ch in arg[1..].chars() {
11945                    match ch {
11946                        'a' => remove_all = true,
11947                        'g' => is_global = true,
11948                        's' => is_suffix = true,
11949                        'm' => {} // pattern match, ignore for now
11950                        _ => {
11951                            eprintln!("zshrs: unalias: bad option: -{}", ch);
11952                            return 1;
11953                        }
11954                    }
11955                }
11956            } else {
11957                positional_args.push(arg.clone());
11958            }
11959        }
11960
11961        if remove_all {
11962            if is_suffix {
11963                self.suffix_aliases.clear();
11964            } else if is_global {
11965                self.global_aliases.clear();
11966            } else {
11967                // -a without -g/-s clears all three
11968                self.aliases.clear();
11969                self.global_aliases.clear();
11970                self.suffix_aliases.clear();
11971            }
11972            return 0;
11973        }
11974
11975        if positional_args.is_empty() {
11976            eprintln!("zshrs: unalias: usage: unalias [-agsm] name [name ...]");
11977            return 1;
11978        }
11979
11980        for name in positional_args {
11981            let removed = if is_suffix {
11982                self.suffix_aliases.remove(&name).is_some()
11983            } else if is_global {
11984                self.global_aliases.remove(&name).is_some()
11985            } else {
11986                self.aliases.remove(&name).is_some()
11987            };
11988            if !removed {
11989                eprintln!("zshrs: unalias: {}: not found", name);
11990                return 1;
11991            }
11992        }
11993        0
11994    }
11995
11996    fn builtin_set(&mut self, args: &[String]) -> i32 {
11997        if args.is_empty() {
11998            // List all variables and their values (zsh behavior)
11999            let mut vars: Vec<_> = self.variables.iter().collect();
12000            vars.sort_by_key(|(k, _)| *k);
12001            for (k, v) in vars {
12002                println!("{}={}", k, shell_quote(v));
12003            }
12004            // Also print arrays
12005            let mut arrs: Vec<_> = self.arrays.iter().collect();
12006            arrs.sort_by_key(|(k, _)| *k);
12007            for (k, v) in arrs {
12008                let quoted: Vec<String> = v.iter().map(|s| shell_quote(s)).collect();
12009                println!("{}=( {} )", k, quoted.join(" "));
12010            }
12011            return 0;
12012        }
12013
12014        // Check for "+" alone - print just variable names
12015        if args.len() == 1 && args[0] == "+" {
12016            let mut names: Vec<_> = self.variables.keys().collect();
12017            names.extend(self.arrays.keys());
12018            names.sort();
12019            names.dedup();
12020            for name in names {
12021                println!("{}", name);
12022            }
12023            return 0;
12024        }
12025
12026        let mut iter = args.iter().peekable();
12027        let mut set_array: Option<bool> = None; // Some(true) = -A, Some(false) = +A
12028        let mut array_name: Option<String> = None;
12029        let mut sort_asc = false;
12030        let mut sort_desc = false;
12031
12032        while let Some(arg) = iter.next() {
12033            match arg.as_str() {
12034                "-o" => {
12035                    // -o with no arg: print all options in "option on/off" format
12036                    if iter.peek().is_none()
12037                        || iter
12038                            .peek()
12039                            .map(|s| s.starts_with('-') || s.starts_with('+'))
12040                            .unwrap_or(false)
12041                    {
12042                        self.print_options_table();
12043                        continue;
12044                    }
12045                    if let Some(opt) = iter.next() {
12046                        let (name, enable) = Self::normalize_option_name(opt);
12047                        self.options.insert(name, enable);
12048                    }
12049                }
12050                "+o" => {
12051                    // +o with no arg: print options in re-entrant format
12052                    if iter.peek().is_none()
12053                        || iter
12054                            .peek()
12055                            .map(|s| s.starts_with('-') || s.starts_with('+'))
12056                            .unwrap_or(false)
12057                    {
12058                        self.print_options_reentrant();
12059                        continue;
12060                    }
12061                    if let Some(opt) = iter.next() {
12062                        let (name, enable) = Self::normalize_option_name(opt);
12063                        self.options.insert(name, !enable);
12064                    }
12065                }
12066                "-A" => {
12067                    set_array = Some(true);
12068                    if let Some(name) = iter.next() {
12069                        if !name.starts_with('-') && !name.starts_with('+') {
12070                            array_name = Some(name.clone());
12071                        }
12072                    }
12073                    if array_name.is_none() {
12074                        // Print all arrays with values
12075                        let mut arrs: Vec<_> = self.arrays.iter().collect();
12076                        arrs.sort_by_key(|(k, _)| *k);
12077                        for (k, v) in arrs {
12078                            let quoted: Vec<String> = v.iter().map(|s| shell_quote(s)).collect();
12079                            println!("{}=( {} )", k, quoted.join(" "));
12080                        }
12081                        return 0;
12082                    }
12083                }
12084                "+A" => {
12085                    set_array = Some(false);
12086                    if let Some(name) = iter.next() {
12087                        if !name.starts_with('-') && !name.starts_with('+') {
12088                            array_name = Some(name.clone());
12089                        }
12090                    }
12091                    if array_name.is_none() {
12092                        // Print array names only
12093                        let mut names: Vec<_> = self.arrays.keys().collect();
12094                        names.sort();
12095                        for name in names {
12096                            println!("{}", name);
12097                        }
12098                        return 0;
12099                    }
12100                }
12101                "-s" => sort_asc = true,
12102                "+s" => sort_desc = true,
12103                "-e" => {
12104                    self.options.insert("errexit".to_string(), true);
12105                }
12106                "+e" => {
12107                    self.options.insert("errexit".to_string(), false);
12108                }
12109                "-x" => {
12110                    self.options.insert("xtrace".to_string(), true);
12111                }
12112                "+x" => {
12113                    self.options.insert("xtrace".to_string(), false);
12114                }
12115                "-u" => {
12116                    self.options.insert("nounset".to_string(), true);
12117                }
12118                "+u" => {
12119                    self.options.insert("nounset".to_string(), false);
12120                }
12121                "-v" => {
12122                    self.options.insert("verbose".to_string(), true);
12123                }
12124                "+v" => {
12125                    self.options.insert("verbose".to_string(), false);
12126                }
12127                "-n" => {
12128                    self.options.insert("exec".to_string(), false);
12129                }
12130                "+n" => {
12131                    self.options.insert("exec".to_string(), true);
12132                }
12133                "-f" => {
12134                    self.options.insert("glob".to_string(), false);
12135                }
12136                "+f" => {
12137                    self.options.insert("glob".to_string(), true);
12138                }
12139                "-m" => {
12140                    self.options.insert("monitor".to_string(), true);
12141                }
12142                "+m" => {
12143                    self.options.insert("monitor".to_string(), false);
12144                }
12145                "-C" => {
12146                    self.options.insert("clobber".to_string(), false);
12147                }
12148                "+C" => {
12149                    self.options.insert("clobber".to_string(), true);
12150                }
12151                "-b" => {
12152                    self.options.insert("notify".to_string(), true);
12153                }
12154                "+b" => {
12155                    self.options.insert("notify".to_string(), false);
12156                }
12157                "--" => {
12158                    let remaining: Vec<String> = iter.cloned().collect();
12159                    if let Some(ref name) = array_name {
12160                        let mut values = remaining;
12161                        if sort_asc {
12162                            values.sort();
12163                        } else if sort_desc {
12164                            values.sort();
12165                            values.reverse();
12166                        }
12167                        if set_array == Some(true) {
12168                            self.arrays.insert(name.clone(), values);
12169                        } else {
12170                            // +A: replace initial elements
12171                            let arr = self.arrays.entry(name.clone()).or_default();
12172                            for (i, v) in values.into_iter().enumerate() {
12173                                if i < arr.len() {
12174                                    arr[i] = v;
12175                                } else {
12176                                    arr.push(v);
12177                                }
12178                            }
12179                        }
12180                    } else if remaining.is_empty() {
12181                        // "set --" with nothing after unsets positional params
12182                        self.positional_params.clear();
12183                    } else {
12184                        let mut values = remaining;
12185                        if sort_asc {
12186                            values.sort();
12187                        } else if sort_desc {
12188                            values.sort();
12189                            values.reverse();
12190                        }
12191                        self.positional_params = values;
12192                    }
12193                    return 0;
12194                }
12195                _ => {
12196                    // Handle single-letter options like -ex (multiple options)
12197                    if arg.starts_with('-') && arg.len() > 1 {
12198                        for c in arg[1..].chars() {
12199                            match c {
12200                                'e' => {
12201                                    self.options.insert("errexit".to_string(), true);
12202                                }
12203                                'x' => {
12204                                    self.options.insert("xtrace".to_string(), true);
12205                                }
12206                                'u' => {
12207                                    self.options.insert("nounset".to_string(), true);
12208                                }
12209                                'v' => {
12210                                    self.options.insert("verbose".to_string(), true);
12211                                }
12212                                'n' => {
12213                                    self.options.insert("exec".to_string(), false);
12214                                }
12215                                'f' => {
12216                                    self.options.insert("glob".to_string(), false);
12217                                }
12218                                'm' => {
12219                                    self.options.insert("monitor".to_string(), true);
12220                                }
12221                                'C' => {
12222                                    self.options.insert("clobber".to_string(), false);
12223                                }
12224                                'b' => {
12225                                    self.options.insert("notify".to_string(), true);
12226                                }
12227                                _ => {
12228                                    eprintln!("zshrs: set: -{}: invalid option", c);
12229                                    return 1;
12230                                }
12231                            }
12232                        }
12233                        continue;
12234                    }
12235                    if arg.starts_with('+') && arg.len() > 1 {
12236                        for c in arg[1..].chars() {
12237                            match c {
12238                                'e' => {
12239                                    self.options.insert("errexit".to_string(), false);
12240                                }
12241                                'x' => {
12242                                    self.options.insert("xtrace".to_string(), false);
12243                                }
12244                                'u' => {
12245                                    self.options.insert("nounset".to_string(), false);
12246                                }
12247                                'v' => {
12248                                    self.options.insert("verbose".to_string(), false);
12249                                }
12250                                'n' => {
12251                                    self.options.insert("exec".to_string(), true);
12252                                }
12253                                'f' => {
12254                                    self.options.insert("glob".to_string(), true);
12255                                }
12256                                'm' => {
12257                                    self.options.insert("monitor".to_string(), false);
12258                                }
12259                                'C' => {
12260                                    self.options.insert("clobber".to_string(), true);
12261                                }
12262                                'b' => {
12263                                    self.options.insert("notify".to_string(), false);
12264                                }
12265                                _ => {
12266                                    eprintln!("zshrs: set: +{}: invalid option", c);
12267                                    return 1;
12268                                }
12269                            }
12270                        }
12271                        continue;
12272                    }
12273                    // Treat as positional params
12274                    let mut values: Vec<String> =
12275                        std::iter::once(arg.clone()).chain(iter.cloned()).collect();
12276                    if sort_asc {
12277                        values.sort();
12278                    } else if sort_desc {
12279                        values.sort();
12280                        values.reverse();
12281                    }
12282                    if let Some(ref name) = array_name {
12283                        if set_array == Some(true) {
12284                            self.arrays.insert(name.clone(), values);
12285                        } else {
12286                            let arr = self.arrays.entry(name.clone()).or_default();
12287                            for (i, v) in values.into_iter().enumerate() {
12288                                if i < arr.len() {
12289                                    arr[i] = v;
12290                                } else {
12291                                    arr.push(v);
12292                                }
12293                            }
12294                        }
12295                    } else {
12296                        self.positional_params = values;
12297                    }
12298                    return 0;
12299                }
12300            }
12301        }
12302        0
12303    }
12304
12305    fn default_on_options() -> &'static [&'static str] {
12306        &[
12307            "aliases",
12308            "alwayslastprompt",
12309            "appendhistory",
12310            "autolist",
12311            "automenu",
12312            "autoparamkeys",
12313            "autoparamslash",
12314            "autoremoveslash",
12315            "badpattern",
12316            "banghist",
12317            "bareglobqual",
12318            "beep",
12319            "bgnice",
12320            "caseglob",
12321            "casematch",
12322            "checkjobs",
12323            "checkrunningjobs",
12324            "clobber",
12325            "debugbeforecmd",
12326            "equals",
12327            "evallineno",
12328            "exec",
12329            "flowcontrol",
12330            "functionargzero",
12331            "glob",
12332            "globalexport",
12333            "globalrcs",
12334            "hashcmds",
12335            "hashdirs",
12336            "hashlistall",
12337            "histbeep",
12338            "histsavebycopy",
12339            "hup",
12340            "interactive",
12341            "listambiguous",
12342            "listbeep",
12343            "listtypes",
12344            "monitor",
12345            "multibyte",
12346            "multifuncdef",
12347            "multios",
12348            "nomatch",
12349            "notify",
12350            "promptcr",
12351            "promptpercent",
12352            "promptsp",
12353            "rcs",
12354            "shinstdin",
12355            "shortloops",
12356            "unset",
12357            "zle",
12358        ]
12359    }
12360
12361    fn print_options_table(&self) {
12362        let mut opts: Vec<_> = Self::all_zsh_options().to_vec();
12363        opts.sort();
12364        let defaults_on = Self::default_on_options();
12365        for &opt in &opts {
12366            let enabled = self.options.get(opt).copied().unwrap_or(false);
12367            let is_default_on = defaults_on.contains(&opt);
12368            // zsh format: for default-ON options, show "noOPTION off" when on, "noOPTION on" when off
12369            // for default-OFF options, show "OPTION off" when off, "OPTION on" when on
12370            let (display_name, display_state) = if is_default_on {
12371                (format!("no{}", opt), if enabled { "off" } else { "on" })
12372            } else {
12373                (opt.to_string(), if enabled { "on" } else { "off" })
12374            };
12375            println!("{:<22}{}", display_name, display_state);
12376        }
12377    }
12378
12379    fn print_options_reentrant(&self) {
12380        let mut opts: Vec<_> = Self::all_zsh_options().to_vec();
12381        opts.sort();
12382        let defaults_on = Self::default_on_options();
12383        for &opt in &opts {
12384            let enabled = self.options.get(opt).copied().unwrap_or(false);
12385            let is_default_on = defaults_on.contains(&opt);
12386            // zsh format: use noOPTION for default-on options
12387            let (display_name, use_minus) = if is_default_on {
12388                (format!("no{}", opt), !enabled)
12389            } else {
12390                (opt.to_string(), enabled)
12391            };
12392            if use_minus {
12393                println!("set -o {}", display_name);
12394            } else {
12395                println!("set +o {}", display_name);
12396            }
12397        }
12398    }
12399
12400    /// caller - display call stack (bash)
12401    fn builtin_caller(&self, args: &[String]) -> i32 {
12402        let depth: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
12403        // In a real implementation, we'd track the call stack
12404        // For now, show basic info
12405        if depth == 0 {
12406            println!("1 main");
12407        } else {
12408            println!("{} main", depth);
12409        }
12410        0
12411    }
12412
12413    /// doctor - diagnostic report of shell health, caches, and performance
12414    fn builtin_doctor(&self, _args: &[String]) -> i32 {
12415        let green = |s: &str| format!("\x1b[32m{}\x1b[0m", s);
12416        let red = |s: &str| format!("\x1b[31m{}\x1b[0m", s);
12417        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
12418        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
12419        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
12420
12421        println!("{}", bold("zshrs doctor"));
12422        println!("{}", dim(&"=".repeat(60)));
12423        println!();
12424
12425        // --- Environment ---
12426        println!("{}", bold("Environment"));
12427        println!("  version:    zshrs {}", env!("CARGO_PKG_VERSION"));
12428        println!("  pid:        {}", std::process::id());
12429        let cwd = env::current_dir()
12430            .map(|p| p.to_string_lossy().to_string())
12431            .unwrap_or_else(|_| "?".to_string());
12432        println!("  cwd:        {}", cwd);
12433        println!(
12434            "  shell:      {}",
12435            env::var("SHELL").unwrap_or_else(|_| "?".to_string())
12436        );
12437        println!("  pool size:  {}", self.worker_pool.size());
12438        println!(
12439            "  pool done:  {} tasks completed",
12440            self.worker_pool.completed()
12441        );
12442        println!("  pool queue: {} pending", self.worker_pool.queue_depth());
12443        println!();
12444
12445        // --- Config ---
12446        println!("{}", bold("Config"));
12447        let config_path = crate::config::config_path();
12448        if config_path.exists() {
12449            println!("  {}  {}", green("*"), config_path.display());
12450        } else {
12451            println!(
12452                "  {}  {} {}",
12453                dim("-"),
12454                config_path.display(),
12455                dim("(using defaults)")
12456            );
12457        }
12458        println!();
12459
12460        // --- PATH ---
12461        println!("{}", bold("PATH"));
12462        let path_var = env::var("PATH").unwrap_or_default();
12463        let path_dirs: Vec<&str> = path_var.split(':').filter(|s| !s.is_empty()).collect();
12464        let path_ok = path_dirs
12465            .iter()
12466            .filter(|d| std::path::Path::new(d).is_dir())
12467            .count();
12468        let path_missing = path_dirs.len() - path_ok;
12469        println!(
12470            "  directories: {} total, {} {}, {} {}",
12471            path_dirs.len(),
12472            path_ok,
12473            green("valid"),
12474            path_missing,
12475            if path_missing > 0 {
12476                red("missing")
12477            } else {
12478                green("missing")
12479            },
12480        );
12481        println!("  hash table:  {} entries", self.command_hash.len());
12482        println!();
12483
12484        // --- FPATH ---
12485        println!("{}", bold("FPATH"));
12486        println!("  directories: {}", self.fpath.len());
12487        let fpath_ok = self.fpath.iter().filter(|d| d.is_dir()).count();
12488        let fpath_missing = self.fpath.len() - fpath_ok;
12489        if fpath_missing > 0 {
12490            println!("  {} {} missing fpath directories", red("!"), fpath_missing);
12491        }
12492        println!("  functions:   {} loaded", self.functions.len());
12493        println!("  autoload:    {} pending", self.autoload_pending.len());
12494        println!();
12495
12496        // --- SQLite Caches ---
12497        println!("{}", bold("SQLite Caches"));
12498        if let Some(ref engine) = self.history {
12499            let count = engine.count().unwrap_or(0);
12500            println!("  history:     {} entries  {}", count, green("OK"));
12501        } else {
12502            println!("  history:     {}", yellow("not initialized"));
12503        }
12504
12505        if let Some(ref cache) = self.compsys_cache {
12506            let count = compsys::cache_entry_count(cache);
12507            println!("  compsys:     {} completions  {}", count, green("OK"));
12508
12509            // Check bytecode blob coverage
12510            if let Ok(missing) = cache.get_autoloads_missing_bytecode() {
12511                if missing.is_empty() {
12512                    println!(
12513                        "  bytecode cache:   {}",
12514                        green("all functions compiled to bytecode")
12515                    );
12516                } else {
12517                    println!(
12518                        "  bytecode cache:   {} functions {}",
12519                        missing.len(),
12520                        yellow("missing bytecode blobs")
12521                    );
12522                }
12523            }
12524        } else {
12525            println!("  compsys:     {}", yellow("no cache"));
12526        }
12527
12528        if let Some(ref cache) = self.plugin_cache {
12529            let (plugins, functions) = cache.stats();
12530            println!(
12531                "  plugins:     {} plugins, {} cached functions  {}",
12532                plugins,
12533                functions,
12534                green("OK")
12535            );
12536        } else {
12537            println!("  plugins:     {}", yellow("no cache"));
12538        }
12539        println!();
12540
12541        // --- Shell State ---
12542        println!("{}", bold("Shell State"));
12543        println!("  aliases:     {}", self.aliases.len());
12544        println!("  global:      {} aliases", self.global_aliases.len());
12545        println!("  suffix:      {} aliases", self.suffix_aliases.len());
12546        println!("  variables:   {}", self.variables.len());
12547        println!("  arrays:      {}", self.arrays.len());
12548        println!("  assoc:       {}", self.assoc_arrays.len());
12549        println!(
12550            "  options:     {} set",
12551            self.options.iter().filter(|(_, v)| **v).count()
12552        );
12553        println!("  traps:       {} active", self.traps.len());
12554        println!(
12555            "  hooks:       {} registered",
12556            self.hook_functions.values().map(|v| v.len()).sum::<usize>()
12557        );
12558        println!();
12559
12560        // --- Log ---
12561        println!("{}", bold("Log"));
12562        let log_path = crate::log::log_path();
12563        if log_path.exists() {
12564            let size = std::fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
12565            println!("  {}  {} bytes", log_path.display(), size);
12566        } else {
12567            println!("  {}", dim("no log file yet"));
12568        }
12569        println!();
12570
12571        // --- Profiling ---
12572        println!("{}", bold("Profiling"));
12573        println!(
12574            "  chrome tracing: {}",
12575            if crate::log::profiling_enabled() {
12576                green("enabled")
12577            } else {
12578                dim("disabled")
12579            }
12580        );
12581        println!(
12582            "  flamegraph:     {}",
12583            if crate::log::flamegraph_enabled() {
12584                green("enabled")
12585            } else {
12586                dim("disabled")
12587            }
12588        );
12589        println!(
12590            "  prometheus:     {}",
12591            if crate::log::prometheus_enabled() {
12592                green("enabled")
12593            } else {
12594                dim("disabled")
12595            }
12596        );
12597        println!();
12598
12599        0
12600    }
12601
12602    /// dbview — browse zshrs SQLite cache tables without SQL.
12603    ///
12604    /// Usage:
12605    ///   dbview                      — list all tables and row counts
12606    ///   dbview autoloads             — dump autoloads table (name, source, body len, ast len)
12607    ///   dbview autoloads _git        — show single row by name
12608    ///   dbview comps                 — dump comps table
12609    ///   dbview history               — recent history entries
12610    ///   dbview history <pattern>     — search history
12611    ///   dbview plugins               — plugin cache entries
12612    ///   dbview executables            — PATH executables cache
12613    ///   dbview <table> --count       — just the count
12614    fn builtin_dbview(&self, args: &[String]) -> i32 {
12615        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
12616        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
12617        let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
12618        let green = |s: &str| format!("\x1b[32m{}\x1b[0m", s);
12619        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
12620
12621        if args.is_empty() {
12622            // List all tables with row counts
12623            println!("{}", bold("zshrs SQLite caches"));
12624            println!();
12625
12626            if let Some(ref cache) = self.compsys_cache {
12627                println!("  {} {}", bold("compsys.db"), dim("(completion cache)"));
12628                if let Ok(n) = cache.count_table("autoloads") {
12629                    let bc_count = cache
12630                        .count_table_where("autoloads", "bytecode IS NOT NULL")
12631                        .unwrap_or(0);
12632                    println!("    autoloads:    {:>6} rows  ({} compiled)", n, bc_count);
12633                }
12634                if let Ok(n) = cache.count_table("comps") {
12635                    println!("    comps:        {:>6} rows", n);
12636                }
12637                if let Ok(n) = cache.count_table("services") {
12638                    println!("    services:     {:>6} rows", n);
12639                }
12640                if let Ok(n) = cache.count_table("patcomps") {
12641                    println!("    patcomps:     {:>6} rows", n);
12642                }
12643                if let Ok(n) = cache.count_table("executables") {
12644                    println!("    executables:  {:>6} rows", n);
12645                }
12646                if let Ok(n) = cache.count_table("zstyles") {
12647                    println!("    zstyles:      {:>6} rows", n);
12648                }
12649                println!();
12650            }
12651
12652            if let Some(ref engine) = self.history {
12653                println!("  {} {}", bold("history.db"), dim("(command history)"));
12654                if let Ok(n) = engine.count() {
12655                    println!("    entries:      {:>6} rows", n);
12656                }
12657                println!();
12658            }
12659
12660            if let Some(ref cache) = self.plugin_cache {
12661                let (plugins, functions) = cache.stats();
12662                println!("  {} {}", bold("plugins.db"), dim("(plugin source cache)"));
12663                println!("    plugins:      {:>6} rows", plugins);
12664                println!("    functions:    {:>6} rows", functions);
12665                println!();
12666            }
12667
12668            println!("  Usage: {} <table> [name] [--count]", cyan("dbview"));
12669            return 0;
12670        }
12671
12672        let table = args[0].as_str();
12673        let filter = args.get(1).map(|s| s.as_str());
12674        let count_only = args.iter().any(|a| a == "--count" || a == "-c");
12675
12676        match table {
12677            "autoloads" => {
12678                let Some(ref cache) = self.compsys_cache else {
12679                    eprintln!("dbview: no compsys cache");
12680                    return 1;
12681                };
12682
12683                if count_only {
12684                    let n = cache.count_table("autoloads").unwrap_or(0);
12685                    println!("{}", n);
12686                    return 0;
12687                }
12688
12689                if let Some(name) = filter {
12690                    // Single row lookup
12691                    match cache.get_autoload(name) {
12692                        Ok(Some(stub)) => {
12693                            println!("{}", bold(&format!("autoload: {}", name)));
12694                            println!("  source:   {}", stub.source);
12695                            println!(
12696                                "  body:     {} bytes",
12697                                stub.body.as_ref().map(|b| b.len()).unwrap_or(0)
12698                            );
12699                            match cache.get_autoload_bytecode(name) {
12700                                Ok(Some(blob)) => {
12701                                    println!("  bytecode: {} {} bytes", green("YES"), blob.len())
12702                                }
12703                                _ => println!("  bytecode: {}", yellow("NULL")),
12704                            }
12705                            // Show first few lines of body
12706                            if let Some(ref body) = stub.body {
12707                                println!("  preview:");
12708                                for (i, line) in body.lines().take(10).enumerate() {
12709                                    println!("    {:>3}: {}", i + 1, dim(line));
12710                                }
12711                                let total = body.lines().count();
12712                                if total > 10 {
12713                                    println!("    {} ({} more lines)", dim("..."), total - 10);
12714                                }
12715                            }
12716                        }
12717                        _ => {
12718                            eprintln!("dbview: autoload '{}' not found", name);
12719                            return 1;
12720                        }
12721                    }
12722                    return 0;
12723                }
12724
12725                // Dump all autoloads
12726                let conn = &cache.conn();
12727                match conn.prepare("SELECT name, source, length(body), length(bytecode) FROM autoloads ORDER BY name LIMIT 200") {
12728                    Ok(mut stmt) => {
12729                        let rows = stmt.query_map([], |row| {
12730                            Ok((
12731                                row.get::<_, String>(0)?,
12732                                row.get::<_, String>(1)?,
12733                                row.get::<_, Option<i64>>(2)?,
12734                                row.get::<_, Option<i64>>(3)?,
12735                            ))
12736                        });
12737                        if let Ok(rows) = rows {
12738                            println!("{:<40} {:>8} {:>8}  {}", bold("NAME"), bold("BODY"), bold("BYTECODE"), bold("SOURCE"));
12739                            let mut count = 0;
12740                            for row in rows.flatten() {
12741                                let (name, source, body_len, ast_len) = row;
12742                                let ast_str = match ast_len {
12743                                    Some(n) => green(&format!("{:>8}", n)),
12744                                    None => yellow(&format!("{:>8}", "NULL")),
12745                                };
12746                                let body_str = match body_len {
12747                                    Some(n) => format!("{:>8}", n),
12748                                    None => dim("NULL").to_string(),
12749                                };
12750                                // Truncate source path for display
12751                                let src_short = if source.len() > 50 {
12752                                    format!("...{}", &source[source.len() - 47..])
12753                                } else {
12754                                    source
12755                                };
12756                                println!("{:<40} {} {}  {}", name, body_str, ast_str, dim(&src_short));
12757                                count += 1;
12758                            }
12759                            println!("\n{} rows shown (LIMIT 200)", count);
12760                        }
12761                    }
12762                    Err(e) => {
12763                        eprintln!("dbview: query failed: {}", e);
12764                        return 1;
12765                    }
12766                }
12767            }
12768
12769            "comps" => {
12770                let Some(ref cache) = self.compsys_cache else {
12771                    eprintln!("dbview: no compsys cache");
12772                    return 1;
12773                };
12774                if count_only {
12775                    println!("{}", cache.count_table("comps").unwrap_or(0));
12776                    return 0;
12777                }
12778                let conn = cache.conn();
12779                let query = if let Some(pat) = filter {
12780                    format!("SELECT command, function FROM comps WHERE command LIKE '%{}%' ORDER BY command LIMIT 100", pat)
12781                } else {
12782                    "SELECT command, function FROM comps ORDER BY command LIMIT 100".to_string()
12783                };
12784                match conn.prepare(&query) {
12785                    Ok(mut stmt) => {
12786                        println!("{:<40} {}", bold("COMMAND"), bold("FUNCTION"));
12787                        let rows = stmt.query_map([], |row| {
12788                            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
12789                        });
12790                        if let Ok(rows) = rows {
12791                            for row in rows.flatten() {
12792                                println!("{:<40} {}", row.0, cyan(&row.1));
12793                            }
12794                        }
12795                    }
12796                    Err(e) => {
12797                        eprintln!("dbview: {}", e);
12798                        return 1;
12799                    }
12800                }
12801            }
12802
12803            "executables" => {
12804                let Some(ref cache) = self.compsys_cache else {
12805                    eprintln!("dbview: no compsys cache");
12806                    return 1;
12807                };
12808                if count_only {
12809                    println!("{}", cache.count_table("executables").unwrap_or(0));
12810                    return 0;
12811                }
12812                let conn = cache.conn();
12813                let query = if let Some(pat) = filter {
12814                    format!("SELECT name, path FROM executables WHERE name LIKE '%{}%' ORDER BY name LIMIT 100", pat)
12815                } else {
12816                    "SELECT name, path FROM executables ORDER BY name LIMIT 100".to_string()
12817                };
12818                match conn.prepare(&query) {
12819                    Ok(mut stmt) => {
12820                        println!("{:<30} {}", bold("NAME"), bold("PATH"));
12821                        let rows = stmt.query_map([], |row| {
12822                            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
12823                        });
12824                        if let Ok(rows) = rows {
12825                            for row in rows.flatten() {
12826                                println!("{:<30} {}", row.0, dim(&row.1));
12827                            }
12828                        }
12829                    }
12830                    Err(e) => {
12831                        eprintln!("dbview: {}", e);
12832                        return 1;
12833                    }
12834                }
12835            }
12836
12837            "history" => {
12838                let Some(ref engine) = self.history else {
12839                    eprintln!("dbview: no history engine");
12840                    return 1;
12841                };
12842                if count_only {
12843                    println!("{}", engine.count().unwrap_or(0));
12844                    return 0;
12845                }
12846                if let Some(pat) = filter {
12847                    if let Ok(entries) = engine.search(pat, 20) {
12848                        for e in entries {
12849                            println!(
12850                                "  {} {} {}",
12851                                dim(&e.timestamp.to_string()),
12852                                cyan(&e.command),
12853                                dim(&format!("[{}]", e.exit_code.unwrap_or(0)))
12854                            );
12855                        }
12856                    }
12857                } else if let Ok(entries) = engine.recent(20) {
12858                    for e in entries {
12859                        println!(
12860                            "  {} {} {}",
12861                            dim(&e.timestamp.to_string()),
12862                            cyan(&e.command),
12863                            dim(&format!("[{}]", e.exit_code.unwrap_or(0)))
12864                        );
12865                    }
12866                }
12867            }
12868
12869            "plugins" => {
12870                let Some(ref cache) = self.plugin_cache else {
12871                    eprintln!("dbview: no plugin cache");
12872                    return 1;
12873                };
12874                let (plugins, functions) = cache.stats();
12875                println!("{} plugins, {} cached functions", plugins, functions);
12876            }
12877
12878            _ => {
12879                eprintln!("dbview: unknown table '{}'. Available: autoloads, comps, executables, history, plugins", table);
12880                return 1;
12881            }
12882        }
12883
12884        0
12885    }
12886
12887    /// profile — in-process command profiling with nanosecond accuracy.
12888    ///
12889    /// Unlike `time` (which measures one command) or `zprof` (which only
12890    /// profiles function calls), `profile` traces every execute_command,
12891    /// expansion, glob, and builtin dispatch inside the block.
12892    ///
12893    /// Usage:
12894    ///   profile { commands }     — profile a block
12895    ///   profile -s 'script'     — profile a script string
12896    ///   profile -f func         — profile a function call
12897    ///   profile --clear         — clear accumulated profile data
12898    ///   profile --dump          — show accumulated profile data
12899    fn builtin_profile(&mut self, args: &[String]) -> i32 {
12900        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
12901        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
12902        let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
12903        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
12904
12905        if args.is_empty() {
12906            println!("Usage: profile {{ commands }}");
12907            println!("       profile -s 'script string'");
12908            println!("       profile -f function_name [args...]");
12909            println!("       profile --clear");
12910            println!("       profile --dump");
12911            return 0;
12912        }
12913
12914        if args[0] == "--clear" {
12915            self.profiler = crate::zprof::Profiler::new();
12916            println!("profile data cleared");
12917            return 0;
12918        }
12919
12920        if args[0] == "--dump" {
12921            let (_, output) = crate::zprof::builtin_zprof(
12922                &mut self.profiler,
12923                &crate::zprof::ZprofOptions { clear: false },
12924            );
12925            if !output.is_empty() {
12926                print!("{}", output);
12927            } else {
12928                println!("{}", dim("no profile data"));
12929            }
12930            return 0;
12931        }
12932
12933        // Determine what to profile
12934        let code = if args[0] == "-s" {
12935            // profile -s 'script string'
12936            if args.len() < 2 {
12937                eprintln!("profile: -s requires a script string");
12938                return 1;
12939            }
12940            args[1..].join(" ")
12941        } else if args[0] == "-f" {
12942            // profile -f func_name [args...]
12943            if args.len() < 2 {
12944                eprintln!("profile: -f requires a function name");
12945                return 1;
12946            }
12947            args[1..].join(" ")
12948        } else {
12949            // profile { commands } — args is the block body
12950            args.join(" ")
12951        };
12952
12953        // Enable profiling, run, collect results
12954        let was_enabled = self.profiling_enabled;
12955        self.profiling_enabled = true;
12956        self.profiler = crate::zprof::Profiler::new(); // fresh data for this run
12957
12958        let t0 = std::time::Instant::now();
12959        let result = self.execute_script(&code);
12960        let elapsed = t0.elapsed();
12961        let status = match result {
12962            Ok(s) => s,
12963            Err(e) => {
12964                eprintln!("profile: {}", e);
12965                1
12966            }
12967        };
12968
12969        // Collect timing data
12970        println!();
12971        println!("{}", bold("profile results"));
12972        println!("{}", dim(&"─".repeat(60)));
12973        let dur_str = if elapsed.as_secs() > 0 {
12974            format!("{:.3}s", elapsed.as_secs_f64())
12975        } else if elapsed.as_millis() > 0 {
12976            format!("{:.3}ms", elapsed.as_secs_f64() * 1000.0)
12977        } else {
12978            format!("{:.1}µs", elapsed.as_secs_f64() * 1_000_000.0)
12979        };
12980        println!("  total:     {}", cyan(&dur_str));
12981        println!("  status:    {}", status);
12982        println!();
12983
12984        // Show function-level breakdown from profiler
12985        let (_, output) = crate::zprof::builtin_zprof(
12986            &mut self.profiler,
12987            &crate::zprof::ZprofOptions { clear: false },
12988        );
12989        if !output.is_empty() {
12990            println!("{}", bold("function breakdown"));
12991            print!("{}", output);
12992        }
12993
12994        // Per-command breakdown from tracing (if tracing is at debug level)
12995        println!();
12996        println!(
12997            "  {} set ZSHRS_LOG=trace for per-command tracing",
12998            yellow("tip:")
12999        );
13000        println!(
13001            "  {} output: {}",
13002            dim("log"),
13003            dim(&crate::log::log_path().display().to_string())
13004        );
13005
13006        self.profiling_enabled = was_enabled;
13007        status
13008    }
13009
13010    // ═══════════════════════════════════════════════════════════════════
13011    // AOP INTERCEPT — the killer builtin
13012    // ═══════════════════════════════════════════════════════════════════
13013
13014    /// Check intercepts for a command. Returns Some(result) if an around
13015    /// advice fully handled the command, None to proceed normally.
13016    fn run_intercepts(
13017        &mut self,
13018        cmd_name: &str,
13019        full_cmd: &str,
13020        args: &[String],
13021    ) -> Option<Result<i32, String>> {
13022        // Collect matching intercepts (clone to avoid borrow issues)
13023        let matching: Vec<Intercept> = self
13024            .intercepts
13025            .iter()
13026            .filter(|i| intercept_matches(&i.pattern, cmd_name, full_cmd))
13027            .cloned()
13028            .collect();
13029
13030        if matching.is_empty() {
13031            return None;
13032        }
13033
13034        // Set INTERCEPT_NAME and INTERCEPT_ARGS for advice code
13035        self.variables
13036            .insert("INTERCEPT_NAME".to_string(), cmd_name.to_string());
13037        self.variables
13038            .insert("INTERCEPT_ARGS".to_string(), args.join(" "));
13039        self.variables
13040            .insert("INTERCEPT_CMD".to_string(), full_cmd.to_string());
13041
13042        // Run before advice
13043        for advice in matching
13044            .iter()
13045            .filter(|i| matches!(i.kind, AdviceKind::Before))
13046        {
13047            let _ = self.execute_advice(&advice.code);
13048        }
13049
13050        // Check for around advice — first match wins
13051        let around = matching
13052            .iter()
13053            .find(|i| matches!(i.kind, AdviceKind::Around));
13054
13055        let t0 = std::time::Instant::now();
13056
13057        let result = if let Some(advice) = around {
13058            // Around advice: set INTERCEPT_PROCEED flag, run advice code.
13059            // If advice calls `intercept_proceed`, the original command runs.
13060            self.variables
13061                .insert("__intercept_proceed".to_string(), "0".to_string());
13062            let advice_result = self.execute_advice(&advice.code);
13063
13064            // Check if intercept_proceed was called
13065            let proceeded = self
13066                .variables
13067                .get("__intercept_proceed")
13068                .map(|v| v == "1")
13069                .unwrap_or(false);
13070
13071            if proceeded {
13072                // The original command was already executed inside the advice
13073                advice_result
13074            } else {
13075                // Advice didn't call proceed — command was suppressed
13076                advice_result
13077            }
13078        } else {
13079            // No around advice — run the original command.
13080            // We return None to let the normal dispatch continue.
13081            // But we still need after advice to fire, so we can't return None here
13082            // if there are after advices. Run the command ourselves.
13083            let has_after = matching.iter().any(|i| matches!(i.kind, AdviceKind::After));
13084            if !has_after {
13085                // Only before advice, no after — let normal dispatch continue
13086                return None;
13087            }
13088
13089            // Has after advice — we must run the command and then run after advice
13090            self.run_original_command(cmd_name, args)
13091        };
13092
13093        let elapsed = t0.elapsed();
13094
13095        // Set timing variable for after advice
13096        let ms = elapsed.as_secs_f64() * 1000.0;
13097        self.variables
13098            .insert("INTERCEPT_MS".to_string(), format!("{:.3}", ms));
13099        self.variables
13100            .insert("INTERCEPT_US".to_string(), format!("{:.0}", ms * 1000.0));
13101
13102        // Run after advice
13103        for advice in matching
13104            .iter()
13105            .filter(|i| matches!(i.kind, AdviceKind::After))
13106        {
13107            let _ = self.execute_advice(&advice.code);
13108        }
13109
13110        // Clean up
13111        self.variables.remove("INTERCEPT_NAME");
13112        self.variables.remove("INTERCEPT_ARGS");
13113        self.variables.remove("INTERCEPT_CMD");
13114        self.variables.remove("INTERCEPT_MS");
13115        self.variables.remove("INTERCEPT_US");
13116        self.variables.remove("__intercept_proceed");
13117
13118        Some(result)
13119    }
13120
13121    /// Execute the original command (used by around/after intercept dispatch).
13122    /// Execute advice code — dispatches @ prefix to stryke (fat binary),
13123    /// everything else to the shell parser. No fork. Machine code speed.
13124    fn execute_advice(&mut self, code: &str) -> Result<i32, String> {
13125        let code = code.trim();
13126        if code.starts_with('@') {
13127            let stryke_code = code.trim_start_matches('@').trim();
13128            if let Some(status) = crate::try_stryke_dispatch(stryke_code) {
13129                self.last_status = status;
13130                return Ok(status);
13131            }
13132            // No stryke handler (thin binary) — fall through to shell
13133        }
13134        self.execute_script(code)
13135    }
13136
13137    fn run_original_command(&mut self, cmd_name: &str, args: &[String]) -> Result<i32, String> {
13138        // Try function
13139        if let Some(func) = self.functions.get(cmd_name).cloned() {
13140            return self.call_function(&func, args);
13141        }
13142        if self.maybe_autoload(cmd_name) {
13143            if let Some(func) = self.functions.get(cmd_name).cloned() {
13144                return self.call_function(&func, args);
13145            }
13146        }
13147        // External command
13148        self.execute_external(cmd_name, &args.to_vec(), &[])
13149    }
13150
13151    /// intercept builtin — register AOP advice on commands.
13152    ///
13153    /// Usage:
13154    ///   intercept before <pattern> { code }
13155    ///   intercept after <pattern> { code }
13156    ///   intercept around <pattern> { code }
13157    ///   intercept list                       — show all intercepts
13158    ///   intercept remove <id>                — remove by ID
13159    ///   intercept clear                      — remove all
13160    fn builtin_intercept(&mut self, args: &[String]) -> i32 {
13161        if args.is_empty() {
13162            println!("Usage: intercept <before|after|around> <pattern> {{ code }}");
13163            println!("       intercept list | remove <id> | clear");
13164            return 0;
13165        }
13166
13167        match args[0].as_str() {
13168            "list" => {
13169                if self.intercepts.is_empty() {
13170                    println!("no intercepts registered");
13171                } else {
13172                    let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
13173                    let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
13174                    println!(
13175                        "{:>4}  {:<8}  {:<20}  {}",
13176                        bold("ID"),
13177                        bold("KIND"),
13178                        bold("PATTERN"),
13179                        bold("CODE")
13180                    );
13181                    for i in &self.intercepts {
13182                        let kind = match i.kind {
13183                            AdviceKind::Before => "before",
13184                            AdviceKind::After => "after",
13185                            AdviceKind::Around => "around",
13186                        };
13187                        let code_preview = if i.code.len() > 40 {
13188                            format!("{}...", &i.code[..37])
13189                        } else {
13190                            i.code.clone()
13191                        };
13192                        println!(
13193                            "{:>4}  {:<8}  {:<20}  {}",
13194                            cyan(&i.id.to_string()),
13195                            kind,
13196                            i.pattern,
13197                            code_preview
13198                        );
13199                    }
13200                }
13201                0
13202            }
13203            "clear" => {
13204                let count = self.intercepts.len();
13205                self.intercepts.clear();
13206                println!("cleared {} intercepts", count);
13207                0
13208            }
13209            "remove" => {
13210                if args.len() < 2 {
13211                    eprintln!("intercept remove: requires ID");
13212                    return 1;
13213                }
13214                if let Ok(id) = args[1].parse::<u32>() {
13215                    let before = self.intercepts.len();
13216                    self.intercepts.retain(|i| i.id != id);
13217                    if self.intercepts.len() < before {
13218                        println!("removed intercept {}", id);
13219                        0
13220                    } else {
13221                        eprintln!("intercept: no intercept with ID {}", id);
13222                        1
13223                    }
13224                } else {
13225                    eprintln!("intercept remove: invalid ID");
13226                    1
13227                }
13228            }
13229            "before" | "after" | "around" => {
13230                let kind = match args[0].as_str() {
13231                    "before" => AdviceKind::Before,
13232                    "after" => AdviceKind::After,
13233                    "around" => AdviceKind::Around,
13234                    _ => unreachable!(),
13235                };
13236
13237                if args.len() < 3 {
13238                    eprintln!("intercept {}: requires <pattern> {{ code }}", args[0]);
13239                    return 1;
13240                }
13241
13242                let pattern = args[1].clone();
13243                // Join remaining args as the code (handles { code } or 'code')
13244                let code = args[2..].join(" ");
13245                // Strip surrounding braces if present
13246                let code = code.trim().to_string();
13247                let code = if code.starts_with('{') && code.ends_with('}') {
13248                    code[1..code.len() - 1].trim().to_string()
13249                } else {
13250                    code
13251                };
13252
13253                let id = self.intercepts.iter().map(|i| i.id).max().unwrap_or(0) + 1;
13254                self.intercepts.push(Intercept {
13255                    pattern,
13256                    kind: kind.clone(),
13257                    code: code.clone(),
13258                    id,
13259                });
13260
13261                let kind_str = match kind {
13262                    AdviceKind::Before => "before",
13263                    AdviceKind::After => "after",
13264                    AdviceKind::Around => "around",
13265                };
13266                println!(
13267                    "intercept #{}: {} {} → {}",
13268                    id,
13269                    kind_str,
13270                    self.intercepts.last().unwrap().pattern,
13271                    if code.len() > 50 {
13272                        format!("{}...", &code[..47])
13273                    } else {
13274                        code
13275                    }
13276                );
13277                0
13278            }
13279            _ => {
13280                eprintln!(
13281                    "intercept: unknown subcommand '{}'. Use before|after|around|list|remove|clear",
13282                    args[0]
13283                );
13284                1
13285            }
13286        }
13287    }
13288
13289    /// intercept_proceed — called from around advice to execute the original command.
13290    fn builtin_intercept_proceed(&mut self, _args: &[String]) -> i32 {
13291        self.variables
13292            .insert("__intercept_proceed".to_string(), "1".to_string());
13293        // Run the original command using saved INTERCEPT_NAME/INTERCEPT_ARGS
13294        let cmd_name = self
13295            .variables
13296            .get("INTERCEPT_NAME")
13297            .cloned()
13298            .unwrap_or_default();
13299        let args_str = self
13300            .variables
13301            .get("INTERCEPT_ARGS")
13302            .cloned()
13303            .unwrap_or_default();
13304        let args: Vec<String> = if args_str.is_empty() {
13305            Vec::new()
13306        } else {
13307            args_str.split_whitespace().map(|s| s.to_string()).collect()
13308        };
13309        match self.run_original_command(&cmd_name, &args) {
13310            Ok(status) => status,
13311            Err(e) => {
13312                eprintln!("intercept_proceed: {}", e);
13313                1
13314            }
13315        }
13316    }
13317
13318    // ═══════════════════════════════════════════════════════════════════
13319    // CONCURRENT PRIMITIVES — ship work to the worker pool from shell
13320    // No stryke dependency. Pure zshrs. Thin binary gets full parallelism.
13321    // ═══════════════════════════════════════════════════════════════════
13322
13323    /// async { cmd } — run command on worker pool, return job ID immediately.
13324    /// Output captured in background, retrieve with `await $id`.
13325    ///
13326    /// Usage:
13327    ///   id=$(async 'sleep 2; echo done')
13328    ///   ... do other work ...
13329    ///   result=$(await $id)
13330    fn builtin_async(&mut self, args: &[String]) -> i32 {
13331        if args.is_empty() {
13332            eprintln!("async: requires a command string");
13333            return 1;
13334        }
13335
13336        let code = args.join(" ");
13337        let id = self.next_async_id;
13338        self.next_async_id += 1;
13339
13340        let (tx, rx) = crossbeam_channel::bounded::<(i32, String)>(1);
13341        let pool = std::sync::Arc::clone(&self.worker_pool);
13342
13343        pool.submit(move || {
13344            // Execute in a subprocess to capture stdout
13345            use std::process::{Command, Stdio};
13346            let output = Command::new("sh")
13347                .args(["-c", &code])
13348                .stdout(Stdio::piped())
13349                .stderr(Stdio::inherit())
13350                .output();
13351            match output {
13352                Ok(out) => {
13353                    let stdout = String::from_utf8_lossy(&out.stdout).to_string();
13354                    let status = out.status.code().unwrap_or(1);
13355                    let _ = tx.send((status, stdout));
13356                }
13357                Err(_) => {
13358                    let _ = tx.send((127, String::new()));
13359                }
13360            }
13361        });
13362
13363        self.async_jobs.insert(id, rx);
13364        // Print the job ID so it can be captured: id=$(async 'cmd')
13365        println!("{}", id);
13366        0
13367    }
13368
13369    /// await $id — block until async job completes, print its stdout, return its status.
13370    ///
13371    /// Usage:
13372    ///   id=$(async 'expensive_command')
13373    ///   await $id    # blocks until done, prints output
13374    ///   echo $?      # exit status of the async command
13375    fn builtin_await(&mut self, args: &[String]) -> i32 {
13376        if args.is_empty() {
13377            eprintln!("await: requires a job ID");
13378            return 1;
13379        }
13380
13381        let id: u32 = match args[0].parse() {
13382            Ok(n) => n,
13383            Err(_) => {
13384                eprintln!("await: invalid job ID '{}'", args[0]);
13385                return 1;
13386            }
13387        };
13388
13389        let rx = match self.async_jobs.remove(&id) {
13390            Some(rx) => rx,
13391            None => {
13392                eprintln!("await: no async job with ID {}", id);
13393                return 1;
13394            }
13395        };
13396
13397        // Block until the job completes
13398        match rx.recv() {
13399            Ok((status, stdout)) => {
13400                if !stdout.is_empty() {
13401                    print!("{}", stdout);
13402                }
13403                self.last_status = status;
13404                status
13405            }
13406            Err(_) => {
13407                eprintln!("await: job {} died without result", id);
13408                1
13409            }
13410        }
13411    }
13412
13413    /// pmap 'cmd {}' arg1 arg2 arg3 — parallel map across worker pool.
13414    /// Runs `cmd` for each argument, replacing `{}` with the argument.
13415    /// Output is collected in order. Returns 0 if all succeed.
13416    ///
13417    /// Usage:
13418    ///   pmap 'gzip {}' *.log
13419    ///   pmap 'echo {}' a b c d
13420    ///   ls *.rs | pmap 'wc -l {}'
13421    fn builtin_pmap(&mut self, args: &[String]) -> i32 {
13422        if args.len() < 2 {
13423            eprintln!("pmap: requires 'command {{}}' followed by arguments");
13424            return 1;
13425        }
13426
13427        let template = &args[0];
13428        let items = &args[1..];
13429
13430        // Ship each item to the pool
13431        let mut receivers = Vec::with_capacity(items.len());
13432        for item in items {
13433            let cmd = template.replace("{}", item);
13434            let rx = self.worker_pool.submit_with_result(move || {
13435                use std::process::{Command, Stdio};
13436                let output = Command::new("sh")
13437                    .args(["-c", &cmd])
13438                    .stdout(Stdio::piped())
13439                    .stderr(Stdio::inherit())
13440                    .output();
13441                match output {
13442                    Ok(out) => (
13443                        out.status.code().unwrap_or(1),
13444                        String::from_utf8_lossy(&out.stdout).to_string(),
13445                    ),
13446                    Err(_) => (127, String::new()),
13447                }
13448            });
13449            receivers.push(rx);
13450        }
13451
13452        // Collect results in order
13453        let mut any_fail = false;
13454        for rx in receivers {
13455            if let Ok((status, stdout)) = rx.recv() {
13456                if !stdout.is_empty() {
13457                    print!("{}", stdout);
13458                }
13459                if status != 0 {
13460                    any_fail = true;
13461                }
13462            }
13463        }
13464
13465        if any_fail {
13466            1
13467        } else {
13468            0
13469        }
13470    }
13471
13472    /// pgrep 'pattern' arg1 arg2 ... — parallel grep/filter across worker pool.
13473    /// Runs the pattern command for each argument, prints args where command succeeds.
13474    ///
13475    /// Usage:
13476    ///   pgrep 'test -f {}' /path/a /path/b /path/c
13477    ///   pgrep 'grep -q TODO {}' *.rs
13478    fn builtin_pgrep(&mut self, args: &[String]) -> i32 {
13479        if args.len() < 2 {
13480            eprintln!("pgrep: requires 'test_command {{}}' followed by arguments");
13481            return 1;
13482        }
13483
13484        let template = &args[0];
13485        let items = &args[1..];
13486
13487        let mut receivers: Vec<(String, crossbeam_channel::Receiver<bool>)> =
13488            Vec::with_capacity(items.len());
13489        for item in items {
13490            let cmd = template.replace("{}", item);
13491            let rx = self.worker_pool.submit_with_result(move || {
13492                use std::process::{Command, Stdio};
13493                Command::new("sh")
13494                    .args(["-c", &cmd])
13495                    .stdout(Stdio::null())
13496                    .stderr(Stdio::null())
13497                    .status()
13498                    .map(|s| s.success())
13499                    .unwrap_or(false)
13500            });
13501            receivers.push((item.clone(), rx));
13502        }
13503
13504        for (item, rx) in receivers {
13505            if let Ok(true) = rx.recv() {
13506                println!("{}", item);
13507            }
13508        }
13509
13510        0
13511    }
13512
13513    /// peach 'cmd {}' arg1 arg2 ... — parallel for-each, no output ordering.
13514    /// Like pmap but doesn't collect output — fire-and-forget, print as completed.
13515    ///
13516    /// Usage:
13517    ///   peach 'convert {} {}.png' *.svg
13518    ///   peach 'rsync -a {} remote:{}' dir1 dir2 dir3
13519    fn builtin_peach(&mut self, args: &[String]) -> i32 {
13520        if args.len() < 2 {
13521            eprintln!("peach: requires 'command {{}}' followed by arguments");
13522            return 1;
13523        }
13524
13525        let template = &args[0];
13526        let items = &args[1..];
13527
13528        let (tx, rx) = crossbeam_channel::unbounded::<(String, i32, String)>();
13529
13530        for item in items {
13531            let cmd = template.replace("{}", item);
13532            let item_clone = item.clone();
13533            let tx = tx.clone();
13534            self.worker_pool.submit(move || {
13535                use std::process::{Command, Stdio};
13536                let output = Command::new("sh")
13537                    .args(["-c", &cmd])
13538                    .stdout(Stdio::piped())
13539                    .stderr(Stdio::inherit())
13540                    .output();
13541                match output {
13542                    Ok(out) => {
13543                        let stdout = String::from_utf8_lossy(&out.stdout).to_string();
13544                        let status = out.status.code().unwrap_or(1);
13545                        let _ = tx.send((item_clone, status, stdout));
13546                    }
13547                    Err(_) => {
13548                        let _ = tx.send((item_clone, 127, String::new()));
13549                    }
13550                }
13551            });
13552        }
13553        drop(tx);
13554
13555        let mut any_fail = false;
13556        for (_, status, stdout) in rx {
13557            if !stdout.is_empty() {
13558                print!("{}", stdout);
13559            }
13560            if status != 0 {
13561                any_fail = true;
13562            }
13563        }
13564
13565        if any_fail {
13566            1
13567        } else {
13568            0
13569        }
13570    }
13571
13572    /// barrier cmd1 ::: cmd2 ::: cmd3 — run commands in parallel, wait for ALL to complete.
13573    /// Returns the worst (highest) exit status.
13574    ///
13575    /// Usage:
13576    ///   barrier 'make -C proj1' ::: 'make -C proj2' ::: 'make -C proj3'
13577    ///   barrier 'npm test' ::: 'cargo test' ::: 'pytest'
13578    fn builtin_barrier(&mut self, args: &[String]) -> i32 {
13579        if args.is_empty() {
13580            eprintln!("barrier: requires commands separated by :::");
13581            return 1;
13582        }
13583
13584        // Split on ::: delimiter
13585        let mut commands: Vec<String> = Vec::new();
13586        let mut current = String::new();
13587        for arg in args {
13588            if arg == ":::" {
13589                if !current.is_empty() {
13590                    commands.push(current.trim().to_string());
13591                    current.clear();
13592                }
13593            } else {
13594                if !current.is_empty() {
13595                    current.push(' ');
13596                }
13597                current.push_str(arg);
13598            }
13599        }
13600        if !current.is_empty() {
13601            commands.push(current.trim().to_string());
13602        }
13603
13604        if commands.is_empty() {
13605            return 0;
13606        }
13607
13608        // Ship all to pool
13609        let mut receivers = Vec::with_capacity(commands.len());
13610        for cmd in &commands {
13611            let cmd = cmd.clone();
13612            let rx = self.worker_pool.submit_with_result(move || {
13613                use std::process::{Command, Stdio};
13614                Command::new("sh")
13615                    .args(["-c", &cmd])
13616                    .stdout(Stdio::inherit())
13617                    .stderr(Stdio::inherit())
13618                    .status()
13619                    .map(|s| s.code().unwrap_or(1))
13620                    .unwrap_or(127)
13621            });
13622            receivers.push(rx);
13623        }
13624
13625        // Wait for all — return worst status
13626        let mut worst = 0i32;
13627        for rx in receivers {
13628            if let Ok(status) = rx.recv() {
13629                if status > worst {
13630                    worst = status;
13631                }
13632            }
13633        }
13634
13635        self.last_status = worst;
13636        worst
13637    }
13638
13639    /// help - display help for builtins (bash)
13640    fn builtin_help(&self, args: &[String]) -> i32 {
13641        if args.is_empty() {
13642            println!("zshrs shell builtins:");
13643            println!("");
13644            println!("  alias, bg, bind, break, builtin, cd, command, continue,");
13645            println!("  declare, dirs, disown, echo, enable, eval, exec, exit,");
13646            println!("  export, false, fc, fg, getopts, hash, help, history,");
13647            println!("  jobs, kill, let, local, logout, popd, printf, pushd,");
13648            println!("  pwd, read, readonly, return, set, shift, shopt, source,");
13649            println!("  suspend, test, times, trap, true, type, typeset, ulimit,");
13650            println!("  umask, unalias, unset, wait, whence, where, which");
13651            println!("");
13652            println!("Type 'help name' for more information about 'name'.");
13653            return 0;
13654        }
13655
13656        let cmd = &args[0];
13657        match cmd.as_str() {
13658            "cd" => println!("cd: cd [-L|-P] [dir]\n    Change the shell working directory."),
13659            "echo" => println!("echo: echo [-neE] [arg ...]\n    Write arguments to standard output."),
13660            "export" => println!("export: export [-fn] [name[=value] ...]\n    Set export attribute for shell variables."),
13661            "alias" => println!("alias: alias [-p] [name[=value] ...]\n    Define or display aliases."),
13662            "history" => println!("history: history [-c] [-d offset] [n]\n    Display or manipulate the history list."),
13663            "jobs" => println!("jobs: jobs [-lnprs] [jobspec ...]\n    Display status of jobs."),
13664            "kill" => println!("kill: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n    Send a signal to a job."),
13665            "read" => println!("read: read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]\n    Read a line from standard input."),
13666            "set" => println!("set: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]\n    Set or unset values of shell options and positional parameters."),
13667            "test" | "[" => println!("test: test [expr]\n    Evaluate conditional expression."),
13668            "type" => println!("type: type [-afptP] name [name ...]\n    Display information about command type."),
13669            _ => println!("{}: no help available", cmd),
13670        }
13671        0
13672    }
13673
13674    /// readarray/mapfile - read lines into array (bash)
13675    fn builtin_readarray(&mut self, args: &[String]) -> i32 {
13676        use std::io::{BufRead, BufReader};
13677
13678        let mut array_name = "MAPFILE".to_string();
13679        let mut delimiter = '\n';
13680        let mut count = 0usize; // 0 = unlimited
13681        let mut skip = 0usize;
13682        let mut strip_trailing = false;
13683        let mut callback: Option<String> = None;
13684        let mut callback_quantum = 0usize;
13685
13686        let mut i = 0;
13687        while i < args.len() {
13688            match args[i].as_str() {
13689                "-d" => {
13690                    i += 1;
13691                    if i < args.len() && !args[i].is_empty() {
13692                        delimiter = args[i].chars().next().unwrap_or('\n');
13693                    }
13694                }
13695                "-n" => {
13696                    i += 1;
13697                    if i < args.len() {
13698                        count = args[i].parse().unwrap_or(0);
13699                    }
13700                }
13701                "-O" => {
13702                    i += 1;
13703                    // Origin - start index (ignored, we always start at 0)
13704                }
13705                "-s" => {
13706                    i += 1;
13707                    if i < args.len() {
13708                        skip = args[i].parse().unwrap_or(0);
13709                    }
13710                }
13711                "-t" => strip_trailing = true,
13712                "-C" => {
13713                    i += 1;
13714                    if i < args.len() {
13715                        callback = Some(args[i].clone());
13716                    }
13717                }
13718                "-c" => {
13719                    i += 1;
13720                    if i < args.len() {
13721                        callback_quantum = args[i].parse().unwrap_or(5000);
13722                    }
13723                }
13724                "-u" => {
13725                    i += 1;
13726                    // fd - ignored, we read from stdin
13727                }
13728                s if !s.starts_with('-') => {
13729                    array_name = s.to_string();
13730                }
13731                _ => {}
13732            }
13733            i += 1;
13734        }
13735
13736        let stdin = std::io::stdin();
13737        let reader = BufReader::new(stdin.lock());
13738        let mut lines = Vec::new();
13739        let mut line_count = 0usize;
13740
13741        for line_result in reader.lines() {
13742            if let Ok(mut line) = line_result {
13743                line_count += 1;
13744
13745                if line_count <= skip {
13746                    continue;
13747                }
13748
13749                if strip_trailing {
13750                    while line.ends_with('\n') || line.ends_with('\r') {
13751                        line.pop();
13752                    }
13753                }
13754
13755                lines.push(line);
13756
13757                if count > 0 && lines.len() >= count {
13758                    break;
13759                }
13760            }
13761        }
13762
13763        self.arrays.insert(array_name, lines);
13764        let _ = (callback, callback_quantum);
13765        0
13766    }
13767
13768    fn builtin_shopt(&mut self, args: &[String]) -> i32 {
13769        if args.is_empty() {
13770            // List all shell options
13771            for (opt, val) in &self.options {
13772                println!("shopt {} {}", if *val { "-s" } else { "-u" }, opt);
13773            }
13774            return 0;
13775        }
13776
13777        let mut set = None;
13778        let mut opts = Vec::new();
13779
13780        for arg in args {
13781            match arg.as_str() {
13782                "-s" => set = Some(true),
13783                "-u" => set = Some(false),
13784                "-p" => {
13785                    // Print option status
13786                    for opt in &opts {
13787                        let val = self.options.get(opt).copied().unwrap_or(false);
13788                        println!("shopt {} {}", if val { "-s" } else { "-u" }, opt);
13789                    }
13790                    return 0;
13791                }
13792                _ => opts.push(arg.clone()),
13793            }
13794        }
13795
13796        if let Some(enable) = set {
13797            for opt in &opts {
13798                self.options.insert(opt.clone(), enable);
13799            }
13800        } else {
13801            // Query options
13802            for opt in &opts {
13803                let val = self.options.get(opt).copied().unwrap_or(false);
13804                println!("shopt {} {}", if val { "-s" } else { "-u" }, opt);
13805            }
13806        }
13807        0
13808    }
13809
13810    /// zsh-compatible setopt builtin
13811    fn builtin_setopt(&mut self, args: &[String]) -> i32 {
13812        if args.is_empty() {
13813            // List options that differ from compiled-in defaults (zsh behavior)
13814            // For default-ON options: show "noOPTION" if currently OFF
13815            // For default-OFF options: show "OPTION" if currently ON
13816            let defaults_on = Self::default_on_options();
13817            let mut diff_opts: Vec<String> = Vec::new();
13818
13819            for &opt in Self::all_zsh_options() {
13820                let enabled = self.options.get(opt).copied().unwrap_or(false);
13821                let is_default_on = defaults_on.contains(&opt);
13822
13823                if is_default_on && !enabled {
13824                    // Default ON but currently OFF -> show noOPTION
13825                    diff_opts.push(format!("no{}", opt));
13826                } else if !is_default_on && enabled {
13827                    // Default OFF but currently ON -> show OPTION
13828                    diff_opts.push(opt.to_string());
13829                }
13830            }
13831            diff_opts.sort();
13832            for opt in diff_opts {
13833                println!("{}", opt);
13834            }
13835            return 0;
13836        }
13837
13838        let mut use_pattern = false;
13839        let mut iter = args.iter().peekable();
13840
13841        while let Some(arg) = iter.next() {
13842            match arg.as_str() {
13843                "-m" => use_pattern = true,
13844                "-o" => {
13845                    // -o option_name: set option
13846                    if let Some(opt) = iter.next() {
13847                        let (name, enable) = Self::normalize_option_name(opt);
13848                        self.options.insert(name, enable);
13849                    }
13850                }
13851                "+o" => {
13852                    // +o option_name: unset option
13853                    if let Some(opt) = iter.next() {
13854                        let (name, enable) = Self::normalize_option_name(opt);
13855                        self.options.insert(name, !enable);
13856                    }
13857                }
13858                _ => {
13859                    if use_pattern {
13860                        // Match pattern against all options
13861                        for opt in Self::all_zsh_options() {
13862                            if Self::option_matches_pattern(opt, arg) {
13863                                self.options.insert(opt.to_string(), true);
13864                            }
13865                        }
13866                    } else {
13867                        let (name, enable) = Self::normalize_option_name(arg);
13868                        // Verify it's a valid option (zsh doesn't error on bad names in setopt)
13869                        self.options.insert(name, enable);
13870                    }
13871                }
13872            }
13873        }
13874        0
13875    }
13876
13877    /// zsh-compatible unsetopt builtin
13878    fn builtin_unsetopt(&mut self, args: &[String]) -> i32 {
13879        if args.is_empty() {
13880            // List all options in the format you'd pass to unsetopt to disable them
13881            // For default-ON options: show "noOPTION" (to turn it off)
13882            // For default-OFF options: show "OPTION" (already off, but this is what you'd type)
13883            let defaults_on = Self::default_on_options();
13884            let mut all_opts: Vec<String> = Vec::new();
13885
13886            for &opt in Self::all_zsh_options() {
13887                let is_default_on = defaults_on.contains(&opt);
13888                if is_default_on {
13889                    all_opts.push(format!("no{}", opt));
13890                } else {
13891                    all_opts.push(opt.to_string());
13892                }
13893            }
13894            all_opts.sort();
13895            for opt in all_opts {
13896                println!("{}", opt);
13897            }
13898            return 0;
13899        }
13900
13901        let mut use_pattern = false;
13902        let mut iter = args.iter().peekable();
13903
13904        while let Some(arg) = iter.next() {
13905            match arg.as_str() {
13906                "-m" => use_pattern = true,
13907                "-o" => {
13908                    // -o option_name: unset option
13909                    if let Some(opt) = iter.next() {
13910                        let (name, enable) = Self::normalize_option_name(opt);
13911                        self.options.insert(name, !enable);
13912                    }
13913                }
13914                "+o" => {
13915                    // +o option_name: set option (opposite in unsetopt)
13916                    if let Some(opt) = iter.next() {
13917                        let (name, enable) = Self::normalize_option_name(opt);
13918                        self.options.insert(name, enable);
13919                    }
13920                }
13921                _ => {
13922                    if use_pattern {
13923                        for opt in Self::all_zsh_options() {
13924                            if Self::option_matches_pattern(opt, arg) {
13925                                self.options.insert(opt.to_string(), false);
13926                            }
13927                        }
13928                    } else {
13929                        let (name, enable) = Self::normalize_option_name(arg);
13930                        // unsetopt turns OFF the option (or ON if "no" prefix)
13931                        self.options.insert(name, !enable);
13932                    }
13933                }
13934            }
13935        }
13936        0
13937    }
13938
13939    fn builtin_getopts(&mut self, args: &[String]) -> i32 {
13940        if args.len() < 2 {
13941            eprintln!("zshrs: getopts: usage: getopts optstring name [arg ...]");
13942            return 1;
13943        }
13944
13945        let optstring = &args[0];
13946        let varname = &args[1];
13947        let opt_args: Vec<&str> = if args.len() > 2 {
13948            args[2..].iter().map(|s| s.as_str()).collect()
13949        } else {
13950            self.positional_params.iter().map(|s| s.as_str()).collect()
13951        };
13952
13953        // Get current OPTIND
13954        let optind: usize = self
13955            .variables
13956            .get("OPTIND")
13957            .and_then(|s| s.parse().ok())
13958            .unwrap_or(1);
13959
13960        if optind > opt_args.len() {
13961            self.variables.insert(varname.to_string(), "?".to_string());
13962            return 1;
13963        }
13964
13965        let current_arg = opt_args[optind - 1];
13966
13967        if !current_arg.starts_with('-') || current_arg == "-" {
13968            self.variables.insert(varname.to_string(), "?".to_string());
13969            return 1;
13970        }
13971
13972        if current_arg == "--" {
13973            self.variables
13974                .insert("OPTIND".to_string(), (optind + 1).to_string());
13975            self.variables.insert(varname.to_string(), "?".to_string());
13976            return 1;
13977        }
13978
13979        // Get current option position within the argument
13980        let optpos: usize = self
13981            .variables
13982            .get("_OPTPOS")
13983            .and_then(|s| s.parse().ok())
13984            .unwrap_or(1);
13985
13986        let opt_char = current_arg.chars().nth(optpos);
13987
13988        if let Some(c) = opt_char {
13989            // Look up option in optstring
13990            let opt_idx = optstring.find(c);
13991
13992            match opt_idx {
13993                Some(idx) => {
13994                    // Check if option takes an argument
13995                    let takes_arg = optstring.chars().nth(idx + 1) == Some(':');
13996
13997                    if takes_arg {
13998                        // Get argument
13999                        let arg = if optpos + 1 < current_arg.len() {
14000                            // Argument is rest of current arg
14001                            current_arg[optpos + 1..].to_string()
14002                        } else if optind < opt_args.len() {
14003                            // Argument is next arg
14004                            self.variables
14005                                .insert("OPTIND".to_string(), (optind + 2).to_string());
14006                            self.variables.remove("_OPTPOS");
14007                            opt_args[optind].to_string()
14008                        } else {
14009                            // Missing argument
14010                            self.variables.insert(varname.to_string(), "?".to_string());
14011                            if !optstring.starts_with(':') {
14012                                eprintln!("zshrs: getopts: option requires an argument -- {}", c);
14013                            }
14014                            self.variables.insert("OPTARG".to_string(), c.to_string());
14015                            return 1;
14016                        };
14017
14018                        self.variables.insert("OPTARG".to_string(), arg);
14019                        self.variables
14020                            .insert("OPTIND".to_string(), (optind + 1).to_string());
14021                        self.variables.remove("_OPTPOS");
14022                    } else {
14023                        // No argument needed
14024                        if optpos + 1 < current_arg.len() {
14025                            // More options in this arg
14026                            self.variables
14027                                .insert("_OPTPOS".to_string(), (optpos + 1).to_string());
14028                        } else {
14029                            // Move to next arg
14030                            self.variables
14031                                .insert("OPTIND".to_string(), (optind + 1).to_string());
14032                            self.variables.remove("_OPTPOS");
14033                        }
14034                    }
14035
14036                    self.variables.insert(varname.to_string(), c.to_string());
14037                    0
14038                }
14039                None => {
14040                    // Unknown option
14041                    if !optstring.starts_with(':') {
14042                        eprintln!("zshrs: getopts: illegal option -- {}", c);
14043                    }
14044                    self.variables.insert(varname.to_string(), "?".to_string());
14045                    self.variables.insert("OPTARG".to_string(), c.to_string());
14046
14047                    // Advance to next option/arg
14048                    if optpos + 1 < current_arg.len() {
14049                        self.variables
14050                            .insert("_OPTPOS".to_string(), (optpos + 1).to_string());
14051                    } else {
14052                        self.variables
14053                            .insert("OPTIND".to_string(), (optind + 1).to_string());
14054                        self.variables.remove("_OPTPOS");
14055                    }
14056                    0
14057                }
14058            }
14059        } else {
14060            // No more options in current arg
14061            self.variables
14062                .insert("OPTIND".to_string(), (optind + 1).to_string());
14063            self.variables.remove("_OPTPOS");
14064            self.variables.insert(varname.to_string(), "?".to_string());
14065            1
14066        }
14067    }
14068
14069    fn builtin_type(&mut self, args: &[String]) -> i32 {
14070        if args.is_empty() {
14071            return 0;
14072        }
14073
14074        let mut show_all = false;
14075        let mut path_only = false;
14076        let mut silent = false;
14077        let mut show_type = false;
14078        let mut names = Vec::new();
14079
14080        let mut iter = args.iter();
14081        while let Some(arg) = iter.next() {
14082            if arg.starts_with('-') && arg.len() > 1 {
14083                for c in arg[1..].chars() {
14084                    match c {
14085                        'a' => show_all = true,
14086                        'p' => path_only = true,
14087                        'P' => path_only = true,
14088                        's' => silent = true,
14089                        't' => show_type = true,
14090                        'f' => {} // ignore functions (we still show them)
14091                        'w' => {} // like -t but different format
14092                        _ => {}
14093                    }
14094                }
14095            } else {
14096                names.push(arg.clone());
14097            }
14098        }
14099
14100        if names.is_empty() {
14101            return 0;
14102        }
14103
14104        let mut status = 0;
14105        for name in &names {
14106            let mut found_any = false;
14107
14108            // Check for alias (skip if -p)
14109            if !path_only && self.aliases.contains_key(name) {
14110                found_any = true;
14111                if !silent {
14112                    if show_type {
14113                        println!("alias");
14114                    } else {
14115                        println!(
14116                            "{} is aliased to `{}'",
14117                            name,
14118                            self.aliases.get(name).unwrap()
14119                        );
14120                    }
14121                }
14122                if !show_all {
14123                    continue;
14124                }
14125            }
14126
14127            // Check for function (skip if -p)
14128            if !path_only && self.functions.contains_key(name) {
14129                found_any = true;
14130                if !silent {
14131                    if show_type {
14132                        println!("function");
14133                    } else {
14134                        println!("{} is a shell function", name);
14135                    }
14136                }
14137                if !show_all {
14138                    continue;
14139                }
14140            }
14141
14142            // Check for builtin (skip if -p)
14143            if !path_only && (self.is_builtin(name) || name == ":" || name == "[") {
14144                found_any = true;
14145                if !silent {
14146                    if show_type {
14147                        println!("builtin");
14148                    } else {
14149                        println!("{} is a shell builtin", name);
14150                    }
14151                }
14152                if !show_all {
14153                    continue;
14154                }
14155            }
14156
14157            // Check for external command in PATH
14158            if let Ok(path_env) = std::env::var("PATH") {
14159                for dir in path_env.split(':') {
14160                    let full_path = format!("{}/{}", dir, name);
14161                    if std::path::Path::new(&full_path).exists() {
14162                        found_any = true;
14163                        if !silent {
14164                            if show_type {
14165                                println!("file");
14166                            } else {
14167                                println!("{} is {}", name, full_path);
14168                            }
14169                        }
14170                        if !show_all {
14171                            break;
14172                        }
14173                    }
14174                }
14175            }
14176
14177            if !found_any {
14178                if !silent {
14179                    eprintln!("zshrs: type: {}: not found", name);
14180                }
14181                status = 1;
14182            }
14183        }
14184        status
14185    }
14186
14187    fn builtin_hash(&mut self, args: &[String]) -> i32 {
14188        // hash [ -Ldfmrv ] [ name[=value] ] ...
14189        // hash -r clears the hash table
14190        // hash -d manages named directories
14191        // hash -f fills the table with all PATH commands
14192        // hash -m matches patterns
14193        // hash -v verbose
14194        // hash -L list in form suitable for reinput
14195
14196        let mut dir_mode = false;
14197        let mut rehash = false;
14198        let mut fill_all = false;
14199        let mut pattern_match = false;
14200        let mut verbose = false;
14201        let mut list_form = false;
14202        let mut names = Vec::new();
14203
14204        let mut i = 0;
14205        while i < args.len() {
14206            let arg = &args[i];
14207            if arg.starts_with('-') && arg.len() > 1 {
14208                for ch in arg[1..].chars() {
14209                    match ch {
14210                        'd' => dir_mode = true,
14211                        'r' => rehash = true,
14212                        'f' => fill_all = true,
14213                        'm' => pattern_match = true,
14214                        'v' => verbose = true,
14215                        'L' => list_form = true,
14216                        _ => {}
14217                    }
14218                }
14219            } else {
14220                names.push(arg.clone());
14221            }
14222            i += 1;
14223        }
14224
14225        // -r: clear hash table
14226        if rehash && !dir_mode && names.is_empty() {
14227            self.command_hash.clear();
14228            return 0;
14229        }
14230
14231        // -f: fill hash table with all commands in PATH
14232        if fill_all {
14233            if let Ok(path_var) = env::var("PATH") {
14234                for dir in path_var.split(':') {
14235                    if let Ok(entries) = std::fs::read_dir(dir) {
14236                        for entry in entries.flatten() {
14237                            if let Ok(ft) = entry.file_type() {
14238                                if ft.is_file() || ft.is_symlink() {
14239                                    if let Some(name) = entry.file_name().to_str() {
14240                                        let path = entry.path().to_string_lossy().to_string();
14241                                        self.command_hash.insert(name.to_string(), path);
14242                                    }
14243                                }
14244                            }
14245                        }
14246                    }
14247                }
14248            }
14249            return 0;
14250        }
14251
14252        if dir_mode {
14253            // Named directories mode (hash -d)
14254            if names.is_empty() {
14255                // List named directories
14256                for (name, path) in &self.named_dirs {
14257                    if list_form {
14258                        println!("hash -d {}={}", name, path.display());
14259                    } else if verbose {
14260                        println!("{}={}", name, path.display());
14261                    } else {
14262                        println!("{}={}", name, path.display());
14263                    }
14264                }
14265                return 0;
14266            }
14267
14268            if rehash {
14269                // Remove named directories
14270                if pattern_match {
14271                    // -m: pattern matching
14272                    let to_remove: Vec<String> = self
14273                        .named_dirs
14274                        .keys()
14275                        .filter(|k| {
14276                            names.iter().any(|pat| {
14277                                let pattern = pat.replace("*", ".*").replace("?", ".");
14278                                regex::Regex::new(&format!("^{}$", pattern))
14279                                    .map(|r| r.is_match(k))
14280                                    .unwrap_or(false)
14281                            })
14282                        })
14283                        .cloned()
14284                        .collect();
14285                    for name in to_remove {
14286                        self.named_dirs.remove(&name);
14287                    }
14288                } else {
14289                    for name in &names {
14290                        self.named_dirs.remove(name);
14291                    }
14292                }
14293                return 0;
14294            }
14295
14296            // Add named directories
14297            for name in &names {
14298                if let Some((n, p)) = name.split_once('=') {
14299                    self.add_named_dir(n, p);
14300                } else {
14301                    eprintln!("hash: -d: {} not in name=value format", name);
14302                    return 1;
14303                }
14304            }
14305            return 0;
14306        }
14307
14308        // Regular hash - command path lookup
14309        if names.is_empty() {
14310            // List all hashed commands
14311            for (name, path) in &self.command_hash {
14312                if list_form {
14313                    println!("hash {}={}", name, path);
14314                } else {
14315                    println!("{}={}", name, path);
14316                }
14317            }
14318            return 0;
14319        }
14320
14321        for name in &names {
14322            if let Some((cmd, path)) = name.split_once('=') {
14323                // Explicit assignment
14324                self.command_hash.insert(cmd.to_string(), path.to_string());
14325                if verbose {
14326                    println!("{}={}", cmd, path);
14327                }
14328            } else if let Some(path) = self.find_in_path(name) {
14329                // Look up in PATH and hash it
14330                self.command_hash.insert(name.clone(), path.clone());
14331                if verbose {
14332                    println!("{}={}", name, path);
14333                }
14334            } else {
14335                eprintln!("zshrs: hash: {}: not found", name);
14336                return 1;
14337            }
14338        }
14339        0
14340    }
14341
14342    /// add-zsh-hook builtin - add function to hook
14343    fn builtin_add_zsh_hook(&mut self, args: &[String]) -> i32 {
14344        // add-zsh-hook [-d] hook function
14345        if args.len() < 2 {
14346            eprintln!("usage: add-zsh-hook [-d] hook function");
14347            return 1;
14348        }
14349
14350        let (delete, hook, func) = if args[0] == "-d" {
14351            if args.len() < 3 {
14352                eprintln!("usage: add-zsh-hook -d hook function");
14353                return 1;
14354            }
14355            (true, &args[1], &args[2])
14356        } else {
14357            (false, &args[0], &args[1])
14358        };
14359
14360        if delete {
14361            // Remove function from hook
14362            if let Some(funcs) = self.hook_functions.get_mut(hook.as_str()) {
14363                funcs.retain(|f| f != func);
14364            }
14365        } else {
14366            // Add function to hook
14367            self.add_hook(hook, func);
14368        }
14369        0
14370    }
14371
14372    fn builtin_command(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
14373        // command [ -pvV ] simple command
14374        // -p: use default PATH
14375        // -v: print path (like which)
14376        // -V: verbose description (like type)
14377        let mut use_default_path = false;
14378        let mut print_path = false;
14379        let mut verbose = false;
14380        let mut positional_args: Vec<&str> = Vec::new();
14381
14382        let mut i = 0;
14383        while i < args.len() {
14384            let arg = &args[i];
14385            if arg.starts_with('-') && arg.len() > 1 && positional_args.is_empty() {
14386                for ch in arg[1..].chars() {
14387                    match ch {
14388                        'p' => use_default_path = true,
14389                        'v' => print_path = true,
14390                        'V' => verbose = true,
14391                        '-' => {
14392                            // -- ends options
14393                            i += 1;
14394                            break;
14395                        }
14396                        _ => {
14397                            eprintln!("command: bad option: -{}", ch);
14398                            return 1;
14399                        }
14400                    }
14401                }
14402            } else {
14403                positional_args.push(arg);
14404            }
14405            i += 1;
14406        }
14407
14408        // Add remaining args after --
14409        while i < args.len() {
14410            positional_args.push(&args[i]);
14411            i += 1;
14412        }
14413
14414        if positional_args.is_empty() {
14415            return 0;
14416        }
14417
14418        let cmd = positional_args[0];
14419
14420        // -v or -V: just print info about command
14421        if print_path || verbose {
14422            // Search PATH for command
14423            let path_var = if use_default_path {
14424                "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string()
14425            } else {
14426                env::var("PATH").unwrap_or_default()
14427            };
14428
14429            for dir in path_var.split(':') {
14430                let full_path = PathBuf::from(dir).join(cmd);
14431                if full_path.exists() && full_path.is_file() {
14432                    if verbose {
14433                        println!("{} is {}", cmd, full_path.display());
14434                    } else {
14435                        println!("{}", full_path.display());
14436                    }
14437                    return 0;
14438                }
14439            }
14440
14441            if verbose {
14442                eprintln!("{} not found", cmd);
14443            }
14444            return 1;
14445        }
14446
14447        // Execute as external command (bypassing functions and aliases)
14448        let cmd_args: Vec<String> = positional_args[1..].iter().map(|s| s.to_string()).collect();
14449
14450        if use_default_path {
14451            // Temporarily set PATH
14452            let old_path = env::var("PATH").ok();
14453            env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin");
14454            let result = self
14455                .execute_external(
14456                    cmd,
14457                    &cmd_args
14458                        .iter()
14459                        .map(|s| s.as_str())
14460                        .collect::<Vec<_>>()
14461                        .join(" ")
14462                        .split_whitespace()
14463                        .map(String::from)
14464                        .collect::<Vec<_>>(),
14465                    redirects,
14466                )
14467                .unwrap_or(127);
14468            if let Some(p) = old_path {
14469                env::set_var("PATH", p);
14470            }
14471            result
14472        } else {
14473            self.execute_external(cmd, &cmd_args, redirects)
14474                .unwrap_or(127)
14475        }
14476    }
14477
14478    fn builtin_builtin(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
14479        // Run builtin, bypassing functions and aliases
14480        if args.is_empty() {
14481            return 0;
14482        }
14483
14484        let cmd = &args[0];
14485        let cmd_args = &args[1..];
14486
14487        match cmd.as_str() {
14488            "cd" => self.builtin_cd(cmd_args),
14489            "pwd" => self.builtin_pwd(redirects),
14490            "echo" => self.builtin_echo(cmd_args, redirects),
14491            "export" => self.builtin_export(cmd_args),
14492            "unset" => self.builtin_unset(cmd_args),
14493            "exit" => self.builtin_exit(cmd_args),
14494            "return" => self.builtin_return(cmd_args),
14495            "true" => 0,
14496            "false" => 1,
14497            ":" => 0,
14498            "test" | "[" => self.builtin_test(cmd_args),
14499            "local" => self.builtin_local(cmd_args),
14500            "declare" | "typeset" => self.builtin_declare(cmd_args),
14501            "read" => self.builtin_read(cmd_args),
14502            "shift" => self.builtin_shift(cmd_args),
14503            "eval" => self.builtin_eval(cmd_args),
14504            "alias" => self.builtin_alias(cmd_args),
14505            "unalias" => self.builtin_unalias(cmd_args),
14506            "set" => self.builtin_set(cmd_args),
14507            "shopt" => self.builtin_shopt(cmd_args),
14508            "getopts" => self.builtin_getopts(cmd_args),
14509            "type" => self.builtin_type(cmd_args),
14510            "hash" => self.builtin_hash(cmd_args),
14511            "add-zsh-hook" => self.builtin_add_zsh_hook(cmd_args),
14512            "autoload" => self.builtin_autoload(cmd_args),
14513            "source" | "." => self.builtin_source(cmd_args),
14514            "functions" => self.builtin_functions(cmd_args),
14515            "zle" => self.builtin_zle(cmd_args),
14516            "bindkey" => self.builtin_bindkey(cmd_args),
14517            "setopt" => self.builtin_setopt(cmd_args),
14518            "unsetopt" => self.builtin_unsetopt(cmd_args),
14519            "emulate" => self.builtin_emulate(cmd_args),
14520            "zstyle" => self.builtin_zstyle(cmd_args),
14521            "compadd" => self.builtin_compadd(cmd_args),
14522            "compset" => self.builtin_compset(cmd_args),
14523            "compdef" => self.builtin_compdef(cmd_args),
14524            "compinit" => self.builtin_compinit(cmd_args),
14525            "cdreplay" => self.builtin_cdreplay(cmd_args),
14526            "zmodload" => self.builtin_zmodload(cmd_args),
14527            "zcompile" => self.builtin_zcompile(cmd_args),
14528            "zformat" => self.builtin_zformat(cmd_args),
14529            "zprof" => self.builtin_zprof(cmd_args),
14530            "print" => self.builtin_print(cmd_args),
14531            "printf" => self.builtin_printf(cmd_args),
14532            "command" => self.builtin_command(cmd_args, redirects),
14533            "whence" => self.builtin_whence(cmd_args),
14534            "which" => self.builtin_which(cmd_args),
14535            "where" => self.builtin_where(cmd_args),
14536            "fc" => self.builtin_fc(cmd_args),
14537            "history" => self.builtin_history(cmd_args),
14538            "dirs" => self.builtin_dirs(cmd_args),
14539            "pushd" => self.builtin_pushd(cmd_args),
14540            "popd" => self.builtin_popd(cmd_args),
14541            "bg" => self.builtin_bg(cmd_args),
14542            "fg" => self.builtin_fg(cmd_args),
14543            "jobs" => self.builtin_jobs(cmd_args),
14544            "kill" => self.builtin_kill(cmd_args),
14545            "wait" => self.builtin_wait(cmd_args),
14546            "trap" => self.builtin_trap(cmd_args),
14547            "umask" => self.builtin_umask(cmd_args),
14548            "ulimit" => self.builtin_ulimit(cmd_args),
14549            "times" => self.builtin_times(cmd_args),
14550            "let" => self.builtin_let(cmd_args),
14551            "integer" => self.builtin_integer(cmd_args),
14552            "float" => self.builtin_float(cmd_args),
14553            "readonly" => self.builtin_readonly(cmd_args),
14554            _ => {
14555                eprintln!("zshrs: builtin: {}: not a shell builtin", cmd);
14556                1
14557            }
14558        }
14559    }
14560
14561    fn builtin_let(&mut self, args: &[String]) -> i32 {
14562        if args.is_empty() {
14563            return 1;
14564        }
14565
14566        let mut result = 0i64;
14567        for expr in args {
14568            result = self.evaluate_arithmetic_expr(expr);
14569        }
14570
14571        // let returns 1 if last expression evaluates to 0, 0 otherwise
14572        if result == 0 {
14573            1
14574        } else {
14575            0
14576        }
14577    }
14578
14579    /// Generate completion candidates
14580    fn builtin_compgen(&self, args: &[String]) -> i32 {
14581        let mut i = 0;
14582        let mut prefix = String::new();
14583        let mut actions = Vec::new();
14584        let mut wordlist = None;
14585        let mut globpat = None;
14586
14587        while i < args.len() {
14588            match args[i].as_str() {
14589                "-W" => {
14590                    i += 1;
14591                    if i < args.len() {
14592                        wordlist = Some(args[i].clone());
14593                    }
14594                }
14595                "-G" => {
14596                    i += 1;
14597                    if i < args.len() {
14598                        globpat = Some(args[i].clone());
14599                    }
14600                }
14601                "-a" => actions.push("alias"),
14602                "-b" => actions.push("builtin"),
14603                "-c" => actions.push("command"),
14604                "-d" => actions.push("directory"),
14605                "-e" => actions.push("export"),
14606                "-f" => actions.push("file"),
14607                "-j" => actions.push("job"),
14608                "-k" => actions.push("keyword"),
14609                "-u" => actions.push("user"),
14610                "-v" => actions.push("variable"),
14611                s if !s.starts_with('-') => prefix = s.to_string(),
14612                _ => {}
14613            }
14614            i += 1;
14615        }
14616
14617        let mut results = Vec::new();
14618
14619        // Generate based on actions
14620        for action in actions {
14621            match action {
14622                "alias" => {
14623                    for name in self.aliases.keys() {
14624                        if name.starts_with(&prefix) {
14625                            results.push(name.clone());
14626                        }
14627                    }
14628                }
14629                "builtin" => {
14630                    for name in [
14631                        "cd", "pwd", "echo", "export", "unset", "source", "exit", "return", "true",
14632                        "false", ":", "test", "[", "local", "declare", "jobs", "fg", "bg", "kill",
14633                        "disown", "wait", "alias", "unalias", "set", "shopt",
14634                    ] {
14635                        if name.starts_with(&prefix) {
14636                            results.push(name.to_string());
14637                        }
14638                    }
14639                }
14640                "directory" => {
14641                    if let Ok(entries) = std::fs::read_dir(".") {
14642                        for entry in entries.flatten() {
14643                            if let Ok(ft) = entry.file_type() {
14644                                if ft.is_dir() {
14645                                    let name = entry.file_name().to_string_lossy().to_string();
14646                                    if name.starts_with(&prefix) {
14647                                        results.push(name);
14648                                    }
14649                                }
14650                            }
14651                        }
14652                    }
14653                }
14654                "file" => {
14655                    if let Ok(entries) = std::fs::read_dir(".") {
14656                        for entry in entries.flatten() {
14657                            let name = entry.file_name().to_string_lossy().to_string();
14658                            if name.starts_with(&prefix) {
14659                                results.push(name);
14660                            }
14661                        }
14662                    }
14663                }
14664                "variable" => {
14665                    for name in self.variables.keys() {
14666                        if name.starts_with(&prefix) {
14667                            results.push(name.clone());
14668                        }
14669                    }
14670                    for name in std::env::vars().map(|(k, _)| k) {
14671                        if name.starts_with(&prefix) && !results.contains(&name) {
14672                            results.push(name);
14673                        }
14674                    }
14675                }
14676                _ => {}
14677            }
14678        }
14679
14680        // Handle wordlist
14681        if let Some(words) = wordlist {
14682            for word in words.split_whitespace() {
14683                if word.starts_with(&prefix) {
14684                    results.push(word.to_string());
14685                }
14686            }
14687        }
14688
14689        // Handle glob pattern
14690        if let Some(_pattern) = globpat {
14691            let full_pattern = format!("{}*", prefix);
14692            if let Ok(paths) = glob::glob(&full_pattern) {
14693                for path in paths.flatten() {
14694                    results.push(path.to_string_lossy().to_string());
14695                }
14696            }
14697        }
14698
14699        results.sort();
14700        results.dedup();
14701        for r in results {
14702            println!("{}", r);
14703        }
14704        0
14705    }
14706
14707    /// Define completion spec for a command
14708    fn builtin_complete(&mut self, args: &[String]) -> i32 {
14709        if args.is_empty() {
14710            // List all completion specs
14711            for (cmd, spec) in &self.completions {
14712                let mut parts = vec!["complete".to_string()];
14713                for action in &spec.actions {
14714                    parts.push(format!("-{}", action));
14715                }
14716                if let Some(ref w) = spec.wordlist {
14717                    parts.push("-W".to_string());
14718                    parts.push(format!("'{}'", w));
14719                }
14720                if let Some(ref f) = spec.function {
14721                    parts.push("-F".to_string());
14722                    parts.push(f.clone());
14723                }
14724                if let Some(ref c) = spec.command {
14725                    parts.push("-C".to_string());
14726                    parts.push(c.clone());
14727                }
14728                parts.push(cmd.clone());
14729                println!("{}", parts.join(" "));
14730            }
14731            return 0;
14732        }
14733
14734        let mut spec = CompSpec::default();
14735        let mut commands = Vec::new();
14736        let mut i = 0;
14737
14738        while i < args.len() {
14739            match args[i].as_str() {
14740                "-W" => {
14741                    i += 1;
14742                    if i < args.len() {
14743                        spec.wordlist = Some(args[i].clone());
14744                    }
14745                }
14746                "-F" => {
14747                    i += 1;
14748                    if i < args.len() {
14749                        spec.function = Some(args[i].clone());
14750                    }
14751                }
14752                "-C" => {
14753                    i += 1;
14754                    if i < args.len() {
14755                        spec.command = Some(args[i].clone());
14756                    }
14757                }
14758                "-G" => {
14759                    i += 1;
14760                    if i < args.len() {
14761                        spec.globpat = Some(args[i].clone());
14762                    }
14763                }
14764                "-P" => {
14765                    i += 1;
14766                    if i < args.len() {
14767                        spec.prefix = Some(args[i].clone());
14768                    }
14769                }
14770                "-S" => {
14771                    i += 1;
14772                    if i < args.len() {
14773                        spec.suffix = Some(args[i].clone());
14774                    }
14775                }
14776                "-a" => spec.actions.push("a".to_string()),
14777                "-b" => spec.actions.push("b".to_string()),
14778                "-c" => spec.actions.push("c".to_string()),
14779                "-d" => spec.actions.push("d".to_string()),
14780                "-e" => spec.actions.push("e".to_string()),
14781                "-f" => spec.actions.push("f".to_string()),
14782                "-j" => spec.actions.push("j".to_string()),
14783                "-r" => {
14784                    // Remove completion spec
14785                    i += 1;
14786                    while i < args.len() {
14787                        self.completions.remove(&args[i]);
14788                        i += 1;
14789                    }
14790                    return 0;
14791                }
14792                s if !s.starts_with('-') => commands.push(s.to_string()),
14793                _ => {}
14794            }
14795            i += 1;
14796        }
14797
14798        for cmd in commands {
14799            self.completions.insert(cmd, spec.clone());
14800        }
14801        0
14802    }
14803
14804    /// Modify completion options
14805    fn builtin_compopt(&mut self, args: &[String]) -> i32 {
14806        // Basic stub - just accept the options
14807        let _ = args;
14808        0
14809    }
14810
14811    /// zsh compadd - add completion matches
14812    fn builtin_compadd(&mut self, args: &[String]) -> i32 {
14813        // Basic stub for zsh completion system
14814        // In a full implementation, this would add completion candidates
14815        let _ = args;
14816        0
14817    }
14818
14819    /// zsh compset - modify completion prefix/suffix
14820    fn builtin_compset(&mut self, args: &[String]) -> i32 {
14821        // Basic stub for zsh completion system
14822        let _ = args;
14823        0
14824    }
14825
14826    /// compdef - register completion functions for commands
14827    /// Usage: compdef _git git
14828    ///        compdef _docker docker docker-compose
14829    ///        compdef -d git  # delete
14830    fn builtin_compdef(&mut self, args: &[String]) -> i32 {
14831        if let Some(cache) = &mut self.compsys_cache {
14832            compsys::compdef::compdef_execute(cache, args)
14833        } else {
14834            // No cache - defer for cdreplay (zinit turbo mode)
14835            self.deferred_compdefs.push(args.to_vec());
14836            0
14837        }
14838    }
14839
14840    /// compinit - initialize the completion system
14841    /// Scans fpath for completion functions and registers them
14842    #[tracing::instrument(level = "info", skip(self))]
14843    fn builtin_compinit(&mut self, args: &[String]) -> i32 {
14844        // Parse options
14845        // -C: use cache if valid (skip fpath scan)
14846        // -D: don't dump (don't write .zcompdump)
14847        // -d file: specify dump file
14848        // -u: use insecure dirs anyway  -i: silently ignore insecure dirs
14849        // -q: quiet
14850        let mut quiet = false;
14851        let mut no_dump = false;
14852        let mut dump_file: Option<String> = None;
14853        let mut use_cache = false;
14854        let mut ignore_insecure = false;
14855        let mut use_insecure = false;
14856
14857        let mut i = 0;
14858        while i < args.len() {
14859            match args[i].as_str() {
14860                "-q" => quiet = true,
14861                "-C" => use_cache = true,
14862                "-D" => no_dump = true,
14863                "-d" => {
14864                    i += 1;
14865                    if i < args.len() {
14866                        dump_file = Some(args[i].clone());
14867                    }
14868                }
14869                "-u" => use_insecure = true,
14870                "-i" => ignore_insecure = true,
14871                _ => {}
14872            }
14873            i += 1;
14874        }
14875
14876        // Run compaudit with SQLite cache (unless -u skips it entirely)
14877        if !use_insecure && !self.posix_mode {
14878            if let Some(ref cache) = self.plugin_cache {
14879                let insecure = cache.compaudit_cached(&self.fpath);
14880                if !insecure.is_empty() && !ignore_insecure {
14881                    if !quiet {
14882                        eprintln!("compinit: insecure directories:");
14883                        for d in &insecure {
14884                            eprintln!("  {}", d);
14885                        }
14886                        eprintln!("compinit: run with -i to ignore or -u to use anyway");
14887                    }
14888                    return 1;
14889                }
14890            }
14891        }
14892
14893        // ZSH COMPAT MODE: Use traditional zsh algorithm (fpath scan, .zcompdump, no SQLite)
14894        if self.zsh_compat {
14895            return self.compinit_compat(quiet, no_dump, dump_file, use_cache);
14896        }
14897
14898        // ZSHRS MODE: Use SQLite cache with function bodies
14899
14900        // Try to use existing cache if -C and cache is valid
14901        if use_cache {
14902            if let Some(cache) = &self.compsys_cache {
14903                if compsys::cache_is_valid(cache) {
14904                    // Load from cache instead of rescanning
14905                    if let Ok(result) = compsys::load_from_cache(cache) {
14906                        if !quiet {
14907                            tracing::info!(
14908                                comps = result.comps.len(),
14909                                "compinit: using cached completions"
14910                            );
14911                        }
14912                        self.assoc_arrays.insert("_comps".to_string(), result.comps);
14913                        self.assoc_arrays
14914                            .insert("_services".to_string(), result.services);
14915                        self.assoc_arrays
14916                            .insert("_patcomps".to_string(), result.patcomps);
14917
14918                        // Background: fill bytecode blobs for any autoloads that have body but no ast.
14919                        // This populates the cache so subsequent autoload calls skip parsing.
14920                        if let Some(ref cache) = self.compsys_cache {
14921                            if let Ok(missing) = cache.count_autoloads_missing_bytecode() {
14922                                if missing > 0 {
14923                                    tracing::info!(
14924                                        count = missing,
14925                                        "compinit: backfilling bytecode blobs on worker pool"
14926                                    );
14927                                    let cache_path = compsys::cache::default_cache_path();
14928                                    let total_missing = missing;
14929                                    self.worker_pool.submit(move || {
14930                                        let mut cache = match compsys::cache::CompsysCache::open(&cache_path) {
14931                                            Ok(c) => c,
14932                                            Err(_) => return,
14933                                        };
14934                                        // Loop in batches of 100: fetch 100 bodies from SQLite,
14935                                        // parse them, write bytecode blobs back, repeat until none left.
14936                                        // Peak memory: ~100 function bodies + ASTs at a time.
14937                                        let mut total_cached = 0usize;
14938                                        loop {
14939                                            let stubs = match cache.get_autoloads_missing_bytecode_batch(100) {
14940                                                Ok(s) if !s.is_empty() => s,
14941                                                _ => break,
14942                                            };
14943                                            let mut batch: Vec<(String, Vec<u8>)> = Vec::with_capacity(stubs.len());
14944                                            for (name, body) in &stubs {
14945                                                let mut parser = crate::parser::ShellParser::new(body);
14946                                                if let Ok(commands) = parser.parse_script() {
14947                                                    if !commands.is_empty() {
14948                                                        let compiler = crate::shell_compiler::ShellCompiler::new();
14949                                                        let chunk = compiler.compile(&commands);
14950                                                        if let Ok(blob) = bincode::serialize(&chunk) {
14951                                                            batch.push((name.clone(), blob));
14952                                                        }
14953                                                    }
14954                                                }
14955                                            }
14956                                            total_cached += batch.len();
14957                                            if let Err(e) = cache.set_autoload_bytecodes_bulk(&batch) {
14958                                                tracing::warn!(error = %e, "compinit: bytecode backfill batch failed");
14959                                                break;
14960                                            }
14961                                            // If we got fewer than 100 results, we're done
14962                                            if stubs.len() < 100 {
14963                                                break;
14964                                            }
14965                                        }
14966                                        tracing::info!(
14967                                            cached = total_cached,
14968                                            total = total_missing,
14969                                            "compinit: bytecode backfill complete"
14970                                        );
14971                                    });
14972                                }
14973                            }
14974                        }
14975
14976                        return 0;
14977                    }
14978                }
14979            }
14980        }
14981
14982        // Ship compinit to worker pool — no ad-hoc thread spawn.
14983        // The heavy work (scan + SQLite write) runs on a pool thread.
14984        // Results are merged into shell state lazily via drain_compinit_bg().
14985        let fpath = self.fpath.clone();
14986        let fpath_count = fpath.len();
14987        let pool_size = self.worker_pool.size();
14988        let (tx, rx) = std::sync::mpsc::channel();
14989        let bg_start = std::time::Instant::now();
14990        tracing::info!(
14991            fpath_dirs = fpath_count,
14992            worker_pool = pool_size,
14993            "compinit: shipping to worker pool"
14994        );
14995        self.worker_pool.submit(move || {
14996            tracing::debug!("compinit-bg: thread started");
14997            let cache_path = compsys::cache::default_cache_path();
14998            if let Some(parent) = cache_path.parent() {
14999                let _ = std::fs::create_dir_all(parent);
15000            }
15001            // Remove old DB to start fresh
15002            let _ = std::fs::remove_file(&cache_path);
15003            let _ = std::fs::remove_file(format!("{}-shm", cache_path.display()));
15004            let _ = std::fs::remove_file(format!("{}-wal", cache_path.display()));
15005
15006            let mut cache = match compsys::cache::CompsysCache::open(&cache_path) {
15007                Ok(c) => c,
15008                Err(e) => {
15009                    tracing::error!("compinit: failed to create cache: {}", e);
15010                    return;
15011                }
15012            };
15013
15014            let result = match compsys::build_cache_from_fpath(&fpath, &mut cache) {
15015                Ok(r) => r,
15016                Err(e) => {
15017                    tracing::error!("compinit: scan failed: {}", e);
15018                    return;
15019                }
15020            };
15021
15022            tracing::info!(
15023                functions = result.files_scanned,
15024                comps = result.comps.len(),
15025                dirs = result.dirs_scanned,
15026                ms = result.scan_time_ms,
15027                "compinit: background scan complete"
15028            );
15029
15030            // Pre-parse function bodies and cache bytecode blobs.
15031            // Stream: parse one → serialize → write → drop. Never accumulate.
15032            // 16k functions × ~10KB AST = OOM if held in memory.
15033            let parse_start = std::time::Instant::now();
15034            let mut parse_ok = 0usize;
15035            let mut parse_fail = 0usize;
15036            let mut no_body = 0usize;
15037            let batch_size = 100;
15038            let mut batch: Vec<(String, Vec<u8>)> = Vec::with_capacity(batch_size);
15039
15040            for file in &result.files {
15041                if let Some(ref body) = file.body {
15042                    let mut parser = crate::parser::ShellParser::new(body);
15043                    match parser.parse_script() {
15044                        Ok(commands) if !commands.is_empty() => {
15045                            // Compile AST → fusevm bytecodes, then serialize the Chunk
15046                            let compiler = crate::shell_compiler::ShellCompiler::new();
15047                            let chunk = compiler.compile(&commands);
15048                            if let Ok(blob) = bincode::serialize(&chunk) {
15049                                batch.push((file.name.clone(), blob));
15050                                parse_ok += 1;
15051                                if batch.len() >= batch_size {
15052                                    let _ = cache.set_autoload_bytecodes_bulk(&batch);
15053                                    batch.clear();
15054                                }
15055                            }
15056                        }
15057                        Ok(_) => {
15058                            parse_fail += 1;
15059                        }
15060                        Err(_) => {
15061                            parse_fail += 1;
15062                        }
15063                    }
15064                } else {
15065                    no_body += 1;
15066                }
15067            }
15068            // Flush remaining
15069            if !batch.is_empty() {
15070                let _ = cache.set_autoload_bytecodes_bulk(&batch);
15071                batch.clear();
15072            }
15073
15074            tracing::info!(
15075                cached = parse_ok,
15076                failed = parse_fail,
15077                no_body = no_body,
15078                total = result.files.len(),
15079                ms = parse_start.elapsed().as_millis() as u64,
15080                "compinit: bytecode blobs cached"
15081            );
15082
15083            let _ = tx.send(CompInitBgResult { result, cache });
15084        });
15085
15086        self.compinit_pending = Some((rx, bg_start));
15087        0
15088    }
15089
15090    /// Non-blocking drain of background compinit results.
15091    /// Call this before any completion lookup (prompt, tab-complete, etc.).
15092    /// If the background thread hasn't finished yet, this is a no-op.
15093    pub fn drain_compinit_bg(&mut self) {
15094        if let Some((rx, start)) = self.compinit_pending.take() {
15095            match rx.try_recv() {
15096                Ok(bg) => {
15097                    let comps = bg.result.comps.len();
15098                    self.assoc_arrays
15099                        .insert("_comps".to_string(), bg.result.comps);
15100                    self.assoc_arrays
15101                        .insert("_services".to_string(), bg.result.services);
15102                    self.assoc_arrays
15103                        .insert("_patcomps".to_string(), bg.result.patcomps);
15104                    self.compsys_cache = Some(bg.cache);
15105                    tracing::info!(
15106                        wall_ms = start.elapsed().as_millis() as u64,
15107                        comps,
15108                        "compinit: background results merged"
15109                    );
15110                }
15111                Err(std::sync::mpsc::TryRecvError::Empty) => {
15112                    // Not ready yet — put the receiver back for next poll
15113                    self.compinit_pending = Some((rx, start));
15114                }
15115                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
15116                    tracing::warn!("compinit: background thread died without sending results");
15117                }
15118            }
15119        }
15120    }
15121
15122    /// Traditional zsh compinit (--zsh-compat mode)
15123    /// Uses fpath scanning, .zcompdump files, no SQLite
15124    fn compinit_compat(
15125        &mut self,
15126        quiet: bool,
15127        no_dump: bool,
15128        dump_file: Option<String>,
15129        use_cache: bool,
15130    ) -> i32 {
15131        let zdotdir = self
15132            .variables
15133            .get("ZDOTDIR")
15134            .cloned()
15135            .or_else(|| std::env::var("ZDOTDIR").ok())
15136            .unwrap_or_else(|| std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()));
15137
15138        let dump_path = dump_file
15139            .map(PathBuf::from)
15140            .unwrap_or_else(|| PathBuf::from(&zdotdir).join(".zcompdump"));
15141
15142        // -C: Try to use existing .zcompdump if valid
15143        if use_cache && dump_path.exists() {
15144            if compsys::check_dump(&dump_path, &self.fpath, "zshrs-0.1.0") {
15145                // Valid dump - source it to load _comps
15146                // For now, just rescan (proper impl would source the dump file)
15147                if !quiet {
15148                    tracing::info!("compinit: .zcompdump valid, rescanning for compat");
15149                }
15150            }
15151        }
15152
15153        // Full fpath scan (traditional zsh algorithm)
15154        let result = compsys::compinit(&self.fpath);
15155
15156        if !quiet {
15157            tracing::info!(
15158                functions = result.files_scanned,
15159                comps = result.comps.len(),
15160                dirs = result.dirs_scanned,
15161                ms = result.scan_time_ms,
15162                "compinit: fpath scan complete"
15163            );
15164        }
15165
15166        // Write .zcompdump unless -D
15167        if !no_dump {
15168            let _ = compsys::compdump(&result, &dump_path, "zshrs-0.1.0");
15169        }
15170
15171        // Set up _comps associative array
15172        self.assoc_arrays
15173            .insert("_comps".to_string(), result.comps.clone());
15174        self.assoc_arrays
15175            .insert("_services".to_string(), result.services.clone());
15176        self.assoc_arrays
15177            .insert("_patcomps".to_string(), result.patcomps.clone());
15178
15179        // No SQLite cache in compat mode
15180        self.compsys_cache = None;
15181
15182        0
15183    }
15184
15185    /// cdreplay - replay deferred compdef calls (zinit turbo mode)
15186    /// Usage: cdreplay [-q]
15187    fn builtin_cdreplay(&mut self, args: &[String]) -> i32 {
15188        let quiet = args.contains(&"-q".to_string());
15189
15190        if self.deferred_compdefs.is_empty() {
15191            return 0;
15192        }
15193
15194        let deferred = std::mem::take(&mut self.deferred_compdefs);
15195        let count = deferred.len();
15196
15197        if let Some(cache) = &mut self.compsys_cache {
15198            for compdef_args in deferred {
15199                compsys::compdef::compdef_execute(cache, &compdef_args);
15200            }
15201        }
15202
15203        if !quiet {
15204            eprintln!("cdreplay: replayed {} compdef calls", count);
15205        }
15206
15207        0
15208    }
15209
15210    /// zsh zstyle - configure styles for completion
15211    fn builtin_zstyle(&mut self, args: &[String]) -> i32 {
15212        if args.is_empty() {
15213            // List all styles
15214            for (pattern, style, values) in self.style_table.list(None) {
15215                println!("zstyle '{}' {} {}", pattern, style, values.join(" "));
15216            }
15217            return 0;
15218        }
15219
15220        // Handle options
15221        if args[0].starts_with('-') {
15222            match args[0].as_str() {
15223                "-d" => {
15224                    // Delete style
15225                    let pattern = args.get(1).map(|s| s.as_str());
15226                    let style = args.get(2).map(|s| s.as_str());
15227                    self.style_table.delete(pattern, style);
15228                    return 0;
15229                }
15230                "-g" => {
15231                    // Get style into array
15232                    if args.len() >= 4 {
15233                        let array_name = &args[1];
15234                        let context = &args[2];
15235                        let style = &args[3];
15236                        if let Some(values) = self.style_table.get(context, style) {
15237                            self.arrays.insert(array_name.clone(), values.to_vec());
15238                            return 0;
15239                        }
15240                    }
15241                    return 1;
15242                }
15243                "-s" => {
15244                    // Get style as scalar
15245                    if args.len() >= 4 {
15246                        let var_name = &args[1];
15247                        let context = &args[2];
15248                        let style = &args[3];
15249                        let sep = args.get(4).map(|s| s.as_str()).unwrap_or(" ");
15250                        if let Some(values) = self.style_table.get(context, style) {
15251                            self.variables.insert(var_name.clone(), values.join(sep));
15252                            return 0;
15253                        }
15254                    }
15255                    return 1;
15256                }
15257                "-t" => {
15258                    // Test style (check if true/yes)
15259                    if args.len() >= 3 {
15260                        let context = &args[1];
15261                        let style = &args[2];
15262                        return if self.style_table.test_bool(context, style).unwrap_or(false) {
15263                            0
15264                        } else {
15265                            1
15266                        };
15267                    }
15268                    return 1;
15269                }
15270                "-L" => {
15271                    // List in re-usable format
15272                    for (pattern, style, values) in self.style_table.list(None) {
15273                        let values_str = values
15274                            .iter()
15275                            .map(|v| format!("'{}'", v.replace('\'', "'\\''")))
15276                            .collect::<Vec<_>>()
15277                            .join(" ");
15278                        println!("zstyle '{}' {} {}", pattern, style, values_str);
15279                    }
15280                    return 0;
15281                }
15282                _ => {}
15283            }
15284        }
15285
15286        // Set style: zstyle pattern style values...
15287        if args.len() >= 2 {
15288            let pattern = &args[0];
15289            let style = &args[1];
15290            let values: Vec<String> = args[2..].to_vec();
15291            self.style_table.set(pattern, style, values.clone(), false);
15292
15293            // Write to SQLite cache for completion lookups
15294            if let Some(cache) = &self.compsys_cache {
15295                let _ = cache.set_zstyle(pattern, style, &values, false);
15296            }
15297
15298            // Also update legacy zstyles for backward compat
15299            let existing = self
15300                .zstyles
15301                .iter_mut()
15302                .find(|s| s.pattern == *pattern && s.style == *style);
15303            if let Some(s) = existing {
15304                s.values = args[2..].to_vec();
15305            } else {
15306                self.zstyles.push(ZStyle {
15307                    pattern: pattern.clone(),
15308                    style: style.clone(),
15309                    values: args[2..].to_vec(),
15310                });
15311            }
15312        }
15313        0
15314    }
15315
15316    /// Tie a parameter to a GDBM database
15317    /// Usage: ztie -d db/gdbm -f /path/to/db.gdbm [-r] PARAM_NAME
15318    fn builtin_ztie(&mut self, args: &[String]) -> i32 {
15319        use crate::db_gdbm;
15320
15321        let mut db_type: Option<String> = None;
15322        let mut file_path: Option<String> = None;
15323        let mut readonly = false;
15324        let mut param_args: Vec<String> = Vec::new();
15325
15326        let mut i = 0;
15327        while i < args.len() {
15328            match args[i].as_str() {
15329                "-d" => {
15330                    if i + 1 < args.len() {
15331                        db_type = Some(args[i + 1].clone());
15332                        i += 2;
15333                    } else {
15334                        eprintln!("ztie: -d requires an argument");
15335                        return 1;
15336                    }
15337                }
15338                "-f" => {
15339                    if i + 1 < args.len() {
15340                        file_path = Some(args[i + 1].clone());
15341                        i += 2;
15342                    } else {
15343                        eprintln!("ztie: -f requires an argument");
15344                        return 1;
15345                    }
15346                }
15347                "-r" => {
15348                    readonly = true;
15349                    i += 1;
15350                }
15351                arg if arg.starts_with('-') => {
15352                    eprintln!("ztie: bad option: {}", arg);
15353                    return 1;
15354                }
15355                _ => {
15356                    param_args.push(args[i].clone());
15357                    i += 1;
15358                }
15359            }
15360        }
15361
15362        match db_gdbm::ztie(
15363            &param_args,
15364            readonly,
15365            db_type.as_deref(),
15366            file_path.as_deref(),
15367        ) {
15368            Ok(()) => 0,
15369            Err(e) => {
15370                eprintln!("ztie: {}", e);
15371                1
15372            }
15373        }
15374    }
15375
15376    /// Untie a parameter from its GDBM database
15377    /// Usage: zuntie [-u] PARAM_NAME...
15378    fn builtin_zuntie(&mut self, args: &[String]) -> i32 {
15379        use crate::db_gdbm;
15380
15381        let mut force_unset = false;
15382        let mut param_args: Vec<String> = Vec::new();
15383
15384        for arg in args {
15385            match arg.as_str() {
15386                "-u" => force_unset = true,
15387                a if a.starts_with('-') => {
15388                    eprintln!("zuntie: bad option: {}", a);
15389                    return 1;
15390                }
15391                _ => param_args.push(arg.clone()),
15392            }
15393        }
15394
15395        if param_args.is_empty() {
15396            eprintln!("zuntie: not enough arguments");
15397            return 1;
15398        }
15399
15400        match db_gdbm::zuntie(&param_args, force_unset) {
15401            Ok(()) => 0,
15402            Err(e) => {
15403                eprintln!("zuntie: {}", e);
15404                1
15405            }
15406        }
15407    }
15408
15409    /// Get the path of a tied GDBM database
15410    /// Usage: zgdbmpath PARAM_NAME
15411    /// Sets $REPLY to the path
15412    fn builtin_zgdbmpath(&mut self, args: &[String]) -> i32 {
15413        use crate::db_gdbm;
15414
15415        if args.is_empty() {
15416            eprintln!(
15417                "zgdbmpath: parameter name (whose path is to be written to $REPLY) is required"
15418            );
15419            return 1;
15420        }
15421
15422        match db_gdbm::zgdbmpath(&args[0]) {
15423            Ok(path) => {
15424                self.variables.insert("REPLY".to_string(), path.clone());
15425                std::env::set_var("REPLY", &path);
15426                0
15427            }
15428            Err(e) => {
15429                eprintln!("zgdbmpath: {}", e);
15430                1
15431            }
15432        }
15433    }
15434
15435    /// Push directory onto stack and cd to it
15436    fn builtin_pushd(&mut self, args: &[String]) -> i32 {
15437        // pushd [ -qsLP ] [ arg ]
15438        // pushd [ -qsLP ] old new
15439        // pushd [ -qsLP ] {+|-}n
15440        // -q: quiet (don't print stack)
15441        // -s: no symlink resolution (use -L cd behavior)
15442        // -L: logical directory (resolve .. before symlinks)
15443        // -P: physical directory (resolve symlinks)
15444
15445        let mut quiet = false;
15446        let mut physical = false;
15447        let mut positional_args: Vec<String> = Vec::new();
15448
15449        for arg in args {
15450            if arg.starts_with('-') && arg.len() > 1 {
15451                // Check if it's a stack index
15452                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
15453                    positional_args.push(arg.clone());
15454                    continue;
15455                }
15456                for ch in arg[1..].chars() {
15457                    match ch {
15458                        'q' => quiet = true,
15459                        's' => physical = false,
15460                        'L' => physical = false,
15461                        'P' => physical = true,
15462                        _ => {}
15463                    }
15464                }
15465            } else if arg.starts_with('+') {
15466                positional_args.push(arg.clone());
15467            } else {
15468                positional_args.push(arg.clone());
15469            }
15470        }
15471
15472        let current = match std::env::current_dir() {
15473            Ok(p) => p,
15474            Err(e) => {
15475                eprintln!("pushd: {}", e);
15476                return 1;
15477            }
15478        };
15479
15480        if positional_args.is_empty() {
15481            // Swap top two directories
15482            if self.dir_stack.is_empty() {
15483                eprintln!("pushd: no other directory");
15484                return 1;
15485            }
15486            let target = self.dir_stack.pop().unwrap();
15487            self.dir_stack.push(current.clone());
15488
15489            let resolved = if physical {
15490                target.canonicalize().unwrap_or(target.clone())
15491            } else {
15492                target.clone()
15493            };
15494
15495            if let Err(e) = std::env::set_current_dir(&resolved) {
15496                eprintln!("pushd: {}: {}", target.display(), e);
15497                self.dir_stack.pop();
15498                self.dir_stack.push(target);
15499                return 1;
15500            }
15501            if !quiet {
15502                self.print_dir_stack();
15503            }
15504            return 0;
15505        }
15506
15507        let arg = &positional_args[0];
15508
15509        // Handle +N and -N for rotating the stack
15510        if arg.starts_with('+') || arg.starts_with('-') {
15511            if let Ok(n) = arg[1..].parse::<usize>() {
15512                let total = self.dir_stack.len() + 1;
15513                if n >= total {
15514                    eprintln!("pushd: {}: directory stack index out of range", arg);
15515                    return 1;
15516                }
15517                // Rotate stack
15518                let rotate_pos = if arg.starts_with('+') { n } else { total - n };
15519                let mut full_stack = vec![current.clone()];
15520                full_stack.extend(self.dir_stack.iter().cloned());
15521                full_stack.rotate_left(rotate_pos);
15522
15523                let target = full_stack.remove(0);
15524                self.dir_stack = full_stack;
15525
15526                let resolved = if physical {
15527                    target.canonicalize().unwrap_or(target.clone())
15528                } else {
15529                    target.clone()
15530                };
15531
15532                if let Err(e) = std::env::set_current_dir(&resolved) {
15533                    eprintln!("pushd: {}: {}", target.display(), e);
15534                    return 1;
15535                }
15536                if !quiet {
15537                    self.print_dir_stack();
15538                }
15539                return 0;
15540            }
15541        }
15542
15543        // Regular directory push
15544        let target = PathBuf::from(arg);
15545        let resolved = if physical {
15546            target.canonicalize().unwrap_or(target.clone())
15547        } else {
15548            target.clone()
15549        };
15550
15551        self.dir_stack.push(current);
15552        if let Err(e) = std::env::set_current_dir(&resolved) {
15553            eprintln!("pushd: {}: {}", arg, e);
15554            self.dir_stack.pop();
15555            return 1;
15556        }
15557        if !quiet {
15558            self.print_dir_stack();
15559        }
15560        0
15561    }
15562
15563    /// Pop directory from stack and cd to it
15564    fn builtin_popd(&mut self, args: &[String]) -> i32 {
15565        // popd [ -qsLP ] [ {+|-}n ]
15566        // -q: quiet (don't print stack)
15567        // -s: no symlink resolution
15568        // -L: logical directory
15569        // -P: physical directory
15570
15571        let mut quiet = false;
15572        let mut physical = false;
15573        let mut stack_index: Option<String> = None;
15574
15575        for arg in args {
15576            if arg.starts_with('-') && arg.len() > 1 {
15577                // Check if it's a stack index
15578                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
15579                    stack_index = Some(arg.clone());
15580                    continue;
15581                }
15582                for ch in arg[1..].chars() {
15583                    match ch {
15584                        'q' => quiet = true,
15585                        's' => physical = false,
15586                        'L' => physical = false,
15587                        'P' => physical = true,
15588                        _ => {}
15589                    }
15590                }
15591            } else if arg.starts_with('+') {
15592                stack_index = Some(arg.clone());
15593            }
15594        }
15595
15596        if self.dir_stack.is_empty() {
15597            eprintln!("popd: directory stack empty");
15598            return 1;
15599        }
15600
15601        // Handle +N and -N
15602        if let Some(arg) = stack_index {
15603            if arg.starts_with('+') || arg.starts_with('-') {
15604                if let Ok(n) = arg[1..].parse::<usize>() {
15605                    let total = self.dir_stack.len() + 1;
15606                    if n >= total {
15607                        eprintln!("popd: {}: directory stack index out of range", arg);
15608                        return 1;
15609                    }
15610                    let remove_pos = if arg.starts_with('+') {
15611                        n
15612                    } else {
15613                        total - 1 - n
15614                    };
15615                    if remove_pos == 0 {
15616                        // Remove current and cd to next
15617                        let target = self.dir_stack.remove(0);
15618                        let resolved = if physical {
15619                            target.canonicalize().unwrap_or(target.clone())
15620                        } else {
15621                            target.clone()
15622                        };
15623                        if let Err(e) = std::env::set_current_dir(&resolved) {
15624                            eprintln!("popd: {}: {}", target.display(), e);
15625                            return 1;
15626                        }
15627                    } else {
15628                        self.dir_stack.remove(remove_pos - 1);
15629                    }
15630                    if !quiet {
15631                        self.print_dir_stack();
15632                    }
15633                    return 0;
15634                }
15635            }
15636        }
15637
15638        let target = self.dir_stack.pop().unwrap();
15639        let resolved = if physical {
15640            target.canonicalize().unwrap_or(target.clone())
15641        } else {
15642            target.clone()
15643        };
15644        if let Err(e) = std::env::set_current_dir(&resolved) {
15645            eprintln!("popd: {}: {}", target.display(), e);
15646            self.dir_stack.push(target);
15647            return 1;
15648        }
15649        if !quiet {
15650            self.print_dir_stack();
15651        }
15652        0
15653    }
15654
15655    /// Display directory stack
15656    fn builtin_dirs(&mut self, args: &[String]) -> i32 {
15657        // dirs [ -c ] [ -l ] [ -p ] [ -v ] [ arg ... ]
15658        // -c: clear the directory stack
15659        // -l: full pathnames (don't use ~)
15660        // -p: print one entry per line
15661        // -v: verbose (numbered list)
15662
15663        let mut clear = false;
15664        let mut full_paths = false;
15665        let mut per_line = false;
15666        let mut verbose = false;
15667        let mut indices: Vec<i32> = Vec::new();
15668
15669        for arg in args {
15670            if arg.starts_with('-') && arg.len() > 1 {
15671                // Check if it's a negative index like -2
15672                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
15673                    if let Ok(n) = arg.parse::<i32>() {
15674                        indices.push(n);
15675                        continue;
15676                    }
15677                }
15678                for ch in arg[1..].chars() {
15679                    match ch {
15680                        'c' => clear = true,
15681                        'l' => full_paths = true,
15682                        'p' => per_line = true,
15683                        'v' => verbose = true,
15684                        _ => {}
15685                    }
15686                }
15687            } else if arg.starts_with('+') && arg.len() > 1 {
15688                if let Ok(n) = arg[1..].parse::<i32>() {
15689                    indices.push(n);
15690                }
15691            } else {
15692                // Could be a number
15693                if let Ok(n) = arg.parse::<i32>() {
15694                    indices.push(n);
15695                }
15696            }
15697        }
15698
15699        if clear {
15700            self.dir_stack.clear();
15701            return 0;
15702        }
15703
15704        let current = std::env::current_dir().unwrap_or_default();
15705        let home = dirs::home_dir().unwrap_or_default();
15706
15707        let format_path = |p: &std::path::Path| -> String {
15708            let path_str = p.to_string_lossy().to_string();
15709            if !full_paths {
15710                let home_str = home.to_string_lossy();
15711                if path_str.starts_with(home_str.as_ref()) {
15712                    return format!("~{}", &path_str[home_str.len()..]);
15713                }
15714            }
15715            path_str
15716        };
15717
15718        // If specific indices requested
15719        if !indices.is_empty() {
15720            let stack_len = self.dir_stack.len() + 1; // +1 for current dir
15721            for idx in indices {
15722                let actual_idx = if idx >= 0 {
15723                    idx as usize
15724                } else {
15725                    stack_len.saturating_sub((-idx) as usize)
15726                };
15727
15728                if actual_idx == 0 {
15729                    println!("{}", format_path(&current));
15730                } else if actual_idx <= self.dir_stack.len() {
15731                    // Stack is reversed, so index from end
15732                    let stack_idx = self.dir_stack.len() - actual_idx;
15733                    if let Some(dir) = self.dir_stack.get(stack_idx) {
15734                        println!("{}", format_path(dir));
15735                    }
15736                }
15737            }
15738            return 0;
15739        }
15740
15741        if verbose {
15742            println!(" 0  {}", format_path(&current));
15743            for (i, dir) in self.dir_stack.iter().rev().enumerate() {
15744                println!("{:2}  {}", i + 1, format_path(dir));
15745            }
15746        } else if per_line {
15747            println!("{}", format_path(&current));
15748            for dir in self.dir_stack.iter().rev() {
15749                println!("{}", format_path(dir));
15750            }
15751        } else {
15752            let mut parts = vec![format_path(&current)];
15753            for dir in self.dir_stack.iter().rev() {
15754                parts.push(format_path(dir));
15755            }
15756            println!("{}", parts.join(" "));
15757        }
15758        0
15759    }
15760
15761    fn print_dir_stack(&self) {
15762        let current = std::env::current_dir().unwrap_or_default();
15763        let mut parts = vec![current.to_string_lossy().to_string()];
15764        for dir in self.dir_stack.iter().rev() {
15765            parts.push(dir.to_string_lossy().to_string());
15766        }
15767        println!("{}", parts.join(" "));
15768    }
15769
15770    /// printf builtin - format and print data (zsh/bash compatible)
15771    fn builtin_printf(&self, args: &[String]) -> i32 {
15772        if args.is_empty() {
15773            eprintln!("printf: usage: printf format [arguments]");
15774            return 1;
15775        }
15776
15777        let format = &args[0];
15778        let format_args = &args[1..];
15779        let mut arg_idx = 0;
15780        let mut output = String::new();
15781        let mut chars = format.chars().peekable();
15782
15783        while let Some(c) = chars.next() {
15784            if c == '\\' {
15785                match chars.next() {
15786                    Some('n') => output.push('\n'),
15787                    Some('t') => output.push('\t'),
15788                    Some('r') => output.push('\r'),
15789                    Some('\\') => output.push('\\'),
15790                    Some('a') => output.push('\x07'),
15791                    Some('b') => output.push('\x08'),
15792                    Some('e') | Some('E') => output.push('\x1b'),
15793                    Some('f') => output.push('\x0c'),
15794                    Some('v') => output.push('\x0b'),
15795                    Some('"') => output.push('"'),
15796                    Some('\'') => output.push('\''),
15797                    Some('0') => {
15798                        let mut octal = String::new();
15799                        while octal.len() < 3 {
15800                            if let Some(&d) = chars.peek() {
15801                                if d >= '0' && d <= '7' {
15802                                    octal.push(d);
15803                                    chars.next();
15804                                } else {
15805                                    break;
15806                                }
15807                            } else {
15808                                break;
15809                            }
15810                        }
15811                        if octal.is_empty() {
15812                            output.push('\0');
15813                        } else if let Ok(val) = u8::from_str_radix(&octal, 8) {
15814                            output.push(val as char);
15815                        }
15816                    }
15817                    Some('x') => {
15818                        let mut hex = String::new();
15819                        while hex.len() < 2 {
15820                            if let Some(&d) = chars.peek() {
15821                                if d.is_ascii_hexdigit() {
15822                                    hex.push(d);
15823                                    chars.next();
15824                                } else {
15825                                    break;
15826                                }
15827                            } else {
15828                                break;
15829                            }
15830                        }
15831                        if !hex.is_empty() {
15832                            if let Ok(val) = u8::from_str_radix(&hex, 16) {
15833                                output.push(val as char);
15834                            }
15835                        }
15836                    }
15837                    Some('u') => {
15838                        let mut hex = String::new();
15839                        while hex.len() < 4 {
15840                            if let Some(&d) = chars.peek() {
15841                                if d.is_ascii_hexdigit() {
15842                                    hex.push(d);
15843                                    chars.next();
15844                                } else {
15845                                    break;
15846                                }
15847                            } else {
15848                                break;
15849                            }
15850                        }
15851                        if !hex.is_empty() {
15852                            if let Ok(val) = u32::from_str_radix(&hex, 16) {
15853                                if let Some(c) = char::from_u32(val) {
15854                                    output.push(c);
15855                                }
15856                            }
15857                        }
15858                    }
15859                    Some('U') => {
15860                        let mut hex = String::new();
15861                        while hex.len() < 8 {
15862                            if let Some(&d) = chars.peek() {
15863                                if d.is_ascii_hexdigit() {
15864                                    hex.push(d);
15865                                    chars.next();
15866                                } else {
15867                                    break;
15868                                }
15869                            } else {
15870                                break;
15871                            }
15872                        }
15873                        if !hex.is_empty() {
15874                            if let Ok(val) = u32::from_str_radix(&hex, 16) {
15875                                if let Some(c) = char::from_u32(val) {
15876                                    output.push(c);
15877                                }
15878                            }
15879                        }
15880                    }
15881                    Some('c') => {
15882                        print!("{}", output);
15883                        return 0;
15884                    }
15885                    Some(other) => {
15886                        output.push('\\');
15887                        output.push(other);
15888                    }
15889                    None => output.push('\\'),
15890                }
15891            } else if c == '%' {
15892                if chars.peek() == Some(&'%') {
15893                    chars.next();
15894                    output.push('%');
15895                    continue;
15896                }
15897
15898                let mut flags = String::new();
15899                while let Some(&f) = chars.peek() {
15900                    if f == '-' || f == '+' || f == ' ' || f == '#' || f == '0' {
15901                        flags.push(f);
15902                        chars.next();
15903                    } else {
15904                        break;
15905                    }
15906                }
15907
15908                let mut width = String::new();
15909                if chars.peek() == Some(&'*') {
15910                    chars.next();
15911                    if arg_idx < format_args.len() {
15912                        width = format_args[arg_idx].clone();
15913                        arg_idx += 1;
15914                    }
15915                } else {
15916                    while let Some(&d) = chars.peek() {
15917                        if d.is_ascii_digit() {
15918                            width.push(d);
15919                            chars.next();
15920                        } else {
15921                            break;
15922                        }
15923                    }
15924                }
15925
15926                let mut precision = String::new();
15927                if chars.peek() == Some(&'.') {
15928                    chars.next();
15929                    if chars.peek() == Some(&'*') {
15930                        chars.next();
15931                        if arg_idx < format_args.len() {
15932                            precision = format_args[arg_idx].clone();
15933                            arg_idx += 1;
15934                        }
15935                    } else {
15936                        while let Some(&d) = chars.peek() {
15937                            if d.is_ascii_digit() {
15938                                precision.push(d);
15939                                chars.next();
15940                            } else {
15941                                break;
15942                            }
15943                        }
15944                    }
15945                }
15946
15947                let specifier = chars.next().unwrap_or('s');
15948                let arg = if arg_idx < format_args.len() {
15949                    let a = &format_args[arg_idx];
15950                    arg_idx += 1;
15951                    a.clone()
15952                } else {
15953                    String::new()
15954                };
15955
15956                let width_val: usize = width.parse().unwrap_or(0);
15957                let prec_val: Option<usize> = if precision.is_empty() {
15958                    None
15959                } else {
15960                    precision.parse().ok()
15961                };
15962                let left_align = flags.contains('-');
15963                let zero_pad = flags.contains('0') && !left_align;
15964                let plus_sign = flags.contains('+');
15965                let space_sign = flags.contains(' ') && !plus_sign;
15966                let alt_form = flags.contains('#');
15967
15968                match specifier {
15969                    's' => {
15970                        let mut s = arg;
15971                        if let Some(p) = prec_val {
15972                            s = s.chars().take(p).collect();
15973                        }
15974                        if width_val > s.len() {
15975                            if left_align {
15976                                output.push_str(&s);
15977                                output.push_str(&" ".repeat(width_val - s.len()));
15978                            } else {
15979                                output.push_str(&" ".repeat(width_val - s.len()));
15980                                output.push_str(&s);
15981                            }
15982                        } else {
15983                            output.push_str(&s);
15984                        }
15985                    }
15986                    'b' => {
15987                        let expanded = self.expand_printf_escapes(&arg);
15988                        if let Some(p) = prec_val {
15989                            let s: String = expanded.chars().take(p).collect();
15990                            output.push_str(&s);
15991                        } else {
15992                            output.push_str(&expanded);
15993                        }
15994                    }
15995                    'c' => {
15996                        if let Some(ch) = arg.chars().next() {
15997                            output.push(ch);
15998                        }
15999                    }
16000                    'q' => {
16001                        output.push('\'');
16002                        for ch in arg.chars() {
16003                            if ch == '\'' {
16004                                output.push_str("'\\''");
16005                            } else {
16006                                output.push(ch);
16007                            }
16008                        }
16009                        output.push('\'');
16010                    }
16011                    'd' | 'i' => {
16012                        let val: i64 = if arg.starts_with("0x") || arg.starts_with("0X") {
16013                            i64::from_str_radix(&arg[2..], 16).unwrap_or(0)
16014                        } else if arg.starts_with("0") && arg.len() > 1 && !arg.contains('.') {
16015                            i64::from_str_radix(&arg[1..], 8).unwrap_or(0)
16016                        } else if arg.starts_with('\'') || arg.starts_with('"') {
16017                            arg.chars().nth(1).map(|c| c as i64).unwrap_or(0)
16018                        } else {
16019                            arg.parse().unwrap_or(0)
16020                        };
16021
16022                        let sign = if val < 0 {
16023                            "-"
16024                        } else if plus_sign {
16025                            "+"
16026                        } else if space_sign {
16027                            " "
16028                        } else {
16029                            ""
16030                        };
16031                        let abs_val = val.abs();
16032                        let num_str = abs_val.to_string();
16033                        let total_len = sign.len() + num_str.len();
16034
16035                        if width_val > total_len {
16036                            if left_align {
16037                                output.push_str(sign);
16038                                output.push_str(&num_str);
16039                                output.push_str(&" ".repeat(width_val - total_len));
16040                            } else if zero_pad {
16041                                output.push_str(sign);
16042                                output.push_str(&"0".repeat(width_val - total_len));
16043                                output.push_str(&num_str);
16044                            } else {
16045                                output.push_str(&" ".repeat(width_val - total_len));
16046                                output.push_str(sign);
16047                                output.push_str(&num_str);
16048                            }
16049                        } else {
16050                            output.push_str(sign);
16051                            output.push_str(&num_str);
16052                        }
16053                    }
16054                    'u' => {
16055                        let val: u64 = if arg.starts_with("0x") || arg.starts_with("0X") {
16056                            u64::from_str_radix(&arg[2..], 16).unwrap_or(0)
16057                        } else if arg.starts_with("0") && arg.len() > 1 {
16058                            u64::from_str_radix(&arg[1..], 8).unwrap_or(0)
16059                        } else {
16060                            arg.parse().unwrap_or(0)
16061                        };
16062                        let num_str = val.to_string();
16063                        if width_val > num_str.len() {
16064                            if left_align {
16065                                output.push_str(&num_str);
16066                                output.push_str(&" ".repeat(width_val - num_str.len()));
16067                            } else if zero_pad {
16068                                output.push_str(&"0".repeat(width_val - num_str.len()));
16069                                output.push_str(&num_str);
16070                            } else {
16071                                output.push_str(&" ".repeat(width_val - num_str.len()));
16072                                output.push_str(&num_str);
16073                            }
16074                        } else {
16075                            output.push_str(&num_str);
16076                        }
16077                    }
16078                    'o' => {
16079                        let val: u64 = arg.parse().unwrap_or(0);
16080                        let num_str = format!("{:o}", val);
16081                        let prefix = if alt_form && val != 0 { "0" } else { "" };
16082                        let total_len = prefix.len() + num_str.len();
16083                        if width_val > total_len {
16084                            if left_align {
16085                                output.push_str(prefix);
16086                                output.push_str(&num_str);
16087                                output.push_str(&" ".repeat(width_val - total_len));
16088                            } else {
16089                                output.push_str(&" ".repeat(width_val - total_len));
16090                                output.push_str(prefix);
16091                                output.push_str(&num_str);
16092                            }
16093                        } else {
16094                            output.push_str(prefix);
16095                            output.push_str(&num_str);
16096                        }
16097                    }
16098                    'x' => {
16099                        let val: u64 = arg.parse().unwrap_or(0);
16100                        let num_str = format!("{:x}", val);
16101                        let prefix = if alt_form && val != 0 { "0x" } else { "" };
16102                        let total_len = prefix.len() + num_str.len();
16103                        if width_val > total_len {
16104                            if left_align {
16105                                output.push_str(prefix);
16106                                output.push_str(&num_str);
16107                                output.push_str(&" ".repeat(width_val - total_len));
16108                            } else {
16109                                output.push_str(&" ".repeat(width_val - total_len));
16110                                output.push_str(prefix);
16111                                output.push_str(&num_str);
16112                            }
16113                        } else {
16114                            output.push_str(prefix);
16115                            output.push_str(&num_str);
16116                        }
16117                    }
16118                    'X' => {
16119                        let val: u64 = arg.parse().unwrap_or(0);
16120                        let num_str = format!("{:X}", val);
16121                        let prefix = if alt_form && val != 0 { "0X" } else { "" };
16122                        let total_len = prefix.len() + num_str.len();
16123                        if width_val > total_len {
16124                            if left_align {
16125                                output.push_str(prefix);
16126                                output.push_str(&num_str);
16127                                output.push_str(&" ".repeat(width_val - total_len));
16128                            } else {
16129                                output.push_str(&" ".repeat(width_val - total_len));
16130                                output.push_str(prefix);
16131                                output.push_str(&num_str);
16132                            }
16133                        } else {
16134                            output.push_str(prefix);
16135                            output.push_str(&num_str);
16136                        }
16137                    }
16138                    'e' | 'E' => {
16139                        let val: f64 = arg.parse().unwrap_or(0.0);
16140                        let prec = prec_val.unwrap_or(6);
16141                        let formatted = if specifier == 'e' {
16142                            format!("{:.prec$e}", val, prec = prec)
16143                        } else {
16144                            format!("{:.prec$E}", val, prec = prec)
16145                        };
16146                        if width_val > formatted.len() {
16147                            if left_align {
16148                                output.push_str(&formatted);
16149                                output.push_str(&" ".repeat(width_val - formatted.len()));
16150                            } else {
16151                                output.push_str(&" ".repeat(width_val - formatted.len()));
16152                                output.push_str(&formatted);
16153                            }
16154                        } else {
16155                            output.push_str(&formatted);
16156                        }
16157                    }
16158                    'f' | 'F' => {
16159                        let val: f64 = arg.parse().unwrap_or(0.0);
16160                        let prec = prec_val.unwrap_or(6);
16161                        let sign = if val < 0.0 {
16162                            "-"
16163                        } else if plus_sign {
16164                            "+"
16165                        } else if space_sign {
16166                            " "
16167                        } else {
16168                            ""
16169                        };
16170                        let formatted = format!("{:.prec$}", val.abs(), prec = prec);
16171                        let total = sign.len() + formatted.len();
16172                        if width_val > total {
16173                            if left_align {
16174                                output.push_str(sign);
16175                                output.push_str(&formatted);
16176                                output.push_str(&" ".repeat(width_val - total));
16177                            } else if zero_pad {
16178                                output.push_str(sign);
16179                                output.push_str(&"0".repeat(width_val - total));
16180                                output.push_str(&formatted);
16181                            } else {
16182                                output.push_str(&" ".repeat(width_val - total));
16183                                output.push_str(sign);
16184                                output.push_str(&formatted);
16185                            }
16186                        } else {
16187                            output.push_str(sign);
16188                            output.push_str(&formatted);
16189                        }
16190                    }
16191                    'g' | 'G' => {
16192                        let val: f64 = arg.parse().unwrap_or(0.0);
16193                        let prec = prec_val.unwrap_or(6).max(1);
16194                        let formatted = if specifier == 'g' {
16195                            format!("{:.prec$}", val, prec = prec)
16196                        } else {
16197                            format!("{:.prec$}", val, prec = prec).to_uppercase()
16198                        };
16199                        output.push_str(&formatted);
16200                    }
16201                    'a' | 'A' => {
16202                        let val: f64 = arg.parse().unwrap_or(0.0);
16203                        let formatted = float_to_hex(val, specifier == 'A');
16204                        output.push_str(&formatted);
16205                    }
16206                    _ => {
16207                        output.push('%');
16208                        output.push(specifier);
16209                    }
16210                }
16211            } else {
16212                output.push(c);
16213            }
16214        }
16215
16216        print!("{}", output);
16217        0
16218    }
16219
16220    fn expand_printf_escapes(&self, s: &str) -> String {
16221        let mut result = String::new();
16222        let mut chars = s.chars().peekable();
16223        while let Some(c) = chars.next() {
16224            if c == '\\' {
16225                match chars.next() {
16226                    Some('n') => result.push('\n'),
16227                    Some('t') => result.push('\t'),
16228                    Some('r') => result.push('\r'),
16229                    Some('\\') => result.push('\\'),
16230                    Some('a') => result.push('\x07'),
16231                    Some('b') => result.push('\x08'),
16232                    Some('e') | Some('E') => result.push('\x1b'),
16233                    Some('f') => result.push('\x0c'),
16234                    Some('v') => result.push('\x0b'),
16235                    Some('0') => {
16236                        let mut octal = String::new();
16237                        while octal.len() < 3 {
16238                            if let Some(&d) = chars.peek() {
16239                                if d >= '0' && d <= '7' {
16240                                    octal.push(d);
16241                                    chars.next();
16242                                } else {
16243                                    break;
16244                                }
16245                            } else {
16246                                break;
16247                            }
16248                        }
16249                        if octal.is_empty() {
16250                            result.push('\0');
16251                        } else if let Ok(val) = u8::from_str_radix(&octal, 8) {
16252                            result.push(val as char);
16253                        }
16254                    }
16255                    Some('c') => break,
16256                    Some(other) => {
16257                        result.push('\\');
16258                        result.push(other);
16259                    }
16260                    None => result.push('\\'),
16261                }
16262            } else {
16263                result.push(c);
16264            }
16265        }
16266        result
16267    }
16268
16269    fn evaluate_arithmetic_expr(&mut self, expr: &str) -> i64 {
16270        self.eval_arith_expr(expr)
16271    }
16272
16273    // ═══════════════════════════════════════════════════════════════════════════
16274    // Additional zsh builtins
16275    // ═══════════════════════════════════════════════════════════════════════════
16276
16277    /// break - exit from for/while/until loop
16278    fn builtin_break(&mut self, args: &[String]) -> i32 {
16279        let levels: i32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
16280        self.breaking = levels.max(1);
16281        0
16282    }
16283
16284    /// continue - skip to next iteration of loop
16285    fn builtin_continue(&mut self, args: &[String]) -> i32 {
16286        let levels: i32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
16287        self.continuing = levels.max(1);
16288        0
16289    }
16290
16291    /// disable - disable shell builtins, aliases, functions
16292    fn builtin_disable(&mut self, args: &[String]) -> i32 {
16293        let mut disable_aliases = false;
16294        let mut disable_builtins = false;
16295        let mut disable_functions = false;
16296        let mut names = Vec::new();
16297
16298        let mut iter = args.iter();
16299        while let Some(arg) = iter.next() {
16300            match arg.as_str() {
16301                "-a" => disable_aliases = true,
16302                "-f" => disable_functions = true,
16303                "-r" => disable_builtins = true,
16304                _ if arg.starts_with('-') => {}
16305                _ => names.push(arg.clone()),
16306            }
16307        }
16308
16309        // Default to builtins if no flags
16310        if !disable_aliases && !disable_functions {
16311            disable_builtins = true;
16312        }
16313
16314        for name in names {
16315            if disable_aliases {
16316                self.aliases.remove(&name);
16317            }
16318            if disable_functions {
16319                self.functions.remove(&name);
16320            }
16321            if disable_builtins {
16322                // Store disabled builtins
16323                self.options.insert(format!("_disabled_{}", name), true);
16324            }
16325        }
16326        0
16327    }
16328
16329    /// enable - enable disabled shell builtins
16330    fn builtin_enable(&mut self, args: &[String]) -> i32 {
16331        for arg in args {
16332            if !arg.starts_with('-') {
16333                self.options.remove(&format!("_disabled_{}", arg));
16334            }
16335        }
16336        0
16337    }
16338
16339    /// emulate - set up zsh emulation mode
16340    fn builtin_emulate(&mut self, args: &[String]) -> i32 {
16341        // emulate [ -lLR ] [ {zsh|sh|ksh|csh} [ flags ... ] ]
16342        // flags can include: -c arg, -o opt, +o opt
16343        let mut local_mode = false;
16344        let mut reset_mode = false;
16345        let mut list_mode = false;
16346        let mut mode: Option<String> = None;
16347        let mut command_arg: Option<String> = None;
16348        let mut extra_set_opts: Vec<String> = Vec::new();
16349        let mut extra_unset_opts: Vec<String> = Vec::new();
16350
16351        let mut i = 0;
16352        while i < args.len() {
16353            let arg = &args[i];
16354
16355            if arg == "-c" {
16356                // -c arg: evaluate arg in emulation mode
16357                i += 1;
16358                if i < args.len() {
16359                    command_arg = Some(args[i].clone());
16360                } else {
16361                    eprintln!("emulate: -c requires an argument");
16362                    return 1;
16363                }
16364            } else if arg == "-o" {
16365                // -o opt: set option
16366                i += 1;
16367                if i < args.len() {
16368                    extra_set_opts.push(args[i].clone());
16369                } else {
16370                    eprintln!("emulate: -o requires an argument");
16371                    return 1;
16372                }
16373            } else if arg == "+o" {
16374                // +o opt: unset option
16375                i += 1;
16376                if i < args.len() {
16377                    extra_unset_opts.push(args[i].clone());
16378                } else {
16379                    eprintln!("emulate: +o requires an argument");
16380                    return 1;
16381                }
16382            } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
16383                // Parse combined flags like -LR
16384                for ch in arg[1..].chars() {
16385                    match ch {
16386                        'L' => local_mode = true,
16387                        'R' => reset_mode = true,
16388                        'l' => list_mode = true,
16389                        _ => {
16390                            eprintln!("emulate: bad option: -{}", ch);
16391                            return 1;
16392                        }
16393                    }
16394                }
16395            } else if arg.starts_with('+') && arg.len() > 1 {
16396                // +X flags (unset single-letter options)
16397                for ch in arg[1..].chars() {
16398                    // Map single-letter to option name if needed
16399                    extra_unset_opts.push(ch.to_string());
16400                }
16401            } else if mode.is_none() {
16402                mode = Some(arg.clone());
16403            }
16404            i += 1;
16405        }
16406
16407        // -L and -c are mutually exclusive
16408        if local_mode && command_arg.is_some() {
16409            eprintln!("emulate: -L and -c are mutually exclusive");
16410            return 1;
16411        }
16412
16413        // No argument: print current emulation mode
16414        if mode.is_none() && !list_mode {
16415            let current = self
16416                .variables
16417                .get("EMULATE")
16418                .cloned()
16419                .unwrap_or_else(|| "zsh".to_string());
16420            println!("{}", current);
16421            return 0;
16422        }
16423
16424        let mode = mode.unwrap_or_else(|| "zsh".to_string());
16425
16426        // Get the options that would be set for this mode
16427        let (set_opts, unset_opts) = Self::emulate_mode_options(&mode, reset_mode);
16428
16429        // -l: just list the options, don't apply
16430        if list_mode {
16431            for opt in &set_opts {
16432                println!("{}", opt);
16433            }
16434            for opt in &unset_opts {
16435                println!("no{}", opt);
16436            }
16437            if local_mode {
16438                println!("localoptions");
16439                println!("localpatterns");
16440                println!("localtraps");
16441            }
16442            return 0;
16443        }
16444
16445        // Save current state if -c is used
16446        let saved_options = if command_arg.is_some() {
16447            Some(self.options.clone())
16448        } else {
16449            None
16450        };
16451        let saved_emulate = if command_arg.is_some() {
16452            self.variables.get("EMULATE").cloned()
16453        } else {
16454            None
16455        };
16456
16457        // Apply the emulation
16458        self.variables.insert("EMULATE".to_string(), mode.clone());
16459
16460        // Set options for this mode
16461        for opt in &set_opts {
16462            let opt_name = opt.to_lowercase().replace('_', "");
16463            self.options.insert(opt_name, true);
16464        }
16465        for opt in &unset_opts {
16466            let opt_name = opt.to_lowercase().replace('_', "");
16467            self.options.insert(opt_name, false);
16468        }
16469
16470        // Apply extra -o / +o options
16471        for opt in &extra_set_opts {
16472            let opt_name = opt.to_lowercase().replace('_', "");
16473            self.options.insert(opt_name, true);
16474        }
16475        for opt in &extra_unset_opts {
16476            let opt_name = opt.to_lowercase().replace('_', "");
16477            self.options.insert(opt_name, false);
16478        }
16479
16480        // -L: set local options/traps
16481        if local_mode {
16482            self.options.insert("localoptions".to_string(), true);
16483            self.options.insert("localpatterns".to_string(), true);
16484            self.options.insert("localtraps".to_string(), true);
16485        }
16486
16487        // -c arg: execute command then restore
16488        let result = if let Some(cmd) = command_arg {
16489            let status = self.execute_script(&cmd).unwrap_or(1);
16490
16491            // Restore saved state
16492            if let Some(opts) = saved_options {
16493                self.options = opts;
16494            }
16495            if let Some(emu) = saved_emulate {
16496                self.variables.insert("EMULATE".to_string(), emu);
16497            } else {
16498                self.variables.remove("EMULATE");
16499            }
16500
16501            status
16502        } else {
16503            0
16504        };
16505
16506        result
16507    }
16508
16509    /// Get options to set/unset for an emulation mode
16510    fn emulate_mode_options(mode: &str, reset: bool) -> (Vec<&'static str>, Vec<&'static str>) {
16511        match mode {
16512            "zsh" => {
16513                if reset {
16514                    // Full reset: return to zsh defaults
16515                    (
16516                        vec![
16517                            "aliases",
16518                            "alwayslastprompt",
16519                            "autolist",
16520                            "automenu",
16521                            "autoparamslash",
16522                            "autoremoveslash",
16523                            "banghist",
16524                            "bareglobqual",
16525                            "completeinword",
16526                            "extendedhistory",
16527                            "functionargzero",
16528                            "glob",
16529                            "hashcmds",
16530                            "hashdirs",
16531                            "histexpand",
16532                            "histignoredups",
16533                            "interactivecomments",
16534                            "listambiguous",
16535                            "listtypes",
16536                            "multios",
16537                            "nomatch",
16538                            "notify",
16539                            "promptpercent",
16540                            "promptsubst",
16541                        ],
16542                        vec![
16543                            "ksharrays",
16544                            "kshglob",
16545                            "shwordsplit",
16546                            "shglob",
16547                            "posixbuiltins",
16548                            "posixidentifiers",
16549                            "posixstrings",
16550                            "bsdecho",
16551                            "ignorebraces",
16552                        ],
16553                    )
16554                } else {
16555                    // Minimal changes for portability
16556                    (vec!["functionargzero"], vec!["ksharrays", "shwordsplit"])
16557                }
16558            }
16559            "sh" => {
16560                let set = vec![
16561                    "ksharrays",
16562                    "shwordsplit",
16563                    "posixbuiltins",
16564                    "shglob",
16565                    "shfileexpansion",
16566                    "globsubst",
16567                    "interactivecomments",
16568                    "rmstarsilent",
16569                    "bsdecho",
16570                    "ignorebraces",
16571                ];
16572                let unset = vec![
16573                    "badpattern",
16574                    "banghist",
16575                    "bgnice",
16576                    "equals",
16577                    "functionargzero",
16578                    "globalexport",
16579                    "multios",
16580                    "nomatch",
16581                    "notify",
16582                    "promptpercent",
16583                ];
16584                (set, unset)
16585            }
16586            "ksh" => {
16587                let set = vec![
16588                    "ksharrays",
16589                    "kshglob",
16590                    "shwordsplit",
16591                    "posixbuiltins",
16592                    "kshoptionprint",
16593                    "localoptions",
16594                    "promptbang",
16595                    "promptsubst",
16596                    "singlelinezle",
16597                    "interactivecomments",
16598                ];
16599                let unset = vec![
16600                    "badpattern",
16601                    "banghist",
16602                    "bgnice",
16603                    "equals",
16604                    "functionargzero",
16605                    "globalexport",
16606                    "multios",
16607                    "nomatch",
16608                    "notify",
16609                    "promptpercent",
16610                ];
16611                (set, unset)
16612            }
16613            "csh" => {
16614                // C shell emulation (limited)
16615                (vec!["cshnullglob", "cshjunkiequotes"], vec!["nomatch"])
16616            }
16617            "bash" => {
16618                let set = vec![
16619                    "ksharrays",
16620                    "shwordsplit",
16621                    "interactivecomments",
16622                    "shfileexpansion",
16623                    "globsubst",
16624                ];
16625                let unset = vec![
16626                    "badpattern",
16627                    "banghist",
16628                    "functionargzero",
16629                    "multios",
16630                    "nomatch",
16631                    "notify",
16632                    "promptpercent",
16633                ];
16634                (set, unset)
16635            }
16636            _ => (vec![], vec![]),
16637        }
16638    }
16639
16640    /// exec - replace the shell with a command
16641    fn builtin_exec(&mut self, args: &[String]) -> i32 {
16642        // exec [ -c ] [ -l ] [ -a argv0 ] [ command [ arg ... ] ]
16643        // -c: clear environment
16644        // -l: place - at front of argv[0] (login shell)
16645        // -a argv0: set argv[0] to specified name
16646
16647        let mut clear_env = false;
16648        let mut login_shell = false;
16649        let mut argv0: Option<String> = None;
16650        let mut cmd_args: Vec<String> = Vec::new();
16651
16652        let mut i = 0;
16653        while i < args.len() {
16654            let arg = &args[i];
16655
16656            if arg == "-c" && cmd_args.is_empty() {
16657                clear_env = true;
16658            } else if arg == "-l" && cmd_args.is_empty() {
16659                login_shell = true;
16660            } else if arg == "-a" && cmd_args.is_empty() {
16661                i += 1;
16662                if i < args.len() {
16663                    argv0 = Some(args[i].clone());
16664                }
16665            } else if arg.starts_with('-') && cmd_args.is_empty() {
16666                // Combined flags like -cl
16667                for ch in arg[1..].chars() {
16668                    match ch {
16669                        'c' => clear_env = true,
16670                        'l' => login_shell = true,
16671                        'a' => {
16672                            i += 1;
16673                            if i < args.len() {
16674                                argv0 = Some(args[i].clone());
16675                            }
16676                        }
16677                        _ => {}
16678                    }
16679                }
16680            } else {
16681                cmd_args.push(arg.clone());
16682            }
16683            i += 1;
16684        }
16685
16686        if cmd_args.is_empty() {
16687            // No command: just modify shell's environment
16688            if clear_env {
16689                for (key, _) in env::vars() {
16690                    env::remove_var(&key);
16691                }
16692            }
16693            return 0;
16694        }
16695
16696        let cmd = &cmd_args[0];
16697        let rest_args: Vec<&str> = cmd_args[1..].iter().map(|s| s.as_str()).collect();
16698
16699        // Determine argv[0]
16700        let effective_argv0 = if let Some(a0) = argv0 {
16701            a0
16702        } else if login_shell {
16703            format!("-{}", cmd)
16704        } else {
16705            cmd.clone()
16706        };
16707
16708        use std::os::unix::process::CommandExt;
16709        let mut command = std::process::Command::new(cmd);
16710        command.arg0(&effective_argv0);
16711        command.args(&rest_args);
16712
16713        if clear_env {
16714            command.env_clear();
16715        }
16716
16717        let err = command.exec();
16718        eprintln!("exec: {}: {}", cmd, err);
16719        1
16720    }
16721
16722    /// float - declare floating point variables
16723    fn builtin_float(&mut self, args: &[String]) -> i32 {
16724        for arg in args {
16725            if arg.starts_with('-') {
16726                continue;
16727            }
16728            if let Some(eq_pos) = arg.find('=') {
16729                let name = &arg[..eq_pos];
16730                let value = &arg[eq_pos + 1..];
16731                let float_val: f64 = value.parse().unwrap_or(0.0);
16732                self.variables
16733                    .insert(name.to_string(), float_val.to_string());
16734                self.options.insert(format!("_float_{}", name), true);
16735            } else {
16736                self.variables.insert(arg.clone(), "0.0".to_string());
16737                self.options.insert(format!("_float_{}", arg), true);
16738            }
16739        }
16740        0
16741    }
16742
16743    /// integer - declare integer variables
16744    fn builtin_integer(&mut self, args: &[String]) -> i32 {
16745        for arg in args {
16746            if arg.starts_with('-') {
16747                continue;
16748            }
16749            if let Some(eq_pos) = arg.find('=') {
16750                let name = &arg[..eq_pos];
16751                let value = &arg[eq_pos + 1..];
16752                let int_val: i64 = value.parse().unwrap_or(0);
16753                self.variables.insert(name.to_string(), int_val.to_string());
16754                self.options.insert(format!("_integer_{}", name), true);
16755            } else {
16756                self.variables.insert(arg.clone(), "0".to_string());
16757                self.options.insert(format!("_integer_{}", arg), true);
16758            }
16759        }
16760        0
16761    }
16762
16763    /// functions - list or manipulate function definitions
16764    fn builtin_functions(&self, args: &[String]) -> i32 {
16765        let mut list_only = false;
16766        let mut show_trace = false;
16767        let mut names: Vec<&str> = Vec::new();
16768
16769        for arg in args {
16770            match arg.as_str() {
16771                "-l" => list_only = true,
16772                "-t" => show_trace = true,
16773                _ if arg.starts_with('-') => {}
16774                _ => names.push(arg),
16775            }
16776        }
16777
16778        if names.is_empty() {
16779            // List all functions
16780            let mut func_names: Vec<_> = self.functions.keys().collect();
16781            func_names.sort();
16782            for name in func_names {
16783                if list_only {
16784                    println!("{}", name);
16785                } else if let Some(func) = self.functions.get(name) {
16786                    let body = crate::text::getpermtext(func);
16787                    println!("{} () {{\n\t{}\n}}", name, body.trim());
16788                }
16789            }
16790        } else {
16791            // Show specific functions
16792            for name in names {
16793                if let Some(func) = self.functions.get(name) {
16794                    if show_trace {
16795                        println!("functions -t {}", name);
16796                    } else {
16797                        let body = crate::text::getpermtext(func);
16798                        println!("{} () {{\n\t{}\n}}", name, body.trim());
16799                    }
16800                } else {
16801                    eprintln!("functions: no such function: {}", name);
16802                    return 1;
16803                }
16804            }
16805        }
16806        0
16807    }
16808
16809    /// print - zsh print builtin with many options
16810    fn builtin_print(&mut self, args: &[String]) -> i32 {
16811        // print [ -abcDilmnNoOpPrsSz ] [ -u n ] [ -f format ] [ -C cols ]
16812        //       [ -v name ] [ -xX tabstop ] [ -R [ -en ]] [ arg ... ]
16813        let mut no_newline = false;
16814        let mut one_per_line = false;
16815        let mut interpret_escapes = true; // zsh default is to interpret
16816        let mut raw_mode = false;
16817        let mut prompt_expand = false;
16818        let mut fd: i32 = 1; // stdout
16819        let mut columns = 0usize;
16820        let mut null_terminate = false;
16821        let mut push_to_stack = false;
16822        let mut add_to_history = false;
16823        let mut sort_asc = false;
16824        let mut sort_desc = false;
16825        let mut named_dir_subst = false;
16826        let mut store_var: Option<String> = None;
16827        let mut format_string: Option<String> = None;
16828        let mut output_args: Vec<String> = Vec::new();
16829
16830        let mut i = 0;
16831        while i < args.len() {
16832            let arg = &args[i];
16833
16834            if arg == "--" {
16835                i += 1;
16836                while i < args.len() {
16837                    output_args.push(args[i].clone());
16838                    i += 1;
16839                }
16840                break;
16841            }
16842
16843            if arg.starts_with('-')
16844                && arg.len() > 1
16845                && !arg
16846                    .chars()
16847                    .nth(1)
16848                    .map(|c| c.is_ascii_digit())
16849                    .unwrap_or(false)
16850            {
16851                let mut chars = arg[1..].chars().peekable();
16852                while let Some(ch) = chars.next() {
16853                    match ch {
16854                        'n' => no_newline = true,
16855                        'l' => one_per_line = true,
16856                        'r' => {
16857                            raw_mode = true;
16858                            interpret_escapes = false;
16859                        }
16860                        'R' => {
16861                            raw_mode = true;
16862                            interpret_escapes = false;
16863                        }
16864                        'e' => interpret_escapes = true,
16865                        'E' => interpret_escapes = false,
16866                        'P' => prompt_expand = true,
16867                        'N' => null_terminate = true,
16868                        'z' => push_to_stack = true,
16869                        's' => add_to_history = true,
16870                        'o' => sort_asc = true,
16871                        'O' => sort_desc = true,
16872                        'D' => named_dir_subst = true,
16873                        'c' => columns = 1,
16874                        'a' | 'b' | 'i' | 'm' | 'p' | 'S' | 'x' | 'X' => {} // TODO
16875                        'u' => {
16876                            // -u n: output to fd n
16877                            let rest: String = chars.collect();
16878                            if !rest.is_empty() {
16879                                fd = rest.parse().unwrap_or(1);
16880                            } else {
16881                                i += 1;
16882                                if i < args.len() {
16883                                    fd = args[i].parse().unwrap_or(1);
16884                                }
16885                            }
16886                            break;
16887                        }
16888                        'C' => {
16889                            // -C n: n columns
16890                            let rest: String = chars.collect();
16891                            if !rest.is_empty() {
16892                                columns = rest.parse().unwrap_or(0);
16893                            } else {
16894                                i += 1;
16895                                if i < args.len() {
16896                                    columns = args[i].parse().unwrap_or(0);
16897                                }
16898                            }
16899                            break;
16900                        }
16901                        'v' => {
16902                            // -v name: store in variable
16903                            let rest: String = chars.collect();
16904                            if !rest.is_empty() {
16905                                store_var = Some(rest);
16906                            } else {
16907                                i += 1;
16908                                if i < args.len() {
16909                                    store_var = Some(args[i].clone());
16910                                }
16911                            }
16912                            break;
16913                        }
16914                        'f' => {
16915                            // -f format: printf-style format
16916                            let rest: String = chars.collect();
16917                            if !rest.is_empty() {
16918                                format_string = Some(rest);
16919                            } else {
16920                                i += 1;
16921                                if i < args.len() {
16922                                    format_string = Some(args[i].clone());
16923                                }
16924                            }
16925                            break;
16926                        }
16927                        _ => {}
16928                    }
16929                }
16930            } else {
16931                output_args.push(arg.clone());
16932            }
16933            i += 1;
16934        }
16935
16936        let _ = push_to_stack; // TODO: implement push to buffer stack
16937        let _ = fd; // TODO: implement fd selection
16938
16939        // Sort if requested
16940        if sort_asc {
16941            output_args.sort();
16942        } else if sort_desc {
16943            output_args.sort_by(|a, b| b.cmp(a));
16944        }
16945
16946        // Handle -f format
16947        if let Some(fmt) = format_string {
16948            let output = self.printf_format(&fmt, &output_args);
16949            if let Some(var) = store_var {
16950                self.variables.insert(var, output);
16951            } else {
16952                print!("{}", output);
16953            }
16954            return 0;
16955        }
16956
16957        // Process output
16958        let processed: Vec<String> = output_args
16959            .iter()
16960            .map(|s| {
16961                let mut result = s.clone();
16962                if prompt_expand {
16963                    result = self.expand_prompt_string(&result);
16964                }
16965                if interpret_escapes && !raw_mode {
16966                    result = self.expand_printf_escapes(&result);
16967                }
16968                if named_dir_subst {
16969                    // Replace home dir with ~
16970                    if let Ok(home) = env::var("HOME") {
16971                        if result.starts_with(&home) {
16972                            result = format!("~{}", &result[home.len()..]);
16973                        }
16974                    }
16975                    // Replace named dirs
16976                    for (name, path) in &self.named_dirs {
16977                        let path_str = path.to_string_lossy();
16978                        if result.starts_with(path_str.as_ref()) {
16979                            result = format!("~{}{}", name, &result[path_str.len()..]);
16980                            break;
16981                        }
16982                    }
16983                }
16984                result
16985            })
16986            .collect();
16987
16988        // Determine separator and terminator
16989        let separator = if one_per_line { "\n" } else { " " };
16990        let terminator = if null_terminate {
16991            "\0"
16992        } else if no_newline {
16993            ""
16994        } else {
16995            "\n"
16996        };
16997
16998        // Build output
16999        let output = if one_per_line {
17000            processed.join("\n")
17001        } else if columns > 0 {
17002            // Column output - calculate column widths
17003            let mut result = String::new();
17004            let num_items = processed.len();
17005            let rows = (num_items + columns - 1) / columns;
17006            for row in 0..rows {
17007                let mut row_items = Vec::new();
17008                for col in 0..columns {
17009                    let idx = row + col * rows;
17010                    if idx < num_items {
17011                        row_items.push(processed[idx].as_str());
17012                    }
17013                }
17014                result.push_str(&row_items.join("\t"));
17015                if row < rows - 1 {
17016                    result.push('\n');
17017                }
17018            }
17019            result
17020        } else {
17021            processed.join(separator)
17022        };
17023
17024        // Add to history if -s
17025        if add_to_history {
17026            if let Some(ref mut engine) = self.history {
17027                let _ = engine.add(&output, None);
17028            }
17029        }
17030
17031        // Store in variable or print
17032        if let Some(var) = store_var {
17033            self.variables.insert(var, output);
17034        } else {
17035            print!("{}{}", output, terminator);
17036        }
17037
17038        0
17039    }
17040
17041    fn printf_format(&self, format: &str, args: &[String]) -> String {
17042        let mut result = String::new();
17043        let mut arg_idx = 0;
17044        let mut chars = format.chars().peekable();
17045
17046        while let Some(ch) = chars.next() {
17047            if ch == '%' {
17048                if chars.peek() == Some(&'%') {
17049                    chars.next();
17050                    result.push('%');
17051                    continue;
17052                }
17053
17054                // Parse format specifier
17055                let mut spec = String::from("%");
17056
17057                // Flags
17058                while let Some(&c) = chars.peek() {
17059                    if c == '-' || c == '+' || c == ' ' || c == '#' || c == '0' {
17060                        spec.push(c);
17061                        chars.next();
17062                    } else {
17063                        break;
17064                    }
17065                }
17066
17067                // Width
17068                while let Some(&c) = chars.peek() {
17069                    if c.is_ascii_digit() {
17070                        spec.push(c);
17071                        chars.next();
17072                    } else {
17073                        break;
17074                    }
17075                }
17076
17077                // Precision
17078                if chars.peek() == Some(&'.') {
17079                    spec.push('.');
17080                    chars.next();
17081                    while let Some(&c) = chars.peek() {
17082                        if c.is_ascii_digit() {
17083                            spec.push(c);
17084                            chars.next();
17085                        } else {
17086                            break;
17087                        }
17088                    }
17089                }
17090
17091                // Conversion specifier
17092                if let Some(conv) = chars.next() {
17093                    let arg = args.get(arg_idx).map(|s| s.as_str()).unwrap_or("");
17094                    arg_idx += 1;
17095
17096                    match conv {
17097                        's' => result.push_str(arg),
17098                        'd' | 'i' => {
17099                            let n: i64 = arg.parse().unwrap_or(0);
17100                            result.push_str(&n.to_string());
17101                        }
17102                        'u' => {
17103                            let n: u64 = arg.parse().unwrap_or(0);
17104                            result.push_str(&n.to_string());
17105                        }
17106                        'x' => {
17107                            let n: i64 = arg.parse().unwrap_or(0);
17108                            result.push_str(&format!("{:x}", n));
17109                        }
17110                        'X' => {
17111                            let n: i64 = arg.parse().unwrap_or(0);
17112                            result.push_str(&format!("{:X}", n));
17113                        }
17114                        'o' => {
17115                            let n: i64 = arg.parse().unwrap_or(0);
17116                            result.push_str(&format!("{:o}", n));
17117                        }
17118                        'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
17119                            let n: f64 = arg.parse().unwrap_or(0.0);
17120                            result.push_str(&format!("{}", n));
17121                        }
17122                        'c' => {
17123                            if let Some(c) = arg.chars().next() {
17124                                result.push(c);
17125                            }
17126                        }
17127                        'b' => {
17128                            result.push_str(&self.expand_printf_escapes(arg));
17129                        }
17130                        'n' => result.push('\n'),
17131                        _ => {
17132                            result.push('%');
17133                            result.push(conv);
17134                        }
17135                    }
17136                }
17137            } else {
17138                result.push(ch);
17139            }
17140        }
17141
17142        result
17143    }
17144
17145    /// whence - show how a command would be interpreted
17146    fn builtin_whence(&self, args: &[String]) -> i32 {
17147        // whence [ -vcwfpamsS ] [ -x num ] name ...
17148        // -v: verbose (like type)
17149        // -c: csh-style output
17150        // -w: print word type (alias, builtin, command, function, hashed, reserved, none)
17151        // -f: skip functions
17152        // -p: search path only
17153        // -a: show all matches
17154        // -m: pattern match with glob
17155        // -s: show symlink resolution
17156        // -S: show steps of symlink resolution
17157        // -x num: expand tabs to num spaces
17158
17159        let mut verbose = false;
17160        let mut csh_style = false;
17161        let mut word_type = false;
17162        let mut skip_functions = false;
17163        let mut path_only = false;
17164        let mut show_all = false;
17165        let mut pattern_mode = false;
17166        let mut show_symlink = false;
17167        let mut show_symlink_steps = false;
17168        let mut tab_expand: Option<usize> = None;
17169        let mut names: Vec<&str> = Vec::new();
17170
17171        let mut i = 0;
17172        while i < args.len() {
17173            let arg = &args[i];
17174
17175            if arg == "--" {
17176                i += 1;
17177                while i < args.len() {
17178                    names.push(&args[i]);
17179                    i += 1;
17180                }
17181                break;
17182            }
17183
17184            if arg.starts_with('-') && arg.len() > 1 {
17185                let mut chars = arg[1..].chars().peekable();
17186                while let Some(ch) = chars.next() {
17187                    match ch {
17188                        'v' => verbose = true,
17189                        'c' => csh_style = true,
17190                        'w' => word_type = true,
17191                        'f' => skip_functions = true,
17192                        'p' => path_only = true,
17193                        'a' => show_all = true,
17194                        'm' => pattern_mode = true,
17195                        's' => show_symlink = true,
17196                        'S' => show_symlink_steps = true,
17197                        'x' => {
17198                            // -x num: tab expansion
17199                            let rest: String = chars.collect();
17200                            if !rest.is_empty() {
17201                                tab_expand = rest.parse().ok();
17202                            } else {
17203                                i += 1;
17204                                if i < args.len() {
17205                                    tab_expand = args[i].parse().ok();
17206                                }
17207                            }
17208                            break;
17209                        }
17210                        _ => {}
17211                    }
17212                }
17213            } else {
17214                names.push(arg);
17215            }
17216            i += 1;
17217        }
17218
17219        let _ = csh_style; // TODO: implement csh-style output
17220        let _ = pattern_mode; // TODO: implement glob pattern matching
17221        let _ = tab_expand;
17222
17223        let mut status = 0;
17224        for name in names {
17225            let mut found = false;
17226            let mut word = "none";
17227
17228            if !path_only {
17229                // Check reserved words
17230                if self.is_reserved_word(name) {
17231                    found = true;
17232                    word = "reserved";
17233                    if word_type {
17234                        println!("{}: {}", name, word);
17235                    } else if verbose {
17236                        println!("{} is a reserved word", name);
17237                    } else {
17238                        println!("{}", name);
17239                    }
17240                    if !show_all {
17241                        continue;
17242                    }
17243                }
17244
17245                // Check aliases
17246                if let Some(alias_val) = self.aliases.get(name) {
17247                    found = true;
17248                    word = "alias";
17249                    if word_type {
17250                        println!("{}: {}", name, word);
17251                    } else if verbose {
17252                        println!("{} is an alias for {}", name, alias_val);
17253                    } else {
17254                        println!("{}", alias_val);
17255                    }
17256                    if !show_all {
17257                        continue;
17258                    }
17259                }
17260
17261                // Check functions (unless -f)
17262                if !skip_functions && self.functions.contains_key(name) {
17263                    found = true;
17264                    word = "function";
17265                    if word_type {
17266                        println!("{}: {}", name, word);
17267                    } else if verbose {
17268                        println!("{} is a shell function", name);
17269                    } else {
17270                        println!("{}", name);
17271                    }
17272                    if !show_all {
17273                        continue;
17274                    }
17275                }
17276
17277                // Check builtins
17278                if self.is_builtin(name) {
17279                    found = true;
17280                    word = "builtin";
17281                    if word_type {
17282                        println!("{}: {}", name, word);
17283                    } else if verbose {
17284                        println!("{} is a shell builtin", name);
17285                    } else {
17286                        println!("{}", name);
17287                    }
17288                    if !show_all {
17289                        continue;
17290                    }
17291                }
17292
17293                // Check hashed commands (named_dirs can serve as a command hash)
17294                // The hash builtin adds to named_dirs for now
17295                if let Some(path) = self.named_dirs.get(name) {
17296                    found = true;
17297                    word = "hashed";
17298                    if word_type {
17299                        println!("{}: {}", name, word);
17300                    } else if verbose {
17301                        println!("{} is hashed ({})", name, path.display());
17302                    } else {
17303                        println!("{}", path.display());
17304                    }
17305                    if !show_all {
17306                        continue;
17307                    }
17308                }
17309            }
17310
17311            // Check PATH
17312            if let Some(path) = self.find_in_path(name) {
17313                found = true;
17314                word = "command";
17315
17316                // Handle symlink resolution
17317                let display_path = if show_symlink || show_symlink_steps {
17318                    let p = std::path::Path::new(&path);
17319                    if show_symlink_steps {
17320                        let mut current = p.to_path_buf();
17321                        let mut steps = vec![path.clone()];
17322                        while let Ok(target) = std::fs::read_link(&current) {
17323                            let resolved = if target.is_absolute() {
17324                                target.clone()
17325                            } else {
17326                                current
17327                                    .parent()
17328                                    .unwrap_or(std::path::Path::new("/"))
17329                                    .join(&target)
17330                            };
17331                            steps.push(resolved.to_string_lossy().to_string());
17332                            current = resolved;
17333                        }
17334                        steps.join(" -> ")
17335                    } else {
17336                        match p.canonicalize() {
17337                            Ok(resolved) => format!("{} -> {}", path, resolved.display()),
17338                            Err(_) => path.clone(),
17339                        }
17340                    }
17341                } else {
17342                    path.clone()
17343                };
17344
17345                if word_type {
17346                    println!("{}: {}", name, word);
17347                } else if verbose {
17348                    println!("{} is {}", name, display_path);
17349                } else {
17350                    println!("{}", display_path);
17351                }
17352            }
17353
17354            if !found {
17355                if word_type {
17356                    println!("{}: none", name);
17357                } else if verbose {
17358                    println!("{} not found", name);
17359                }
17360                status = 1;
17361            }
17362        }
17363        status
17364    }
17365
17366    fn is_reserved_word(&self, name: &str) -> bool {
17367        matches!(
17368            name,
17369            "if" | "then"
17370                | "else"
17371                | "elif"
17372                | "fi"
17373                | "case"
17374                | "esac"
17375                | "for"
17376                | "select"
17377                | "while"
17378                | "until"
17379                | "do"
17380                | "done"
17381                | "in"
17382                | "function"
17383                | "time"
17384                | "coproc"
17385                | "{"
17386                | "}"
17387                | "!"
17388                | "[["
17389                | "]]"
17390                | "(("
17391                | "))"
17392        )
17393    }
17394
17395    /// where - show all locations of a command
17396    fn builtin_where(&self, args: &[String]) -> i32 {
17397        // where is like whence -ca
17398        let mut new_args = vec!["-a".to_string(), "-v".to_string()];
17399        new_args.extend(args.iter().cloned());
17400        self.builtin_whence(&new_args)
17401    }
17402
17403    /// which - show path of command
17404    fn builtin_which(&self, args: &[String]) -> i32 {
17405        // which is like whence -c
17406        let mut new_args = vec!["-c".to_string()];
17407        new_args.extend(args.iter().cloned());
17408        self.builtin_whence(&new_args)
17409    }
17410
17411    /// Helper to check if name is a builtin
17412    /// O(1) builtin check via static HashSet — replaces 130+ arm linear match
17413    fn is_builtin(&self, name: &str) -> bool {
17414        BUILTIN_SET.contains(name) || name.starts_with('_')
17415    }
17416
17417    /// Helper to find command in PATH — checks command_hash first for O(1) hit
17418    fn find_in_path(&self, name: &str) -> Option<String> {
17419        // O(1) hash table lookup from rehash
17420        if let Some(path) = self.command_hash.get(name) {
17421            return Some(path.clone());
17422        }
17423        // Fallback: linear PATH walk
17424        let path_var = env::var("PATH").unwrap_or_default();
17425        for dir in path_var.split(':') {
17426            let full_path = format!("{}/{}", dir, name);
17427            if std::path::Path::new(&full_path).exists() {
17428                return Some(full_path);
17429            }
17430        }
17431        None
17432    }
17433
17434    /// ulimit - get/set resource limits
17435    fn builtin_ulimit(&self, args: &[String]) -> i32 {
17436        use libc::{getrlimit, rlimit, setrlimit};
17437        use libc::{RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE};
17438        use libc::{RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK};
17439
17440        let mut resource = RLIMIT_FSIZE; // default: file size
17441        let mut hard = false;
17442        let mut soft = true;
17443        let mut value: Option<u64> = None;
17444
17445        let mut iter = args.iter();
17446        while let Some(arg) = iter.next() {
17447            match arg.as_str() {
17448                "-H" => {
17449                    hard = true;
17450                    soft = false;
17451                }
17452                "-S" => {
17453                    soft = true;
17454                    hard = false;
17455                }
17456                "-a" => {
17457                    // Print all limits
17458                    self.print_all_limits(soft);
17459                    return 0;
17460                }
17461                "-c" => resource = RLIMIT_CORE,
17462                "-d" => resource = RLIMIT_DATA,
17463                "-f" => resource = RLIMIT_FSIZE,
17464                "-n" => resource = RLIMIT_NOFILE,
17465                "-s" => resource = RLIMIT_STACK,
17466                "-t" => resource = RLIMIT_CPU,
17467                "-u" => resource = RLIMIT_NPROC,
17468                "-v" => resource = RLIMIT_AS,
17469                "-m" => resource = RLIMIT_RSS,
17470                "unlimited" => value = Some(libc::RLIM_INFINITY as u64),
17471                _ if !arg.starts_with('-') => {
17472                    value = arg.parse().ok();
17473                }
17474                _ => {}
17475            }
17476        }
17477
17478        let mut rlim = rlimit {
17479            rlim_cur: 0,
17480            rlim_max: 0,
17481        };
17482        unsafe {
17483            if getrlimit(resource, &mut rlim) != 0 {
17484                eprintln!("ulimit: cannot get limit");
17485                return 1;
17486            }
17487        }
17488
17489        if let Some(v) = value {
17490            // Set limit
17491            if soft {
17492                rlim.rlim_cur = v as libc::rlim_t;
17493            }
17494            if hard {
17495                rlim.rlim_max = v as libc::rlim_t;
17496            }
17497            unsafe {
17498                if setrlimit(resource, &rlim) != 0 {
17499                    eprintln!("ulimit: cannot set limit");
17500                    return 1;
17501                }
17502            }
17503        } else {
17504            // Print limit
17505            let limit = if hard { rlim.rlim_max } else { rlim.rlim_cur };
17506            if limit == libc::RLIM_INFINITY as libc::rlim_t {
17507                println!("unlimited");
17508            } else {
17509                println!("{}", limit);
17510            }
17511        }
17512        0
17513    }
17514
17515    fn print_all_limits(&self, soft: bool) {
17516        use libc::{getrlimit, rlimit};
17517        use libc::{RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE};
17518        use libc::{RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK};
17519
17520        let limits = [
17521            (RLIMIT_CORE, "core file size", "blocks", 512),
17522            (RLIMIT_DATA, "data seg size", "kbytes", 1024),
17523            (RLIMIT_FSIZE, "file size", "blocks", 512),
17524            (RLIMIT_NOFILE, "open files", "", 1),
17525            (RLIMIT_STACK, "stack size", "kbytes", 1024),
17526            (RLIMIT_CPU, "cpu time", "seconds", 1),
17527            (RLIMIT_NPROC, "max user processes", "", 1),
17528            (RLIMIT_AS, "virtual memory", "kbytes", 1024),
17529            (RLIMIT_RSS, "max memory size", "kbytes", 1024),
17530        ];
17531
17532        for (resource, name, unit, divisor) in limits {
17533            let mut rlim = rlimit {
17534                rlim_cur: 0,
17535                rlim_max: 0,
17536            };
17537            unsafe {
17538                if getrlimit(resource, &mut rlim) == 0 {
17539                    let limit = if soft { rlim.rlim_cur } else { rlim.rlim_max };
17540                    let unit_str = if unit.is_empty() {
17541                        ""
17542                    } else {
17543                        &format!("({})", unit)
17544                    };
17545                    if limit == libc::RLIM_INFINITY as libc::rlim_t {
17546                        println!("{:25} {} unlimited", name, unit_str);
17547                    } else {
17548                        println!("{:25} {} {}", name, unit_str, limit / divisor);
17549                    }
17550                }
17551            }
17552        }
17553    }
17554
17555    /// limit - csh-style resource limits
17556    fn builtin_limit(&self, args: &[String]) -> i32 {
17557        // Delegate to ulimit with csh-style names
17558        if args.is_empty() {
17559            // Print all resource limits in csh format
17560            use libc::{getrlimit, rlimit, RLIM_INFINITY};
17561            let resources = [
17562                (libc::RLIMIT_CPU, "cputime", 1, "seconds"),
17563                (libc::RLIMIT_FSIZE, "filesize", 1024, "kB"),
17564                (libc::RLIMIT_DATA, "datasize", 1024, "kB"),
17565                (libc::RLIMIT_STACK, "stacksize", 1024, "kB"),
17566                (libc::RLIMIT_CORE, "coredumpsize", 1024, "kB"),
17567                (libc::RLIMIT_RSS, "memoryuse", 1024, "kB"),
17568                #[cfg(target_os = "linux")]
17569                (libc::RLIMIT_NPROC, "maxproc", 1, ""),
17570                (libc::RLIMIT_NOFILE, "descriptors", 1, ""),
17571            ];
17572            for (res, name, divisor, unit) in resources {
17573                let mut rl: rlimit = unsafe { std::mem::zeroed() };
17574                unsafe {
17575                    getrlimit(res, &mut rl);
17576                }
17577                let val = if rl.rlim_cur == RLIM_INFINITY as u64 {
17578                    "unlimited".to_string()
17579                } else {
17580                    let v = rl.rlim_cur as u64 / divisor;
17581                    if unit.is_empty() {
17582                        format!("{}", v)
17583                    } else {
17584                        format!("{}{}", v, unit)
17585                    }
17586                };
17587                println!("{:<16}{}", name, val);
17588            }
17589            return 0;
17590        }
17591        self.builtin_ulimit(args)
17592    }
17593
17594    /// unlimit - remove resource limits
17595    fn builtin_unlimit(&self, args: &[String]) -> i32 {
17596        let mut new_args = args.to_vec();
17597        new_args.push("unlimited".to_string());
17598        self.builtin_ulimit(&new_args)
17599    }
17600
17601    /// umask - get/set file creation mask
17602    fn builtin_umask(&self, args: &[String]) -> i32 {
17603        use libc::umask;
17604
17605        let mut symbolic = false;
17606        let mut value: Option<&str> = None;
17607
17608        for arg in args {
17609            match arg.as_str() {
17610                "-S" => symbolic = true,
17611                _ if !arg.starts_with('-') => value = Some(arg),
17612                _ => {}
17613            }
17614        }
17615
17616        if let Some(v) = value {
17617            // Set umask
17618            if let Ok(mask) = u32::from_str_radix(v, 8) {
17619                unsafe {
17620                    umask(mask as libc::mode_t);
17621                }
17622            } else {
17623                eprintln!("umask: invalid mask: {}", v);
17624                return 1;
17625            }
17626        } else {
17627            // Get umask
17628            let mask = unsafe {
17629                let m = umask(0);
17630                umask(m);
17631                m
17632            };
17633            if symbolic {
17634                let u = 7 - ((mask >> 6) & 7);
17635                let g = 7 - ((mask >> 3) & 7);
17636                let o = 7 - (mask & 7);
17637                println!(
17638                    "u={}{}{}g={}{}{}o={}{}{}",
17639                    if u & 4 != 0 { "r" } else { "" },
17640                    if u & 2 != 0 { "w" } else { "" },
17641                    if u & 1 != 0 { "x" } else { "" },
17642                    if g & 4 != 0 { "r" } else { "" },
17643                    if g & 2 != 0 { "w" } else { "" },
17644                    if g & 1 != 0 { "x" } else { "" },
17645                    if o & 4 != 0 { "r" } else { "" },
17646                    if o & 2 != 0 { "w" } else { "" },
17647                    if o & 1 != 0 { "x" } else { "" },
17648                );
17649            } else {
17650                println!("{:04o}", mask);
17651            }
17652        }
17653        0
17654    }
17655
17656    /// rehash - rebuild command hash table
17657    fn builtin_rehash(&mut self, args: &[String]) -> i32 {
17658        // rehash [ -d ] [ -f ] [ -v ]
17659        // -d: rehash named directories
17660        // -f: force rehash of all commands in PATH
17661        // -v: verbose (print each command being hashed)
17662
17663        let mut rehash_dirs = false;
17664        let mut force = false;
17665        let mut verbose = false;
17666
17667        for arg in args {
17668            if arg.starts_with('-') {
17669                for ch in arg[1..].chars() {
17670                    match ch {
17671                        'd' => rehash_dirs = true,
17672                        'f' => force = true,
17673                        'v' => verbose = true,
17674                        _ => {}
17675                    }
17676                }
17677            }
17678        }
17679
17680        if rehash_dirs {
17681            // Rebuild named directories from special params like ~user
17682            // For now just clear and rebuild from HOME
17683            self.named_dirs.clear();
17684            if let Ok(home) = env::var("HOME") {
17685                self.named_dirs.insert(String::new(), PathBuf::from(&home)); // ~ without name
17686            }
17687            return 0;
17688        }
17689
17690        // Clear command hash table
17691        self.command_hash.clear();
17692
17693        if force {
17694            // Parallel PATH scan — each PATH dir on a pool thread.
17695            // zsh does this single-threaded; we fan out across workers.
17696            if let Ok(path_var) = env::var("PATH") {
17697                let dirs: Vec<String> = path_var
17698                    .split(':')
17699                    .filter(|s| !s.is_empty())
17700                    .map(|s| s.to_string())
17701                    .collect();
17702
17703                let (tx, rx) = std::sync::mpsc::channel::<Vec<(String, String)>>();
17704
17705                for dir in dirs {
17706                    let tx = tx.clone();
17707                    self.worker_pool.submit(move || {
17708                        let mut batch = Vec::new();
17709                        if let Ok(entries) = std::fs::read_dir(&dir) {
17710                            for entry in entries.flatten() {
17711                                if let Ok(ft) = entry.file_type() {
17712                                    if ft.is_file() || ft.is_symlink() {
17713                                        if let Some(name) = entry.file_name().to_str() {
17714                                            let path = entry.path().to_string_lossy().to_string();
17715                                            batch.push((name.to_string(), path));
17716                                        }
17717                                    }
17718                                }
17719                            }
17720                        }
17721                        let _ = tx.send(batch);
17722                    });
17723                }
17724                drop(tx);
17725
17726                for batch in rx {
17727                    for (name, path) in batch {
17728                        if verbose {
17729                            println!("{}={}", name, path);
17730                        }
17731                        self.command_hash.insert(name, path);
17732                    }
17733                }
17734            }
17735        }
17736
17737        0
17738    }
17739
17740    /// unhash - remove entries from hash table
17741    fn builtin_unhash(&mut self, args: &[String]) -> i32 {
17742        let mut remove_aliases = false;
17743        let mut remove_functions = false;
17744        let mut remove_dirs = false;
17745        let mut names: Vec<&str> = Vec::new();
17746
17747        for arg in args {
17748            match arg.as_str() {
17749                "-a" => remove_aliases = true,
17750                "-f" => remove_functions = true,
17751                "-d" => remove_dirs = true,
17752                "-m" => {} // pattern matching (TODO)
17753                _ if arg.starts_with('-') => {}
17754                _ => names.push(arg),
17755            }
17756        }
17757
17758        for name in names {
17759            if remove_aliases {
17760                self.aliases.remove(name);
17761            }
17762            if remove_functions {
17763                self.functions.remove(name);
17764            }
17765            if remove_dirs {
17766                // Remove from named directories (TODO)
17767            }
17768        }
17769        0
17770    }
17771
17772    /// times - print accumulated user and system times
17773    fn builtin_times(&self, _args: &[String]) -> i32 {
17774        use libc::{getrusage, rusage, RUSAGE_CHILDREN, RUSAGE_SELF};
17775
17776        let mut self_usage: rusage = unsafe { std::mem::zeroed() };
17777        let mut child_usage: rusage = unsafe { std::mem::zeroed() };
17778
17779        unsafe {
17780            getrusage(RUSAGE_SELF, &mut self_usage);
17781            getrusage(RUSAGE_CHILDREN, &mut child_usage);
17782        }
17783
17784        let self_user =
17785            self_usage.ru_utime.tv_sec as f64 + self_usage.ru_utime.tv_usec as f64 / 1_000_000.0;
17786        let self_sys =
17787            self_usage.ru_stime.tv_sec as f64 + self_usage.ru_stime.tv_usec as f64 / 1_000_000.0;
17788        let child_user =
17789            child_usage.ru_utime.tv_sec as f64 + child_usage.ru_utime.tv_usec as f64 / 1_000_000.0;
17790        let child_sys =
17791            child_usage.ru_stime.tv_sec as f64 + child_usage.ru_stime.tv_usec as f64 / 1_000_000.0;
17792
17793        println!("{:.3}s {:.3}s", self_user, self_sys);
17794        println!("{:.3}s {:.3}s", child_user, child_sys);
17795        0
17796    }
17797
17798    /// zmodload - load/unload zsh modules (stub)
17799    fn builtin_zmodload(&mut self, args: &[String]) -> i32 {
17800        let mut list_loaded = false;
17801        let mut unload = false;
17802        let mut modules: Vec<&str> = Vec::new();
17803
17804        for arg in args {
17805            match arg.as_str() {
17806                "-l" | "-L" => list_loaded = true,
17807                "-u" => unload = true,
17808                "-a" | "-b" | "-c" | "-d" | "-e" | "-f" | "-i" | "-p" | "-s" => {}
17809                _ if arg.starts_with('-') => {}
17810                _ => modules.push(arg),
17811            }
17812        }
17813
17814        if list_loaded || modules.is_empty() {
17815            // List loaded modules (stub - we don't really have modules)
17816            println!("zsh/complete");
17817            println!("zsh/complist");
17818            println!("zsh/parameter");
17819            println!("zsh/zutil");
17820            return 0;
17821        }
17822
17823        for module in modules {
17824            if unload {
17825                // Unload module (stub)
17826                self.options.remove(&format!("_module_{}", module));
17827            } else {
17828                // Load module (stub)
17829                self.options.insert(format!("_module_{}", module), true);
17830            }
17831        }
17832        0
17833    }
17834
17835    /// r - redo last command (alias for fc -e -)
17836    fn builtin_r(&mut self, args: &[String]) -> i32 {
17837        let mut fc_args = vec!["-e".to_string(), "-".to_string()];
17838        fc_args.extend(args.iter().cloned());
17839        self.builtin_fc(&fc_args)
17840    }
17841
17842    /// ttyctl - control terminal settings
17843    fn builtin_ttyctl(&self, args: &[String]) -> i32 {
17844        for arg in args {
17845            match arg.as_str() {
17846                "-f" => {
17847                    // Freeze terminal settings
17848                    // In a full implementation, this would save terminal state
17849                }
17850                "-u" => {
17851                    // Unfreeze terminal settings
17852                }
17853                _ => {}
17854            }
17855        }
17856        0
17857    }
17858
17859    /// noglob - run command without globbing
17860    fn builtin_noglob(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
17861        if args.is_empty() {
17862            return 0;
17863        }
17864
17865        // Temporarily disable globbing
17866        let saved = self.options.get("noglob").cloned();
17867        self.options.insert("noglob".to_string(), true);
17868
17869        // Execute the command
17870        let status = self.builtin_command(args, redirects);
17871
17872        // Restore globbing state
17873        if let Some(v) = saved {
17874            self.options.insert("noglob".to_string(), v);
17875        } else {
17876            self.options.remove("noglob");
17877        }
17878
17879        status
17880    }
17881
17882    // ═══════════════════════════════════════════════════════════════════════════
17883    // zsh module builtins
17884    // ═══════════════════════════════════════════════════════════════════════════
17885
17886    /// zstat - file status (zsh/stat module)
17887    fn builtin_zstat(&self, args: &[String]) -> i32 {
17888        use std::os::unix::fs::MetadataExt;
17889        use std::os::unix::fs::PermissionsExt;
17890
17891        let mut show_all = true;
17892        let mut symbolic_mode = false;
17893        let mut show_link = false;
17894        let mut _as_array = false;
17895        let mut _array_name = String::new();
17896        let mut format_time = String::new();
17897        let mut elements: Vec<String> = Vec::new();
17898        let mut files: Vec<&str> = Vec::new();
17899
17900        let mut iter = args.iter().peekable();
17901        while let Some(arg) = iter.next() {
17902            match arg.as_str() {
17903                "-s" => symbolic_mode = true,
17904                "-L" => show_link = true,
17905                "-N" => {} // Don't resolve symlinks
17906                "-n" => {} // Numeric user/group
17907                "-o" => show_all = false,
17908                "-A" => {
17909                    _as_array = true;
17910                    if let Some(name) = iter.next() {
17911                        _array_name = name.clone();
17912                    }
17913                }
17914                "-F" => {
17915                    if let Some(fmt) = iter.next() {
17916                        format_time = fmt.clone();
17917                    }
17918                }
17919                s if s.starts_with('+') => {
17920                    elements.push(s[1..].to_string());
17921                    show_all = false;
17922                }
17923                s if !s.starts_with('-') => files.push(s),
17924                _ => {}
17925            }
17926        }
17927
17928        if files.is_empty() {
17929            eprintln!("zstat: no files specified");
17930            return 1;
17931        }
17932
17933        for file in files {
17934            let meta = if show_link {
17935                std::fs::symlink_metadata(file)
17936            } else {
17937                std::fs::metadata(file)
17938            };
17939
17940            let meta = match meta {
17941                Ok(m) => m,
17942                Err(e) => {
17943                    eprintln!("zstat: {}: {}", file, e);
17944                    return 1;
17945                }
17946            };
17947
17948            let output_element = |name: &str, value: &str| {
17949                if _as_array {
17950                    // Would need mutable self to store in array
17951                    println!("{}={}", name, value);
17952                } else if show_all || elements.contains(&name.to_string()) {
17953                    println!("{}: {}", name, value);
17954                }
17955            };
17956
17957            output_element("device", &meta.dev().to_string());
17958            output_element("inode", &meta.ino().to_string());
17959
17960            if symbolic_mode {
17961                let mode = meta.permissions().mode();
17962                let mode_str = format!(
17963                    "{}{}{}{}{}{}{}{}{}{}",
17964                    match mode & 0o170000 {
17965                        0o040000 => 'd',
17966                        0o120000 => 'l',
17967                        0o100000 => '-',
17968                        0o060000 => 'b',
17969                        0o020000 => 'c',
17970                        0o010000 => 'p',
17971                        0o140000 => 's',
17972                        _ => '?',
17973                    },
17974                    if mode & 0o400 != 0 { 'r' } else { '-' },
17975                    if mode & 0o200 != 0 { 'w' } else { '-' },
17976                    if mode & 0o4000 != 0 {
17977                        's'
17978                    } else if mode & 0o100 != 0 {
17979                        'x'
17980                    } else {
17981                        '-'
17982                    },
17983                    if mode & 0o040 != 0 { 'r' } else { '-' },
17984                    if mode & 0o020 != 0 { 'w' } else { '-' },
17985                    if mode & 0o2000 != 0 {
17986                        's'
17987                    } else if mode & 0o010 != 0 {
17988                        'x'
17989                    } else {
17990                        '-'
17991                    },
17992                    if mode & 0o004 != 0 { 'r' } else { '-' },
17993                    if mode & 0o002 != 0 { 'w' } else { '-' },
17994                    if mode & 0o1000 != 0 {
17995                        't'
17996                    } else if mode & 0o001 != 0 {
17997                        'x'
17998                    } else {
17999                        '-'
18000                    },
18001                );
18002                output_element("mode", &mode_str);
18003            } else {
18004                output_element("mode", &format!("{:o}", meta.permissions().mode()));
18005            }
18006
18007            output_element("nlink", &meta.nlink().to_string());
18008            output_element("uid", &meta.uid().to_string());
18009            output_element("gid", &meta.gid().to_string());
18010            output_element("rdev", &meta.rdev().to_string());
18011            output_element("size", &meta.len().to_string());
18012
18013            let format_timestamp = |secs: i64| -> String {
18014                if format_time.is_empty() {
18015                    secs.to_string()
18016                } else {
18017                    chrono::DateTime::from_timestamp(secs, 0)
18018                        .map(|dt| dt.format(&format_time).to_string())
18019                        .unwrap_or_else(|| secs.to_string())
18020                }
18021            };
18022
18023            output_element("atime", &format_timestamp(meta.atime()));
18024            output_element("mtime", &format_timestamp(meta.mtime()));
18025            output_element("ctime", &format_timestamp(meta.ctime()));
18026            output_element("blksize", &meta.blksize().to_string());
18027            output_element("blocks", &meta.blocks().to_string());
18028
18029            if show_link && meta.file_type().is_symlink() {
18030                if let Ok(target) = std::fs::read_link(file) {
18031                    output_element("link", &target.to_string_lossy());
18032                }
18033            }
18034        }
18035
18036        0
18037    }
18038
18039    /// strftime - format date/time (zsh/datetime module)
18040    fn builtin_strftime(&self, args: &[String]) -> i32 {
18041        let mut format = "%c".to_string();
18042        let mut timestamp: Option<i64> = None;
18043        let mut to_var = false;
18044        let mut var_name = String::new();
18045
18046        let mut iter = args.iter();
18047        while let Some(arg) = iter.next() {
18048            match arg.as_str() {
18049                "-s" => {
18050                    to_var = true;
18051                    if let Some(name) = iter.next() {
18052                        var_name = name.clone();
18053                    }
18054                }
18055                "-r" => {
18056                    // Reference time from a variable
18057                    if let Some(ts_str) = iter.next() {
18058                        timestamp = ts_str.parse().ok();
18059                    }
18060                }
18061                s if !s.starts_with('-') => {
18062                    if format == "%c" {
18063                        format = s.to_string();
18064                    } else if timestamp.is_none() {
18065                        timestamp = s.parse().ok();
18066                    }
18067                }
18068                _ => {}
18069            }
18070        }
18071
18072        let ts = timestamp.unwrap_or_else(|| chrono::Local::now().timestamp());
18073
18074        let result = chrono::DateTime::from_timestamp(ts, 0)
18075            .map(|dt: chrono::DateTime<chrono::Utc>| {
18076                dt.with_timezone(&chrono::Local).format(&format).to_string()
18077            })
18078            .unwrap_or_else(|| "invalid timestamp".to_string());
18079
18080        if to_var && !var_name.is_empty() {
18081            // Would need mutable self
18082            println!("{}={}", var_name, result);
18083        } else {
18084            println!("{}", result);
18085        }
18086
18087        0
18088    }
18089
18090    /// zsleep - sleep with fractional seconds
18091    fn builtin_zsleep(&self, args: &[String]) -> i32 {
18092        if args.is_empty() {
18093            eprintln!("zsleep: missing argument");
18094            return 1;
18095        }
18096
18097        let secs: f64 = match args[0].parse() {
18098            Ok(s) => s,
18099            Err(_) => {
18100                eprintln!("zsleep: invalid number: {}", args[0]);
18101                return 1;
18102            }
18103        };
18104
18105        std::thread::sleep(std::time::Duration::from_secs_f64(secs));
18106        0
18107    }
18108
18109    /// zsystem - system interface (zsh/system module)
18110    /// Ported from zsh/Src/Modules/system.c bin_zsystem() lines 805-816
18111    fn builtin_zsystem(&mut self, args: &[String]) -> i32 {
18112        if args.is_empty() {
18113            eprintln!("zsystem: subcommand expected");
18114            return 1;
18115        }
18116        match args[0].as_str() {
18117            "flock" => self.builtin_zsystem_flock(&args[1..]),
18118            "supports" => self.builtin_zsystem_supports(&args[1..]),
18119            _ => {
18120                eprintln!("zsystem: unknown subcommand: {}", args[0]);
18121                1
18122            }
18123        }
18124    }
18125
18126    /// zsystem supports - ported from system.c bin_zsystem_supports() lines 780-801
18127    fn builtin_zsystem_supports(&self, args: &[String]) -> i32 {
18128        if args.is_empty() {
18129            eprintln!("zsystem: supports: not enough arguments");
18130            return 255;
18131        }
18132        if args.len() > 1 {
18133            eprintln!("zsystem: supports: too many arguments");
18134            return 255;
18135        }
18136        match args[0].as_str() {
18137            "supports" | "flock" => 0,
18138            _ => 1,
18139        }
18140    }
18141
18142    /// zsystem flock - ported from system.c bin_zsystem_flock() lines 546-774
18143    fn builtin_zsystem_flock(&mut self, args: &[String]) -> i32 {
18144        #[cfg(unix)]
18145        {
18146            use std::os::unix::io::AsRawFd;
18147
18148            let mut cloexec = true;
18149            let mut readlock = false;
18150            let mut timeout: Option<f64> = None;
18151            let mut fdvar: Option<String> = None;
18152            let mut file: Option<&str> = None;
18153
18154            let mut i = 0;
18155            while i < args.len() {
18156                let arg = &args[i];
18157                if arg == "--" {
18158                    i += 1;
18159                    if i < args.len() {
18160                        file = Some(&args[i]);
18161                    }
18162                    break;
18163                }
18164                if !arg.starts_with('-') {
18165                    file = Some(arg);
18166                    break;
18167                }
18168                let mut chars = arg[1..].chars().peekable();
18169                while let Some(c) = chars.next() {
18170                    match c {
18171                        'e' => cloexec = false,
18172                        'r' => readlock = true,
18173                        'u' => return 0,
18174                        'f' => {
18175                            let rest: String = chars.collect();
18176                            if !rest.is_empty() {
18177                                fdvar = Some(rest);
18178                            } else {
18179                                i += 1;
18180                                if i < args.len() {
18181                                    fdvar = Some(args[i].clone());
18182                                } else {
18183                                    eprintln!("zsystem: flock: option f requires a variable name");
18184                                    return 1;
18185                                }
18186                            }
18187                            break;
18188                        }
18189                        't' => {
18190                            let rest: String = chars.collect();
18191                            let val = if !rest.is_empty() {
18192                                rest
18193                            } else {
18194                                i += 1;
18195                                if i < args.len() {
18196                                    args[i].clone()
18197                                } else {
18198                                    eprintln!(
18199                                        "zsystem: flock: option t requires a numeric timeout"
18200                                    );
18201                                    return 1;
18202                                }
18203                            };
18204                            match val.parse::<f64>() {
18205                                Ok(t) => timeout = Some(t),
18206                                Err(_) => {
18207                                    eprintln!("zsystem: flock: invalid timeout value: '{}'", val);
18208                                    return 1;
18209                                }
18210                            }
18211                            break;
18212                        }
18213                        'i' => {
18214                            let rest: String = chars.collect();
18215                            if rest.is_empty() {
18216                                i += 1;
18217                                if i >= args.len() {
18218                                    eprintln!("zsystem: flock: option i requires a numeric retry interval");
18219                                    return 1;
18220                                }
18221                            }
18222                            break;
18223                        }
18224                        _ => {
18225                            eprintln!("zsystem: flock: unknown option: -{}", c);
18226                            return 1;
18227                        }
18228                    }
18229                }
18230                i += 1;
18231            }
18232
18233            let filepath = match file {
18234                Some(f) => f,
18235                None => {
18236                    eprintln!("zsystem: flock: not enough arguments");
18237                    return 1;
18238                }
18239            };
18240
18241            use std::fs::OpenOptions;
18242            let file_handle = match OpenOptions::new()
18243                .read(true)
18244                .write(!readlock)
18245                .create(true)
18246                .truncate(false)
18247                .open(filepath)
18248            {
18249                Ok(f) => f,
18250                Err(e) => {
18251                    eprintln!("zsystem: flock: {}: {}", filepath, e);
18252                    return 1;
18253                }
18254            };
18255
18256            let lock_type = if readlock {
18257                libc::F_RDLCK as i16
18258            } else {
18259                libc::F_WRLCK as i16
18260            };
18261
18262            let mut flock = libc::flock {
18263                l_type: lock_type,
18264                l_whence: libc::SEEK_SET as i16,
18265                l_start: 0,
18266                l_len: 0,
18267                l_pid: 0,
18268            };
18269
18270            let cmd = if timeout.is_some() {
18271                libc::F_SETLK
18272            } else {
18273                libc::F_SETLKW
18274            };
18275            let start = std::time::Instant::now();
18276            let timeout_duration = timeout.map(|t| std::time::Duration::from_secs_f64(t));
18277
18278            loop {
18279                let ret = unsafe { libc::fcntl(file_handle.as_raw_fd(), cmd, &mut flock) };
18280                if ret == 0 {
18281                    if let Some(ref var) = fdvar {
18282                        let fd = file_handle.as_raw_fd();
18283                        std::mem::forget(file_handle);
18284                        self.variables.insert(var.clone(), fd.to_string());
18285                    } else {
18286                        std::mem::forget(file_handle);
18287                    }
18288                    let _ = cloexec;
18289                    return 0;
18290                }
18291                let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
18292                if errno != libc::EACCES && errno != libc::EAGAIN {
18293                    eprintln!(
18294                        "zsystem: flock: {}: {}",
18295                        filepath,
18296                        std::io::Error::last_os_error()
18297                    );
18298                    return 1;
18299                }
18300                if let Some(td) = timeout_duration {
18301                    if start.elapsed() >= td {
18302                        return 2;
18303                    }
18304                    std::thread::sleep(std::time::Duration::from_millis(100));
18305                } else {
18306                    eprintln!(
18307                        "zsystem: flock: {}: {}",
18308                        filepath,
18309                        std::io::Error::last_os_error()
18310                    );
18311                    return 1;
18312                }
18313            }
18314        }
18315        #[cfg(not(unix))]
18316        {
18317            eprintln!("zsystem: flock: not supported on this platform");
18318            1
18319        }
18320    }
18321
18322    /// sync - flush filesystem buffers
18323    /// Port from zsh/Src/Modules/files.c bin_sync() lines 52-57
18324    fn builtin_sync(&self, _args: &[String]) -> i32 {
18325        #[cfg(unix)]
18326        unsafe {
18327            libc::sync();
18328        }
18329        0
18330    }
18331
18332    /// mkdir - create directories
18333    /// Port from zsh/Src/Modules/files.c bin_mkdir() lines 62-111
18334    fn builtin_mkdir(&self, args: &[String]) -> i32 {
18335        let mut mode: u32 = 0o777;
18336        let mut parents = false;
18337        let mut dirs: Vec<&str> = Vec::new();
18338
18339        let mut i = 0;
18340        while i < args.len() {
18341            let arg = &args[i];
18342            if arg == "-p" {
18343                parents = true;
18344            } else if arg == "-m" && i + 1 < args.len() {
18345                i += 1;
18346                mode = u32::from_str_radix(&args[i], 8).unwrap_or(0o777);
18347            } else if arg.starts_with("-m") {
18348                mode = u32::from_str_radix(&arg[2..], 8).unwrap_or(0o777);
18349            } else if !arg.starts_with('-') || arg == "-" || arg == "--" {
18350                if arg == "--" {
18351                    dirs.extend(args[i + 1..].iter().map(|s| s.as_str()));
18352                    break;
18353                }
18354                dirs.push(arg);
18355            }
18356            i += 1;
18357        }
18358
18359        let mut err = 0;
18360        for dir in dirs {
18361            let path = std::path::Path::new(dir);
18362            let result = if parents {
18363                std::fs::create_dir_all(path)
18364            } else {
18365                std::fs::create_dir(path)
18366            };
18367            if let Err(e) = result {
18368                eprintln!("mkdir: cannot create directory '{}': {}", dir, e);
18369                err = 1;
18370            } else {
18371                #[cfg(unix)]
18372                {
18373                    use std::os::unix::fs::PermissionsExt;
18374                    let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode));
18375                }
18376            }
18377        }
18378        err
18379    }
18380
18381    /// rmdir - remove directories
18382    /// Port from zsh/Src/Modules/files.c bin_rmdir() lines 149-166
18383    fn builtin_rmdir(&self, args: &[String]) -> i32 {
18384        let mut err = 0;
18385        for arg in args {
18386            if arg.starts_with('-') {
18387                continue;
18388            }
18389            if let Err(e) = std::fs::remove_dir(arg) {
18390                eprintln!("rmdir: cannot remove '{}': {}", arg, e);
18391                err = 1;
18392            }
18393        }
18394        err
18395    }
18396
18397    /// ln - create links
18398    /// Port from zsh/Src/Modules/files.c bin_ln() lines 200-294
18399    fn builtin_ln(&self, args: &[String]) -> i32 {
18400        let mut symbolic = false;
18401        let mut force = false;
18402        let mut no_deref = false;
18403        let mut files: Vec<&str> = Vec::new();
18404
18405        for arg in args {
18406            match arg.as_str() {
18407                "-s" => symbolic = true,
18408                "-f" => force = true,
18409                "-n" | "-h" => no_deref = true,
18410                s if !s.starts_with('-') => files.push(s),
18411                _ => {}
18412            }
18413        }
18414
18415        if files.len() < 2 {
18416            if files.len() == 1 {
18417                let src = files[0];
18418                let target = std::path::Path::new(src)
18419                    .file_name()
18420                    .map(|n| n.to_string_lossy().to_string())
18421                    .unwrap_or_else(|| src.to_string());
18422                files.push(Box::leak(target.into_boxed_str()));
18423            } else {
18424                eprintln!("ln: missing file operand");
18425                return 1;
18426            }
18427        }
18428
18429        let target = files.pop().unwrap();
18430        let target_path = std::path::Path::new(target);
18431        let is_dir = !no_deref && target_path.is_dir();
18432
18433        for src in files {
18434            let dest = if is_dir {
18435                format!(
18436                    "{}/{}",
18437                    target,
18438                    std::path::Path::new(src)
18439                        .file_name()
18440                        .map(|n| n.to_string_lossy().to_string())
18441                        .unwrap_or_else(|| src.to_string())
18442                )
18443            } else {
18444                target.to_string()
18445            };
18446
18447            let dest_path = std::path::Path::new(&dest);
18448            if force && dest_path.exists() {
18449                let _ = std::fs::remove_file(&dest);
18450            }
18451
18452            let result = if symbolic {
18453                #[cfg(unix)]
18454                {
18455                    std::os::unix::fs::symlink(src, &dest)
18456                }
18457                #[cfg(not(unix))]
18458                {
18459                    Err(std::io::Error::new(
18460                        std::io::ErrorKind::Unsupported,
18461                        "symlinks not supported",
18462                    ))
18463                }
18464            } else {
18465                std::fs::hard_link(src, &dest)
18466            };
18467
18468            if let Err(e) = result {
18469                eprintln!("ln: cannot create link '{}' -> '{}': {}", dest, src, e);
18470                return 1;
18471            }
18472        }
18473        0
18474    }
18475
18476    /// mv - move/rename files
18477    /// Port from zsh/Src/Modules/files.c bin_ln()/domove() for mv mode
18478    fn builtin_mv(&self, args: &[String]) -> i32 {
18479        let mut force = false;
18480        let mut interactive = false;
18481        let mut verbose = false;
18482        let mut files: Vec<&str> = Vec::new();
18483
18484        for arg in args {
18485            match arg.as_str() {
18486                "-f" => force = true,
18487                "-i" => interactive = true,
18488                "-v" => verbose = true,
18489                s if !s.starts_with('-') => files.push(s),
18490                _ => {}
18491            }
18492        }
18493
18494        if files.len() < 2 {
18495            eprintln!("mv: missing file operand");
18496            return 1;
18497        }
18498
18499        let target = files.pop().unwrap();
18500        let target_path = std::path::Path::new(target);
18501        let is_dir = target_path.is_dir();
18502
18503        for src in files {
18504            let dest = if is_dir {
18505                format!(
18506                    "{}/{}",
18507                    target,
18508                    std::path::Path::new(src)
18509                        .file_name()
18510                        .map(|n| n.to_string_lossy().to_string())
18511                        .unwrap_or_else(|| src.to_string())
18512                )
18513            } else {
18514                target.to_string()
18515            };
18516
18517            let dest_path = std::path::Path::new(&dest);
18518            if dest_path.exists() && !force {
18519                if interactive {
18520                    eprint!("mv: overwrite '{}'? ", dest);
18521                    let mut response = String::new();
18522                    if std::io::stdin().read_line(&mut response).is_err()
18523                        || !response.trim().eq_ignore_ascii_case("y")
18524                    {
18525                        continue;
18526                    }
18527                } else {
18528                    eprintln!("mv: cannot overwrite '{}': File exists", dest);
18529                    return 1;
18530                }
18531            }
18532
18533            if let Err(e) = std::fs::rename(src, &dest) {
18534                eprintln!("mv: cannot move '{}' to '{}': {}", src, dest, e);
18535                return 1;
18536            }
18537
18538            if verbose {
18539                println!("'{}' -> '{}'", src, dest);
18540            }
18541        }
18542        0
18543    }
18544
18545    /// cp - copy files
18546    /// Port from zsh/Src/Modules/files.c recursive copy functionality
18547    fn builtin_cp(&self, args: &[String]) -> i32 {
18548        let mut recursive = false;
18549        let mut force = false;
18550        let mut interactive = false;
18551        let mut preserve = false;
18552        let mut verbose = false;
18553        let mut files: Vec<&str> = Vec::new();
18554
18555        for arg in args {
18556            match arg.as_str() {
18557                "-r" | "-R" => recursive = true,
18558                "-f" => force = true,
18559                "-i" => interactive = true,
18560                "-p" => preserve = true,
18561                "-v" => verbose = true,
18562                s if !s.starts_with('-') => files.push(s),
18563                _ => {}
18564            }
18565        }
18566
18567        let _ = preserve; // unused for now
18568
18569        if files.len() < 2 {
18570            eprintln!("cp: missing file operand");
18571            return 1;
18572        }
18573
18574        let target = files.pop().unwrap();
18575        let target_path = std::path::Path::new(target);
18576        let is_dir = target_path.is_dir();
18577
18578        for src in files {
18579            let src_path = std::path::Path::new(src);
18580            let dest = if is_dir {
18581                format!(
18582                    "{}/{}",
18583                    target,
18584                    src_path
18585                        .file_name()
18586                        .map(|n| n.to_string_lossy().to_string())
18587                        .unwrap_or_else(|| src.to_string())
18588                )
18589            } else {
18590                target.to_string()
18591            };
18592
18593            let dest_path = std::path::Path::new(&dest);
18594            if dest_path.exists() && !force {
18595                if interactive {
18596                    eprint!("cp: overwrite '{}'? ", dest);
18597                    let mut response = String::new();
18598                    if std::io::stdin().read_line(&mut response).is_err()
18599                        || !response.trim().eq_ignore_ascii_case("y")
18600                    {
18601                        continue;
18602                    }
18603                }
18604            }
18605
18606            let result = if src_path.is_dir() {
18607                if recursive {
18608                    Self::copy_dir_recursive(src_path, dest_path)
18609                } else {
18610                    eprintln!("cp: -r not specified; omitting directory '{}'", src);
18611                    continue;
18612                }
18613            } else {
18614                std::fs::copy(src, &dest).map(|_| ())
18615            };
18616
18617            if let Err(e) = result {
18618                eprintln!("cp: cannot copy '{}' to '{}': {}", src, dest, e);
18619                return 1;
18620            }
18621
18622            if verbose {
18623                println!("'{}' -> '{}'", src, dest);
18624            }
18625        }
18626        0
18627    }
18628
18629    fn copy_dir_recursive(src: &std::path::Path, dest: &std::path::Path) -> std::io::Result<()> {
18630        if !dest.exists() {
18631            std::fs::create_dir_all(dest)?;
18632        }
18633        for entry in std::fs::read_dir(src)? {
18634            let entry = entry?;
18635            let file_type = entry.file_type()?;
18636            let src_path = entry.path();
18637            let dest_path = dest.join(entry.file_name());
18638
18639            if file_type.is_dir() {
18640                Self::copy_dir_recursive(&src_path, &dest_path)?;
18641            } else {
18642                std::fs::copy(&src_path, &dest_path)?;
18643            }
18644        }
18645        Ok(())
18646    }
18647
18648    /// rm - remove files
18649    fn builtin_rm(&self, args: &[String]) -> i32 {
18650        let mut recursive = false;
18651        let mut force = false;
18652        let mut interactive = false;
18653        let mut verbose = false;
18654        let mut files: Vec<&str> = Vec::new();
18655
18656        for arg in args {
18657            match arg.as_str() {
18658                "-r" | "-R" => recursive = true,
18659                "-f" => force = true,
18660                "-i" => interactive = true,
18661                "-v" => verbose = true,
18662                "-rf" | "-fr" => {
18663                    recursive = true;
18664                    force = true;
18665                }
18666                s if !s.starts_with('-') => files.push(s),
18667                _ => {}
18668            }
18669        }
18670
18671        for file in files {
18672            let path = std::path::Path::new(file);
18673
18674            if !path.exists() {
18675                if !force {
18676                    eprintln!("rm: cannot remove '{}': No such file or directory", file);
18677                    return 1;
18678                }
18679                continue;
18680            }
18681
18682            if interactive {
18683                let file_type = if path.is_dir() { "directory" } else { "file" };
18684                eprint!("rm: remove {} '{}'? ", file_type, file);
18685                let mut response = String::new();
18686                if std::io::stdin().read_line(&mut response).is_err()
18687                    || !response.trim().eq_ignore_ascii_case("y")
18688                {
18689                    continue;
18690                }
18691            }
18692
18693            let result = if path.is_dir() {
18694                if recursive {
18695                    std::fs::remove_dir_all(path)
18696                } else {
18697                    eprintln!("rm: cannot remove '{}': Is a directory", file);
18698                    return 1;
18699                }
18700            } else {
18701                std::fs::remove_file(path)
18702            };
18703
18704            if let Err(e) = result {
18705                if !force {
18706                    eprintln!("rm: cannot remove '{}': {}", file, e);
18707                    return 1;
18708                }
18709            } else if verbose {
18710                println!("removed '{}'", file);
18711            }
18712        }
18713        0
18714    }
18715
18716    /// chown - change file owner (Unix only)
18717    #[cfg(unix)]
18718    fn builtin_chown(&self, args: &[String]) -> i32 {
18719        use std::os::unix::fs::MetadataExt;
18720
18721        let mut recursive = false;
18722        let mut positional: Vec<&str> = Vec::new();
18723
18724        for arg in args {
18725            match arg.as_str() {
18726                "-R" => recursive = true,
18727                "-h" => {} // don't deference symlinks (default on most systems)
18728                s if !s.starts_with('-') => positional.push(s),
18729                _ => {}
18730            }
18731        }
18732
18733        if positional.len() < 2 {
18734            eprintln!("chown: missing operand");
18735            return 1;
18736        }
18737
18738        let owner_spec = positional[0];
18739        let files = &positional[1..];
18740
18741        // Parse owner[:group]
18742        let (user, group) = if let Some(colon_pos) = owner_spec.find(':') {
18743            (&owner_spec[..colon_pos], Some(&owner_spec[colon_pos + 1..]))
18744        } else {
18745            (owner_spec, None)
18746        };
18747
18748        let uid: u32 = if user.is_empty() {
18749            u32::MAX
18750        } else if let Ok(id) = user.parse() {
18751            id
18752        } else {
18753            // Look up user name
18754            unsafe {
18755                let c_user = std::ffi::CString::new(user).unwrap();
18756                let pw = libc::getpwnam(c_user.as_ptr());
18757                if pw.is_null() {
18758                    eprintln!("chown: invalid user: '{}'", user);
18759                    return 1;
18760                }
18761                (*pw).pw_uid
18762            }
18763        };
18764
18765        let gid: u32 = match group {
18766            Some(g) if !g.is_empty() => {
18767                if let Ok(id) = g.parse() {
18768                    id
18769                } else {
18770                    unsafe {
18771                        let c_group = std::ffi::CString::new(g).unwrap();
18772                        let gr = libc::getgrnam(c_group.as_ptr());
18773                        if gr.is_null() {
18774                            eprintln!("chown: invalid group: '{}'", g);
18775                            return 1;
18776                        }
18777                        (*gr).gr_gid
18778                    }
18779                }
18780            }
18781            _ => u32::MAX,
18782        };
18783
18784        fn do_chown(path: &std::path::Path, uid: u32, gid: u32, recursive: bool) -> i32 {
18785            let c_path = match std::ffi::CString::new(path.to_string_lossy().as_bytes()) {
18786                Ok(p) => p,
18787                Err(_) => return 1,
18788            };
18789
18790            let ret = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
18791            if ret != 0 {
18792                eprintln!(
18793                    "chown: changing ownership of '{}': {}",
18794                    path.display(),
18795                    std::io::Error::last_os_error()
18796                );
18797                return 1;
18798            }
18799
18800            if recursive && path.is_dir() {
18801                if let Ok(entries) = std::fs::read_dir(path) {
18802                    for entry in entries.flatten() {
18803                        if do_chown(&entry.path(), uid, gid, true) != 0 {
18804                            return 1;
18805                        }
18806                    }
18807                }
18808            }
18809            0
18810        }
18811
18812        for file in files {
18813            if do_chown(std::path::Path::new(file), uid, gid, recursive) != 0 {
18814                return 1;
18815            }
18816        }
18817        0
18818    }
18819
18820    #[cfg(not(unix))]
18821    fn builtin_chown(&self, _args: &[String]) -> i32 {
18822        eprintln!("chown: not supported on this platform");
18823        1
18824    }
18825
18826    /// chmod - change file permissions
18827    fn builtin_chmod(&self, args: &[String]) -> i32 {
18828        let mut recursive = false;
18829        let mut positional: Vec<&str> = Vec::new();
18830
18831        for arg in args {
18832            match arg.as_str() {
18833                "-R" => recursive = true,
18834                s if !s.starts_with('-') => positional.push(s),
18835                _ => {}
18836            }
18837        }
18838
18839        if positional.len() < 2 {
18840            eprintln!("chmod: missing operand");
18841            return 1;
18842        }
18843
18844        let mode_spec = positional[0];
18845        let files = &positional[1..];
18846
18847        // Parse mode (octal or symbolic)
18848        let mode: Option<u32> = u32::from_str_radix(mode_spec, 8).ok();
18849
18850        if mode.is_none() {
18851            // Symbolic mode not fully implemented
18852            eprintln!("chmod: symbolic mode not implemented, use octal");
18853            return 1;
18854        }
18855
18856        let mode = mode.unwrap();
18857
18858        fn do_chmod(path: &std::path::Path, mode: u32, recursive: bool) -> i32 {
18859            #[cfg(unix)]
18860            {
18861                use std::os::unix::fs::PermissionsExt;
18862                if let Err(e) =
18863                    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
18864                {
18865                    eprintln!("chmod: changing permissions of '{}': {}", path.display(), e);
18866                    return 1;
18867                }
18868
18869                if recursive && path.is_dir() {
18870                    if let Ok(entries) = std::fs::read_dir(path) {
18871                        for entry in entries.flatten() {
18872                            if do_chmod(&entry.path(), mode, true) != 0 {
18873                                return 1;
18874                            }
18875                        }
18876                    }
18877                }
18878            }
18879            #[cfg(not(unix))]
18880            {
18881                let _ = (path, mode, recursive);
18882            }
18883            0
18884        }
18885
18886        for file in files {
18887            if do_chmod(std::path::Path::new(file), mode, recursive) != 0 {
18888                return 1;
18889            }
18890        }
18891        0
18892    }
18893
18894    /// zln/zmv/zcp - file operations (zsh/files module)
18895    fn builtin_zfiles(&self, cmd: &str, args: &[String]) -> i32 {
18896        let mut force = false;
18897        let mut verbose = false;
18898        let mut files: Vec<&str> = Vec::new();
18899
18900        for arg in args {
18901            match arg.as_str() {
18902                "-f" => force = true,
18903                "-v" => verbose = true,
18904                "-i" => {} // interactive - ignored
18905                s if !s.starts_with('-') => files.push(s),
18906                _ => {}
18907            }
18908        }
18909
18910        if files.len() < 2 {
18911            eprintln!("{}: missing operand", cmd);
18912            return 1;
18913        }
18914
18915        let target = files.pop().unwrap();
18916        let target_is_dir = std::path::Path::new(target).is_dir();
18917
18918        for src in files {
18919            let dest = if target_is_dir {
18920                format!(
18921                    "{}/{}",
18922                    target,
18923                    std::path::Path::new(src)
18924                        .file_name()
18925                        .map(|n| n.to_string_lossy().to_string())
18926                        .unwrap_or_else(|| src.to_string())
18927                )
18928            } else {
18929                target.to_string()
18930            };
18931
18932            if !force && std::path::Path::new(&dest).exists() {
18933                eprintln!("{}: '{}' already exists", cmd, dest);
18934                continue;
18935            }
18936
18937            let result = match cmd {
18938                "zln" => {
18939                    #[cfg(unix)]
18940                    {
18941                        std::os::unix::fs::symlink(src, &dest)
18942                    }
18943                    #[cfg(not(unix))]
18944                    {
18945                        Err(std::io::Error::new(
18946                            std::io::ErrorKind::Unsupported,
18947                            "symlinks not supported",
18948                        ))
18949                    }
18950                }
18951                "zcp" => std::fs::copy(src, &dest).map(|_| ()),
18952                "zmv" => std::fs::rename(src, &dest),
18953                _ => Ok(()),
18954            };
18955
18956            match result {
18957                Ok(()) => {
18958                    if verbose {
18959                        println!("{} -> {}", src, dest);
18960                    }
18961                }
18962                Err(e) => {
18963                    eprintln!("{}: {}: {}", cmd, src, e);
18964                    return 1;
18965                }
18966            }
18967        }
18968
18969        0
18970    }
18971
18972    /// coproc - manage coprocesses
18973    fn builtin_coproc(&mut self, args: &[String]) -> i32 {
18974        // Basic coproc implementation
18975        if args.is_empty() {
18976            // List coprocesses
18977            println!("(no coprocesses)");
18978            return 0;
18979        }
18980
18981        // Start a coprocess
18982        let cmd = args.join(" ");
18983        match std::process::Command::new("sh")
18984            .arg("-c")
18985            .arg(&cmd)
18986            .stdin(std::process::Stdio::piped())
18987            .stdout(std::process::Stdio::piped())
18988            .spawn()
18989        {
18990            Ok(child) => {
18991                println!("[coproc] {}", child.id());
18992                0
18993            }
18994            Err(e) => {
18995                eprintln!("coproc: {}", e);
18996                1
18997            }
18998        }
18999    }
19000
19001    /// zparseopts - parse options from positional parameters
19002    fn builtin_zparseopts(&mut self, args: &[String]) -> i32 {
19003        let mut remove_parsed = false; // -D
19004        let mut keep_going = false; // -E
19005        let mut fail_on_error = false; // -F
19006        let mut keep_values = false; // -K
19007        let mut _map_names = false; // -M (TODO: implement)
19008        let mut array_name: Option<String> = None; // -a
19009        let mut assoc_name: Option<String> = None; // -A
19010        let mut specs: Vec<String> = Vec::new();
19011
19012        let mut iter = args.iter().peekable();
19013
19014        // Parse zparseopts options
19015        while let Some(arg) = iter.next() {
19016            match arg.as_str() {
19017                "-D" => remove_parsed = true,
19018                "-E" => keep_going = true,
19019                "-F" => fail_on_error = true,
19020                "-K" => keep_values = true,
19021                "-M" => _map_names = true,
19022                "-a" => {
19023                    if let Some(name) = iter.next() {
19024                        array_name = Some(name.clone());
19025                    }
19026                }
19027                "-A" => {
19028                    if let Some(name) = iter.next() {
19029                        assoc_name = Some(name.clone());
19030                    }
19031                }
19032                "-" | "--" => break,
19033                s if !s.starts_with('-') || s.contains('=') || s.contains(':') => {
19034                    specs.push(s.to_string());
19035                }
19036                _ => specs.push(arg.clone()),
19037            }
19038        }
19039
19040        // Collect remaining specs
19041        for arg in iter {
19042            specs.push(arg.clone());
19043        }
19044
19045        // Parse the specs to understand what options we're looking for
19046        #[derive(Clone)]
19047        struct OptSpec {
19048            name: String,
19049            takes_arg: bool,
19050            optional_arg: bool,
19051            #[allow(dead_code)]
19052            append: bool,
19053            target_array: Option<String>,
19054        }
19055
19056        let mut opt_specs: Vec<OptSpec> = Vec::new();
19057        for spec in &specs {
19058            let mut s = spec.as_str();
19059            let mut target = None;
19060
19061            // Check for =array at end
19062            if let Some(eq_pos) = s.rfind('=') {
19063                if !s[eq_pos + 1..].contains(':') {
19064                    target = Some(s[eq_pos + 1..].to_string());
19065                    s = &s[..eq_pos];
19066                }
19067            }
19068
19069            let append = s.ends_with('+') || s.contains("+:");
19070            let s = s.trim_end_matches('+');
19071
19072            let (name, takes_arg, optional_arg) = if s.ends_with("::") {
19073                (s.trim_end_matches(':').trim_end_matches(':'), true, true)
19074            } else if s.ends_with(':') {
19075                (s.trim_end_matches(':'), true, false)
19076            } else {
19077                (s, false, false)
19078            };
19079
19080            opt_specs.push(OptSpec {
19081                name: name.to_string(),
19082                takes_arg,
19083                optional_arg,
19084                append,
19085                target_array: target,
19086            });
19087        }
19088
19089        // Get positional parameters to parse
19090        let positionals: Vec<String> = (1..=99)
19091            .map(|i| self.get_variable(&i.to_string()))
19092            .take_while(|v| !v.is_empty())
19093            .collect();
19094
19095        // Results
19096        let mut results: Vec<(String, Option<String>)> = Vec::new();
19097        let mut i = 0;
19098        let mut parsed_count = 0;
19099
19100        while i < positionals.len() {
19101            let arg = &positionals[i];
19102
19103            if arg == "-" || arg == "--" {
19104                parsed_count = i + 1;
19105                break;
19106            }
19107
19108            if !arg.starts_with('-') {
19109                if !keep_going {
19110                    break;
19111                }
19112                i += 1;
19113                continue;
19114            }
19115
19116            // Try to match against specs
19117            let opt_name = arg.trim_start_matches('-');
19118            let mut matched = false;
19119
19120            for spec in &opt_specs {
19121                if opt_name == spec.name || opt_name.starts_with(&format!("{}=", spec.name)) {
19122                    matched = true;
19123
19124                    if spec.takes_arg {
19125                        let arg_value = if opt_name.contains('=') {
19126                            Some(opt_name.splitn(2, '=').nth(1).unwrap_or("").to_string())
19127                        } else if i + 1 < positionals.len()
19128                            && (!positionals[i + 1].starts_with('-') || spec.optional_arg)
19129                        {
19130                            i += 1;
19131                            Some(positionals[i].clone())
19132                        } else if spec.optional_arg {
19133                            None
19134                        } else if fail_on_error {
19135                            eprintln!("zparseopts: missing argument for option: {}", spec.name);
19136                            return 1;
19137                        } else {
19138                            None
19139                        };
19140                        results.push((format!("-{}", spec.name), arg_value));
19141                    } else {
19142                        results.push((format!("-{}", spec.name), None));
19143                    }
19144                    break;
19145                }
19146            }
19147
19148            if !matched && !keep_going {
19149                break;
19150            }
19151
19152            i += 1;
19153            parsed_count = i;
19154        }
19155
19156        // Store results in array
19157        if let Some(arr_name) = &array_name {
19158            let mut arr_values: Vec<String> = Vec::new();
19159            for (opt, val) in &results {
19160                arr_values.push(opt.clone());
19161                if let Some(v) = val {
19162                    arr_values.push(v.clone());
19163                }
19164            }
19165            self.arrays.insert(arr_name.clone(), arr_values);
19166        }
19167
19168        // Store in associative array
19169        if let Some(assoc) = &assoc_name {
19170            let mut map: HashMap<String, String> = HashMap::new();
19171            for (opt, val) in &results {
19172                map.insert(opt.clone(), val.clone().unwrap_or_default());
19173            }
19174            self.assoc_arrays.insert(assoc.clone(), map);
19175        }
19176
19177        // Store in per-option arrays
19178        for spec in &opt_specs {
19179            if let Some(target) = &spec.target_array {
19180                let values: Vec<String> = results
19181                    .iter()
19182                    .filter(|(opt, _)| opt.trim_start_matches('-') == spec.name)
19183                    .flat_map(|(opt, val)| {
19184                        let mut v = vec![opt.clone()];
19185                        if let Some(arg) = val {
19186                            v.push(arg.clone());
19187                        }
19188                        v
19189                    })
19190                    .collect();
19191                if !values.is_empty() || !keep_values {
19192                    self.arrays.insert(target.clone(), values);
19193                }
19194            }
19195        }
19196
19197        // Remove parsed arguments if -D
19198        if remove_parsed && parsed_count > 0 {
19199            for i in 1..=parsed_count {
19200                self.variables.remove(&i.to_string());
19201                std::env::remove_var(i.to_string());
19202            }
19203            // Shift remaining
19204            let remaining: Vec<String> = ((parsed_count + 1)..=99)
19205                .map(|i| self.get_variable(&i.to_string()))
19206                .take_while(|v| !v.is_empty())
19207                .collect();
19208            for (i, val) in remaining.iter().enumerate() {
19209                self.variables.insert((i + 1).to_string(), val.clone());
19210            }
19211        }
19212
19213        0
19214    }
19215
19216    /// readonly - mark variables as read-only
19217    fn builtin_readonly(&mut self, args: &[String]) -> i32 {
19218        if args.is_empty() {
19219            // List readonly variables
19220            for name in &self.readonly_vars {
19221                if let Some(val) = self.variables.get(name) {
19222                    println!("readonly {}={}", name, val);
19223                }
19224            }
19225            return 0;
19226        }
19227
19228        for arg in args {
19229            if arg == "-p" {
19230                for name in &self.readonly_vars {
19231                    if let Some(val) = self.variables.get(name) {
19232                        println!("declare -r {}=\"{}\"", name, val);
19233                    }
19234                }
19235            } else if let Some(eq_pos) = arg.find('=') {
19236                let name = &arg[..eq_pos];
19237                let value = &arg[eq_pos + 1..];
19238                self.variables.insert(name.to_string(), value.to_string());
19239                self.readonly_vars.insert(name.to_string());
19240            } else {
19241                self.readonly_vars.insert(arg.clone());
19242            }
19243        }
19244        0
19245    }
19246
19247    /// unfunction - remove function definitions
19248    fn builtin_unfunction(&mut self, args: &[String]) -> i32 {
19249        for name in args {
19250            if self.functions.remove(name).is_none() {
19251                eprintln!("unfunction: no such function: {}", name);
19252            }
19253        }
19254        0
19255    }
19256
19257    /// getln - read line from buffer
19258    fn builtin_getln(&mut self, args: &[String]) -> i32 {
19259        if args.is_empty() {
19260            eprintln!("getln: missing variable name");
19261            return 1;
19262        }
19263        // Read from line buffer (simplified - just reads from stdin)
19264        let mut line = String::new();
19265        if std::io::stdin().read_line(&mut line).is_ok() {
19266            let line = line.trim_end_matches('\n');
19267            self.variables.insert(args[0].clone(), line.to_string());
19268            0
19269        } else {
19270            1
19271        }
19272    }
19273
19274    /// pushln - push line to buffer
19275    fn builtin_pushln(&mut self, args: &[String]) -> i32 {
19276        for arg in args {
19277            println!("{}", arg);
19278        }
19279        0
19280    }
19281
19282    /// bindkey - key binding management
19283    fn builtin_bindkey(&mut self, args: &[String]) -> i32 {
19284        use crate::zle::{zle, KeymapName};
19285
19286        if args.is_empty() {
19287            // List all bindings in main keymap
19288            let zle = zle();
19289            for (keys, widget) in zle
19290                .keymaps
19291                .get(&KeymapName::Main)
19292                .map(|km| km.list_bindings().collect::<Vec<_>>())
19293                .unwrap_or_default()
19294            {
19295                println!("\"{}\" {}", keys, widget);
19296            }
19297            return 0;
19298        }
19299
19300        let mut iter = args.iter().peekable();
19301        let mut keymap = KeymapName::Main;
19302        let mut list_mode = false;
19303        let mut list_all = false;
19304        let mut remove = false;
19305
19306        while let Some(arg) = iter.next() {
19307            match arg.as_str() {
19308                "-l" => {
19309                    list_mode = true;
19310                }
19311                "-L" => {
19312                    list_mode = true;
19313                    list_all = true;
19314                }
19315                "-la" | "-lL" => {
19316                    list_mode = true;
19317                    list_all = true;
19318                }
19319                "-M" => {
19320                    if let Some(name) = iter.next() {
19321                        if let Some(km) = KeymapName::from_str(name) {
19322                            keymap = km;
19323                        }
19324                    }
19325                }
19326                "-r" => {
19327                    remove = true;
19328                }
19329                "-A" => {
19330                    // Link keymaps - stub
19331                    return 0;
19332                }
19333                "-N" => {
19334                    // Create new keymap - stub
19335                    return 0;
19336                }
19337                "-e" => {
19338                    keymap = KeymapName::Emacs;
19339                }
19340                "-v" => {
19341                    keymap = KeymapName::ViInsert;
19342                }
19343                "-a" => {
19344                    keymap = KeymapName::ViCommand;
19345                }
19346                key if !key.starts_with('-') => {
19347                    // Key sequence - next arg is widget
19348                    if let Some(widget) = iter.next() {
19349                        let mut zle = zle();
19350                        if remove {
19351                            zle.unbind_key(keymap, key);
19352                        } else {
19353                            zle.bind_key(keymap, key, widget);
19354                        }
19355                    }
19356                    return 0;
19357                }
19358                _ => {}
19359            }
19360        }
19361
19362        if list_mode {
19363            let zle = zle();
19364            if list_all {
19365                for km_name in &[
19366                    KeymapName::Emacs,
19367                    KeymapName::ViInsert,
19368                    KeymapName::ViCommand,
19369                ] {
19370                    println!("{}", km_name.as_str());
19371                }
19372            } else {
19373                if let Some(km) = zle.keymaps.get(&keymap) {
19374                    for (keys, widget) in km.list_bindings() {
19375                        println!("bindkey \"{}\" {}", keys, widget);
19376                    }
19377                }
19378            }
19379        }
19380
19381        0
19382    }
19383
19384    /// zle - line editor control
19385    fn builtin_zle(&mut self, args: &[String]) -> i32 {
19386        use crate::zle::zle;
19387
19388        if args.is_empty() {
19389            return 0;
19390        }
19391
19392        let mut iter = args.iter().peekable();
19393
19394        while let Some(arg) = iter.next() {
19395            match arg.as_str() {
19396                "-l" => {
19397                    // List widgets
19398                    let zle = zle();
19399                    let mut widgets: Vec<&str> = zle.list_widgets();
19400                    widgets.sort();
19401                    for w in widgets {
19402                        println!("{}", w);
19403                    }
19404                    return 0;
19405                }
19406                "-la" | "-lL" => {
19407                    // List all widgets with details
19408                    let zle = zle();
19409                    let mut widgets: Vec<&str> = zle.list_widgets();
19410                    widgets.sort();
19411                    for w in widgets {
19412                        println!("{}", w);
19413                    }
19414                    return 0;
19415                }
19416                "-N" => {
19417                    // Define new widget: zle -N widget-name [function]
19418                    if let Some(widget_name) = iter.next() {
19419                        let func_name = iter
19420                            .next()
19421                            .map(|s| s.as_str())
19422                            .unwrap_or(widget_name.as_str());
19423                        let mut zle = zle();
19424                        zle.define_widget(widget_name, func_name);
19425                    }
19426                    return 0;
19427                }
19428                "-D" => {
19429                    // Delete widget - stub
19430                    return 0;
19431                }
19432                "-A" => {
19433                    // Define widget alias - stub
19434                    return 0;
19435                }
19436                "-R" => {
19437                    // Redisplay
19438                    return 0;
19439                }
19440                "-U" => {
19441                    // Unget characters - stub
19442                    return 0;
19443                }
19444                "-K" => {
19445                    // Select keymap - stub
19446                    return 0;
19447                }
19448                "-F" => {
19449                    // Install file descriptor handler - stub
19450                    return 0;
19451                }
19452                "-M" => {
19453                    // Display message - stub
19454                    return 0;
19455                }
19456                "-I" => {
19457                    // Invalidate completion - stub
19458                    return 0;
19459                }
19460                "-f" => {
19461                    // Check widget exists
19462                    if let Some(name) = iter.next() {
19463                        let zle = zle();
19464                        return if zle.get_widget(name).is_some() { 0 } else { 1 };
19465                    }
19466                    return 1;
19467                }
19468                widget_name if !widget_name.starts_with('-') => {
19469                    // Call widget
19470                    let mut zle = zle();
19471                    match zle.execute_widget(widget_name, None) {
19472                        crate::zle::WidgetResult::Ok => return 0,
19473                        crate::zle::WidgetResult::Error(e) => {
19474                            eprintln!("zle: {}", e);
19475                            return 1;
19476                        }
19477                        crate::zle::WidgetResult::CallFunction(func) => {
19478                            // Would need to call shell function
19479                            drop(zle);
19480                            if let Some(f) = self.functions.get(&func).cloned() {
19481                                return self.call_function(&f, &[]).unwrap_or(1);
19482                            }
19483                            return 1;
19484                        }
19485                        _ => return 0,
19486                    }
19487                }
19488                _ => {}
19489            }
19490        }
19491
19492        0
19493    }
19494
19495    /// sched - scheduled command execution (stub)
19496    fn builtin_sched(&mut self, args: &[String]) -> i32 {
19497        use std::time::{Duration, SystemTime};
19498
19499        if args.is_empty() {
19500            // List scheduled commands
19501            if self.scheduled_commands.is_empty() {
19502                return 0;
19503            }
19504            let now = SystemTime::now();
19505            for cmd in &self.scheduled_commands {
19506                let remaining = cmd.run_at.duration_since(now).unwrap_or(Duration::ZERO);
19507                println!("{:3}  +{:5}  {}", cmd.id, remaining.as_secs(), cmd.command);
19508            }
19509            return 0;
19510        }
19511
19512        let mut i = 0;
19513        while i < args.len() {
19514            match args[i].as_str() {
19515                "-" => {
19516                    // Remove scheduled item
19517                    i += 1;
19518                    if i >= args.len() {
19519                        eprintln!("sched: -: need item number");
19520                        return 1;
19521                    }
19522                    if let Ok(id) = args[i].parse::<u32>() {
19523                        self.scheduled_commands.retain(|c| c.id != id);
19524                        return 0;
19525                    } else {
19526                        eprintln!("sched: invalid item number");
19527                        return 1;
19528                    }
19529                }
19530                "+" => {
19531                    // Schedule relative time
19532                    i += 1;
19533                    if i >= args.len() {
19534                        eprintln!("sched: +: need time");
19535                        return 1;
19536                    }
19537                    let secs: u64 = args[i].parse().unwrap_or(0);
19538                    i += 1;
19539                    let command = args[i..].join(" ");
19540
19541                    let id = self.scheduled_commands.len() as u32 + 1;
19542                    self.scheduled_commands.push(ScheduledCommand {
19543                        id,
19544                        run_at: SystemTime::now() + Duration::from_secs(secs),
19545                        command,
19546                    });
19547                    return 0;
19548                }
19549                time_str => {
19550                    // Parse HH:MM or HH:MM:SS
19551                    let parts: Vec<&str> = time_str.split(':').collect();
19552                    if parts.len() >= 2 {
19553                        let hour: u32 = parts[0].parse().unwrap_or(0);
19554                        let min: u32 = parts[1].parse().unwrap_or(0);
19555                        let sec: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
19556
19557                        // Calculate duration until that time today/tomorrow
19558                        let now = SystemTime::now();
19559                        let target_secs = (hour * 3600 + min * 60 + sec) as u64;
19560                        let _day_secs = 86400u64;
19561
19562                        // Simplified: just add as seconds from now
19563                        let run_at = now + Duration::from_secs(target_secs);
19564
19565                        i += 1;
19566                        let command = args[i..].join(" ");
19567
19568                        let id = self.scheduled_commands.len() as u32 + 1;
19569                        self.scheduled_commands.push(ScheduledCommand {
19570                            id,
19571                            run_at,
19572                            command,
19573                        });
19574                        return 0;
19575                    } else {
19576                        eprintln!("sched: invalid time format");
19577                        return 1;
19578                    }
19579                }
19580            }
19581        }
19582        0
19583    }
19584
19585    /// zcompile - compile shell scripts to ZWC format
19586    fn builtin_zcompile(&mut self, args: &[String]) -> i32 {
19587        use crate::zwc::{ZwcBuilder, ZwcFile};
19588
19589        let mut list_mode = false; // -t: list functions in zwc
19590        let mut compile_current = false; // -c: compile current functions
19591        let mut compile_auto = false; // -a: compile autoload functions
19592        let mut files: Vec<String> = Vec::new();
19593
19594        let mut i = 0;
19595        while i < args.len() {
19596            let arg = &args[i];
19597            if arg.starts_with('-') && arg.len() > 1 {
19598                for c in arg[1..].chars() {
19599                    match c {
19600                        't' => list_mode = true,
19601                        'c' => compile_current = true,
19602                        'a' => compile_auto = true,
19603                        'U' | 'M' | 'R' | 'm' | 'z' | 'k' => {} // ignored for now
19604                        _ => {
19605                            eprintln!("zcompile: unknown option: -{}", c);
19606                            return 1;
19607                        }
19608                    }
19609                }
19610            } else {
19611                files.push(arg.clone());
19612            }
19613            i += 1;
19614        }
19615
19616        if files.is_empty() {
19617            eprintln!("zcompile: not enough arguments");
19618            return 1;
19619        }
19620
19621        // -t mode: list functions in ZWC file
19622        if list_mode {
19623            let zwc_path = if files[0].ends_with(".zwc") {
19624                files[0].clone()
19625            } else {
19626                format!("{}.zwc", files[0])
19627            };
19628
19629            match ZwcFile::load(&zwc_path) {
19630                Ok(zwc) => {
19631                    println!("zwc file for zshrs-{}", env!("CARGO_PKG_VERSION"));
19632                    if files.len() > 1 {
19633                        // Check specific functions
19634                        for name in &files[1..] {
19635                            if zwc.get_function(name).is_some() {
19636                                println!("{}", name);
19637                            } else {
19638                                eprintln!("zcompile: function not found: {}", name);
19639                                return 1;
19640                            }
19641                        }
19642                    } else {
19643                        // List all functions
19644                        for name in zwc.list_functions() {
19645                            println!("{}", name);
19646                        }
19647                    }
19648                    return 0;
19649                }
19650                Err(e) => {
19651                    eprintln!("zcompile: can't read zwc file: {}: {}", zwc_path, e);
19652                    return 1;
19653                }
19654            }
19655        }
19656
19657        // -c or -a mode: compile current/autoload functions
19658        if compile_current || compile_auto {
19659            let zwc_path = if files[0].ends_with(".zwc") {
19660                files[0].clone()
19661            } else {
19662                format!("{}.zwc", files[0])
19663            };
19664
19665            let mut builder = ZwcBuilder::new();
19666
19667            if files.len() > 1 {
19668                // Compile specific functions
19669                for name in &files[1..] {
19670                    if let Some(func) = self.functions.get(name) {
19671                        // Serialize the function (simplified - just store as comment for now)
19672                        let source = format!("# Compiled function: {}\n# Body: {:?}", name, func);
19673                        builder.add_source(name, &source);
19674                    } else if compile_auto && self.autoload_pending.contains_key(name) {
19675                        // Try to load autoload function source
19676                        if let Some(path) = self.find_function_file(name) {
19677                            if let Err(e) = builder.add_file(&path) {
19678                                eprintln!("zcompile: can't read {}: {}", name, e);
19679                                return 1;
19680                            }
19681                        }
19682                    } else {
19683                        eprintln!("zcompile: no such function: {}", name);
19684                        return 1;
19685                    }
19686                }
19687            } else {
19688                // Compile all functions
19689                for (name, func) in &self.functions {
19690                    let source = format!("# Compiled function: {}\n# Body: {:?}", name, func);
19691                    builder.add_source(name, &source);
19692                }
19693            }
19694
19695            if let Err(e) = builder.write(&zwc_path) {
19696                eprintln!("zcompile: can't write {}: {}", zwc_path, e);
19697                return 1;
19698            }
19699            return 0;
19700        }
19701
19702        // Default: compile files to ZWC
19703        let zwc_path = if files[0].ends_with(".zwc") {
19704            files[0].clone()
19705        } else {
19706            format!("{}.zwc", files[0])
19707        };
19708
19709        let mut builder = ZwcBuilder::new();
19710
19711        // If only one file given, it's both the source and output base
19712        let source_files = if files.len() == 1 {
19713            // Check if it's a directory
19714            let path = std::path::Path::new(&files[0]);
19715            if path.is_dir() {
19716                // Compile all files in directory
19717                match std::fs::read_dir(path) {
19718                    Ok(entries) => {
19719                        for entry in entries.flatten() {
19720                            let p = entry.path();
19721                            if p.is_file() && !p.extension().map_or(false, |e| e == "zwc") {
19722                                if let Err(e) = builder.add_file(&p) {
19723                                    eprintln!("zcompile: can't read {:?}: {}", p, e);
19724                                }
19725                            }
19726                        }
19727                    }
19728                    Err(e) => {
19729                        eprintln!("zcompile: can't read directory: {}", e);
19730                        return 1;
19731                    }
19732                }
19733                vec![]
19734            } else {
19735                vec![files[0].clone()]
19736            }
19737        } else {
19738            files[1..].to_vec()
19739        };
19740
19741        for file in &source_files {
19742            let path = std::path::Path::new(file);
19743            if let Err(e) = builder.add_file(path) {
19744                eprintln!("zcompile: can't read {}: {}", file, e);
19745                return 1;
19746            }
19747        }
19748
19749        if let Err(e) = builder.write(&zwc_path) {
19750            eprintln!("zcompile: can't write {}: {}", zwc_path, e);
19751            return 1;
19752        }
19753
19754        0
19755    }
19756
19757    /// zformat - format strings
19758    fn builtin_zformat(&self, args: &[String]) -> i32 {
19759        if args.len() < 2 {
19760            eprintln!("zformat: not enough arguments");
19761            return 1;
19762        }
19763
19764        match args[0].as_str() {
19765            "-f" => {
19766                // Format string: zformat -f var format specs...
19767                if args.len() < 3 {
19768                    return 1;
19769                }
19770                let _var_name = &args[1];
19771                let format = &args[2];
19772                let specs: HashMap<char, &str> = args[3..]
19773                    .iter()
19774                    .filter_map(|s| {
19775                        let mut chars = s.chars();
19776                        let key = chars.next()?;
19777                        if chars.next() == Some(':') {
19778                            Some((key, &s[2..]))
19779                        } else {
19780                            None
19781                        }
19782                    })
19783                    .collect();
19784
19785                let mut result = String::new();
19786                let mut chars = format.chars().peekable();
19787                while let Some(c) = chars.next() {
19788                    if c == '%' {
19789                        if let Some(&spec_char) = chars.peek() {
19790                            if let Some(replacement) = specs.get(&spec_char) {
19791                                result.push_str(replacement);
19792                                chars.next();
19793                                continue;
19794                            }
19795                        }
19796                    }
19797                    result.push(c);
19798                }
19799                println!("{}", result);
19800            }
19801            "-a" => {
19802                // Format into array elements: zformat -a array sep specs...
19803                // Each spec is "text:value" or "text:value:cond"
19804                if args.len() < 4 {
19805                    eprintln!("zformat -a: need array, separator, and specs");
19806                    return 1;
19807                }
19808                let _array_name = &args[1];
19809                let sep = &args[2];
19810
19811                let mut results = Vec::new();
19812                for spec in &args[3..] {
19813                    let parts: Vec<&str> = spec.splitn(3, ':').collect();
19814                    if parts.len() >= 2 {
19815                        let text = parts[0];
19816                        let value = parts[1];
19817                        let cond = parts.get(2).copied();
19818
19819                        // If condition exists and is empty/false, skip
19820                        if let Some(c) = cond {
19821                            if c.is_empty() || c == "0" {
19822                                continue;
19823                            }
19824                        }
19825
19826                        if !value.is_empty() {
19827                            results.push(format!("{}{}{}", text, sep, value));
19828                        }
19829                    }
19830                }
19831
19832                for r in results {
19833                    println!("{}", r);
19834                }
19835            }
19836            _ => {
19837                eprintln!("zformat: unknown option: {}", args[0]);
19838                return 1;
19839            }
19840        }
19841        0
19842    }
19843
19844    /// vared - visually edit a variable
19845    fn builtin_vared(&mut self, args: &[String]) -> i32 {
19846        if args.is_empty() {
19847            eprintln!("vared: not enough arguments");
19848            return 1;
19849        }
19850
19851        let mut var_name = String::new();
19852        let mut prompt = String::new();
19853        let mut rprompt = String::new();
19854        let mut _history = false; // TODO: implement history completion
19855        let mut i = 0;
19856
19857        while i < args.len() {
19858            match args[i].as_str() {
19859                "-p" if i + 1 < args.len() => {
19860                    i += 1;
19861                    prompt = args[i].clone();
19862                }
19863                "-r" if i + 1 < args.len() => {
19864                    i += 1;
19865                    rprompt = args[i].clone();
19866                }
19867                "-h" => _history = true,
19868                "-c" => {} // Use completion - ignored
19869                "-e" => {} // Use emacs mode - ignored
19870                "-M" | "-m" => {
19871                    i += 1;
19872                } // Main/alt keymap - skip arg
19873                "-a" | "-A" => {
19874                    i += 1;
19875                } // Array assignment - skip arg
19876                s if !s.starts_with('-') => {
19877                    var_name = s.to_string();
19878                }
19879                _ => {}
19880            }
19881            i += 1;
19882        }
19883
19884        if var_name.is_empty() {
19885            eprintln!("vared: not enough arguments");
19886            return 1;
19887        }
19888
19889        // Get current value
19890        let current = self.get_variable(&var_name);
19891
19892        // Simple line editing using stdin
19893        if !prompt.is_empty() {
19894            eprint!("{}", prompt);
19895        }
19896        print!("{}", current);
19897        if !rprompt.is_empty() {
19898            eprint!("{}", rprompt);
19899        }
19900
19901        let mut input = String::new();
19902        if std::io::stdin().read_line(&mut input).is_ok() {
19903            let value = input.trim_end_matches('\n').to_string();
19904            self.variables.insert(var_name, value);
19905            return 0;
19906        }
19907        1
19908    }
19909
19910    /// echotc - output termcap value
19911    fn builtin_echotc(&self, args: &[String]) -> i32 {
19912        if args.is_empty() {
19913            eprintln!("echotc: not enough arguments");
19914            return 1;
19915        }
19916
19917        // Common termcap capabilities
19918        match args[0].as_str() {
19919            "cl" => print!("\x1b[H\x1b[2J"), // clear screen
19920            "cd" => print!("\x1b[J"),        // clear to end of display
19921            "ce" => print!("\x1b[K"),        // clear to end of line
19922            "cm" => {
19923                // cursor motion - needs row, col args
19924                if args.len() >= 3 {
19925                    if let (Ok(row), Ok(col)) = (args[1].parse::<u32>(), args[2].parse::<u32>()) {
19926                        print!("\x1b[{};{}H", row + 1, col + 1);
19927                    }
19928                }
19929            }
19930            "up" => print!("\x1b[A"),    // cursor up
19931            "do" => print!("\x1b[B"),    // cursor down
19932            "le" => print!("\x1b[D"),    // cursor left
19933            "nd" => print!("\x1b[C"),    // cursor right
19934            "ho" => print!("\x1b[H"),    // home cursor
19935            "vi" => print!("\x1b[?25l"), // invisible cursor
19936            "ve" => print!("\x1b[?25h"), // visible cursor
19937            "so" => print!("\x1b[7m"),   // standout mode
19938            "se" => print!("\x1b[27m"),  // end standout
19939            "us" => print!("\x1b[4m"),   // underline
19940            "ue" => print!("\x1b[24m"),  // end underline
19941            "md" => print!("\x1b[1m"),   // bold
19942            "me" => print!("\x1b[0m"),   // end all attributes
19943            "mr" => print!("\x1b[7m"),   // reverse video
19944            "AF" | "setaf" => {
19945                // Set foreground color
19946                if args.len() >= 2 {
19947                    if let Ok(color) = args[1].parse::<u32>() {
19948                        print!("\x1b[38;5;{}m", color);
19949                    }
19950                }
19951            }
19952            "AB" | "setab" => {
19953                // Set background color
19954                if args.len() >= 2 {
19955                    if let Ok(color) = args[1].parse::<u32>() {
19956                        print!("\x1b[48;5;{}m", color);
19957                    }
19958                }
19959            }
19960            "Co" | "colors" => {
19961                // Number of colors - assume 256
19962                println!("256");
19963            }
19964            "co" | "cols" => {
19965                // Number of columns
19966                println!(
19967                    "{}",
19968                    std::env::var("COLUMNS")
19969                        .ok()
19970                        .and_then(|s| s.parse().ok())
19971                        .unwrap_or(80u16)
19972                );
19973            }
19974            "li" | "lines" => {
19975                // Number of lines
19976                println!(
19977                    "{}",
19978                    std::env::var("LINES")
19979                        .ok()
19980                        .and_then(|s| s.parse().ok())
19981                        .unwrap_or(24u16)
19982                );
19983            }
19984            cap => {
19985                eprintln!("echotc: unknown capability: {}", cap);
19986                return 1;
19987            }
19988        }
19989        use std::io::Write;
19990        let _ = std::io::stdout().flush();
19991        0
19992    }
19993
19994    /// echoti - output terminfo value
19995    fn builtin_echoti(&self, args: &[String]) -> i32 {
19996        // echoti is similar to echotc but uses terminfo names
19997        // For simplicity, we'll use the same implementation
19998        self.builtin_echotc(args)
19999    }
20000
20001    /// zpty - manage pseudo-terminals
20002    fn builtin_zpty(&mut self, args: &[String]) -> i32 {
20003        use std::io::{Read, Write};
20004        use std::process::{Command, Stdio};
20005
20006        if args.is_empty() {
20007            // List all ptys
20008            if self.zptys.is_empty() {
20009                return 0;
20010            }
20011            for (name, state) in &self.zptys {
20012                println!("{}: {} (pid {})", name, state.cmd, state.pid);
20013            }
20014            return 0;
20015        }
20016
20017        let mut i = 0;
20018        while i < args.len() {
20019            match args[i].as_str() {
20020                "-d" => {
20021                    // Delete pty
20022                    i += 1;
20023                    if i >= args.len() {
20024                        eprintln!("zpty: -d requires pty name");
20025                        return 1;
20026                    }
20027                    let name = &args[i];
20028                    if let Some(mut state) = self.zptys.remove(name) {
20029                        if let Some(ref mut child) = state.child {
20030                            let _ = child.kill();
20031                        }
20032                        return 0;
20033                    } else {
20034                        eprintln!("zpty: no such pty: {}", name);
20035                        return 1;
20036                    }
20037                }
20038                "-w" => {
20039                    // Write to pty: zpty -w name string...
20040                    i += 1;
20041                    if i >= args.len() {
20042                        eprintln!("zpty: -w requires pty name");
20043                        return 1;
20044                    }
20045                    let name = args[i].clone();
20046                    i += 1;
20047                    let data = args[i..].join(" ") + "\n";
20048
20049                    if let Some(state) = self.zptys.get_mut(&name) {
20050                        if let Some(ref mut stdin) = state.stdin {
20051                            if stdin.write_all(data.as_bytes()).is_ok() {
20052                                let _ = stdin.flush();
20053                                return 0;
20054                            }
20055                        }
20056                        eprintln!("zpty: write failed");
20057                        return 1;
20058                    } else {
20059                        eprintln!("zpty: no such pty: {}", name);
20060                        return 1;
20061                    }
20062                }
20063                "-r" => {
20064                    // Read from pty: zpty -r name [param]
20065                    i += 1;
20066                    if i >= args.len() {
20067                        eprintln!("zpty: -r requires pty name");
20068                        return 1;
20069                    }
20070                    let name = args[i].clone();
20071                    i += 1;
20072                    let var_name = if i < args.len() {
20073                        args[i].clone()
20074                    } else {
20075                        "REPLY".to_string()
20076                    };
20077
20078                    if let Some(state) = self.zptys.get_mut(&name) {
20079                        if let Some(ref mut stdout) = state.stdout {
20080                            let mut buf = vec![0u8; 4096];
20081                            match stdout.read(&mut buf) {
20082                                Ok(n) => {
20083                                    let data = String::from_utf8_lossy(&buf[..n]).to_string();
20084                                    self.variables.insert(var_name, data);
20085                                    return 0;
20086                                }
20087                                Err(_) => return 1,
20088                            }
20089                        }
20090                        return 1;
20091                    } else {
20092                        eprintln!("zpty: no such pty: {}", name);
20093                        return 1;
20094                    }
20095                }
20096                "-t" => {
20097                    // Test if data available
20098                    i += 1;
20099                    if i >= args.len() {
20100                        return 1;
20101                    }
20102                    let name = &args[i];
20103                    if self.zptys.contains_key(name) {
20104                        return 0; // Assume data available if pty exists
20105                    }
20106                    return 1;
20107                }
20108                "-L" => {
20109                    // List in script-friendly format
20110                    for (name, state) in &self.zptys {
20111                        println!("zpty {} {}", name, state.cmd);
20112                    }
20113                    return 0;
20114                }
20115                "-b" | "-e" => {
20116                    // Options: -b (blocking), -e (echo)
20117                    i += 1;
20118                    continue;
20119                }
20120                name if !name.starts_with('-') => {
20121                    // Create new pty: zpty name command [args...]
20122                    i += 1;
20123                    if i >= args.len() {
20124                        eprintln!("zpty: command required");
20125                        return 1;
20126                    }
20127                    let cmd_str = args[i..].join(" ");
20128
20129                    match Command::new("sh")
20130                        .arg("-c")
20131                        .arg(&cmd_str)
20132                        .stdin(Stdio::piped())
20133                        .stdout(Stdio::piped())
20134                        .stderr(Stdio::piped())
20135                        .spawn()
20136                    {
20137                        Ok(mut child) => {
20138                            let pid = child.id();
20139                            let stdin = child.stdin.take();
20140                            let stdout = child.stdout.take();
20141
20142                            self.zptys.insert(
20143                                name.to_string(),
20144                                ZptyState {
20145                                    pid,
20146                                    cmd: cmd_str,
20147                                    stdin,
20148                                    stdout,
20149                                    child: Some(child),
20150                                },
20151                            );
20152                            return 0;
20153                        }
20154                        Err(e) => {
20155                            eprintln!("zpty: failed to start: {}", e);
20156                            return 1;
20157                        }
20158                    }
20159                }
20160                _ => {
20161                    i += 1;
20162                }
20163            }
20164            i += 1;
20165        }
20166        0
20167    }
20168
20169    /// zprof - profiling support
20170    fn builtin_zprof(&mut self, args: &[String]) -> i32 {
20171        use crate::zprof::ZprofOptions;
20172
20173        let options = ZprofOptions {
20174            clear: args.iter().any(|a| a == "-c"),
20175        };
20176
20177        let (status, output) = crate::zprof::builtin_zprof(&mut self.profiler, &options);
20178        if !output.is_empty() {
20179            print!("{}", output);
20180        }
20181        status
20182    }
20183
20184    /// zsocket - create/manage sockets
20185    fn builtin_zsocket(&mut self, args: &[String]) -> i32 {
20186        use std::os::unix::net::{UnixListener, UnixStream};
20187
20188        if args.is_empty() {
20189            // List open sockets
20190            if self.unix_sockets.is_empty() {
20191                return 0;
20192            }
20193            for (fd, state) in &self.unix_sockets {
20194                let path = state
20195                    .path
20196                    .as_ref()
20197                    .map(|p| p.display().to_string())
20198                    .unwrap_or_default();
20199                let status = if state.listening {
20200                    "listening"
20201                } else {
20202                    "connected"
20203                };
20204                println!("{}: {} ({})", fd, path, status);
20205            }
20206            return 0;
20207        }
20208
20209        let mut i = 0;
20210        let mut verbose = false;
20211        let mut var_name = "REPLY".to_string();
20212
20213        while i < args.len() {
20214            match args[i].as_str() {
20215                "-v" => {
20216                    verbose = true;
20217                    i += 1;
20218                    if i < args.len() && !args[i].starts_with('-') {
20219                        var_name = args[i].clone();
20220                    }
20221                }
20222                "-l" => {
20223                    // Listen on Unix socket: zsocket -l path
20224                    i += 1;
20225                    if i >= args.len() {
20226                        eprintln!("zsocket: -l requires path");
20227                        return 1;
20228                    }
20229                    let path = PathBuf::from(&args[i]);
20230
20231                    // Remove existing socket file
20232                    let _ = std::fs::remove_file(&path);
20233
20234                    match UnixListener::bind(&path) {
20235                        Ok(listener) => {
20236                            let fd = self.next_fd;
20237                            self.next_fd += 1;
20238
20239                            self.unix_sockets.insert(
20240                                fd,
20241                                UnixSocketState {
20242                                    path: Some(path),
20243                                    listening: true,
20244                                    stream: None,
20245                                    listener: Some(listener),
20246                                },
20247                            );
20248
20249                            if verbose {
20250                                self.variables.insert(var_name.clone(), fd.to_string());
20251                            }
20252                            println!("{}", fd);
20253                            return 0;
20254                        }
20255                        Err(e) => {
20256                            eprintln!("zsocket: bind failed: {}", e);
20257                            return 1;
20258                        }
20259                    }
20260                }
20261                "-a" => {
20262                    // Accept connection: zsocket -a fd
20263                    i += 1;
20264                    if i >= args.len() {
20265                        eprintln!("zsocket: -a requires fd");
20266                        return 1;
20267                    }
20268                    let listen_fd: i32 = args[i].parse().unwrap_or(-1);
20269
20270                    if let Some(state) = self.unix_sockets.get(&listen_fd) {
20271                        if let Some(ref listener) = state.listener {
20272                            match listener.accept() {
20273                                Ok((stream, _addr)) => {
20274                                    let new_fd = self.next_fd;
20275                                    self.next_fd += 1;
20276
20277                                    self.unix_sockets.insert(
20278                                        new_fd,
20279                                        UnixSocketState {
20280                                            path: None,
20281                                            listening: false,
20282                                            stream: Some(stream),
20283                                            listener: None,
20284                                        },
20285                                    );
20286
20287                                    if verbose {
20288                                        self.variables.insert(var_name.clone(), new_fd.to_string());
20289                                    }
20290                                    println!("{}", new_fd);
20291                                    return 0;
20292                                }
20293                                Err(e) => {
20294                                    eprintln!("zsocket: accept failed: {}", e);
20295                                    return 1;
20296                                }
20297                            }
20298                        }
20299                    }
20300                    eprintln!("zsocket: invalid fd");
20301                    return 1;
20302                }
20303                "-d" => {
20304                    // Close socket: zsocket -d fd
20305                    i += 1;
20306                    if i >= args.len() {
20307                        eprintln!("zsocket: -d requires fd");
20308                        return 1;
20309                    }
20310                    let fd: i32 = args[i].parse().unwrap_or(-1);
20311
20312                    if let Some(state) = self.unix_sockets.remove(&fd) {
20313                        if let Some(path) = state.path {
20314                            let _ = std::fs::remove_file(path);
20315                        }
20316                        return 0;
20317                    }
20318                    eprintln!("zsocket: no such fd");
20319                    return 1;
20320                }
20321                path if !path.starts_with('-') => {
20322                    // Connect to Unix socket: zsocket path
20323                    match UnixStream::connect(path) {
20324                        Ok(stream) => {
20325                            let fd = self.next_fd;
20326                            self.next_fd += 1;
20327
20328                            self.unix_sockets.insert(
20329                                fd,
20330                                UnixSocketState {
20331                                    path: Some(PathBuf::from(path)),
20332                                    listening: false,
20333                                    stream: Some(stream),
20334                                    listener: None,
20335                                },
20336                            );
20337
20338                            if verbose {
20339                                self.variables.insert(var_name.clone(), fd.to_string());
20340                            }
20341                            println!("{}", fd);
20342                            return 0;
20343                        }
20344                        Err(e) => {
20345                            eprintln!("zsocket: connect failed: {}", e);
20346                            return 1;
20347                        }
20348                    }
20349                }
20350                _ => {}
20351            }
20352            i += 1;
20353        }
20354        0
20355    }
20356
20357    /// ztcp - TCP socket operations
20358    fn builtin_ztcp(&mut self, args: &[String]) -> i32 {
20359        // Similar to zsocket but TCP specific
20360        self.builtin_zsocket(args)
20361    }
20362
20363    /// zregexparse - parse with regex
20364    fn builtin_zregexparse(&mut self, args: &[String]) -> i32 {
20365        if args.len() < 2 {
20366            eprintln!("zregexparse: usage: zregexparse var pattern [string]");
20367            return 1;
20368        }
20369
20370        let var_name = &args[0];
20371        let pattern = &args[1];
20372        let string = if args.len() > 2 {
20373            args[2].clone()
20374        } else {
20375            self.variables.get("REPLY").cloned().unwrap_or_default()
20376        };
20377
20378        match regex::Regex::new(pattern) {
20379            Ok(re) => {
20380                if let Some(captures) = re.captures(&string) {
20381                    // Store full match in var
20382                    if let Some(m) = captures.get(0) {
20383                        self.variables
20384                            .insert(var_name.clone(), m.as_str().to_string());
20385                    }
20386
20387                    // Store capture groups in MATCH array
20388                    let mut match_array = Vec::new();
20389                    let mut mbegin_array = Vec::new();
20390                    let mut mend_array = Vec::new();
20391
20392                    for (i, cap) in captures.iter().enumerate() {
20393                        if let Some(c) = cap {
20394                            match_array.push(c.as_str().to_string());
20395                            mbegin_array.push((c.start() + 1).to_string());
20396                            mend_array.push(c.end().to_string());
20397                            self.variables
20398                                .insert(format!("match[{}]", i), c.as_str().to_string());
20399                        }
20400                    }
20401                    self.arrays.insert("match".to_string(), match_array);
20402                    self.arrays.insert("mbegin".to_string(), mbegin_array);
20403                    self.arrays.insert("mend".to_string(), mend_array);
20404
20405                    // Store match positions
20406                    if let Some(m) = captures.get(0) {
20407                        self.variables
20408                            .insert("MBEGIN".to_string(), (m.start() + 1).to_string());
20409                        self.variables
20410                            .insert("MEND".to_string(), m.end().to_string());
20411                    }
20412
20413                    0
20414                } else {
20415                    1
20416                }
20417            }
20418            Err(e) => {
20419                eprintln!("zregexparse: invalid regex: {}", e);
20420                2
20421            }
20422        }
20423    }
20424
20425    /// clone - create a subshell with forked state
20426    fn builtin_clone(&mut self, args: &[String]) -> i32 {
20427        use std::process::Command;
20428
20429        // clone creates a subshell that shares the parent's state
20430        // We simulate this by spawning a new zshrs process
20431        let mut cmd =
20432            Command::new(std::env::current_exe().unwrap_or_else(|_| PathBuf::from("zshrs")));
20433
20434        if !args.is_empty() {
20435            // If args provided, run them in the subshell
20436            cmd.arg("-c").arg(args.join(" "));
20437        }
20438
20439        // Export current variables to child
20440        for (k, v) in &self.variables {
20441            cmd.env(k, v);
20442        }
20443
20444        match cmd.spawn() {
20445            Ok(mut child) => match child.wait() {
20446                Ok(status) => status.code().unwrap_or(0),
20447                Err(_) => 1,
20448            },
20449            Err(e) => {
20450                eprintln!("clone: failed to spawn subshell: {}", e);
20451                1
20452            }
20453        }
20454    }
20455
20456    /// log - same as logout for login shells
20457    fn builtin_log(&mut self, args: &[String]) -> i32 {
20458        self.builtin_exit(args)
20459    }
20460
20461    // Completion system builtins (stubs for compsys)
20462
20463    /// comparguments - parse completion arguments
20464    fn builtin_comparguments(&mut self, _args: &[String]) -> i32 {
20465        // Used internally by _arguments
20466        0
20467    }
20468
20469    /// compcall - call completion function
20470    fn builtin_compcall(&mut self, _args: &[String]) -> i32 {
20471        // Calls the completion function
20472        0
20473    }
20474
20475    /// compctl - old-style completion (deprecated)
20476    fn builtin_compctl(&mut self, args: &[String]) -> i32 {
20477        if args.is_empty() {
20478            println!("compctl: old-style completion system");
20479            println!("Use the new completion system (compsys) instead");
20480            return 0;
20481        }
20482        // Parse compctl options for backwards compatibility
20483        0
20484    }
20485
20486    /// compdescribe - describe completions
20487    fn builtin_compdescribe(&mut self, _args: &[String]) -> i32 {
20488        0
20489    }
20490
20491    /// compfiles - complete files
20492    fn builtin_compfiles(&mut self, _args: &[String]) -> i32 {
20493        0
20494    }
20495
20496    /// compgroups - group completions
20497    fn builtin_compgroups(&mut self, _args: &[String]) -> i32 {
20498        0
20499    }
20500
20501    /// compquote - quote completion strings
20502    fn builtin_compquote(&mut self, _args: &[String]) -> i32 {
20503        0
20504    }
20505
20506    /// comptags - manage completion tags
20507    fn builtin_comptags(&mut self, args: &[String]) -> i32 {
20508        if args.is_empty() {
20509            return 1;
20510        }
20511        match args[0].as_str() {
20512            "-i" => {
20513                // Initialize tags
20514                0
20515            }
20516            "-S" => {
20517                // Set tags
20518                0
20519            }
20520            _ => 1,
20521        }
20522    }
20523
20524    /// comptry - try completion
20525    fn builtin_comptry(&mut self, _args: &[String]) -> i32 {
20526        1 // No match
20527    }
20528
20529    /// compvalues - complete values
20530    fn builtin_compvalues(&mut self, _args: &[String]) -> i32 {
20531        0
20532    }
20533
20534    /// cap/getcap/setcap - Linux capabilities (stub on macOS)
20535    fn builtin_cap(&self, args: &[String]) -> i32 {
20536        // Linux capabilities are not available on macOS
20537        // On Linux, these would interact with libcap
20538        if args.is_empty() {
20539            println!("cap: display/set capabilities");
20540            println!("  getcap file...  - display capabilities");
20541            println!("  setcap caps file - set capabilities");
20542            return 0;
20543        }
20544
20545        #[cfg(target_os = "linux")]
20546        {
20547            // On Linux, we could use libcap bindings
20548            // For now, just run the external commands
20549            let status = std::process::Command::new(&args[0])
20550                .args(&args[1..])
20551                .status();
20552            return status.map(|s| s.code().unwrap_or(1)).unwrap_or(1);
20553        }
20554
20555        #[cfg(not(target_os = "linux"))]
20556        {
20557            eprintln!("cap: capabilities not supported on this platform");
20558            1
20559        }
20560    }
20561
20562    /// zcurses - curses interface (stub)
20563    fn builtin_zcurses(&mut self, args: &[String]) -> i32 {
20564        if args.is_empty() {
20565            eprintln!("zcurses: requires subcommand");
20566            return 1;
20567        }
20568
20569        match args[0].as_str() {
20570            "init" => {
20571                // Initialize curses
20572                println!("zcurses: would initialize curses");
20573                0
20574            }
20575            "end" => {
20576                // End curses mode
20577                println!("zcurses: would end curses");
20578                0
20579            }
20580            "addwin" => {
20581                // Add a window
20582                0
20583            }
20584            "delwin" => {
20585                // Delete a window
20586                0
20587            }
20588            "refresh" => {
20589                // Refresh display
20590                0
20591            }
20592            "move" => {
20593                // Move cursor
20594                0
20595            }
20596            "clear" => {
20597                // Clear window
20598                0
20599            }
20600            "char" | "string" => {
20601                // Output character/string
20602                0
20603            }
20604            "border" => {
20605                // Draw border
20606                0
20607            }
20608            "attr" => {
20609                // Set attributes
20610                0
20611            }
20612            "color" => {
20613                // Set colors
20614                0
20615            }
20616            "scroll" => {
20617                // Scroll window
20618                0
20619            }
20620            "input" => {
20621                // Get input
20622                0
20623            }
20624            "mouse" => {
20625                // Mouse support
20626                0
20627            }
20628            "querychar" => {
20629                // Query character at position
20630                0
20631            }
20632            "resize" => {
20633                // Resize window
20634                0
20635            }
20636            cmd => {
20637                eprintln!("zcurses: unknown subcommand: {}", cmd);
20638                1
20639            }
20640        }
20641    }
20642
20643    /// sysread - low-level read (zsh/system module)
20644    fn builtin_sysread(&mut self, args: &[String]) -> i32 {
20645        use std::io::Read;
20646
20647        let mut fd = 0i32; // stdin
20648        let mut count: Option<usize> = None;
20649        let mut var_name = "REPLY".to_string();
20650        let mut i = 0;
20651
20652        while i < args.len() {
20653            match args[i].as_str() {
20654                "-c" if i + 1 < args.len() => {
20655                    i += 1;
20656                    count = args[i].parse().ok();
20657                }
20658                "-i" if i + 1 < args.len() => {
20659                    i += 1;
20660                    fd = args[i].parse().unwrap_or(0);
20661                }
20662                "-o" if i + 1 < args.len() => {
20663                    i += 1;
20664                    var_name = args[i].clone();
20665                }
20666                "-t" if i + 1 < args.len() => {
20667                    i += 1;
20668                    // Timeout - ignored for now
20669                }
20670                _ => {
20671                    var_name = args[i].clone();
20672                }
20673            }
20674            i += 1;
20675        }
20676
20677        let mut buffer = vec![0u8; count.unwrap_or(8192)];
20678
20679        // Only support stdin for now
20680        if fd == 0 {
20681            match std::io::stdin().read(&mut buffer) {
20682                Ok(n) => {
20683                    buffer.truncate(n);
20684                    let s = String::from_utf8_lossy(&buffer).to_string();
20685                    self.variables.insert(var_name, s);
20686                    0
20687                }
20688                Err(_) => 1,
20689            }
20690        } else {
20691            eprintln!("sysread: only fd 0 (stdin) supported");
20692            1
20693        }
20694    }
20695
20696    /// syswrite - low-level write (zsh/system module)
20697    fn builtin_syswrite(&mut self, args: &[String]) -> i32 {
20698        use std::io::Write;
20699
20700        let mut fd = 1i32; // stdout
20701        let mut data = String::new();
20702        let mut i = 0;
20703
20704        while i < args.len() {
20705            match args[i].as_str() {
20706                "-o" if i + 1 < args.len() => {
20707                    i += 1;
20708                    fd = args[i].parse().unwrap_or(1);
20709                }
20710                "-c" if i + 1 < args.len() => {
20711                    i += 1;
20712                    // Count - ignored
20713                }
20714                _ => {
20715                    data = args[i].clone();
20716                }
20717            }
20718            i += 1;
20719        }
20720
20721        match fd {
20722            1 => {
20723                let _ = std::io::stdout().write_all(data.as_bytes());
20724                let _ = std::io::stdout().flush();
20725                0
20726            }
20727            2 => {
20728                let _ = std::io::stderr().write_all(data.as_bytes());
20729                let _ = std::io::stderr().flush();
20730                0
20731            }
20732            _ => {
20733                eprintln!("syswrite: only fd 1 (stdout) and 2 (stderr) supported");
20734                1
20735            }
20736        }
20737    }
20738
20739    /// syserror - get error message (zsh/system module)
20740    fn builtin_syserror(&self, args: &[String]) -> i32 {
20741        let errno = if args.is_empty() {
20742            // Use last errno
20743            std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
20744        } else {
20745            args[0].parse().unwrap_or(0)
20746        };
20747
20748        let err = std::io::Error::from_raw_os_error(errno);
20749        println!("{}", err);
20750        0
20751    }
20752
20753    /// sysopen - open file descriptor (zsh/system module)
20754    fn builtin_sysopen(&mut self, args: &[String]) -> i32 {
20755        use std::fs::OpenOptions;
20756
20757        let mut filename = String::new();
20758        let mut var_name = "REPLY".to_string();
20759        let mut read = false;
20760        let mut write = false;
20761        let mut append = false;
20762        let mut create = false;
20763        let mut truncate = false;
20764
20765        let mut i = 0;
20766        while i < args.len() {
20767            match args[i].as_str() {
20768                "-r" => read = true,
20769                "-w" => write = true,
20770                "-a" => append = true,
20771                "-c" => create = true,
20772                "-t" => truncate = true,
20773                "-u" => {
20774                    i += 1;
20775                    if i < args.len() {
20776                        var_name = args[i].clone();
20777                    }
20778                }
20779                "-o" => {
20780                    i += 1;
20781                    // Mode flags like O_RDONLY etc - parse as needed
20782                }
20783                s if !s.starts_with('-') => {
20784                    filename = s.to_string();
20785                }
20786                _ => {}
20787            }
20788            i += 1;
20789        }
20790
20791        if filename.is_empty() {
20792            eprintln!("sysopen: need filename");
20793            return 1;
20794        }
20795
20796        // Default to read if nothing specified
20797        if !read && !write && !append {
20798            read = true;
20799        }
20800
20801        let file = OpenOptions::new()
20802            .read(read)
20803            .write(write || append || truncate)
20804            .append(append)
20805            .create(create || write)
20806            .truncate(truncate)
20807            .open(&filename);
20808
20809        match file {
20810            Ok(f) => {
20811                let fd = self.next_fd;
20812                self.next_fd += 1;
20813                self.open_fds.insert(fd, f);
20814                self.variables.insert(var_name, fd.to_string());
20815                0
20816            }
20817            Err(e) => {
20818                eprintln!("sysopen: {}: {}", filename, e);
20819                1
20820            }
20821        }
20822    }
20823
20824    /// sysseek - seek on file descriptor (zsh/system module)
20825    fn builtin_sysseek(&mut self, args: &[String]) -> i32 {
20826        use std::io::{Seek, SeekFrom};
20827
20828        let mut fd = -1i32;
20829        let mut offset = 0i64;
20830        let mut whence = SeekFrom::Start(0);
20831
20832        let mut i = 0;
20833        while i < args.len() {
20834            match args[i].as_str() {
20835                "-u" => {
20836                    i += 1;
20837                    if i < args.len() {
20838                        fd = args[i].parse().unwrap_or(-1);
20839                    }
20840                }
20841                "-w" => {
20842                    i += 1;
20843                    if i < args.len() {
20844                        whence = match args[i].as_str() {
20845                            "start" | "set" | "0" => SeekFrom::Start(offset as u64),
20846                            "current" | "cur" | "1" => SeekFrom::Current(offset),
20847                            "end" | "2" => SeekFrom::End(offset),
20848                            _ => SeekFrom::Start(offset as u64),
20849                        };
20850                    }
20851                }
20852                s if !s.starts_with('-') => {
20853                    offset = s.parse().unwrap_or(0);
20854                }
20855                _ => {}
20856            }
20857            i += 1;
20858        }
20859
20860        if fd < 0 {
20861            eprintln!("sysseek: need fd (-u)");
20862            return 1;
20863        }
20864
20865        // Update whence with actual offset
20866        whence = match whence {
20867            SeekFrom::Start(_) => SeekFrom::Start(offset as u64),
20868            SeekFrom::Current(_) => SeekFrom::Current(offset),
20869            SeekFrom::End(_) => SeekFrom::End(offset),
20870        };
20871
20872        if let Some(file) = self.open_fds.get_mut(&fd) {
20873            match file.seek(whence) {
20874                Ok(pos) => {
20875                    self.variables.insert("REPLY".to_string(), pos.to_string());
20876                    0
20877                }
20878                Err(e) => {
20879                    eprintln!("sysseek: {}", e);
20880                    1
20881                }
20882            }
20883        } else {
20884            eprintln!("sysseek: bad fd: {}", fd);
20885            1
20886        }
20887    }
20888
20889    /// private - declare private variables (zsh/param/private module)
20890    fn builtin_private(&mut self, args: &[String]) -> i32 {
20891        // Similar to local but with stricter scoping
20892        self.builtin_local(args)
20893    }
20894
20895    /// zgetattr/zsetattr/zdelattr/zlistattr - extended attributes (zsh/attr module)
20896    fn builtin_zattr(&self, cmd: &str, args: &[String]) -> i32 {
20897        match cmd {
20898            "zgetattr" => {
20899                if args.len() < 2 {
20900                    eprintln!("zgetattr: need file and attribute name");
20901                    return 1;
20902                }
20903                #[cfg(target_os = "macos")]
20904                {
20905                    // macOS uses xattr
20906                    let output = std::process::Command::new("xattr")
20907                        .arg("-p")
20908                        .arg(&args[1])
20909                        .arg(&args[0])
20910                        .output();
20911                    if let Ok(out) = output {
20912                        print!("{}", String::from_utf8_lossy(&out.stdout));
20913                        return if out.status.success() { 0 } else { 1 };
20914                    }
20915                }
20916                #[cfg(target_os = "linux")]
20917                {
20918                    let output = std::process::Command::new("getfattr")
20919                        .arg("-n")
20920                        .arg(&args[1])
20921                        .arg(&args[0])
20922                        .output();
20923                    if let Ok(out) = output {
20924                        print!("{}", String::from_utf8_lossy(&out.stdout));
20925                        return if out.status.success() { 0 } else { 1 };
20926                    }
20927                }
20928                1
20929            }
20930            "zsetattr" => {
20931                if args.len() < 3 {
20932                    eprintln!("zsetattr: need file, attribute name, and value");
20933                    return 1;
20934                }
20935                #[cfg(target_os = "macos")]
20936                {
20937                    let status = std::process::Command::new("xattr")
20938                        .arg("-w")
20939                        .arg(&args[1])
20940                        .arg(&args[2])
20941                        .arg(&args[0])
20942                        .status();
20943                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
20944                }
20945                #[cfg(target_os = "linux")]
20946                {
20947                    let status = std::process::Command::new("setfattr")
20948                        .arg("-n")
20949                        .arg(&args[1])
20950                        .arg("-v")
20951                        .arg(&args[2])
20952                        .arg(&args[0])
20953                        .status();
20954                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
20955                }
20956                #[allow(unreachable_code)]
20957                1
20958            }
20959            "zdelattr" => {
20960                if args.len() < 2 {
20961                    eprintln!("zdelattr: need file and attribute name");
20962                    return 1;
20963                }
20964                #[cfg(target_os = "macos")]
20965                {
20966                    let status = std::process::Command::new("xattr")
20967                        .arg("-d")
20968                        .arg(&args[1])
20969                        .arg(&args[0])
20970                        .status();
20971                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
20972                }
20973                #[cfg(target_os = "linux")]
20974                {
20975                    let status = std::process::Command::new("setfattr")
20976                        .arg("-x")
20977                        .arg(&args[1])
20978                        .arg(&args[0])
20979                        .status();
20980                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
20981                }
20982                #[allow(unreachable_code)]
20983                1
20984            }
20985            "zlistattr" => {
20986                if args.is_empty() {
20987                    eprintln!("zlistattr: need file");
20988                    return 1;
20989                }
20990                #[cfg(target_os = "macos")]
20991                {
20992                    let output = std::process::Command::new("xattr").arg(&args[0]).output();
20993                    if let Ok(out) = output {
20994                        print!("{}", String::from_utf8_lossy(&out.stdout));
20995                        return if out.status.success() { 0 } else { 1 };
20996                    }
20997                }
20998                #[cfg(target_os = "linux")]
20999                {
21000                    let output = std::process::Command::new("getfattr")
21001                        .arg("-d")
21002                        .arg(&args[0])
21003                        .output();
21004                    if let Ok(out) = output {
21005                        print!("{}", String::from_utf8_lossy(&out.stdout));
21006                        return if out.status.success() { 0 } else { 1 };
21007                    }
21008                }
21009                1
21010            }
21011            _ => 1,
21012        }
21013    }
21014
21015    /// zftp - FTP client builtin
21016    fn builtin_zftp(&mut self, args: &[String]) -> i32 {
21017        if args.is_empty() {
21018            println!("zftp: FTP client");
21019            println!("  zftp open host [port]");
21020            println!("  zftp login [user [password]]");
21021            println!("  zftp cd dir");
21022            println!("  zftp get file [localfile]");
21023            println!("  zftp put file [remotefile]");
21024            println!("  zftp ls [dir]");
21025            println!("  zftp close");
21026            return 0;
21027        }
21028
21029        match args[0].as_str() {
21030            "open" => {
21031                if args.len() < 2 {
21032                    eprintln!("zftp open: need hostname");
21033                    return 1;
21034                }
21035                // Would connect to FTP server
21036                println!("zftp: would connect to {}", args[1]);
21037                0
21038            }
21039            "login" => {
21040                // Would authenticate
21041                println!("zftp: would login");
21042                0
21043            }
21044            "cd" => {
21045                if args.len() < 2 {
21046                    eprintln!("zftp cd: need directory");
21047                    return 1;
21048                }
21049                println!("zftp: would cd to {}", args[1]);
21050                0
21051            }
21052            "get" => {
21053                if args.len() < 2 {
21054                    eprintln!("zftp get: need filename");
21055                    return 1;
21056                }
21057                println!("zftp: would download {}", args[1]);
21058                0
21059            }
21060            "put" => {
21061                if args.len() < 2 {
21062                    eprintln!("zftp put: need filename");
21063                    return 1;
21064                }
21065                println!("zftp: would upload {}", args[1]);
21066                0
21067            }
21068            "ls" => {
21069                println!("zftp: would list directory");
21070                0
21071            }
21072            "close" | "quit" => {
21073                println!("zftp: would close connection");
21074                0
21075            }
21076            "params" => {
21077                // Display/set FTP parameters
21078                println!("ZFTP_HOST=");
21079                println!("ZFTP_PORT=21");
21080                println!("ZFTP_USER=");
21081                println!("ZFTP_PWD=");
21082                println!("ZFTP_TYPE=A");
21083                0
21084            }
21085            cmd => {
21086                eprintln!("zftp: unknown command: {}", cmd);
21087                1
21088            }
21089        }
21090    }
21091
21092    /// promptinit - initialize prompt theme system
21093    fn builtin_promptinit(&mut self, _args: &[String]) -> i32 {
21094        self.arrays.insert(
21095            "prompt_themes".to_string(),
21096            vec![
21097                "adam1".to_string(),
21098                "adam2".to_string(),
21099                "bart".to_string(),
21100                "bigfade".to_string(),
21101                "clint".to_string(),
21102                "default".to_string(),
21103                "elite".to_string(),
21104                "elite2".to_string(),
21105                "fade".to_string(),
21106                "fire".to_string(),
21107                "minimal".to_string(),
21108                "off".to_string(),
21109                "oliver".to_string(),
21110                "pws".to_string(),
21111                "redhat".to_string(),
21112                "restore".to_string(),
21113                "suse".to_string(),
21114                "walters".to_string(),
21115                "zefram".to_string(),
21116            ],
21117        );
21118        self.variables
21119            .insert("prompt_theme".to_string(), "default".to_string());
21120        0
21121    }
21122
21123    /// prompt - set or list prompt themes
21124    fn builtin_prompt(&mut self, args: &[String]) -> i32 {
21125        if args.is_empty() {
21126            let theme = self
21127                .variables
21128                .get("prompt_theme")
21129                .cloned()
21130                .unwrap_or_else(|| "default".to_string());
21131            println!("Current prompt theme: {}", theme);
21132            return 0;
21133        }
21134        match args[0].as_str() {
21135            "-l" | "--list" => {
21136                println!("Available prompt themes:");
21137                if let Some(themes) = self.arrays.get("prompt_themes") {
21138                    for theme in themes {
21139                        println!("  {}", theme);
21140                    }
21141                }
21142            }
21143            "-p" | "--preview" => {
21144                let theme = args.get(1).map(|s| s.as_str()).unwrap_or("default");
21145                self.apply_prompt_theme(theme, true);
21146            }
21147            "-h" | "--help" => {
21148                println!("prompt [options] [theme]");
21149                println!("  -l, --list     List available themes");
21150                println!("  -p, --preview  Preview a theme");
21151                println!("  -s, --setup    Set up a theme");
21152            }
21153            _ => {
21154                let theme = if args[0].starts_with('-') {
21155                    args.get(1).map(|s| s.as_str()).unwrap_or("default")
21156                } else {
21157                    args[0].as_str()
21158                };
21159                self.apply_prompt_theme(theme, false);
21160            }
21161        }
21162        0
21163    }
21164
21165    fn apply_prompt_theme(&mut self, theme: &str, preview: bool) {
21166        let (ps1, rps1) = match theme {
21167            "minimal" => ("%# ", ""),
21168            "off" => ("$ ", ""),
21169            "adam1" => (
21170                "%B%F{cyan}%n@%m %F{blue}%~%f%b %# ",
21171                "%F{yellow}%D{%H:%M}%f",
21172            ),
21173            "redhat" => ("[%n@%m %~]$ ", ""),
21174            _ => ("%n@%m %~ %# ", ""),
21175        };
21176        if preview {
21177            println!("PS1={:?}", ps1);
21178            println!("RPS1={:?}", rps1);
21179        } else {
21180            self.variables.insert("PS1".to_string(), ps1.to_string());
21181            self.variables.insert("RPS1".to_string(), rps1.to_string());
21182            self.variables
21183                .insert("prompt_theme".to_string(), theme.to_string());
21184        }
21185    }
21186
21187    /// pcre_compile - compile a PCRE pattern
21188    fn builtin_pcre_compile(&mut self, args: &[String]) -> i32 {
21189        use crate::pcre::{pcre_compile, PcreCompileOptions};
21190
21191        let mut pattern = String::new();
21192        let mut options = PcreCompileOptions::default();
21193
21194        for arg in args {
21195            match arg.as_str() {
21196                "-a" => options.anchored = true,
21197                "-i" => options.caseless = true,
21198                "-m" => options.multiline = true,
21199                "-s" => options.dotall = true,
21200                "-x" => options.extended = true,
21201                s if !s.starts_with('-') => pattern = s.to_string(),
21202                _ => {}
21203            }
21204        }
21205
21206        if pattern.is_empty() {
21207            eprintln!("pcre_compile: no pattern specified");
21208            return 1;
21209        }
21210
21211        match pcre_compile(&pattern, &options, &mut self.pcre_state) {
21212            Ok(()) => 0,
21213            Err(e) => {
21214                eprintln!("pcre_compile: {}", e);
21215                1
21216            }
21217        }
21218    }
21219
21220    /// pcre_match - match string against compiled PCRE
21221    fn builtin_pcre_match(&mut self, args: &[String]) -> i32 {
21222        use crate::pcre::{pcre_match, PcreMatchOptions};
21223
21224        let mut var_name = "MATCH".to_string();
21225        let mut array_name = "match".to_string();
21226        let mut string = String::new();
21227        let mut i = 0;
21228
21229        while i < args.len() {
21230            match args[i].as_str() {
21231                "-v" => {
21232                    i += 1;
21233                    if i < args.len() {
21234                        var_name = args[i].clone();
21235                    }
21236                }
21237                "-a" => {
21238                    i += 1;
21239                    if i < args.len() {
21240                        array_name = args[i].clone();
21241                    }
21242                }
21243                s if !s.starts_with('-') => string = s.to_string(),
21244                _ => {}
21245            }
21246            i += 1;
21247        }
21248
21249        let options = PcreMatchOptions {
21250            match_var: Some(var_name.clone()),
21251            array_var: Some(array_name.clone()),
21252            ..Default::default()
21253        };
21254
21255        match pcre_match(&string, &options, &self.pcre_state) {
21256            Ok(result) => {
21257                if result.matched {
21258                    if let Some(m) = result.full_match {
21259                        self.variables.insert(var_name, m);
21260                    }
21261                    let matches: Vec<String> =
21262                        result.captures.into_iter().filter_map(|c| c).collect();
21263                    self.arrays.insert(array_name, matches);
21264                    0
21265                } else {
21266                    1
21267                }
21268            }
21269            Err(e) => {
21270                eprintln!("pcre_match: {}", e);
21271                1
21272            }
21273        }
21274    }
21275
21276    /// pcre_study - optimize compiled PCRE (no-op in Rust regex)
21277    fn builtin_pcre_study(&mut self, _args: &[String]) -> i32 {
21278        use crate::pcre::pcre_study;
21279
21280        match pcre_study(&self.pcre_state) {
21281            Ok(()) => 0,
21282            Err(e) => {
21283                eprintln!("pcre_study: {}", e);
21284                1
21285            }
21286        }
21287    }
21288
21289    // =========================================================================
21290    // Process control functions - Port from exec.c
21291    // =========================================================================
21292
21293    /// Fork a new process
21294    /// Port of zfork() from exec.c
21295    pub fn zfork(&mut self, flags: ForkFlags) -> std::io::Result<ForkResult> {
21296        // Check for job control
21297        let can_background = self.options.get("monitor").copied().unwrap_or(false);
21298
21299        unsafe {
21300            match libc::fork() {
21301                -1 => Err(std::io::Error::last_os_error()),
21302                0 => {
21303                    // Child process
21304                    if !flags.contains(ForkFlags::NOJOB) && can_background {
21305                        // Set up job control
21306                        let pid = libc::getpid();
21307                        if flags.contains(ForkFlags::NEWGRP) {
21308                            libc::setpgid(0, 0);
21309                        }
21310                        if flags.contains(ForkFlags::FGTTY) {
21311                            libc::tcsetpgrp(0, pid);
21312                        }
21313                    }
21314
21315                    // Reset signal handlers
21316                    if !flags.contains(ForkFlags::KEEPSIGS) {
21317                        self.reset_signals();
21318                    }
21319
21320                    Ok(ForkResult::Child)
21321                }
21322                pid => {
21323                    // Parent process
21324                    if !flags.contains(ForkFlags::NOJOB) {
21325                        // Add to job table
21326                        self.add_child_process(pid);
21327                    }
21328                    Ok(ForkResult::Parent(pid))
21329                }
21330            }
21331        }
21332    }
21333
21334    /// Add a child process to tracking
21335    fn add_child_process(&mut self, pid: i32) {
21336        // Would track in job table
21337        self.variables.insert("!".to_string(), pid.to_string());
21338    }
21339
21340    /// Reset signal handlers to defaults
21341    fn reset_signals(&self) {
21342        unsafe {
21343            libc::signal(libc::SIGINT, libc::SIG_DFL);
21344            libc::signal(libc::SIGQUIT, libc::SIG_DFL);
21345            libc::signal(libc::SIGTERM, libc::SIG_DFL);
21346            libc::signal(libc::SIGTSTP, libc::SIG_DFL);
21347            libc::signal(libc::SIGTTIN, libc::SIG_DFL);
21348            libc::signal(libc::SIGTTOU, libc::SIG_DFL);
21349            libc::signal(libc::SIGCHLD, libc::SIG_DFL);
21350        }
21351    }
21352
21353    /// Execute a command in the current process (exec family)
21354    /// Port of zexecve() from exec.c
21355    pub fn zexecve(&self, cmd: &str, args: &[String]) -> ! {
21356        use std::ffi::CString;
21357        use std::os::unix::ffi::OsStrExt;
21358
21359        let c_cmd = CString::new(cmd).expect("CString::new failed");
21360
21361        // Build argv
21362        let c_args: Vec<CString> = std::iter::once(c_cmd.clone())
21363            .chain(args.iter().map(|s| CString::new(s.as_str()).unwrap()))
21364            .collect();
21365
21366        let c_argv: Vec<*const libc::c_char> = c_args
21367            .iter()
21368            .map(|s| s.as_ptr())
21369            .chain(std::iter::once(std::ptr::null()))
21370            .collect();
21371
21372        // Build envp from current environment
21373        let env_vars: Vec<CString> = std::env::vars()
21374            .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
21375            .collect();
21376
21377        let c_envp: Vec<*const libc::c_char> = env_vars
21378            .iter()
21379            .map(|s| s.as_ptr())
21380            .chain(std::iter::once(std::ptr::null()))
21381            .collect();
21382
21383        unsafe {
21384            libc::execve(c_cmd.as_ptr(), c_argv.as_ptr(), c_envp.as_ptr());
21385            // If we get here, exec failed
21386            eprintln!(
21387                "zshrs: exec failed: {}: {}",
21388                cmd,
21389                std::io::Error::last_os_error()
21390            );
21391            std::process::exit(127);
21392        }
21393    }
21394
21395    /// Enter a subshell
21396    /// Port of entersubsh() from exec.c
21397    pub fn entersubsh(&mut self, flags: SubshellFlags) {
21398        // Increment subshell level
21399        let level = self
21400            .get_variable("ZSH_SUBSHELL")
21401            .parse::<i32>()
21402            .unwrap_or(0);
21403        self.variables
21404            .insert("ZSH_SUBSHELL".to_string(), (level + 1).to_string());
21405
21406        // Handle job control
21407        if flags.contains(SubshellFlags::NOMONITOR) {
21408            self.options.insert("monitor".to_string(), false);
21409        }
21410
21411        // Close unneeded fds
21412        if !flags.contains(SubshellFlags::KEEPFDS) {
21413            self.close_extra_fds();
21414        }
21415
21416        // Reset traps
21417        if !flags.contains(SubshellFlags::KEEPTRAPS) {
21418            self.reset_traps();
21419        }
21420    }
21421
21422    /// Close extra file descriptors
21423    fn close_extra_fds(&self) {
21424        // Close fds > 10 (common shell convention)
21425        for fd in 10..256 {
21426            unsafe {
21427                libc::close(fd);
21428            }
21429        }
21430    }
21431
21432    /// Reset all traps
21433    fn reset_traps(&mut self) {
21434        self.traps.clear();
21435    }
21436
21437    /// Execute a shell function
21438    /// Port of doshfunc() from exec.c
21439    pub fn doshfunc(
21440        &mut self,
21441        name: &str,
21442        func: &ShellCommand,
21443        args: &[String],
21444    ) -> Result<i32, String> {
21445        // Save current state
21446        let old_argv = self.positional_params.clone();
21447        let old_funcstack = self.arrays.get("funcstack").cloned();
21448        let old_funcsourcetrace = self.arrays.get("funcsourcetrace").cloned();
21449
21450        // Set positional parameters to function arguments
21451        self.positional_params = args.to_vec();
21452
21453        // Update funcstack
21454        let mut funcstack = old_funcstack.clone().unwrap_or_default();
21455        funcstack.insert(0, name.to_string());
21456        self.arrays.insert("funcstack".to_string(), funcstack);
21457
21458        // Execute function body
21459        let result = self.execute_command(func);
21460
21461        // Restore state
21462        self.positional_params = old_argv;
21463        if let Some(fs) = old_funcstack {
21464            self.arrays.insert("funcstack".to_string(), fs);
21465        } else {
21466            self.arrays.remove("funcstack");
21467        }
21468        if let Some(fst) = old_funcsourcetrace {
21469            self.arrays.insert("funcsourcetrace".to_string(), fst);
21470        }
21471
21472        result
21473    }
21474
21475    /// Execute arithmetic expression
21476    /// Port of execarith() from exec.c
21477    pub fn execarith(&mut self, expr: &str) -> i32 {
21478        let result = self.eval_arith_expr(expr);
21479        if result == 0 {
21480            1
21481        } else {
21482            0
21483        }
21484    }
21485
21486    /// Execute conditional expression
21487    /// Port of execcond() from exec.c
21488    pub fn execcond(&mut self, cond: &CondExpr) -> i32 {
21489        if self.eval_cond_expr(cond) {
21490            0
21491        } else {
21492            1
21493        }
21494    }
21495
21496    /// Execute command and capture time
21497    /// Port of exectime() from exec.c
21498    pub fn exectime(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
21499        use std::time::Instant;
21500
21501        let start = Instant::now();
21502        let result = self.execute_command(cmd);
21503        let elapsed = start.elapsed();
21504
21505        // Print time in zsh format
21506        let user_time = elapsed.as_secs_f64() * 0.7; // Approximation
21507        let sys_time = elapsed.as_secs_f64() * 0.1;
21508        let real_time = elapsed.as_secs_f64();
21509
21510        eprintln!(
21511            "{:.2}s user {:.2}s system {:.0}% cpu {:.3} total",
21512            user_time,
21513            sys_time,
21514            ((user_time + sys_time) / real_time * 100.0).min(100.0),
21515            real_time
21516        );
21517
21518        result
21519    }
21520
21521    /// Find command in PATH
21522    /// Port of findcmd() from exec.c
21523    pub fn findcmd(&self, name: &str, do_hash: bool) -> Option<String> {
21524        // Check command hash table first
21525        if do_hash {
21526            if let Some(path) = self.command_hash.get(name) {
21527                if std::path::Path::new(path).exists() {
21528                    return Some(path.clone());
21529                }
21530            }
21531        }
21532
21533        // Search PATH
21534        if let Ok(path_var) = std::env::var("PATH") {
21535            for dir in path_var.split(':') {
21536                let full_path = format!("{}/{}", dir, name);
21537                if std::path::Path::new(&full_path).is_file() {
21538                    return Some(full_path);
21539                }
21540            }
21541        }
21542
21543        None
21544    }
21545
21546    /// Hash a command (add to command hash table)
21547    /// Port of hashcmd() from exec.c
21548    pub fn hashcmd(&mut self, name: &str, path: &str) {
21549        self.command_hash.insert(name.to_string(), path.to_string());
21550    }
21551
21552    /// Check if command exists and is executable
21553    /// Port of iscom() from exec.c
21554    pub fn iscom(&self, name: &str) -> bool {
21555        // Check if it's a builtin
21556        if self.is_builtin_cmd(name) {
21557            return true;
21558        }
21559
21560        // Check if it's a function
21561        if self.functions.contains_key(name) {
21562            return true;
21563        }
21564
21565        // Check if it's an alias
21566        if self.aliases.contains_key(name) {
21567            return true;
21568        }
21569
21570        // Check in PATH
21571        self.findcmd(name, true).is_some()
21572    }
21573
21574    /// Check if name is a builtin (process control version)
21575    fn is_builtin_cmd(&self, name: &str) -> bool {
21576        BUILTIN_SET.contains(name)
21577    }
21578
21579    /// Close all file descriptors except stdin/stdout/stderr
21580    /// Port of closem() from exec.c
21581    pub fn closem(&self, exceptions: &[i32]) {
21582        for fd in 3..256 {
21583            if !exceptions.contains(&fd) {
21584                unsafe {
21585                    libc::close(fd);
21586                }
21587            }
21588        }
21589    }
21590
21591    /// Create a pipe
21592    /// Port of mpipe() from exec.c
21593    pub fn mpipe(&self) -> std::io::Result<(i32, i32)> {
21594        let mut fds = [0i32; 2];
21595        let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
21596        if result == -1 {
21597            Err(std::io::Error::last_os_error())
21598        } else {
21599            Ok((fds[0], fds[1]))
21600        }
21601    }
21602
21603    /// Add a file descriptor for redirection
21604    /// Port of addfd() from exec.c
21605    pub fn addfd(&self, fd: i32, target_fd: i32, mode: RedirMode) -> std::io::Result<()> {
21606        match mode {
21607            RedirMode::Dup => {
21608                if fd != target_fd {
21609                    unsafe {
21610                        if libc::dup2(fd, target_fd) == -1 {
21611                            return Err(std::io::Error::last_os_error());
21612                        }
21613                    }
21614                }
21615            }
21616            RedirMode::Close => unsafe {
21617                libc::close(target_fd);
21618            },
21619        }
21620        Ok(())
21621    }
21622
21623    /// Get heredoc content
21624    /// Port of gethere() from exec.c
21625    pub fn gethere(&mut self, terminator: &str, strip_tabs: bool) -> String {
21626        let mut content = String::new();
21627
21628        // Would read until terminator is found
21629        // This is simplified - real impl reads from input
21630
21631        if strip_tabs {
21632            content = content
21633                .lines()
21634                .map(|line| line.trim_start_matches('\t'))
21635                .collect::<Vec<_>>()
21636                .join("\n");
21637        }
21638
21639        content
21640    }
21641
21642    /// Get herestring content
21643    /// Port of getherestr() from exec.c
21644    pub fn getherestr(&mut self, word: &str) -> String {
21645        let expanded = self.expand_string(word);
21646        format!("{}\n", expanded)
21647    }
21648
21649    /// Resolve a builtin command
21650    /// Port of resolvebuiltin() from exec.c
21651    pub fn resolvebuiltin(&self, name: &str) -> Option<BuiltinType> {
21652        if self.is_builtin_cmd(name) {
21653            Some(BuiltinType::Normal)
21654        } else {
21655            // Check disabled_builtins if we had that field
21656            None
21657        }
21658    }
21659
21660    /// Check if cd is possible
21661    /// Port of cancd() from exec.c
21662    pub fn cancd(&self, path_str: &str) -> bool {
21663        use std::os::unix::fs::PermissionsExt;
21664
21665        let path = std::path::Path::new(path_str);
21666        if !path.is_dir() {
21667            return false;
21668        }
21669
21670        if let Ok(meta) = path.metadata() {
21671            let mode = meta.permissions().mode();
21672            // Check execute permission (needed for cd)
21673            let uid = unsafe { libc::getuid() };
21674            let gid = unsafe { libc::getgid() };
21675            let file_uid = meta.uid();
21676            let file_gid = meta.gid();
21677
21678            if uid == file_uid {
21679                return (mode & 0o100) != 0;
21680            } else if gid == file_gid {
21681                return (mode & 0o010) != 0;
21682            } else {
21683                return (mode & 0o001) != 0;
21684            }
21685        }
21686
21687        false
21688    }
21689
21690    /// Command not found handler
21691    /// Port of commandnotfound() from exec.c
21692    pub fn commandnotfound(&mut self, name: &str, args: &[String]) -> i32 {
21693        // Check for command_not_found_handler function
21694        if self.functions.contains_key("command_not_found_handler") {
21695            let mut handler_args = vec![name.to_string()];
21696            handler_args.extend(args.iter().cloned());
21697
21698            if let Some(func) = self.functions.get("command_not_found_handler").cloned() {
21699                if let Ok(code) = self.doshfunc("command_not_found_handler", &func, &handler_args) {
21700                    return code;
21701                }
21702            }
21703        }
21704
21705        eprintln!("zshrs: command not found: {}", name);
21706        127
21707    }
21708}
21709
21710use std::os::unix::fs::MetadataExt;
21711
21712bitflags::bitflags! {
21713    /// Flags for zfork()
21714    #[derive(Debug, Clone, Copy, Default)]
21715    pub struct ForkFlags: u32 {
21716        const NOJOB = 1 << 0;    // Don't add to job table
21717        const NEWGRP = 1 << 1;   // Create new process group
21718        const FGTTY = 1 << 2;    // Take foreground terminal
21719        const KEEPSIGS = 1 << 3; // Keep signal handlers
21720    }
21721}
21722
21723bitflags::bitflags! {
21724    /// Flags for entersubsh()
21725    #[derive(Debug, Clone, Copy, Default)]
21726    pub struct SubshellFlags: u32 {
21727        const NOMONITOR = 1 << 0; // Disable job control
21728        const KEEPFDS = 1 << 1;   // Keep file descriptors
21729        const KEEPTRAPS = 1 << 2; // Keep trap handlers
21730    }
21731}
21732
21733/// Result of fork operation
21734#[derive(Debug)]
21735pub enum ForkResult {
21736    Parent(i32), // Contains child PID
21737    Child,
21738}
21739
21740/// Redirection mode
21741#[derive(Debug, Clone, Copy)]
21742pub enum RedirMode {
21743    Dup,
21744    Close,
21745}
21746
21747/// Builtin command type
21748#[derive(Debug, Clone, Copy)]
21749pub enum BuiltinType {
21750    Normal,
21751    Disabled,
21752}