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    // Coreutils builtins (anti-fork, gated by !posix_mode)
815    // ═══════════════════════════════════════════════════════════════════════
816
817    vm.register_builtin(BUILTIN_CAT, |vm, argc| {
818        let args = pop_args(vm, argc);
819        let status = with_executor(|exec| exec.builtin_cat(&args));
820        Value::Status(status)
821    });
822
823    vm.register_builtin(BUILTIN_HEAD, |vm, argc| {
824        let args = pop_args(vm, argc);
825        let status = with_executor(|exec| exec.builtin_head(&args));
826        Value::Status(status)
827    });
828
829    vm.register_builtin(BUILTIN_TAIL, |vm, argc| {
830        let args = pop_args(vm, argc);
831        let status = with_executor(|exec| exec.builtin_tail(&args));
832        Value::Status(status)
833    });
834
835    vm.register_builtin(BUILTIN_WC, |vm, argc| {
836        let args = pop_args(vm, argc);
837        let status = with_executor(|exec| exec.builtin_wc(&args));
838        Value::Status(status)
839    });
840
841    vm.register_builtin(BUILTIN_BASENAME, |vm, argc| {
842        let args = pop_args(vm, argc);
843        let status = with_executor(|exec| exec.builtin_basename(&args));
844        Value::Status(status)
845    });
846
847    vm.register_builtin(BUILTIN_DIRNAME, |vm, argc| {
848        let args = pop_args(vm, argc);
849        let status = with_executor(|exec| exec.builtin_dirname(&args));
850        Value::Status(status)
851    });
852
853    vm.register_builtin(BUILTIN_TOUCH, |vm, argc| {
854        let args = pop_args(vm, argc);
855        let status = with_executor(|exec| exec.builtin_touch(&args));
856        Value::Status(status)
857    });
858
859    vm.register_builtin(BUILTIN_REALPATH, |vm, argc| {
860        let args = pop_args(vm, argc);
861        let status = with_executor(|exec| exec.builtin_realpath(&args));
862        Value::Status(status)
863    });
864
865    vm.register_builtin(BUILTIN_SORT, |vm, argc| {
866        let args = pop_args(vm, argc);
867        let status = with_executor(|exec| exec.builtin_sort(&args));
868        Value::Status(status)
869    });
870
871    vm.register_builtin(BUILTIN_FIND, |vm, argc| {
872        let args = pop_args(vm, argc);
873        let status = with_executor(|exec| exec.builtin_find(&args));
874        Value::Status(status)
875    });
876
877    vm.register_builtin(BUILTIN_UNIQ, |vm, argc| {
878        let args = pop_args(vm, argc);
879        let status = with_executor(|exec| exec.builtin_uniq(&args));
880        Value::Status(status)
881    });
882
883    vm.register_builtin(BUILTIN_CUT, |vm, argc| {
884        let args = pop_args(vm, argc);
885        let status = with_executor(|exec| exec.builtin_cut(&args));
886        Value::Status(status)
887    });
888
889    vm.register_builtin(BUILTIN_TR, |vm, argc| {
890        let args = pop_args(vm, argc);
891        let status = with_executor(|exec| exec.builtin_tr(&args));
892        Value::Status(status)
893    });
894
895    vm.register_builtin(BUILTIN_SEQ, |vm, argc| {
896        let args = pop_args(vm, argc);
897        let status = with_executor(|exec| exec.builtin_seq(&args));
898        Value::Status(status)
899    });
900
901    vm.register_builtin(BUILTIN_REV, |vm, argc| {
902        let args = pop_args(vm, argc);
903        let status = with_executor(|exec| exec.builtin_rev(&args));
904        Value::Status(status)
905    });
906
907    vm.register_builtin(BUILTIN_TEE, |vm, argc| {
908        let args = pop_args(vm, argc);
909        let status = with_executor(|exec| exec.builtin_tee(&args));
910        Value::Status(status)
911    });
912
913    vm.register_builtin(BUILTIN_SLEEP, |vm, argc| {
914        let args = pop_args(vm, argc);
915        let status = with_executor(|exec| exec.builtin_sleep(&args));
916        Value::Status(status)
917    });
918
919    vm.register_builtin(BUILTIN_WHOAMI, |vm, argc| {
920        let args = pop_args(vm, argc);
921        let status = with_executor(|exec| exec.builtin_whoami(&args));
922        Value::Status(status)
923    });
924
925    vm.register_builtin(BUILTIN_ID, |vm, argc| {
926        let args = pop_args(vm, argc);
927        let status = with_executor(|exec| exec.builtin_id(&args));
928        Value::Status(status)
929    });
930
931    vm.register_builtin(BUILTIN_HOSTNAME, |vm, argc| {
932        let args = pop_args(vm, argc);
933        let status = with_executor(|exec| exec.builtin_hostname(&args));
934        Value::Status(status)
935    });
936
937    vm.register_builtin(BUILTIN_UNAME, |vm, argc| {
938        let args = pop_args(vm, argc);
939        let status = with_executor(|exec| exec.builtin_uname(&args));
940        Value::Status(status)
941    });
942
943    vm.register_builtin(BUILTIN_DATE, |vm, argc| {
944        let args = pop_args(vm, argc);
945        let status = with_executor(|exec| exec.builtin_date(&args));
946        Value::Status(status)
947    });
948
949    vm.register_builtin(BUILTIN_MKTEMP, |vm, argc| {
950        let args = pop_args(vm, argc);
951        let status = with_executor(|exec| exec.builtin_mktemp(&args));
952        Value::Status(status)
953    });
954}
955
956/// Pop argc arguments from the VM stack into a Vec<String>.
957#[inline]
958fn pop_args(vm: &mut fusevm::VM, argc: u8) -> Vec<String> {
959    let mut args = Vec::with_capacity(argc as usize);
960    for _ in 0..argc {
961        args.push(vm.pop().to_str());
962    }
963    args.reverse(); // Stack is LIFO, args should be in order
964    args
965}
966
967/// Match an intercept pattern against a command name or full command string.
968/// Supports: exact match, glob ("git *", "_*", "*"), or "all".
969fn intercept_matches(pattern: &str, cmd_name: &str, full_cmd: &str) -> bool {
970    if pattern == "*" || pattern == "all" {
971        return true;
972    }
973    if pattern == cmd_name {
974        return true;
975    }
976    // Glob match against full command (e.g. "git *" matches "git push")
977    if pattern.contains('*') || pattern.contains('?') {
978        if let Ok(pat) = glob::Pattern::new(pattern) {
979            return pat.matches(cmd_name) || pat.matches(full_cmd);
980        }
981    }
982    false
983}
984
985/// Get or compile a regex, caching the result
986fn cached_regex(pattern: &str) -> Option<regex::Regex> {
987    let mut cache = REGEX_CACHE.lock();
988    if let Some(re) = cache.get(pattern) {
989        return Some(re.clone());
990    }
991    match regex::Regex::new(pattern) {
992        Ok(re) => {
993            cache.insert(pattern.to_string(), re.clone());
994            Some(re)
995        }
996        Err(_) => None,
997    }
998}
999
1000/// HashSet of all zsh options for O(1) lookup
1001static ZSH_OPTIONS_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
1002    [
1003        "aliases",
1004        "allexport",
1005        "alwayslastprompt",
1006        "alwaystoend",
1007        "appendcreate",
1008        "appendhistory",
1009        "autocd",
1010        "autocontinue",
1011        "autolist",
1012        "automenu",
1013        "autonamedirs",
1014        "autoparamkeys",
1015        "autoparamslash",
1016        "autopushd",
1017        "autoremoveslash",
1018        "autoresume",
1019        "badpattern",
1020        "banghist",
1021        "bareglobqual",
1022        "bashautolist",
1023        "bashrematch",
1024        "beep",
1025        "bgnice",
1026        "braceccl",
1027        "bsdecho",
1028        "caseglob",
1029        "casematch",
1030        "cbases",
1031        "cdablevars",
1032        "cdsilent",
1033        "chasedots",
1034        "chaselinks",
1035        "checkjobs",
1036        "checkrunningjobs",
1037        "clobber",
1038        "combiningchars",
1039        "completealiases",
1040        "completeinword",
1041        "continueonerror",
1042        "correct",
1043        "correctall",
1044        "cprecedences",
1045        "cshjunkiehistory",
1046        "cshjunkieloops",
1047        "cshjunkiequotes",
1048        "cshnullcmd",
1049        "cshnullglob",
1050        "debugbeforecmd",
1051        "dotglob",
1052        "dvorak",
1053        "emacs",
1054        "equals",
1055        "errexit",
1056        "errreturn",
1057        "evallineno",
1058        "exec",
1059        "extendedglob",
1060        "extendedhistory",
1061        "flowcontrol",
1062        "forcefloat",
1063        "functionargzero",
1064        "glob",
1065        "globassign",
1066        "globcomplete",
1067        "globdots",
1068        "globstarshort",
1069        "globsubst",
1070        "globalexport",
1071        "globalrcs",
1072        "hashall",
1073        "hashcmds",
1074        "hashdirs",
1075        "hashexecutablesonly",
1076        "hashlistall",
1077        "histallowclobber",
1078        "histappend",
1079        "histbeep",
1080        "histexpand",
1081        "histexpiredupsfirst",
1082        "histfcntllock",
1083        "histfindnodups",
1084        "histignorealldups",
1085        "histignoredups",
1086        "histignorespace",
1087        "histlexwords",
1088        "histnofunctions",
1089        "histnostore",
1090        "histreduceblanks",
1091        "histsavebycopy",
1092        "histsavenodups",
1093        "histsubstpattern",
1094        "histverify",
1095        "hup",
1096        "ignorebraces",
1097        "ignoreclosebraces",
1098        "ignoreeof",
1099        "incappendhistory",
1100        "incappendhistorytime",
1101        "interactive",
1102        "interactivecomments",
1103        "ksharrays",
1104        "kshautoload",
1105        "kshglob",
1106        "kshoptionprint",
1107        "kshtypeset",
1108        "kshzerosubscript",
1109        "listambiguous",
1110        "listbeep",
1111        "listpacked",
1112        "listrowsfirst",
1113        "listtypes",
1114        "localloops",
1115        "localoptions",
1116        "localpatterns",
1117        "localtraps",
1118        "log",
1119        "login",
1120        "longlistjobs",
1121        "magicequalsubst",
1122        "mailwarn",
1123        "mailwarning",
1124        "markdirs",
1125        "menucomplete",
1126        "monitor",
1127        "multibyte",
1128        "multifuncdef",
1129        "multios",
1130        "nomatch",
1131        "notify",
1132        "nullglob",
1133        "numericglobsort",
1134        "octalzeroes",
1135        "onecmd",
1136        "overstrike",
1137        "pathdirs",
1138        "pathscript",
1139        "physical",
1140        "pipefail",
1141        "posixaliases",
1142        "posixargzero",
1143        "posixbuiltins",
1144        "posixcd",
1145        "posixidentifiers",
1146        "posixjobs",
1147        "posixstrings",
1148        "posixtraps",
1149        "printeightbit",
1150        "printexitvalue",
1151        "privileged",
1152        "promptbang",
1153        "promptcr",
1154        "promptpercent",
1155        "promptsp",
1156        "promptsubst",
1157        "promptvars",
1158        "pushdignoredups",
1159        "pushdminus",
1160        "pushdsilent",
1161        "pushdtohome",
1162        "rcexpandparam",
1163        "rcquotes",
1164        "rcs",
1165        "recexact",
1166        "rematchpcre",
1167        "restricted",
1168        "rmstarsilent",
1169        "rmstarwait",
1170        "sharehistory",
1171        "shfileexpansion",
1172        "shglob",
1173        "shinstdin",
1174        "shnullcmd",
1175        "shoptionletters",
1176        "shortloops",
1177        "shortrepeat",
1178        "shwordsplit",
1179        "singlecommand",
1180        "singlelinezle",
1181        "sourcetrace",
1182        "stdin",
1183        "sunkeyboardhack",
1184        "trackall",
1185        "transientrprompt",
1186        "trapsasync",
1187        "typesetsilent",
1188        "typesettounset",
1189        "unset",
1190        "verbose",
1191        "vi",
1192        "warncreateglobal",
1193        "warnnestedvar",
1194        "xtrace",
1195        "zle",
1196    ]
1197    .into_iter()
1198    .collect()
1199});
1200
1201/// O(1) builtin lookup — replaces the 130+ arm matches! macro in is_builtin()
1202static BUILTIN_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
1203    [
1204        "cd",
1205        "chdir",
1206        "pwd",
1207        "echo",
1208        "export",
1209        "unset",
1210        "source",
1211        "exit",
1212        "return",
1213        "bye",
1214        "logout",
1215        "log",
1216        "true",
1217        "false",
1218        "test",
1219        "local",
1220        "declare",
1221        "typeset",
1222        "read",
1223        "shift",
1224        "eval",
1225        "jobs",
1226        "fg",
1227        "bg",
1228        "kill",
1229        "disown",
1230        "wait",
1231        "autoload",
1232        "history",
1233        "fc",
1234        "trap",
1235        "suspend",
1236        "alias",
1237        "unalias",
1238        "set",
1239        "shopt",
1240        "setopt",
1241        "unsetopt",
1242        "getopts",
1243        "type",
1244        "hash",
1245        "command",
1246        "builtin",
1247        "let",
1248        "pushd",
1249        "popd",
1250        "dirs",
1251        "printf",
1252        "break",
1253        "continue",
1254        "disable",
1255        "enable",
1256        "emulate",
1257        "exec",
1258        "float",
1259        "integer",
1260        "functions",
1261        "print",
1262        "whence",
1263        "where",
1264        "which",
1265        "ulimit",
1266        "limit",
1267        "unlimit",
1268        "umask",
1269        "rehash",
1270        "unhash",
1271        "times",
1272        "zmodload",
1273        "r",
1274        "ttyctl",
1275        "noglob",
1276        "zstat",
1277        "stat",
1278        "strftime",
1279        "zsleep",
1280        "zln",
1281        "zmv",
1282        "zcp",
1283        "coproc",
1284        "zparseopts",
1285        "readonly",
1286        "unfunction",
1287        "getln",
1288        "pushln",
1289        "bindkey",
1290        "zle",
1291        "sched",
1292        "zformat",
1293        "zcompile",
1294        "vared",
1295        "echotc",
1296        "echoti",
1297        "zpty",
1298        "zprof",
1299        "zsocket",
1300        "ztcp",
1301        "zregexparse",
1302        "clone",
1303        "comparguments",
1304        "compcall",
1305        "compctl",
1306        "compdef",
1307        "compdescribe",
1308        "compfiles",
1309        "compgroups",
1310        "compinit",
1311        "compquote",
1312        "comptags",
1313        "comptry",
1314        "compvalues",
1315        "cdreplay",
1316        "cap",
1317        "getcap",
1318        "setcap",
1319        "zftp",
1320        "zcurses",
1321        "sysread",
1322        "syswrite",
1323        "syserror",
1324        "sysopen",
1325        "sysseek",
1326        "private",
1327        "zgetattr",
1328        "zsetattr",
1329        "zdelattr",
1330        "zlistattr",
1331        "[",
1332        ".",
1333        ":",
1334        "compgen",
1335        "complete",
1336    ]
1337    .into_iter()
1338    .collect()
1339});
1340
1341/// Convert float to hex representation (%a/%A format)
1342fn float_to_hex(val: f64, uppercase: bool) -> String {
1343    if val.is_nan() {
1344        return if uppercase { "NAN" } else { "nan" }.to_string();
1345    }
1346    if val.is_infinite() {
1347        return if val > 0.0 {
1348            if uppercase {
1349                "INF"
1350            } else {
1351                "inf"
1352            }
1353        } else {
1354            if uppercase {
1355                "-INF"
1356            } else {
1357                "-inf"
1358            }
1359        }
1360        .to_string();
1361    }
1362    if val == 0.0 {
1363        let sign = if val.is_sign_negative() { "-" } else { "" };
1364        return if uppercase {
1365            format!("{}0X0P+0", sign)
1366        } else {
1367            format!("{}0x0p+0", sign)
1368        };
1369    }
1370
1371    let sign = if val < 0.0 { "-" } else { "" };
1372    let abs_val = val.abs();
1373    let bits = abs_val.to_bits();
1374    let exponent = ((bits >> 52) & 0x7ff) as i32 - 1023;
1375    let mantissa = bits & 0xfffffffffffff;
1376
1377    let hex_mantissa = format!("{:013x}", mantissa);
1378    let hex_mantissa = hex_mantissa.trim_end_matches('0');
1379    let hex_mantissa = if hex_mantissa.is_empty() {
1380        "0"
1381    } else {
1382        hex_mantissa
1383    };
1384
1385    if uppercase {
1386        format!("{}0X1.{}P{:+}", sign, hex_mantissa.to_uppercase(), exponent)
1387    } else {
1388        format!("{}0x1.{}p{:+}", sign, hex_mantissa, exponent)
1389    }
1390}
1391
1392/// Quote a string for shell output (like zsh's set output)
1393fn shell_quote(s: &str) -> String {
1394    if s.is_empty() {
1395        return "''".to_string();
1396    }
1397    // Check if quoting is needed
1398    let needs_quotes = s.chars().any(|c| {
1399        matches!(
1400            c,
1401            ' ' | '\t'
1402                | '\n'
1403                | '\''
1404                | '"'
1405                | '\\'
1406                | '$'
1407                | '`'
1408                | '!'
1409                | '*'
1410                | '?'
1411                | '['
1412                | ']'
1413                | '{'
1414                | '}'
1415                | '('
1416                | ')'
1417                | '<'
1418                | '>'
1419                | '|'
1420                | '&'
1421                | ';'
1422                | '#'
1423                | '~'
1424        )
1425    });
1426    if !needs_quotes {
1427        return s.to_string();
1428    }
1429    // Use single quotes, escaping single quotes as '\''
1430    format!("'{}'", s.replace('\'', "'\\''"))
1431}
1432
1433/// Quote a value for typeset -p output (re-executable code)
1434/// Uses single quoting only when the value contains special characters
1435fn shell_quote_value(s: &str) -> String {
1436    if s.is_empty() {
1437        return "''".to_string();
1438    }
1439    let needs_quotes = s.chars().any(|c| {
1440        matches!(
1441            c,
1442            ' ' | '\t'
1443                | '\n'
1444                | '\''
1445                | '"'
1446                | '\\'
1447                | '$'
1448                | '`'
1449                | '!'
1450                | '*'
1451                | '?'
1452                | '['
1453                | ']'
1454                | '{'
1455                | '}'
1456                | '('
1457                | ')'
1458                | '<'
1459                | '>'
1460                | '|'
1461                | '&'
1462                | ';'
1463                | '#'
1464                | '~'
1465                | '^'
1466        )
1467    });
1468    if !needs_quotes {
1469        return s.to_string();
1470    }
1471    format!("'{}'", s.replace('\'', "'\\''"))
1472}
1473
1474use crate::jobs::{continue_job, wait_for_child, wait_for_job, JobState, JobTable};
1475use crate::parser::{
1476    CaseTerminator, CompoundCommand, CondExpr, ListOp, Redirect, RedirectOp, ShellCommand,
1477    ShellParser, ShellWord, SimpleCommand, VarModifier, ZshParamFlag,
1478};
1479use crate::zwc::ZwcFile;
1480use std::collections::HashMap;
1481use std::env;
1482use std::fs::{self, File, OpenOptions};
1483use std::io;
1484use std::path::{Path, PathBuf};
1485use std::process::{Child, Command, Stdio};
1486
1487/// A completion specification for the `complete` builtin
1488#[derive(Debug, Clone, Default)]
1489pub struct CompSpec {
1490    pub actions: Vec<String>,     // -a, -b, -c, etc.
1491    pub wordlist: Option<String>, // -W wordlist
1492    pub function: Option<String>, // -F function
1493    pub command: Option<String>,  // -C command
1494    pub globpat: Option<String>,  // -G glob
1495    pub prefix: Option<String>,   // -P prefix
1496    pub suffix: Option<String>,   // -S suffix
1497}
1498
1499/// A single completion match for zsh-style completion
1500#[derive(Debug, Clone)]
1501pub struct CompMatch {
1502    pub word: String,                   // The actual completion word
1503    pub display: Option<String>,        // Display string (-d)
1504    pub prefix: Option<String>,         // -P prefix (inserted but not part of match)
1505    pub suffix: Option<String>,         // -S suffix (inserted but not part of match)
1506    pub hidden_prefix: Option<String>,  // -p hidden prefix
1507    pub hidden_suffix: Option<String>,  // -s hidden suffix
1508    pub ignored_prefix: Option<String>, // -i ignored prefix
1509    pub ignored_suffix: Option<String>, // -I ignored suffix
1510    pub group: Option<String>,          // -J/-V group name
1511    pub description: Option<String>,    // -X explanation
1512    pub remove_suffix: Option<String>,  // -r remove chars
1513    pub file_match: bool,               // -f flag
1514    pub quote_match: bool,              // -q flag
1515}
1516
1517impl Default for CompMatch {
1518    fn default() -> Self {
1519        Self {
1520            word: String::new(),
1521            display: None,
1522            prefix: None,
1523            suffix: None,
1524            hidden_prefix: None,
1525            hidden_suffix: None,
1526            ignored_prefix: None,
1527            ignored_suffix: None,
1528            group: None,
1529            description: None,
1530            remove_suffix: None,
1531            file_match: false,
1532            quote_match: false,
1533        }
1534    }
1535}
1536
1537/// Completion group for organizing matches
1538#[derive(Debug, Clone, Default)]
1539pub struct CompGroup {
1540    pub name: String,
1541    pub matches: Vec<CompMatch>,
1542    pub explanation: Option<String>,
1543    pub sorted: bool,
1544}
1545
1546/// zsh completion state (compstate associative array)
1547#[derive(Debug, Clone, Default)]
1548pub struct CompState {
1549    pub context: String,               // completion context
1550    pub exact: String,                 // exact match handling
1551    pub exact_string: String,          // the exact string if matched
1552    pub ignored: i32,                  // number of ignored matches
1553    pub insert: String,                // what to insert
1554    pub insert_positions: String,      // cursor positions after insert
1555    pub last_prompt: String,           // whether to return to last prompt
1556    pub list: String,                  // listing style
1557    pub list_lines: i32,               // number of lines for listing
1558    pub list_max: i32,                 // max matches to list
1559    pub nmatches: i32,                 // number of matches
1560    pub old_insert: String,            // previous insert value
1561    pub old_list: String,              // previous list value
1562    pub parameter: String,             // parameter being completed
1563    pub pattern_insert: String,        // pattern insert mode
1564    pub pattern_match: String,         // pattern matching mode
1565    pub quote: String,                 // quoting type
1566    pub quoting: String,               // current quoting
1567    pub redirect: String,              // redirection type
1568    pub restore: String,               // restore mode
1569    pub to_end: String,                // move to end mode
1570    pub unambiguous: String,           // unambiguous prefix
1571    pub unambiguous_cursor: i32,       // cursor pos in unambiguous
1572    pub unambiguous_positions: String, // positions in unambiguous
1573    pub vared: String,                 // vared context
1574}
1575
1576/// zstyle entry for completion configuration
1577#[derive(Debug, Clone)]
1578pub struct ZStyle {
1579    pub pattern: String,
1580    pub style: String,
1581    pub values: Vec<String>,
1582}
1583
1584bitflags::bitflags! {
1585    /// Flags for autoloaded functions
1586    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1587    pub struct AutoloadFlags: u32 {
1588        const NO_ALIAS = 0b00000001;      // -U: don't expand aliases
1589        const ZSH_STYLE = 0b00000010;     // -z: zsh-style autoload
1590        const KSH_STYLE = 0b00000100;     // -k: ksh-style autoload
1591        const TRACE = 0b00001000;         // -t: trace execution
1592        const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
1593        const LOADED = 0b00100000;        // function has been loaded
1594    }
1595}
1596
1597/// State for a zpty pseudo-terminal
1598pub struct ZptyState {
1599    pub pid: u32,
1600    pub cmd: String,
1601    pub stdin: Option<std::process::ChildStdin>,
1602    pub stdout: Option<std::process::ChildStdout>,
1603    pub child: Option<std::process::Child>,
1604}
1605
1606/// Scheduled command for sched builtin
1607pub struct ScheduledCommand {
1608    pub id: u32,
1609    pub run_at: std::time::SystemTime,
1610    pub command: String,
1611}
1612
1613/// Profiling entry for zprof
1614#[derive(Clone, Default)]
1615pub struct ProfileEntry {
1616    pub calls: u64,
1617    pub total_time_us: u64,
1618    pub self_time_us: u64,
1619}
1620
1621/// Unix domain socket state
1622pub struct UnixSocketState {
1623    pub path: Option<PathBuf>,
1624    pub listening: bool,
1625    pub stream: Option<std::os::unix::net::UnixStream>,
1626    pub listener: Option<std::os::unix::net::UnixListener>,
1627}
1628
1629pub struct ShellExecutor {
1630    pub functions: HashMap<String, ShellCommand>,
1631    pub aliases: HashMap<String, String>,
1632    pub global_aliases: HashMap<String, String>, // alias -g: expand anywhere
1633    pub suffix_aliases: HashMap<String, String>, // alias -s: expand by file extension
1634    pub last_status: i32,
1635    pub variables: HashMap<String, String>,
1636    pub arrays: HashMap<String, Vec<String>>,
1637    pub assoc_arrays: HashMap<String, HashMap<String, String>>, // zsh associative arrays
1638    pub jobs: JobTable,
1639    pub fpath: Vec<PathBuf>,
1640    pub zwc_cache: HashMap<PathBuf, ZwcFile>,
1641    pub positional_params: Vec<String>,
1642    pub history: Option<HistoryEngine>,
1643    process_sub_counter: u32,
1644    pub traps: HashMap<String, String>,
1645    pub options: HashMap<String, bool>,
1646    pub completions: HashMap<String, CompSpec>, // command -> completion spec
1647    pub dir_stack: Vec<PathBuf>,
1648    // zsh completion system state
1649    pub comp_matches: Vec<CompMatch>, // Current completion matches
1650    pub comp_groups: Vec<CompGroup>,  // Completion groups
1651    pub comp_state: CompState,        // compstate associative array
1652    pub zstyles: Vec<ZStyle>,         // zstyle configurations
1653    pub comp_words: Vec<String>,      // words on command line
1654    pub comp_current: i32,            // current word index (1-based)
1655    pub comp_prefix: String,          // PREFIX parameter
1656    pub comp_suffix: String,          // SUFFIX parameter
1657    pub comp_iprefix: String,         // IPREFIX parameter
1658    pub comp_isuffix: String,         // ISUFFIX parameter
1659    pub readonly_vars: std::collections::HashSet<String>, // Read-only variables
1660    /// Stack for `local` variable save/restore (name, old_value).
1661    pub local_save_stack: Vec<(String, Option<String>)>,
1662    /// Current function scope depth for `local` tracking.
1663    pub local_scope_depth: usize,
1664    pub autoload_pending: HashMap<String, AutoloadFlags>, // Functions marked for autoload
1665    // zsh hooks (precmd, preexec, chpwd, etc.)
1666    pub hook_functions: HashMap<String, Vec<String>>, // hook_name -> [function_names]
1667    // Named directories (hash -d)
1668    pub named_dirs: HashMap<String, PathBuf>, // name -> path
1669    // zpty - pseudo-terminal management
1670    pub zptys: HashMap<String, ZptyState>,
1671    // sysopen - file descriptor management
1672    pub open_fds: HashMap<i32, std::fs::File>,
1673    pub next_fd: i32,
1674    // sched - scheduled commands
1675    pub scheduled_commands: Vec<ScheduledCommand>,
1676    // zprof - profiling data
1677    pub profile_data: HashMap<String, ProfileEntry>,
1678    pub profiling_enabled: bool,
1679    // zsocket - Unix domain sockets
1680    pub unix_sockets: HashMap<i32, UnixSocketState>,
1681    // compsys - completion system cache
1682    pub compsys_cache: Option<CompsysCache>,
1683    // Background compinit — receiver for async fpath scan result
1684    pub compinit_pending: Option<(
1685        std::sync::mpsc::Receiver<CompInitBgResult>,
1686        std::time::Instant,
1687    )>,
1688    // Plugin source cache — stores side effects of source/. in SQLite
1689    pub plugin_cache: Option<crate::plugin_cache::PluginCache>,
1690    // cdreplay - deferred compdef calls for zinit turbo mode
1691    pub deferred_compdefs: Vec<Vec<String>>,
1692    // command hash table (hash builtin)
1693    pub command_hash: HashMap<String, String>,
1694    // Control flow signals
1695    returning: Option<i32>, // Set by return builtin, cleared after function returns
1696    breaking: i32,          // break level (0 = not breaking, N = break N levels)
1697    continuing: i32,        // continue level
1698    // New module state
1699    pub pcre_state: PcreState,
1700    pub tcp_sessions: TcpSessions,
1701    pub zftp: Zftp,
1702    pub profiler: Profiler,
1703    pub style_table: StyleTable,
1704    /// zsh compatibility mode - use .zcompdump, fpath scanning, etc.
1705    pub zsh_compat: bool,
1706    /// POSIX sh strict mode — no SQLite, no worker pool, no zsh extensions
1707    pub posix_mode: bool,
1708    /// Worker thread pool for background tasks (compinit, process subs, etc.)
1709    pub worker_pool: std::sync::Arc<crate::worker::WorkerPool>,
1710    /// AOP intercept table: command/function name → advice chain.
1711    /// Glob patterns supported (e.g. "git *", "*").
1712    pub intercepts: Vec<Intercept>,
1713    /// Async job handles: id → receiver for (status, stdout)
1714    pub async_jobs: HashMap<u32, crossbeam_channel::Receiver<(i32, String)>>,
1715    /// Next async job ID
1716    pub next_async_id: u32,
1717    /// Defer stack: commands to run on scope exit (LIFO).
1718    pub defer_stack: Vec<Vec<String>>,
1719}
1720
1721impl ShellExecutor {
1722    pub fn new() -> Self {
1723        tracing::debug!("ShellExecutor::new() initializing");
1724        // Initialize fpath from FPATH env var or use defaults
1725        let fpath = env::var("FPATH")
1726            .unwrap_or_default()
1727            .split(':')
1728            .filter(|s| !s.is_empty())
1729            .map(PathBuf::from)
1730            .collect();
1731
1732        let history = HistoryEngine::new().ok();
1733
1734        // Initialize standard zsh variables
1735        let mut variables = HashMap::new();
1736        variables.insert("ZSH_VERSION".to_string(), "5.9".to_string());
1737        variables.insert(
1738            "ZSH_PATCHLEVEL".to_string(),
1739            "zsh-5.9-0-g73d3173".to_string(),
1740        );
1741        variables.insert("ZSH_NAME".to_string(), "zsh".to_string());
1742        variables.insert(
1743            "SHLVL".to_string(),
1744            env::var("SHLVL")
1745                .map(|v| {
1746                    v.parse::<i32>()
1747                        .map(|n| (n + 1).to_string())
1748                        .unwrap_or_else(|_| "1".to_string())
1749                })
1750                .unwrap_or_else(|_| "1".to_string()),
1751        );
1752
1753        Self {
1754            functions: HashMap::new(),
1755            aliases: HashMap::new(),
1756            global_aliases: HashMap::new(),
1757            suffix_aliases: HashMap::new(),
1758            last_status: 0,
1759            variables,
1760            arrays: {
1761                let mut a = HashMap::new();
1762                // $path mirrors $PATH (tied array)
1763                let path_dirs: Vec<String> = env::var("PATH")
1764                    .unwrap_or_default()
1765                    .split(':')
1766                    .map(|s| s.to_string())
1767                    .collect();
1768                a.insert("path".to_string(), path_dirs);
1769                a
1770            },
1771            assoc_arrays: HashMap::new(),
1772            jobs: JobTable::new(),
1773            fpath,
1774            zwc_cache: HashMap::new(),
1775            positional_params: Vec::new(),
1776            history,
1777            completions: HashMap::new(),
1778            dir_stack: Vec::new(),
1779            process_sub_counter: 0,
1780            traps: HashMap::new(),
1781            options: Self::default_options(),
1782            // zsh completion system
1783            comp_matches: Vec::new(),
1784            comp_groups: Vec::new(),
1785            comp_state: CompState::default(),
1786            zstyles: Vec::new(),
1787            comp_words: Vec::new(),
1788            comp_current: 0,
1789            comp_prefix: String::new(),
1790            comp_suffix: String::new(),
1791            comp_iprefix: String::new(),
1792            comp_isuffix: String::new(),
1793            readonly_vars: std::collections::HashSet::new(),
1794            local_save_stack: Vec::new(),
1795            local_scope_depth: 0,
1796            autoload_pending: HashMap::new(),
1797            hook_functions: HashMap::new(),
1798            named_dirs: HashMap::new(),
1799            zptys: HashMap::new(),
1800            open_fds: HashMap::new(),
1801            next_fd: 10,
1802            scheduled_commands: Vec::new(),
1803            profile_data: HashMap::new(),
1804            profiling_enabled: false,
1805            unix_sockets: HashMap::new(),
1806            compsys_cache: {
1807                let cache_path = compsys::cache::default_cache_path();
1808                if cache_path.exists() {
1809                    let db_size = std::fs::metadata(&cache_path).map(|m| m.len()).unwrap_or(0);
1810                    match CompsysCache::open(&cache_path) {
1811                        Ok(c) => {
1812                            tracing::info!(
1813                                db_bytes = db_size,
1814                                path = %cache_path.display(),
1815                                "compsys: sqlite cache opened"
1816                            );
1817                            Some(c)
1818                        }
1819                        Err(e) => {
1820                            tracing::warn!(error = %e, "compsys: failed to open cache");
1821                            None
1822                        }
1823                    }
1824                } else {
1825                    tracing::debug!("compsys: no cache at {}", cache_path.display());
1826                    None
1827                }
1828            },
1829            compinit_pending: None, // (receiver, start_time)
1830            plugin_cache: {
1831                let pc_path = crate::plugin_cache::default_cache_path();
1832                if let Some(parent) = pc_path.parent() {
1833                    let _ = std::fs::create_dir_all(parent);
1834                }
1835                match crate::plugin_cache::PluginCache::open(&pc_path) {
1836                    Ok(pc) => {
1837                        let (plugins, functions) = pc.stats();
1838                        tracing::info!(
1839                            plugins,
1840                            cached_functions = functions,
1841                            path = %pc_path.display(),
1842                            "plugin_cache: sqlite opened"
1843                        );
1844                        Some(pc)
1845                    }
1846                    Err(e) => {
1847                        tracing::warn!(error = %e, "plugin_cache: failed to open");
1848                        None
1849                    }
1850                }
1851            },
1852            deferred_compdefs: Vec::new(),
1853            command_hash: HashMap::new(),
1854            returning: None,
1855            breaking: 0,
1856            continuing: 0,
1857            pcre_state: PcreState::new(),
1858            tcp_sessions: TcpSessions::new(),
1859            zftp: Zftp::new(),
1860            profiler: Profiler::new(),
1861            style_table: StyleTable::new(),
1862            zsh_compat: false,
1863            posix_mode: false,
1864            worker_pool: {
1865                let config = crate::config::load();
1866                let pool_size = crate::config::resolve_pool_size(&config.worker_pool);
1867                std::sync::Arc::new(crate::worker::WorkerPool::new(pool_size))
1868            },
1869            intercepts: Vec::new(),
1870            async_jobs: HashMap::new(),
1871            next_async_id: 1,
1872            defer_stack: Vec::new(),
1873        }
1874    }
1875
1876    /// Enter POSIX strict mode — drop all SQLite caches, shrink worker pool to minimum.
1877    /// No zsh extensions, no caching, no threads beyond the bare minimum. Dinosaur mode.
1878    pub fn enter_posix_mode(&mut self) {
1879        self.posix_mode = true;
1880        self.plugin_cache = None;
1881        self.compsys_cache = None;
1882        self.compinit_pending = None;
1883        // Worker pool stays at size 1 — we can't drop it entirely because
1884        // some code paths use it unconditionally, but with 1 thread it's
1885        // effectively serial.
1886        self.worker_pool = std::sync::Arc::new(crate::worker::WorkerPool::new(1));
1887        tracing::info!("POSIX strict mode: SQLite caches dropped, worker pool shrunk to 1");
1888    }
1889
1890    /// Run hook functions (precmd, preexec, chpwd, etc.)
1891    pub fn run_hooks(&mut self, hook_name: &str) {
1892        if let Some(funcs) = self.hook_functions.get(hook_name).cloned() {
1893            for func_name in funcs {
1894                if self.functions.contains_key(&func_name) {
1895                    let _ = self.execute_script(&format!("{}", func_name));
1896                }
1897            }
1898        }
1899        // Also check for hook arrays (e.g., precmd_functions)
1900        let array_name = format!("{}_functions", hook_name);
1901        if let Some(funcs) = self.arrays.get(&array_name).cloned() {
1902            for func_name in funcs {
1903                if self.functions.contains_key(&func_name) {
1904                    let _ = self.execute_script(&format!("{}", func_name));
1905                }
1906            }
1907        }
1908    }
1909
1910    /// Add a function to a hook
1911    pub fn add_hook(&mut self, hook_name: &str, func_name: &str) {
1912        self.hook_functions
1913            .entry(hook_name.to_string())
1914            .or_default()
1915            .push(func_name.to_string());
1916    }
1917
1918    /// Add a named directory (hash -d name=path)
1919    pub fn add_named_dir(&mut self, name: &str, path: &str) {
1920        self.named_dirs
1921            .insert(name.to_string(), PathBuf::from(path));
1922    }
1923
1924    /// Expand ~ with named directories
1925    pub fn expand_tilde_named(&self, path: &str) -> String {
1926        if path.starts_with('~') {
1927            let rest = &path[1..];
1928            // Check for ~name or ~name/...
1929            let (name, suffix) = if let Some(slash_pos) = rest.find('/') {
1930                (&rest[..slash_pos], &rest[slash_pos..])
1931            } else {
1932                (rest, "")
1933            };
1934
1935            if name.is_empty() {
1936                // Regular ~ expansion
1937                if let Ok(home) = std::env::var("HOME") {
1938                    return format!("{}{}", home, suffix);
1939                }
1940            } else if let Some(dir) = self.named_dirs.get(name) {
1941                return format!("{}{}", dir.display(), suffix);
1942            }
1943        }
1944        path.to_string()
1945    }
1946
1947    fn all_zsh_options() -> &'static [&'static str] {
1948        &[
1949            "aliases",
1950            "aliasfuncdef",
1951            "allexport",
1952            "alwayslastprompt",
1953            "alwaystoend",
1954            "appendcreate",
1955            "appendhistory",
1956            "autocd",
1957            "autocontinue",
1958            "autolist",
1959            "automenu",
1960            "autonamedirs",
1961            "autoparamkeys",
1962            "autoparamslash",
1963            "autopushd",
1964            "autoremoveslash",
1965            "autoresume",
1966            "badpattern",
1967            "banghist",
1968            "bareglobqual",
1969            "bashautolist",
1970            "bashrematch",
1971            "beep",
1972            "bgnice",
1973            "braceccl",
1974            "braceexpand",
1975            "bsdecho",
1976            "caseglob",
1977            "casematch",
1978            "casepaths",
1979            "cbases",
1980            "cdablevars",
1981            "cdsilent",
1982            "chasedots",
1983            "chaselinks",
1984            "checkjobs",
1985            "checkrunningjobs",
1986            "clobber",
1987            "clobberempty",
1988            "combiningchars",
1989            "completealiases",
1990            "completeinword",
1991            "continueonerror",
1992            "correct",
1993            "correctall",
1994            "cprecedences",
1995            "cshjunkiehistory",
1996            "cshjunkieloops",
1997            "cshjunkiequotes",
1998            "cshnullcmd",
1999            "cshnullglob",
2000            "debugbeforecmd",
2001            "dotglob",
2002            "dvorak",
2003            "emacs",
2004            "equals",
2005            "errexit",
2006            "errreturn",
2007            "evallineno",
2008            "exec",
2009            "extendedglob",
2010            "extendedhistory",
2011            "flowcontrol",
2012            "forcefloat",
2013            "functionargzero",
2014            "glob",
2015            "globassign",
2016            "globcomplete",
2017            "globdots",
2018            "globstarshort",
2019            "globsubst",
2020            "globalexport",
2021            "globalrcs",
2022            "hashall",
2023            "hashcmds",
2024            "hashdirs",
2025            "hashexecutablesonly",
2026            "hashlistall",
2027            "histallowclobber",
2028            "histappend",
2029            "histbeep",
2030            "histexpand",
2031            "histexpiredupsfirst",
2032            "histfcntllock",
2033            "histfindnodups",
2034            "histignorealldups",
2035            "histignoredups",
2036            "histignorespace",
2037            "histlexwords",
2038            "histnofunctions",
2039            "histnostore",
2040            "histreduceblanks",
2041            "histsavebycopy",
2042            "histsavenodups",
2043            "histsubstpattern",
2044            "histverify",
2045            "hup",
2046            "ignorebraces",
2047            "ignoreclosebraces",
2048            "ignoreeof",
2049            "incappendhistory",
2050            "incappendhistorytime",
2051            "interactive",
2052            "interactivecomments",
2053            "ksharrays",
2054            "kshautoload",
2055            "kshglob",
2056            "kshoptionprint",
2057            "kshtypeset",
2058            "kshzerosubscript",
2059            "listambiguous",
2060            "listbeep",
2061            "listpacked",
2062            "listrowsfirst",
2063            "listtypes",
2064            "localloops",
2065            "localoptions",
2066            "localpatterns",
2067            "localtraps",
2068            "log",
2069            "login",
2070            "longlistjobs",
2071            "magicequalsubst",
2072            "mailwarn",
2073            "mailwarning",
2074            "markdirs",
2075            "menucomplete",
2076            "monitor",
2077            "multibyte",
2078            "multifuncdef",
2079            "multios",
2080            "nomatch",
2081            "notify",
2082            "nullglob",
2083            "numericglobsort",
2084            "octalzeroes",
2085            "onecmd",
2086            "overstrike",
2087            "pathdirs",
2088            "pathscript",
2089            "physical",
2090            "pipefail",
2091            "posixaliases",
2092            "posixargzero",
2093            "posixbuiltins",
2094            "posixcd",
2095            "posixidentifiers",
2096            "posixjobs",
2097            "posixstrings",
2098            "posixtraps",
2099            "printeightbit",
2100            "printexitvalue",
2101            "privileged",
2102            "promptbang",
2103            "promptcr",
2104            "promptpercent",
2105            "promptsp",
2106            "promptsubst",
2107            "promptvars",
2108            "pushdignoredups",
2109            "pushdminus",
2110            "pushdsilent",
2111            "pushdtohome",
2112            "rcexpandparam",
2113            "rcquotes",
2114            "rcs",
2115            "recexact",
2116            "rematchpcre",
2117            "restricted",
2118            "rmstarsilent",
2119            "rmstarwait",
2120            "sharehistory",
2121            "shfileexpansion",
2122            "shglob",
2123            "shinstdin",
2124            "shnullcmd",
2125            "shoptionletters",
2126            "shortloops",
2127            "shortrepeat",
2128            "shwordsplit",
2129            "singlecommand",
2130            "singlelinezle",
2131            "sourcetrace",
2132            "stdin",
2133            "sunkeyboardhack",
2134            "trackall",
2135            "transientrprompt",
2136            "trapsasync",
2137            "typesetsilent",
2138            "typesettounset",
2139            "unset",
2140            "verbose",
2141            "vi",
2142            "warncreateglobal",
2143            "warnnestedvar",
2144            "xtrace",
2145            "zle",
2146        ]
2147    }
2148
2149    fn default_options() -> HashMap<String, bool> {
2150        let mut opts = HashMap::new();
2151        // Initialize all options to false first
2152        for opt in Self::all_zsh_options() {
2153            opts.insert(opt.to_string(), false);
2154        }
2155        // Set zsh defaults (options marked with <D> or <Z> in zshoptions man page)
2156        let defaults_on = [
2157            "aliases",
2158            "alwayslastprompt",
2159            "appendhistory",
2160            "autolist",
2161            "automenu",
2162            "autoparamkeys",
2163            "autoparamslash",
2164            "autoremoveslash",
2165            "badpattern",
2166            "banghist",
2167            "bareglobqual",
2168            "beep",
2169            "bgnice",
2170            "caseglob",
2171            "casematch",
2172            "checkjobs",
2173            "checkrunningjobs",
2174            "clobber",
2175            "debugbeforecmd",
2176            "equals",
2177            "evallineno",
2178            "exec",
2179            "flowcontrol",
2180            "functionargzero",
2181            "glob",
2182            "globalexport",
2183            "globalrcs",
2184            "hashcmds",
2185            "hashdirs",
2186            "hashlistall",
2187            "histbeep",
2188            "histsavebycopy",
2189            "hup",
2190            "interactive",
2191            "listambiguous",
2192            "listbeep",
2193            "listtypes",
2194            "monitor",
2195            "multibyte",
2196            "multifuncdef",
2197            "multios",
2198            "nomatch",
2199            "notify",
2200            "promptcr",
2201            "promptpercent",
2202            "promptsp",
2203            "rcs",
2204            "shinstdin",
2205            "shortloops",
2206            "unset",
2207            "zle",
2208        ];
2209        for opt in defaults_on {
2210            opts.insert(opt.to_string(), true);
2211        }
2212        opts
2213    }
2214
2215    /// Normalize option name: lowercase, remove underscores/hyphens, handle "no" prefix
2216    fn normalize_option_name(name: &str) -> (String, bool) {
2217        let normalized = name.to_lowercase().replace(['-', '_'], "");
2218        if let Some(stripped) = normalized.strip_prefix("no") {
2219            // O(1) lookup in HashSet instead of linear scan
2220            if ZSH_OPTIONS_SET.contains(stripped) {
2221                return (stripped.to_string(), false);
2222            }
2223        }
2224        (normalized, true)
2225    }
2226
2227    /// Check if option name matches a pattern (for -m flag)
2228    fn option_matches_pattern(opt: &str, pattern: &str) -> bool {
2229        let pat = pattern.to_lowercase().replace(['-', '_'], "");
2230        let opt_lower = opt.to_lowercase();
2231
2232        if pat.contains('*') || pat.contains('?') || pat.contains('[') {
2233            let regex_pat = pat.replace('.', "\\.").replace('*', ".*").replace('?', ".");
2234            let full_pattern = format!("^{}$", regex_pat);
2235            cached_regex(&full_pattern)
2236                .map(|re| re.is_match(&opt_lower))
2237                .unwrap_or(false)
2238        } else {
2239            opt_lower == pat
2240        }
2241    }
2242
2243    /// Try to load a function from ZWC files in fpath
2244    pub fn autoload_function(&mut self, name: &str) -> Option<ShellCommand> {
2245        // First check if already loaded
2246        if let Some(func) = self.functions.get(name) {
2247            return Some(func.clone());
2248        }
2249
2250        // Search fpath for the function - use index to avoid borrow issues
2251        for i in 0..self.fpath.len() {
2252            let dir = self.fpath[i].clone();
2253            // Try directory.zwc first
2254            let zwc_path = dir.with_extension("zwc");
2255            if zwc_path.exists() {
2256                if let Some(func) = self.load_function_from_zwc(&zwc_path, name) {
2257                    return Some(func);
2258                }
2259            }
2260
2261            // Try individual function.zwc
2262            let func_zwc = dir.join(format!("{}.zwc", name));
2263            if func_zwc.exists() {
2264                if let Some(func) = self.load_function_from_zwc(&func_zwc, name) {
2265                    return Some(func);
2266                }
2267            }
2268
2269            // Look for directory/*.zwc files containing this function
2270            if dir.is_dir() {
2271                if let Ok(entries) = fs::read_dir(&dir) {
2272                    for entry in entries.flatten() {
2273                        let path = entry.path();
2274                        if path.extension().map_or(false, |e| e == "zwc") {
2275                            if let Some(func) = self.load_function_from_zwc(&path, name) {
2276                                return Some(func);
2277                            }
2278                        }
2279                    }
2280                }
2281            }
2282        }
2283
2284        None
2285    }
2286
2287    /// Load a specific function from a ZWC file
2288    fn load_function_from_zwc(&mut self, path: &Path, name: &str) -> Option<ShellCommand> {
2289        // Check cache
2290        let zwc = if let Some(cached) = self.zwc_cache.get(path) {
2291            cached
2292        } else {
2293            // Load and cache the ZWC file
2294            let zwc = ZwcFile::load(path).ok()?;
2295            self.zwc_cache.insert(path.to_path_buf(), zwc);
2296            self.zwc_cache.get(path)?
2297        };
2298
2299        // Find the function
2300        let func = zwc.get_function(name)?;
2301        let decoded = zwc.decode_function(func)?;
2302
2303        // Convert to shell command and cache
2304        let shell_func = decoded.to_shell_function()?;
2305
2306        // Register the function
2307        if let ShellCommand::FunctionDef(fname, body) = &shell_func {
2308            self.functions.insert(fname.clone(), (**body).clone());
2309        }
2310
2311        Some(shell_func)
2312    }
2313
2314    /// Add a directory to fpath
2315    pub fn add_fpath(&mut self, path: PathBuf) {
2316        if !self.fpath.contains(&path) {
2317            self.fpath.insert(0, path);
2318        }
2319    }
2320
2321    /// Match a string against a shell glob pattern
2322    fn glob_match(&self, s: &str, pattern: &str) -> bool {
2323        // Convert shell glob to regex
2324        let mut regex_pattern = String::from("^");
2325        let mut chars = pattern.chars().peekable();
2326
2327        while let Some(c) = chars.next() {
2328            match c {
2329                '*' => regex_pattern.push_str(".*"),
2330                '?' => regex_pattern.push('.'),
2331                '[' => {
2332                    regex_pattern.push('[');
2333                    // Handle character class
2334                    while let Some(cc) = chars.next() {
2335                        if cc == ']' {
2336                            regex_pattern.push(']');
2337                            break;
2338                        }
2339                        regex_pattern.push(cc);
2340                    }
2341                }
2342                '(' => {
2343                    // Handle alternation (a|b|c) -> (a|b|c)
2344                    regex_pattern.push('(');
2345                }
2346                ')' => regex_pattern.push(')'),
2347                '|' => regex_pattern.push('|'),
2348                '.' | '+' | '^' | '$' | '\\' | '{' | '}' => {
2349                    regex_pattern.push('\\');
2350                    regex_pattern.push(c);
2351                }
2352                _ => regex_pattern.push(c),
2353            }
2354        }
2355        regex_pattern.push('$');
2356
2357        regex::Regex::new(&regex_pattern)
2358            .map(|re| re.is_match(s))
2359            .unwrap_or(false)
2360    }
2361
2362    /// Static glob match — same logic as glob_match but callable without &self,
2363    /// needed for Rayon parallel iterators that can't capture &self.
2364    pub fn glob_match_static(s: &str, pattern: &str) -> bool {
2365        let mut regex_pattern = String::from("^");
2366        let mut chars = pattern.chars().peekable();
2367        while let Some(c) = chars.next() {
2368            match c {
2369                '*' => regex_pattern.push_str(".*"),
2370                '?' => regex_pattern.push('.'),
2371                '[' => {
2372                    regex_pattern.push('[');
2373                    while let Some(cc) = chars.next() {
2374                        if cc == ']' {
2375                            regex_pattern.push(']');
2376                            break;
2377                        }
2378                        regex_pattern.push(cc);
2379                    }
2380                }
2381                '(' => regex_pattern.push('('),
2382                ')' => regex_pattern.push(')'),
2383                '|' => regex_pattern.push('|'),
2384                '.' | '+' | '^' | '$' | '\\' | '{' | '}' => {
2385                    regex_pattern.push('\\');
2386                    regex_pattern.push(c);
2387                }
2388                _ => regex_pattern.push(c),
2389            }
2390        }
2391        regex_pattern.push('$');
2392        regex::Regex::new(&regex_pattern)
2393            .map(|re| re.is_match(s))
2394            .unwrap_or(false)
2395    }
2396
2397    /// Execute a script file with bytecode caching — skips lex+parse+compile on cache hit.
2398    /// Bytecode is stored in SQLite keyed by (path, mtime).
2399    pub fn execute_script_file(&mut self, file_path: &str) -> Result<i32, String> {
2400        use std::path::Path;
2401
2402        let path = Path::new(file_path);
2403        let abs_path = path
2404            .canonicalize()
2405            .unwrap_or_else(|_| path.to_path_buf())
2406            .to_string_lossy()
2407            .to_string();
2408
2409        // Try bytecode cache first
2410        if let Some(ref cache) = self.plugin_cache {
2411            if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(path) {
2412                if let Some(bc_blob) = cache.check_bytecode(&abs_path, mt_s, mt_ns) {
2413                    if let Ok(chunk) = bincode::deserialize::<fusevm::Chunk>(&bc_blob) {
2414                        if !chunk.ops.is_empty() {
2415                            tracing::trace!(
2416                                path = %abs_path,
2417                                ops = chunk.ops.len(),
2418                                "execute_script_file: bytecode cache hit"
2419                            );
2420                            let mut vm = fusevm::VM::new(chunk);
2421                            register_builtins(&mut vm);
2422                            let _ctx = ExecutorContext::enter(self);
2423                            match vm.run() {
2424                                fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
2425                                    self.last_status = vm.last_status;
2426                                }
2427                                fusevm::VMResult::Error(e) => {
2428                                    return Err(format!("VM error: {}", e));
2429                                }
2430                            }
2431                            return Ok(self.last_status);
2432                        }
2433                    }
2434                }
2435            }
2436        }
2437
2438        // Cache miss — read, parse, compile, execute, then cache
2439        let content =
2440            std::fs::read_to_string(file_path).map_err(|e| format!("{}: {}", file_path, e))?;
2441        let expanded = self.expand_history(&content);
2442        let mut parser = ShellParser::new(&expanded);
2443        let commands = parser.parse_script()?;
2444
2445        let compiler = crate::shell_compiler::ShellCompiler::new();
2446        let chunk = compiler.compile(&commands);
2447
2448        // Cache the bytecode for next time
2449        if let Some(ref cache) = self.plugin_cache {
2450            if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(path) {
2451                if let Ok(blob) = bincode::serialize(&chunk) {
2452                    let _ = cache.store_bytecode(&abs_path, mt_s, mt_ns, &blob);
2453                    tracing::trace!(
2454                        path = %abs_path,
2455                        bytes = blob.len(),
2456                        "execute_script_file: bytecode cached"
2457                    );
2458                }
2459            }
2460        }
2461
2462        // Execute
2463        if !chunk.ops.is_empty() {
2464            let mut vm = fusevm::VM::new(chunk);
2465            register_builtins(&mut vm);
2466            let _ctx = ExecutorContext::enter(self);
2467            match vm.run() {
2468                fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
2469                    self.last_status = vm.last_status;
2470                }
2471                fusevm::VMResult::Error(e) => {
2472                    return Err(format!("VM error: {}", e));
2473                }
2474            }
2475        }
2476
2477        Ok(self.last_status)
2478    }
2479
2480    #[tracing::instrument(skip(self, script), fields(len = script.len()))]
2481    pub fn execute_script(&mut self, script: &str) -> Result<i32, String> {
2482        // Expand history references before parsing
2483        let expanded = self.expand_history(script);
2484
2485        let mut parser = ShellParser::new(&expanded);
2486        let commands = parser.parse_script()?;
2487        tracing::trace!(cmds = commands.len(), "execute_script: parsed");
2488
2489        // Compile to fusevm bytecodes and execute on the VM.
2490        // All execution goes through the VM — no tree-walker fallback.
2491        // Builtins dispatch through CallBuiltin, accessing executor state via thread-local.
2492        let compiler = crate::shell_compiler::ShellCompiler::new();
2493        let chunk = compiler.compile(&commands);
2494
2495        if !chunk.ops.is_empty() {
2496            if std::env::var("ZSHRS_DEBUG_OPS").is_ok() {
2497                eprintln!("[DEBUG] Compiled {} ops:", chunk.ops.len());
2498                for (i, op) in chunk.ops.iter().enumerate() {
2499                    eprintln!("  {:3}: {:?}", i, op);
2500                }
2501            }
2502            let mut vm = fusevm::VM::new(chunk);
2503            register_builtins(&mut vm);
2504
2505            // Set executor context for builtin handlers
2506            let _ctx = ExecutorContext::enter(self);
2507
2508            match vm.run() {
2509                fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
2510                    self.last_status = vm.last_status;
2511                }
2512                fusevm::VMResult::Error(e) => {
2513                    return Err(format!("VM error: {}", e));
2514                }
2515            }
2516        }
2517
2518        // Fire EXIT trap if set (matches zsh's zshexit behavior).
2519        // Remove it first to prevent infinite recursion.
2520        if let Some(action) = self.traps.remove("EXIT") {
2521            tracing::debug!("firing EXIT trap");
2522            let _ = self.execute_script(&action);
2523        }
2524
2525        Ok(self.last_status)
2526    }
2527
2528    /// Expand history references: !!, !n, !-n, !string, !?string?
2529    fn expand_history(&self, input: &str) -> String {
2530        let Some(ref engine) = self.history else {
2531            return input.to_string();
2532        };
2533
2534        // Quick check: nothing to expand
2535        if !input.contains('!') && !input.starts_with('^') {
2536            return input.to_string();
2537        }
2538
2539        let history_count = engine.count().unwrap_or(0) as usize;
2540        if history_count == 0 {
2541            return input.to_string();
2542        }
2543
2544        let chars: Vec<char> = input.chars().collect();
2545
2546        // ^foo^bar quick substitution (only at start of input)
2547        if chars.first() == Some(&'^') {
2548            if let Some(expanded) = self.history_quick_subst(&chars, engine) {
2549                return expanded;
2550            }
2551        }
2552
2553        let mut result = String::new();
2554        let mut i = 0;
2555        let mut in_single_quote = false;
2556        let mut in_brace = 0; // Track ${...} nesting
2557        let mut last_subst: Option<(String, String)> = None; // for :& modifier
2558
2559        while i < chars.len() {
2560            // Track single quotes — no history expansion inside them
2561            if chars[i] == '\'' && in_brace == 0 {
2562                in_single_quote = !in_single_quote;
2563                result.push(chars[i]);
2564                i += 1;
2565                continue;
2566            }
2567            if in_single_quote {
2568                result.push(chars[i]);
2569                i += 1;
2570                continue;
2571            }
2572
2573            // Track ${...} nesting
2574            if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' {
2575                in_brace += 1;
2576                result.push(chars[i]);
2577                i += 1;
2578                result.push(chars[i]);
2579                i += 1;
2580                continue;
2581            }
2582            if chars[i] == '}' && in_brace > 0 {
2583                in_brace -= 1;
2584                result.push(chars[i]);
2585                i += 1;
2586                continue;
2587            }
2588
2589            // Backslash-escaped ! is literal
2590            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '!' {
2591                result.push('!');
2592                i += 2;
2593                continue;
2594            }
2595
2596            if chars[i] == '!' && in_brace == 0 {
2597                if i + 1 >= chars.len() {
2598                    // Trailing ! — literal
2599                    result.push('!');
2600                    i += 1;
2601                    continue;
2602                }
2603
2604                let next = chars[i + 1];
2605                // ! followed by space, =, ( — literal (zsh rule)
2606                if next == ' ' || next == '\t' || next == '=' || next == '(' || next == '\n' {
2607                    result.push('!');
2608                    i += 1;
2609                    continue;
2610                }
2611
2612                // Resolve the event string
2613                let (event_str, new_i) = self.history_resolve_event(&chars, i, engine, &result);
2614                if let Some(ev) = event_str {
2615                    // Check for word designators and modifiers
2616                    let (final_str, final_i) = self.history_apply_designators_and_modifiers(
2617                        &chars,
2618                        new_i,
2619                        &ev,
2620                        &mut last_subst,
2621                    );
2622                    result.push_str(&final_str);
2623                    i = final_i;
2624                } else {
2625                    // Could not resolve — keep the ! literal
2626                    result.push('!');
2627                    i += 1;
2628                }
2629                continue;
2630            }
2631            result.push(chars[i]);
2632            i += 1;
2633        }
2634
2635        result
2636    }
2637
2638    /// ^foo^bar quick substitution — replace first occurrence of foo with bar
2639    /// in the previous command.
2640    fn history_quick_subst(
2641        &self,
2642        chars: &[char],
2643        engine: &crate::history::HistoryEngine,
2644    ) -> Option<String> {
2645        let mut i = 1; // skip leading ^
2646        let mut old = String::new();
2647        while i < chars.len() && chars[i] != '^' {
2648            old.push(chars[i]);
2649            i += 1;
2650        }
2651        if i >= chars.len() {
2652            return None;
2653        }
2654        i += 1; // skip middle ^
2655        let mut new = String::new();
2656        while i < chars.len() && chars[i] != '^' && chars[i] != '\n' {
2657            new.push(chars[i]);
2658            i += 1;
2659        }
2660        let prev = engine.get_by_offset(0).ok()??;
2661        Some(prev.command.replacen(&old, &new, 1))
2662    }
2663
2664    /// Resolve which history event ! refers to.  Returns (Some(full_command), index_after_event)
2665    /// or (None, original_index) if we can't resolve.
2666    fn history_resolve_event(
2667        &self,
2668        chars: &[char],
2669        bang_pos: usize,
2670        engine: &crate::history::HistoryEngine,
2671        current_line: &str,
2672    ) -> (Option<String>, usize) {
2673        let mut i = bang_pos + 1; // past the !
2674
2675        // !{...} brace-wrapped event
2676        let in_brace = i < chars.len() && chars[i] == '{';
2677        if in_brace {
2678            i += 1;
2679        }
2680
2681        let c = if i < chars.len() {
2682            chars[i]
2683        } else {
2684            return (None, bang_pos);
2685        };
2686
2687        let (event, new_i) = match c {
2688            '!' => {
2689                // !! — previous command
2690                let entry = engine.get_by_offset(0).ok().flatten();
2691                (entry.map(|e| e.command), i + 1)
2692            }
2693            '#' => {
2694                // !# — current command line so far
2695                (Some(current_line.to_string()), i + 1)
2696            }
2697            '-' => {
2698                // !-n — nth previous command
2699                i += 1;
2700                let start = i;
2701                while i < chars.len() && chars[i].is_ascii_digit() {
2702                    i += 1;
2703                }
2704                if i > start {
2705                    let n: usize = chars[start..i]
2706                        .iter()
2707                        .collect::<String>()
2708                        .parse()
2709                        .unwrap_or(0);
2710                    if n > 0 {
2711                        let entry = engine.get_by_offset(n - 1).ok().flatten();
2712                        (entry.map(|e| e.command), i)
2713                    } else {
2714                        (None, bang_pos)
2715                    }
2716                } else {
2717                    (None, bang_pos)
2718                }
2719            }
2720            '?' => {
2721                // !?string? — contains search
2722                i += 1;
2723                let start = i;
2724                while i < chars.len() && chars[i] != '?' && chars[i] != '\n' {
2725                    i += 1;
2726                }
2727                let search: String = chars[start..i].iter().collect();
2728                if i < chars.len() && chars[i] == '?' {
2729                    i += 1;
2730                }
2731                let entry = engine
2732                    .search(&search, 1)
2733                    .ok()
2734                    .and_then(|v| v.into_iter().next());
2735                (entry.map(|e| e.command), i)
2736            }
2737            c if c.is_ascii_digit() => {
2738                // !n — command by absolute number
2739                let start = i;
2740                while i < chars.len() && chars[i].is_ascii_digit() {
2741                    i += 1;
2742                }
2743                let n: i64 = chars[start..i]
2744                    .iter()
2745                    .collect::<String>()
2746                    .parse()
2747                    .unwrap_or(0);
2748                if n > 0 {
2749                    let entry = engine.get_by_number(n).ok().flatten();
2750                    (entry.map(|e| e.command), i)
2751                } else {
2752                    (None, bang_pos)
2753                }
2754            }
2755            '$' => {
2756                // !$ — last word of previous command (shorthand for !!:$)
2757                let entry = engine.get_by_offset(0).ok().flatten();
2758                let word =
2759                    entry.and_then(|e| Self::history_split_words(&e.command).last().cloned());
2760                // Return the word directly — skip designator parsing
2761                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
2762                    i + 2
2763                } else {
2764                    i + 1
2765                };
2766                return (word, final_i);
2767            }
2768            '^' => {
2769                // !^ — first arg of previous command (shorthand for !!:1)
2770                let entry = engine.get_by_offset(0).ok().flatten();
2771                let word = entry.and_then(|e| {
2772                    let words = Self::history_split_words(&e.command);
2773                    words.get(1).cloned()
2774                });
2775                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
2776                    i + 2
2777                } else {
2778                    i + 1
2779                };
2780                return (word, final_i);
2781            }
2782            '*' => {
2783                // !* — all args of previous command (shorthand for !!:*)
2784                let entry = engine.get_by_offset(0).ok().flatten();
2785                let word = entry.map(|e| {
2786                    let words = Self::history_split_words(&e.command);
2787                    if words.len() > 1 {
2788                        words[1..].join(" ")
2789                    } else {
2790                        String::new()
2791                    }
2792                });
2793                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
2794                    i + 2
2795                } else {
2796                    i + 1
2797                };
2798                return (word, final_i);
2799            }
2800            c if c.is_alphabetic() || c == '_' || c == '/' || c == '.' => {
2801                // !string — prefix search
2802                let start = i;
2803                while i < chars.len()
2804                    && !chars[i].is_whitespace()
2805                    && chars[i] != ':'
2806                    && chars[i] != '!'
2807                    && chars[i] != '}'
2808                {
2809                    i += 1;
2810                }
2811                let prefix: String = chars[start..i].iter().collect();
2812                let entry = engine
2813                    .search_prefix(&prefix, 1)
2814                    .ok()
2815                    .and_then(|v| v.into_iter().next());
2816                (entry.map(|e| e.command), i)
2817            }
2818            _ => (None, bang_pos),
2819        };
2820
2821        // Skip closing brace
2822        let final_i = if in_brace && new_i < chars.len() && chars[new_i] == '}' {
2823            new_i + 1
2824        } else {
2825            new_i
2826        };
2827
2828        (event, final_i)
2829    }
2830
2831    /// Split a command string into words for word designators, respecting quotes.
2832    fn history_split_words(cmd: &str) -> Vec<String> {
2833        let mut words = Vec::new();
2834        let mut current = String::new();
2835        let mut in_sq = false;
2836        let mut in_dq = false;
2837        let mut escaped = false;
2838
2839        for c in cmd.chars() {
2840            if escaped {
2841                current.push(c);
2842                escaped = false;
2843                continue;
2844            }
2845            if c == '\\' {
2846                current.push(c);
2847                escaped = true;
2848                continue;
2849            }
2850            if c == '\'' && !in_dq {
2851                in_sq = !in_sq;
2852                current.push(c);
2853                continue;
2854            }
2855            if c == '"' && !in_sq {
2856                in_dq = !in_dq;
2857                current.push(c);
2858                continue;
2859            }
2860            if c.is_whitespace() && !in_sq && !in_dq {
2861                if !current.is_empty() {
2862                    words.push(std::mem::take(&mut current));
2863                }
2864                continue;
2865            }
2866            current.push(c);
2867        }
2868        if !current.is_empty() {
2869            words.push(current);
2870        }
2871        words
2872    }
2873
2874    /// Apply word designators (:0, :n, :^, :$, :*, :n-m) and modifiers
2875    /// (:h, :t, :r, :e, :s/old/new/, :gs/old/new/, :p, :l, :u, :q, :Q, :a, :A)
2876    /// to an already-resolved event string.
2877    fn history_apply_designators_and_modifiers(
2878        &self,
2879        chars: &[char],
2880        mut i: usize,
2881        event: &str,
2882        last_subst: &mut Option<(String, String)>,
2883    ) -> (String, usize) {
2884        let words = Self::history_split_words(event);
2885        let argc = words.len().saturating_sub(1); // last word index
2886
2887        // Check for word designator — either :N or bare :^ :$ :*
2888        let mut sline = event.to_string();
2889
2890        if i < chars.len() && chars[i] == ':' {
2891            i += 1;
2892            if i < chars.len() {
2893                // Parse word designator
2894                let (farg, larg, new_i) = self.history_parse_word_range(chars, i, argc);
2895                i = new_i;
2896                if farg.is_some() || larg.is_some() {
2897                    let f = farg.unwrap_or(0);
2898                    let l = larg.unwrap_or(argc);
2899                    let selected: Vec<&String> = words
2900                        .iter()
2901                        .enumerate()
2902                        .filter(|(idx, _)| *idx >= f && *idx <= l)
2903                        .map(|(_, w)| w)
2904                        .collect();
2905                    sline = selected
2906                        .iter()
2907                        .map(|s| s.as_str())
2908                        .collect::<Vec<_>>()
2909                        .join(" ");
2910                }
2911            }
2912        } else if i < chars.len() && chars[i] == '*' {
2913            // !!* shorthand for !!:1-$
2914            i += 1;
2915            if words.len() > 1 {
2916                sline = words[1..].join(" ");
2917            } else {
2918                sline = String::new();
2919            }
2920        }
2921
2922        // Apply modifiers (:h :t :r :e :s :gs :p :l :u :q :Q :a :A)
2923        while i < chars.len() && chars[i] == ':' {
2924            i += 1;
2925            if i >= chars.len() {
2926                break;
2927            }
2928            let mut global = false;
2929            if chars[i] == 'g' && i + 1 < chars.len() {
2930                global = true;
2931                i += 1;
2932            }
2933            match chars[i] {
2934                'h' => {
2935                    // Head — remove trailing path component
2936                    i += 1;
2937                    if let Some(pos) = sline.rfind('/') {
2938                        if pos > 0 {
2939                            sline = sline[..pos].to_string();
2940                        } else {
2941                            sline = "/".to_string();
2942                        }
2943                    }
2944                }
2945                't' => {
2946                    // Tail — remove leading path components
2947                    i += 1;
2948                    if let Some(pos) = sline.rfind('/') {
2949                        sline = sline[pos + 1..].to_string();
2950                    }
2951                }
2952                'r' => {
2953                    // Remove extension
2954                    i += 1;
2955                    if let Some(pos) = sline.rfind('.') {
2956                        if pos > 0 && sline[..pos].rfind('/').map_or(true, |sp| sp < pos) {
2957                            sline = sline[..pos].to_string();
2958                        }
2959                    }
2960                }
2961                'e' => {
2962                    // Extension only
2963                    i += 1;
2964                    if let Some(pos) = sline.rfind('.') {
2965                        sline = sline[pos + 1..].to_string();
2966                    } else {
2967                        sline = String::new();
2968                    }
2969                }
2970                'l' => {
2971                    // Lowercase
2972                    i += 1;
2973                    sline = sline.to_lowercase();
2974                }
2975                'u' => {
2976                    // Uppercase
2977                    i += 1;
2978                    sline = sline.to_uppercase();
2979                }
2980                'p' => {
2981                    // Print only, don't execute (we just expand — caller handles this)
2982                    i += 1;
2983                    // For now, just expand — :p suppression would need upstream support
2984                }
2985                'q' => {
2986                    // Quote — single-quote the result
2987                    i += 1;
2988                    sline = format!("'{}'", sline.replace('\'', "'\\''"));
2989                }
2990                'Q' => {
2991                    // Unquote — strip one level of quotes
2992                    i += 1;
2993                    sline = sline.replace('\'', "").replace('"', "");
2994                }
2995                'a' => {
2996                    // Absolute path
2997                    i += 1;
2998                    if !sline.starts_with('/') {
2999                        if let Ok(cwd) = std::env::current_dir() {
3000                            sline = format!("{}/{}", cwd.display(), sline);
3001                        }
3002                    }
3003                }
3004                'A' => {
3005                    // Realpath
3006                    i += 1;
3007                    if let Ok(real) = std::fs::canonicalize(&sline) {
3008                        sline = real.to_string_lossy().to_string();
3009                    }
3010                }
3011                's' | 'S' => {
3012                    // :s/old/new/ or :gs/old/new/
3013                    i += 1;
3014                    if i < chars.len() {
3015                        let delim = chars[i];
3016                        i += 1;
3017                        let mut old_s = String::new();
3018                        while i < chars.len() && chars[i] != delim {
3019                            old_s.push(chars[i]);
3020                            i += 1;
3021                        }
3022                        if i < chars.len() {
3023                            i += 1;
3024                        } // skip delimiter
3025                        let mut new_s = String::new();
3026                        while i < chars.len()
3027                            && chars[i] != delim
3028                            && chars[i] != ':'
3029                            && chars[i] != ' '
3030                        {
3031                            new_s.push(chars[i]);
3032                            i += 1;
3033                        }
3034                        if i < chars.len() && chars[i] == delim {
3035                            i += 1;
3036                        } // skip trailing delimiter
3037                        *last_subst = Some((old_s.clone(), new_s.clone()));
3038                        if global {
3039                            sline = sline.replace(&old_s, &new_s);
3040                        } else {
3041                            sline = sline.replacen(&old_s, &new_s, 1);
3042                        }
3043                    }
3044                }
3045                '&' => {
3046                    // Repeat last substitution
3047                    i += 1;
3048                    if let Some((ref old_s, ref new_s)) = last_subst {
3049                        if global {
3050                            sline = sline.replace(old_s.as_str(), new_s.as_str());
3051                        } else {
3052                            sline = sline.replacen(old_s.as_str(), new_s.as_str(), 1);
3053                        }
3054                    }
3055                }
3056                _ => {
3057                    if global {
3058                        // 'g' was consumed but next char isn't s/S/& — put back
3059                        // by not advancing i further
3060                    }
3061                    break;
3062                }
3063            }
3064        }
3065
3066        (sline, i)
3067    }
3068
3069    /// Parse a word range like 0, 1, ^, $, *, n-m, n-
3070    fn history_parse_word_range(
3071        &self,
3072        chars: &[char],
3073        mut i: usize,
3074        argc: usize,
3075    ) -> (Option<usize>, Option<usize>, usize) {
3076        if i >= chars.len() {
3077            return (None, None, i);
3078        }
3079
3080        // Check for modifiers that aren't word designators
3081        match chars[i] {
3082            'h' | 't' | 'r' | 'e' | 's' | 'S' | 'g' | 'p' | 'q' | 'Q' | 'l' | 'u' | 'a' | 'A'
3083            | '&' => {
3084                // This is a modifier, not a word designator — back up
3085                return (None, None, i - 1); // -1 to re-read the ':'
3086            }
3087            _ => {}
3088        }
3089
3090        let farg = if chars[i] == '^' {
3091            i += 1;
3092            Some(1usize)
3093        } else if chars[i] == '$' {
3094            i += 1;
3095            return (Some(argc), Some(argc), i);
3096        } else if chars[i] == '*' {
3097            i += 1;
3098            return (Some(1), Some(argc), i);
3099        } else if chars[i].is_ascii_digit() {
3100            let start = i;
3101            while i < chars.len() && chars[i].is_ascii_digit() {
3102                i += 1;
3103            }
3104            let n: usize = chars[start..i]
3105                .iter()
3106                .collect::<String>()
3107                .parse()
3108                .unwrap_or(0);
3109            Some(n)
3110        } else {
3111            None
3112        };
3113
3114        // Check for range: n-m or n-
3115        if i < chars.len() && chars[i] == '-' {
3116            i += 1;
3117            if i < chars.len() && chars[i] == '$' {
3118                i += 1;
3119                return (farg, Some(argc), i);
3120            } else if i < chars.len() && chars[i].is_ascii_digit() {
3121                let start = i;
3122                while i < chars.len() && chars[i].is_ascii_digit() {
3123                    i += 1;
3124                }
3125                let m: usize = chars[start..i]
3126                    .iter()
3127                    .collect::<String>()
3128                    .parse()
3129                    .unwrap_or(0);
3130                return (farg, Some(m), i);
3131            } else {
3132                // n- means n to argc-1
3133                return (farg, Some(argc.saturating_sub(1)), i);
3134            }
3135        }
3136
3137        if farg.is_some() {
3138            (farg, farg, i)
3139        } else {
3140            (None, None, i)
3141        }
3142    }
3143
3144    #[tracing::instrument(level = "trace", skip_all)]
3145    pub fn execute_command(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
3146        match cmd {
3147            ShellCommand::Simple(simple) => self.execute_simple(simple),
3148            ShellCommand::Pipeline(cmds, negated) => {
3149                let status = self.execute_pipeline(cmds)?;
3150                if *negated {
3151                    self.last_status = if status == 0 { 1 } else { 0 };
3152                } else {
3153                    self.last_status = status;
3154                }
3155                Ok(self.last_status)
3156            }
3157            ShellCommand::List(items) => self.execute_list(items),
3158            ShellCommand::Compound(compound) => self.execute_compound(compound),
3159            ShellCommand::FunctionDef(name, body) => {
3160                if name.is_empty() {
3161                    // Anonymous function - execute immediately
3162                    let result = self.execute_command(body);
3163                    // Clear returning flag since the anonymous function has completed
3164                    if let Some(ret) = self.returning.take() {
3165                        self.last_status = ret;
3166                        return Ok(ret);
3167                    }
3168                    result
3169                } else {
3170                    // Named function - just define it
3171                    self.functions.insert(name.clone(), (**body).clone());
3172                    self.last_status = 0;
3173                    Ok(0)
3174                }
3175            }
3176        }
3177    }
3178
3179    #[tracing::instrument(level = "trace", skip_all)]
3180    fn execute_simple(&mut self, cmd: &SimpleCommand) -> Result<i32, String> {
3181        // Handle assignments
3182        for (var, val, is_append) in &cmd.assignments {
3183            match val {
3184                ShellWord::ArrayLiteral(elements) => {
3185                    // Array assignment: arr=(a b c) or arr+=(a b c)
3186                    // For associative arrays: assoc=(k1 v1 k2 v2)
3187                    // Use expand_word_split so $(cmd) and $var undergo
3188                    // word splitting into separate array elements (C zsh behavior).
3189                    let new_elements: Vec<String> = elements
3190                        .iter()
3191                        .flat_map(|e| self.expand_word_split(e))
3192                        .collect();
3193
3194                    // Check if this is an associative array
3195                    if self.assoc_arrays.contains_key(var) {
3196                        // Associative array: treat pairs as key-value
3197                        if *is_append {
3198                            let assoc = self.assoc_arrays.get_mut(var).unwrap();
3199                            let mut iter = new_elements.iter();
3200                            while let Some(key) = iter.next() {
3201                                if let Some(val) = iter.next() {
3202                                    assoc.insert(key.clone(), val.clone());
3203                                }
3204                            }
3205                        } else {
3206                            let mut assoc = HashMap::new();
3207                            let mut iter = new_elements.iter();
3208                            while let Some(key) = iter.next() {
3209                                if let Some(val) = iter.next() {
3210                                    assoc.insert(key.clone(), val.clone());
3211                                }
3212                            }
3213                            self.assoc_arrays.insert(var.clone(), assoc);
3214                        }
3215                    } else if *is_append {
3216                        // Append to existing indexed array
3217                        let arr = self.arrays.entry(var.clone()).or_insert_with(Vec::new);
3218                        arr.extend(new_elements);
3219                    } else {
3220                        self.arrays.insert(var.clone(), new_elements);
3221                    }
3222                }
3223                _ => {
3224                    let expanded = self.expand_word(val);
3225
3226                    // Check for array element assignment: arr[idx]=value or assoc[key]=value
3227                    if let Some(bracket_pos) = var.find('[') {
3228                        if var.ends_with(']') {
3229                            let array_name = &var[..bracket_pos];
3230                            let key = &var[bracket_pos + 1..var.len() - 1];
3231                            let key = self.expand_string(key); // Expand the key/index
3232
3233                            // Check if it's an associative array
3234                            if self.assoc_arrays.contains_key(array_name) {
3235                                let assoc = self.assoc_arrays.get_mut(array_name).unwrap();
3236                                if *is_append {
3237                                    let existing = assoc.get(&key).cloned().unwrap_or_default();
3238                                    assoc.insert(key, existing + &expanded);
3239                                } else {
3240                                    assoc.insert(key, expanded);
3241                                }
3242                            } else if let Ok(idx) = key.parse::<i64>() {
3243                                // Regular indexed array
3244                                let idx = if idx < 0 { 0 } else { (idx - 1) as usize }; // zsh is 1-indexed
3245                                let arr = self
3246                                    .arrays
3247                                    .entry(array_name.to_string())
3248                                    .or_insert_with(Vec::new);
3249                                while arr.len() <= idx {
3250                                    arr.push(String::new());
3251                                }
3252                                if *is_append {
3253                                    arr[idx].push_str(&expanded);
3254                                } else {
3255                                    arr[idx] = expanded;
3256                                }
3257                            } else {
3258                                // Non-numeric key on non-assoc array - treat as assoc
3259                                let assoc = self
3260                                    .assoc_arrays
3261                                    .entry(array_name.to_string())
3262                                    .or_insert_with(HashMap::new);
3263                                if *is_append {
3264                                    let existing = assoc.get(&key).cloned().unwrap_or_default();
3265                                    assoc.insert(key, existing + &expanded);
3266                                } else {
3267                                    assoc.insert(key, expanded);
3268                                }
3269                            }
3270                            continue;
3271                        }
3272                    }
3273
3274                    // Regular variable assignment or append
3275                    let final_value = if *is_append {
3276                        let existing = self.variables.get(var).cloned().unwrap_or_default();
3277                        existing + &expanded
3278                    } else {
3279                        expanded
3280                    };
3281
3282                    if self.readonly_vars.contains(var) {
3283                        eprintln!("zshrs: read-only variable: {}", var);
3284                        self.last_status = 1;
3285                        return Ok(1);
3286                    }
3287                    if cmd.words.is_empty() {
3288                        // Just assignment, set in environment
3289                        env::set_var(var, &final_value);
3290                    }
3291                    self.variables.insert(var.clone(), final_value);
3292                }
3293            }
3294        }
3295
3296        if cmd.words.is_empty() {
3297            self.last_status = 0;
3298            return Ok(0);
3299        }
3300
3301        // Check if this is a noglob precommand — suppress glob expansion
3302        let is_noglob = cmd
3303            .words
3304            .first()
3305            .map(|w| self.expand_word(w) == "noglob")
3306            .unwrap_or(false);
3307        let saved_noglob = if is_noglob {
3308            let saved = self.options.get("noglob").copied();
3309            self.options.insert("noglob".to_string(), true);
3310            saved
3311        } else {
3312            None
3313        };
3314
3315        // Pre-launch external command substitutions in parallel before expanding words.
3316        // Each external $(cmd) gets spawned on the worker pool immediately.
3317        // When we reach that word during sequential expansion, we collect the result.
3318        let preflight = self.preflight_command_subs(&cmd.words);
3319
3320        let mut words: Vec<String> = cmd
3321            .words
3322            .iter()
3323            .enumerate()
3324            .flat_map(|(i, w)| {
3325                if let Some(rx) = &preflight[i] {
3326                    // Pre-launched external command sub — collect result
3327                    vec![rx.recv().unwrap_or_default()]
3328                } else {
3329                    self.expand_word_glob(w)
3330                }
3331            })
3332            .collect();
3333
3334        // Restore noglob after expansion
3335        if is_noglob {
3336            match saved_noglob {
3337                Some(v) => {
3338                    self.options.insert("noglob".to_string(), v);
3339                }
3340                None => {
3341                    self.options.remove("noglob");
3342                }
3343            }
3344        }
3345        if words.is_empty() {
3346            self.last_status = 0;
3347            return Ok(0);
3348        }
3349
3350        // Expand global aliases (alias -g) in all word positions
3351        if !self.global_aliases.is_empty() {
3352            let global_aliases = self.global_aliases.clone();
3353            words = words
3354                .into_iter()
3355                .map(|w| global_aliases.get(&w).cloned().unwrap_or(w))
3356                .collect();
3357        }
3358
3359        // xtrace: print expanded command to stderr (zsh -x / set -x)
3360        if self.options.get("xtrace").copied().unwrap_or(false) {
3361            let ps4 = self
3362                .variables
3363                .get("PS4")
3364                .cloned()
3365                .unwrap_or_else(|| "+".to_string());
3366            eprintln!("{}{}", ps4, words.join(" "));
3367        }
3368
3369        // Check for regular alias expansion (alias > builtin > function > command)
3370        let cmd_name = &words[0];
3371        if let Some(alias_value) = self.aliases.get(cmd_name).cloned() {
3372            // Expand the alias: replace cmd_name with alias value, keep remaining args
3373            let expanded_cmd = if words.len() > 1 {
3374                format!("{} {}", alias_value, words[1..].join(" "))
3375            } else {
3376                alias_value
3377            };
3378            // Re-execute the expanded command
3379            return self.execute_script(&expanded_cmd);
3380        }
3381
3382        // Check for suffix alias expansion (alias -s) when command is a file path
3383        if !self.suffix_aliases.is_empty() {
3384            let cmd_path = std::path::Path::new(cmd_name);
3385            if let Some(ext) = cmd_path.extension().and_then(|e| e.to_str()) {
3386                if let Some(handler) = self.suffix_aliases.get(ext).cloned() {
3387                    // Suffix alias: "alias -s txt=vim" makes "foo.txt" run "vim foo.txt"
3388                    let expanded_cmd = format!("{} {}", handler, words.join(" "));
3389                    return self.execute_script(&expanded_cmd);
3390                }
3391            }
3392        }
3393
3394        let args = &words[1..];
3395
3396        // Check if this is `exec` with only redirects (no command args)
3397        // For exec, redirects with {varname} allocate FDs; redirects are permanent
3398        let is_exec_with_redirects_only =
3399            cmd_name == "exec" && args.is_empty() && !cmd.redirects.is_empty();
3400
3401        // Apply redirects for builtins
3402        let mut saved_fds: Vec<(i32, i32)> = Vec::new();
3403        for redirect in &cmd.redirects {
3404            let target = self.expand_word(&redirect.target);
3405
3406            // Handle {varname}>file syntax - allocate new FD and store in variable
3407            if let Some(ref var_name) = redirect.fd_var {
3408                use std::os::unix::io::IntoRawFd;
3409                let file_result = match redirect.op {
3410                    RedirectOp::Write | RedirectOp::Clobber => std::fs::File::create(&target),
3411                    RedirectOp::Append => std::fs::OpenOptions::new()
3412                        .create(true)
3413                        .append(true)
3414                        .open(&target),
3415                    RedirectOp::Read => std::fs::File::open(&target),
3416                    _ => continue,
3417                };
3418                match file_result {
3419                    Ok(file) => {
3420                        let new_fd = file.into_raw_fd();
3421                        self.variables.insert(var_name.clone(), new_fd.to_string());
3422                        // Store allocated FD for potential cleanup (not for exec)
3423                        if !is_exec_with_redirects_only {
3424                            // For non-exec, we might want to track these
3425                        }
3426                    }
3427                    Err(e) => {
3428                        eprintln!("{}: {}: {}", cmd_name, target, e);
3429                        return Ok(1);
3430                    }
3431                }
3432                continue;
3433            }
3434
3435            let fd = redirect.fd.unwrap_or(match redirect.op {
3436                RedirectOp::Read
3437                | RedirectOp::HereDoc
3438                | RedirectOp::HereString
3439                | RedirectOp::ReadWrite => 0,
3440                _ => 1,
3441            });
3442
3443            match redirect.op {
3444                RedirectOp::Write | RedirectOp::Clobber => {
3445                    use std::os::unix::io::IntoRawFd;
3446                    if !is_exec_with_redirects_only {
3447                        let saved = unsafe { libc::dup(fd) };
3448                        if saved >= 0 {
3449                            saved_fds.push((fd, saved));
3450                        }
3451                    }
3452                    if let Ok(file) = std::fs::File::create(&target) {
3453                        let new_fd = file.into_raw_fd();
3454                        unsafe {
3455                            libc::dup2(new_fd, fd);
3456                        }
3457                        unsafe {
3458                            libc::close(new_fd);
3459                        }
3460                    }
3461                }
3462                RedirectOp::Append => {
3463                    use std::os::unix::io::IntoRawFd;
3464                    if !is_exec_with_redirects_only {
3465                        let saved = unsafe { libc::dup(fd) };
3466                        if saved >= 0 {
3467                            saved_fds.push((fd, saved));
3468                        }
3469                    }
3470                    if let Ok(file) = std::fs::OpenOptions::new()
3471                        .create(true)
3472                        .append(true)
3473                        .open(&target)
3474                    {
3475                        let new_fd = file.into_raw_fd();
3476                        unsafe {
3477                            libc::dup2(new_fd, fd);
3478                        }
3479                        unsafe {
3480                            libc::close(new_fd);
3481                        }
3482                    }
3483                }
3484                RedirectOp::Read => {
3485                    use std::os::unix::io::IntoRawFd;
3486                    if !is_exec_with_redirects_only {
3487                        let saved = unsafe { libc::dup(fd) };
3488                        if saved >= 0 {
3489                            saved_fds.push((fd, saved));
3490                        }
3491                    }
3492                    if let Ok(file) = std::fs::File::open(&target) {
3493                        let new_fd = file.into_raw_fd();
3494                        unsafe {
3495                            libc::dup2(new_fd, fd);
3496                        }
3497                        unsafe {
3498                            libc::close(new_fd);
3499                        }
3500                    }
3501                }
3502                RedirectOp::DupWrite | RedirectOp::DupRead => {
3503                    if let Ok(target_fd) = target.parse::<i32>() {
3504                        if !is_exec_with_redirects_only {
3505                            let saved = unsafe { libc::dup(fd) };
3506                            if saved >= 0 {
3507                                saved_fds.push((fd, saved));
3508                            }
3509                        }
3510                        unsafe {
3511                            libc::dup2(target_fd, fd);
3512                        }
3513                    }
3514                }
3515                _ => {}
3516            }
3517        }
3518
3519        // For exec with only redirects, we're done - redirects are applied permanently
3520        if is_exec_with_redirects_only {
3521            self.last_status = 0;
3522            return Ok(0);
3523        }
3524
3525        // Check for shell builtins
3526        let status = match cmd_name.as_str() {
3527            "cd" => self.builtin_cd(args),
3528            "pwd" => self.builtin_pwd(&cmd.redirects),
3529            "echo" => self.builtin_echo(args, &cmd.redirects),
3530            "export" => self.builtin_export(args),
3531            "unset" => self.builtin_unset(args),
3532            "source" | "." => self.builtin_source(args),
3533            "exit" | "bye" | "logout" => self.builtin_exit(args),
3534            "return" => self.builtin_return(args),
3535            "true" => 0,
3536            "false" => 1,
3537            ":" => 0,
3538            "chdir" => self.builtin_cd(args),
3539            "test" | "[" => self.builtin_test(args),
3540            "local" => self.builtin_local(args),
3541            "declare" | "typeset" => self.builtin_declare(args),
3542            "read" => self.builtin_read(args),
3543            "shift" => self.builtin_shift(args),
3544            "eval" => self.builtin_eval(args),
3545            "jobs" => self.builtin_jobs(args),
3546            "fg" => self.builtin_fg(args),
3547            "bg" => self.builtin_bg(args),
3548            "kill" => self.builtin_kill(args),
3549            "disown" => self.builtin_disown(args),
3550            "wait" => self.builtin_wait(args),
3551            "autoload" => self.builtin_autoload(args),
3552            "history" => self.builtin_history(args),
3553            "fc" => self.builtin_fc(args),
3554            "trap" => self.builtin_trap(args),
3555            "suspend" => self.builtin_suspend(args),
3556            "alias" => self.builtin_alias(args),
3557            "unalias" => self.builtin_unalias(args),
3558            "set" => self.builtin_set(args),
3559            "shopt" => self.builtin_shopt(args),
3560            // Bash compatibility
3561            "bind" => self.builtin_bindkey(args),
3562            "caller" => self.builtin_caller(args),
3563            "help" => self.builtin_help(args),
3564            "doctor" => self.builtin_doctor(args),
3565            "dbview" => self.builtin_dbview(args),
3566            "profile" => self.builtin_profile(args),
3567            "intercept" => self.builtin_intercept(args),
3568            "intercept_proceed" => self.builtin_intercept_proceed(args),
3569            // ── Concurrent primitives ──
3570            "async" => self.builtin_async(args),
3571            "await" => self.builtin_await(args),
3572            "pmap" => self.builtin_pmap(args),
3573            "pgrep" => self.builtin_pgrep(args),
3574            "peach" => self.builtin_peach(args),
3575            "barrier" => self.builtin_barrier(args),
3576            "readarray" | "mapfile" => self.builtin_readarray(args),
3577            "setopt" => self.builtin_setopt(args),
3578            "unsetopt" => self.builtin_unsetopt(args),
3579            "getopts" => self.builtin_getopts(args),
3580            "type" => self.builtin_type(args),
3581            "hash" => self.builtin_hash(args),
3582            "add-zsh-hook" => self.builtin_add_zsh_hook(args),
3583            "command" => self.builtin_command(args, &cmd.redirects),
3584            "builtin" => self.builtin_builtin(args, &cmd.redirects),
3585            "let" => self.builtin_let(args),
3586            "compgen" => self.builtin_compgen(args),
3587            "complete" => self.builtin_complete(args),
3588            "compopt" => self.builtin_compopt(args),
3589            "compadd" => self.builtin_compadd(args),
3590            "compset" => self.builtin_compset(args),
3591            "compdef" => self.builtin_compdef(args),
3592            "compinit" => self.builtin_compinit(args),
3593            "cdreplay" => self.builtin_cdreplay(args),
3594            "zstyle" => self.builtin_zstyle(args),
3595            // GDBM database bindings
3596            "ztie" => self.builtin_ztie(args),
3597            "zuntie" => self.builtin_zuntie(args),
3598            "zgdbmpath" => self.builtin_zgdbmpath(args),
3599            "pushd" => self.builtin_pushd(args),
3600            "popd" => self.builtin_popd(args),
3601            "dirs" => self.builtin_dirs(args),
3602            "printf" => self.builtin_printf(args),
3603            // Control flow
3604            "break" => self.builtin_break(args),
3605            "continue" => self.builtin_continue(args),
3606            // Enable/disable builtins
3607            "disable" => self.builtin_disable(args),
3608            "enable" => self.builtin_enable(args),
3609            // Emulation
3610            "emulate" => self.builtin_emulate(args),
3611            // Prompt themes
3612            "promptinit" => self.builtin_promptinit(args),
3613            "prompt" => self.builtin_prompt(args),
3614            // PCRE
3615            "pcre_compile" => self.builtin_pcre_compile(args),
3616            "pcre_match" => self.builtin_pcre_match(args),
3617            "pcre_study" => self.builtin_pcre_study(args),
3618            // Exec
3619            "exec" => self.builtin_exec(args),
3620            // Typed variables
3621            "float" => self.builtin_float(args),
3622            "integer" => self.builtin_integer(args),
3623            // Functions
3624            "functions" => self.builtin_functions(args),
3625            // Print (zsh style)
3626            "print" => self.builtin_print(args),
3627            // Command lookup
3628            "whence" => self.builtin_whence(args),
3629            "where" => self.builtin_where(args),
3630            "which" => self.builtin_which(args),
3631            // Resource limits
3632            "ulimit" => self.builtin_ulimit(args),
3633            "limit" => self.builtin_limit(args),
3634            "unlimit" => self.builtin_unlimit(args),
3635            // File mask
3636            "umask" => self.builtin_umask(args),
3637            // Hash table
3638            "rehash" => self.builtin_rehash(args),
3639            "unhash" => self.builtin_unhash(args),
3640            // Times
3641            "times" => self.builtin_times(args),
3642            // Module loading (stub)
3643            "zmodload" => self.builtin_zmodload(args),
3644            // Redo
3645            "r" => self.builtin_r(args),
3646            // TTY control
3647            "ttyctl" => self.builtin_ttyctl(args),
3648            // Noglob
3649            "noglob" => self.builtin_noglob(args, &cmd.redirects),
3650            // zsh/stat module
3651            "zstat" | "stat" => self.builtin_zstat(args),
3652            // zsh/datetime module
3653            "strftime" => self.builtin_strftime(args),
3654            // sleep with fractional seconds
3655            "zsleep" => self.builtin_zsleep(args),
3656            // zsh/system module - ported from Src/Modules/system.c
3657            "zsystem" => self.builtin_zsystem(args),
3658            // zsh/files module - ported from Src/Modules/files.c
3659            "sync" => self.builtin_sync(args),
3660            "mkdir" => self.builtin_mkdir(args),
3661            "rmdir" => self.builtin_rmdir(args),
3662            "ln" => self.builtin_ln(args),
3663            "mv" => self.builtin_mv(args),
3664            "cp" => self.builtin_cp(args),
3665            "rm" => self.builtin_rm(args),
3666            "chown" => self.builtin_chown(args),
3667            "chmod" => self.builtin_chmod(args),
3668            "zln" | "zmv" | "zcp" => self.builtin_zfiles(cmd_name, args),
3669            // coproc management
3670            "coproc" => self.builtin_coproc(args),
3671            // zparseopts - option parsing
3672            "zparseopts" => self.builtin_zparseopts(args),
3673            // readonly/unfunction
3674            "readonly" => self.builtin_readonly(args),
3675            "unfunction" => self.builtin_unfunction(args),
3676            // getln/pushln
3677            "getln" => self.builtin_getln(args),
3678            "pushln" => self.builtin_pushln(args),
3679            // bindkey stub
3680            "bindkey" => self.builtin_bindkey(args),
3681            // zle stub
3682            "zle" => self.builtin_zle(args),
3683            // sched
3684            "sched" => self.builtin_sched(args),
3685            // zformat
3686            "zformat" => self.builtin_zformat(args),
3687            // zcompile
3688            "zcompile" => self.builtin_zcompile(args),
3689            // vared - visual edit
3690            "vared" => self.builtin_vared(args),
3691            // terminal capabilities
3692            "echotc" => self.builtin_echotc(args),
3693            "echoti" => self.builtin_echoti(args),
3694            // PTY and socket operations
3695            "zpty" => self.builtin_zpty(args),
3696            "zprof" => self.builtin_zprof(args),
3697            "zsocket" => self.builtin_zsocket(args),
3698            "ztcp" => self.builtin_ztcp(args),
3699            "zregexparse" => self.builtin_zregexparse(args),
3700            "clone" => self.builtin_clone(args),
3701            "log" => self.builtin_log(args),
3702            // Completion system builtins
3703            "comparguments" => self.builtin_comparguments(args),
3704            "compcall" => self.builtin_compcall(args),
3705            "compctl" => self.builtin_compctl(args),
3706            "compdescribe" => self.builtin_compdescribe(args),
3707            "compfiles" => self.builtin_compfiles(args),
3708            "compgroups" => self.builtin_compgroups(args),
3709            "compquote" => self.builtin_compquote(args),
3710            "comptags" => self.builtin_comptags(args),
3711            "comptry" => self.builtin_comptry(args),
3712            "compvalues" => self.builtin_compvalues(args),
3713            // Capabilities (Linux-specific, stubs on macOS)
3714            "cap" | "getcap" | "setcap" => self.builtin_cap(args),
3715            // FTP client
3716            "zftp" => self.builtin_zftp(args),
3717            // zsh/curses module
3718            "zcurses" => self.builtin_zcurses(args),
3719            // zsh/system module
3720            "sysread" => self.builtin_sysread(args),
3721            "syswrite" => self.builtin_syswrite(args),
3722            "syserror" => self.builtin_syserror(args),
3723            "sysopen" => self.builtin_sysopen(args),
3724            "sysseek" => self.builtin_sysseek(args),
3725            // zsh/mapfile module
3726            "mapfile" => 0, // mapfile is a special parameter, not a command
3727            // zsh/param/private
3728            "private" => self.builtin_private(args),
3729            // zsh/attr (extended attributes)
3730            "zgetattr" | "zsetattr" | "zdelattr" | "zlistattr" => {
3731                self.builtin_zattr(cmd_name, args)
3732            }
3733            // Completion helper functions (now implemented in Rust compsys crate)
3734            // These are stubs that return success during non-completion execution
3735            "_arguments" | "_describe" | "_description" | "_message" | "_tags" | "_requested"
3736            | "_all_labels" | "_next_label" | "_files" | "_path_files" | "_directories" | "_cd"
3737            | "_default" | "_dispatch" | "_complete" | "_main_complete" | "_normal"
3738            | "_approximate" | "_correct" | "_expand" | "_history" | "_match" | "_menu"
3739            | "_oldlist" | "_list" | "_prefix" | "_generic" | "_wanted" | "_alternative"
3740            | "_values" | "_sequence" | "_sep_parts" | "_multi_parts" | "_combination"
3741            | "_parameters" | "_command" | "_command_names" | "_commands" | "_functions"
3742            | "_aliases" | "_builtins" | "_jobs" | "_pids" | "_process_names" | "_signals"
3743            | "_users" | "_groups" | "_hosts" | "_domains" | "_urls" | "_email_addresses"
3744            | "_options" | "_contexts" | "_set_options" | "_unset_options" | "_vars"
3745            | "_env_variables" | "_shell_variables" | "_arrays" | "_globflags" | "_globquals"
3746            | "_globqual_delims" | "_subscript" | "_history_modifiers" | "_brace_parameter"
3747            | "_tilde" | "_style" | "_cache_invalid" | "_store_cache" | "_retrieve_cache"
3748            | "_call_function" | "_call_program" | "_pick_variant" | "_setup"
3749            | "_comp_priv_prefix" | "_regex_arguments" | "_regex_words" | "_guard"
3750            | "_gnu_generic" | "_long_options" | "_x_arguments" | "_sub_commands"
3751            | "_cmdstring" | "_cmdambivalent" | "_first" | "_precommand" | "_user_at_host"
3752            | "_user_expand" | "_path_commands" | "_globbed_files" | "_have_glob_qual" => {
3753                // Return success - these functions are for completion context only
3754                // The actual completion logic is in the compsys Rust crate
3755                0
3756            }
3757            _ => {
3758                // ── AOP intercept dispatch ──
3759                // Check if any intercepts match this command name.
3760                // Fast path: skip if no intercepts registered.
3761                if !self.intercepts.is_empty() {
3762                    let full_cmd = if args.is_empty() {
3763                        cmd_name.to_string()
3764                    } else {
3765                        format!("{} {}", cmd_name, args.join(" "))
3766                    };
3767                    if let Some(result) = self.run_intercepts(cmd_name, &full_cmd, args) {
3768                        return result;
3769                    }
3770                }
3771
3772                // Check for function
3773                if let Some(func) = self.functions.get(cmd_name).cloned() {
3774                    return self.call_function(&func, args);
3775                }
3776
3777                // Try autoloading from pending autoload list
3778                if self.maybe_autoload(cmd_name) {
3779                    if let Some(func) = self.functions.get(cmd_name).cloned() {
3780                        return self.call_function(&func, args);
3781                    }
3782                }
3783
3784                // Try autoloading from ZWC
3785                if self.autoload_function(cmd_name).is_some() {
3786                    if let Some(func) = self.functions.get(cmd_name).cloned() {
3787                        return self.call_function(&func, args);
3788                    }
3789                }
3790
3791                // External command
3792                self.execute_external(cmd_name, args, &cmd.redirects)?
3793            }
3794        };
3795
3796        // Restore saved fds
3797        for (fd, saved) in saved_fds.into_iter().rev() {
3798            unsafe {
3799                libc::dup2(saved, fd);
3800                libc::close(saved);
3801            }
3802        }
3803
3804        self.last_status = status;
3805        Ok(status)
3806    }
3807
3808    /// Call a function with positional parameters
3809    #[tracing::instrument(level = "debug", skip_all)]
3810    fn call_function(&mut self, func: &ShellCommand, args: &[String]) -> Result<i32, String> {
3811        // Save current positional params
3812        let saved_params = std::mem::take(&mut self.positional_params);
3813
3814        // Save local variable scope — any `local` declarations during this
3815        // function will be reversed on exit (matches zsh's startparamscope/endparamscope).
3816        let saved_local_vars = self.local_save_stack.len();
3817        self.local_scope_depth += 1;
3818
3819        // Set new positional params
3820        self.positional_params = args.to_vec();
3821
3822        // Execute the function
3823        let result = self.execute_command(func);
3824
3825        // Handle return - clear the flag and use its value
3826        let final_result = if let Some(ret) = self.returning.take() {
3827            self.last_status = ret;
3828            Ok(ret)
3829        } else {
3830            result
3831        };
3832
3833        // Restore local variables (endparamscope)
3834        self.local_scope_depth -= 1;
3835        while self.local_save_stack.len() > saved_local_vars {
3836            if let Some((name, old_val)) = self.local_save_stack.pop() {
3837                match old_val {
3838                    Some(v) => {
3839                        self.variables.insert(name, v);
3840                    }
3841                    None => {
3842                        self.variables.remove(&name);
3843                    }
3844                }
3845            }
3846        }
3847
3848        // Restore positional params
3849        self.positional_params = saved_params;
3850
3851        final_result
3852    }
3853
3854    fn execute_external(
3855        &mut self,
3856        cmd: &str,
3857        args: &[String],
3858        redirects: &[Redirect],
3859    ) -> Result<i32, String> {
3860        self.execute_external_bg(cmd, args, redirects, false)
3861    }
3862
3863    fn execute_external_bg(
3864        &mut self,
3865        cmd: &str,
3866        args: &[String],
3867        redirects: &[Redirect],
3868        background: bool,
3869    ) -> Result<i32, String> {
3870        tracing::trace!(cmd, bg = background, "exec external");
3871        let mut command = Command::new(cmd);
3872        command.args(args);
3873
3874        // Apply redirections
3875        for redir in redirects {
3876            let target = self.expand_word(&redir.target);
3877            match redir.op {
3878                RedirectOp::Read => match File::open(&target) {
3879                    Ok(f) => {
3880                        command.stdin(Stdio::from(f));
3881                    }
3882                    Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3883                },
3884                RedirectOp::Write => match File::create(&target) {
3885                    Ok(f) => {
3886                        command.stdout(Stdio::from(f));
3887                    }
3888                    Err(e) => return Err(format!("Cannot create {}: {}", target, e)),
3889                },
3890                RedirectOp::Append => {
3891                    match OpenOptions::new().create(true).append(true).open(&target) {
3892                        Ok(f) => {
3893                            command.stdout(Stdio::from(f));
3894                        }
3895                        Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3896                    }
3897                }
3898                RedirectOp::WriteBoth => match File::create(&target) {
3899                    Ok(f) => {
3900                        let f2 = f
3901                            .try_clone()
3902                            .map_err(|e| format!("Cannot clone fd: {}", e))?;
3903                        command.stdout(Stdio::from(f));
3904                        command.stderr(Stdio::from(f2));
3905                    }
3906                    Err(e) => return Err(format!("Cannot create {}: {}", target, e)),
3907                },
3908                RedirectOp::AppendBoth => {
3909                    match OpenOptions::new().create(true).append(true).open(&target) {
3910                        Ok(f) => {
3911                            let f2 = f
3912                                .try_clone()
3913                                .map_err(|e| format!("Cannot clone fd: {}", e))?;
3914                            command.stdout(Stdio::from(f));
3915                            command.stderr(Stdio::from(f2));
3916                        }
3917                        Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3918                    }
3919                }
3920                RedirectOp::HereDoc => {
3921                    // Here-document - provide content as stdin
3922                    if let Some(ref content) = redir.heredoc_content {
3923                        // Expand variables in content (unless delimiter was quoted)
3924                        let expanded = self.expand_string(content);
3925                        command.stdin(Stdio::piped());
3926                        // Store the content to write after spawn
3927                        // For now, create a temp file
3928                        use std::io::Write;
3929                        let mut temp_file = tempfile::NamedTempFile::new()
3930                            .map_err(|e| format!("Cannot create temp file: {}", e))?;
3931                        temp_file
3932                            .write_all(expanded.as_bytes())
3933                            .map_err(|e| format!("Cannot write to temp file: {}", e))?;
3934                        let temp_path = temp_file.into_temp_path();
3935                        let f = File::open(&temp_path)
3936                            .map_err(|e| format!("Cannot open temp file: {}", e))?;
3937                        command.stdin(Stdio::from(f));
3938                    }
3939                }
3940                RedirectOp::HereString => {
3941                    // Here-string - provide target as stdin
3942                    use std::io::Write;
3943                    let content = format!("{}\n", target);
3944                    let mut temp_file = tempfile::NamedTempFile::new()
3945                        .map_err(|e| format!("Cannot create temp file: {}", e))?;
3946                    temp_file
3947                        .write_all(content.as_bytes())
3948                        .map_err(|e| format!("Cannot write to temp file: {}", e))?;
3949                    let temp_path = temp_file.into_temp_path();
3950                    let f = File::open(&temp_path)
3951                        .map_err(|e| format!("Cannot open temp file: {}", e))?;
3952                    command.stdin(Stdio::from(f));
3953                }
3954                _ => {
3955                    // Other redirections handled simply
3956                }
3957            }
3958
3959            // Handle {varname}>file syntax - store FD in variable
3960            if let Some(ref var_name) = redir.fd_var {
3961                // For {varname}>file, we open the file and store the fd number
3962                // This is typically used with exec, but we'll handle it for commands too
3963                #[cfg(unix)]
3964                {
3965                    use std::os::unix::io::AsRawFd;
3966                    let fd = match redir.op {
3967                        RedirectOp::Write | RedirectOp::Append => {
3968                            let f = if redir.op == RedirectOp::Write {
3969                                File::create(&target)
3970                            } else {
3971                                OpenOptions::new().create(true).append(true).open(&target)
3972                            };
3973                            match f {
3974                                Ok(file) => {
3975                                    let raw_fd = file.as_raw_fd();
3976                                    std::mem::forget(file); // Don't close the file
3977                                    raw_fd
3978                                }
3979                                Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3980                            }
3981                        }
3982                        RedirectOp::Read => match File::open(&target) {
3983                            Ok(file) => {
3984                                let raw_fd = file.as_raw_fd();
3985                                std::mem::forget(file);
3986                                raw_fd
3987                            }
3988                            Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
3989                        },
3990                        _ => continue,
3991                    };
3992                    self.variables.insert(var_name.clone(), fd.to_string());
3993                }
3994            }
3995        }
3996
3997        if background {
3998            match command.spawn() {
3999                Ok(child) => {
4000                    let pid = child.id();
4001                    let cmd_str = format!("{} {}", cmd, args.join(" "));
4002                    let job_id = self.jobs.add_job(child, cmd_str, JobState::Running);
4003                    println!("[{}] {}", job_id, pid);
4004                    Ok(0)
4005                }
4006                Err(e) => {
4007                    if e.kind() == io::ErrorKind::NotFound {
4008                        eprintln!("zshrs: command not found: {}", cmd);
4009                        Ok(127)
4010                    } else {
4011                        Err(format!("zshrs: {}: {}", cmd, e))
4012                    }
4013                }
4014            }
4015        } else {
4016            match command.status() {
4017                Ok(status) => Ok(status.code().unwrap_or(1)),
4018                Err(e) => {
4019                    if e.kind() == io::ErrorKind::NotFound {
4020                        eprintln!("zshrs: command not found: {}", cmd);
4021                        Ok(127)
4022                    } else {
4023                        Err(format!("zshrs: {}: {}", cmd, e))
4024                    }
4025                }
4026            }
4027        }
4028    }
4029
4030    #[tracing::instrument(level = "trace", skip_all, fields(stages = cmds.len()))]
4031    fn execute_pipeline(&mut self, cmds: &[ShellCommand]) -> Result<i32, String> {
4032        if cmds.len() == 1 {
4033            return self.execute_command(&cmds[0]);
4034        }
4035
4036        let mut children: Vec<Child> = Vec::new();
4037        let mut prev_stdout: Option<std::process::ChildStdout> = None;
4038
4039        for (i, cmd) in cmds.iter().enumerate() {
4040            if let ShellCommand::Simple(simple) = cmd {
4041                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
4042                if words.is_empty() {
4043                    continue;
4044                }
4045
4046                let mut command = Command::new(&words[0]);
4047                command.args(&words[1..]);
4048
4049                if let Some(stdout) = prev_stdout.take() {
4050                    command.stdin(Stdio::from(stdout));
4051                }
4052
4053                if i < cmds.len() - 1 {
4054                    command.stdout(Stdio::piped());
4055                }
4056
4057                match command.spawn() {
4058                    Ok(mut child) => {
4059                        prev_stdout = child.stdout.take();
4060                        children.push(child);
4061                    }
4062                    Err(e) => {
4063                        eprintln!("zshrs: {}: {}", words[0], e);
4064                        return Ok(127);
4065                    }
4066                }
4067            }
4068        }
4069
4070        // Wait for all children
4071        let mut last_status = 0;
4072        for mut child in children {
4073            if let Ok(status) = child.wait() {
4074                last_status = status.code().unwrap_or(1);
4075            }
4076        }
4077
4078        Ok(last_status)
4079    }
4080
4081    fn execute_list(&mut self, items: &[(ShellCommand, ListOp)]) -> Result<i32, String> {
4082        for (cmd, op) in items {
4083            // Check if this command should run in background
4084            let background = matches!(op, ListOp::Amp);
4085
4086            let status = if background {
4087                self.execute_command_bg(cmd)?
4088            } else {
4089                self.execute_command(cmd)?
4090            };
4091
4092            // Check for control flow
4093            if self.returning.is_some() || self.breaking > 0 || self.continuing > 0 {
4094                return Ok(status);
4095            }
4096
4097            match op {
4098                ListOp::And => {
4099                    if status != 0 {
4100                        return Ok(status);
4101                    }
4102                }
4103                ListOp::Or => {
4104                    if status == 0 {
4105                        return Ok(0);
4106                    }
4107                }
4108                ListOp::Amp => {
4109                    // Already backgrounded above, continue
4110                }
4111                ListOp::Semi | ListOp::Newline => {
4112                    // Sequential, continue
4113                }
4114            }
4115        }
4116
4117        Ok(self.last_status)
4118    }
4119
4120    fn execute_command_bg(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
4121        // For simple commands, run in background
4122        if let ShellCommand::Simple(simple) = cmd {
4123            if simple.words.is_empty() {
4124                return Ok(0);
4125            }
4126            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
4127            let cmd_name = &words[0];
4128            let args: Vec<String> = words[1..].to_vec();
4129            return self.execute_external_bg(cmd_name, &args, &simple.redirects, true);
4130        }
4131        // For complex commands, just execute normally (could fork in future)
4132        self.execute_command(cmd)
4133    }
4134
4135    #[tracing::instrument(level = "trace", skip_all)]
4136    fn execute_compound(&mut self, compound: &CompoundCommand) -> Result<i32, String> {
4137        match compound {
4138            CompoundCommand::BraceGroup(cmds) => {
4139                for cmd in cmds {
4140                    self.execute_command(cmd)?;
4141                    if self.returning.is_some() {
4142                        break;
4143                    }
4144                }
4145                Ok(self.last_status)
4146            }
4147            CompoundCommand::Subshell(cmds) => {
4148                // Subshell isolates variable changes — save/restore all state.
4149                // In real zsh this forks; we simulate by cloning variables.
4150                let saved_vars = self.variables.clone();
4151                let saved_arrays = self.arrays.clone();
4152                let saved_assoc = self.assoc_arrays.clone();
4153                let saved_params = self.positional_params.clone();
4154
4155                for cmd in cmds {
4156                    self.execute_command(cmd)?;
4157                    if self.returning.is_some() {
4158                        break;
4159                    }
4160                }
4161                let status = self.last_status;
4162
4163                // Restore state — subshell changes are discarded
4164                self.variables = saved_vars;
4165                self.arrays = saved_arrays;
4166                self.assoc_arrays = saved_assoc;
4167                self.positional_params = saved_params;
4168                self.last_status = status;
4169
4170                Ok(status)
4171            }
4172
4173            CompoundCommand::If {
4174                conditions,
4175                else_part,
4176            } => {
4177                for (cond, body) in conditions {
4178                    // Execute condition
4179                    for cmd in cond {
4180                        self.execute_command(cmd)?;
4181                    }
4182
4183                    if self.last_status == 0 {
4184                        // Condition true, execute body
4185                        for cmd in body {
4186                            self.execute_command(cmd)?;
4187                        }
4188                        return Ok(self.last_status);
4189                    }
4190                }
4191
4192                // All conditions false, execute else
4193                if let Some(else_cmds) = else_part {
4194                    for cmd in else_cmds {
4195                        self.execute_command(cmd)?;
4196                    }
4197                }
4198
4199                Ok(self.last_status)
4200            }
4201
4202            CompoundCommand::For { var, words, body } => {
4203                let items: Vec<String> = if let Some(words) = words {
4204                    words
4205                        .iter()
4206                        .flat_map(|w| self.expand_word_split(w))
4207                        .collect()
4208                } else {
4209                    // Iterate over positional parameters
4210                    self.positional_params.clone()
4211                };
4212
4213                for item in items {
4214                    env::set_var(var, &item);
4215                    self.variables.insert(var.clone(), item);
4216
4217                    for cmd in body {
4218                        self.execute_command(cmd)?;
4219                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4220                            break;
4221                        }
4222                    }
4223
4224                    if self.continuing > 0 {
4225                        self.continuing -= 1;
4226                        if self.continuing > 0 {
4227                            break;
4228                        }
4229                        continue;
4230                    }
4231                    if self.breaking > 0 {
4232                        self.breaking -= 1;
4233                        break;
4234                    }
4235                    if self.returning.is_some() {
4236                        break;
4237                    }
4238                }
4239
4240                Ok(self.last_status)
4241            }
4242
4243            CompoundCommand::ForArith {
4244                init,
4245                cond,
4246                step,
4247                body,
4248            } => {
4249                // C-style for loop: for ((init; cond; step))
4250                // Execute init expression (use evaluate_arithmetic_expr for assignment support)
4251                if !init.is_empty() {
4252                    self.evaluate_arithmetic_expr(init);
4253                }
4254
4255                // Loop while condition is true
4256                loop {
4257                    // Evaluate condition (use eval_arith_expr for comparison result)
4258                    if !cond.is_empty() {
4259                        let cond_result = self.eval_arith_expr(cond);
4260                        if cond_result == 0 {
4261                            break;
4262                        }
4263                    }
4264
4265                    // Execute body
4266                    for cmd in body {
4267                        self.execute_command(cmd)?;
4268                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4269                            break;
4270                        }
4271                    }
4272
4273                    if self.continuing > 0 {
4274                        self.continuing -= 1;
4275                        if self.continuing > 0 {
4276                            break;
4277                        }
4278                        continue;
4279                    }
4280                    if self.breaking > 0 {
4281                        self.breaking -= 1;
4282                        break;
4283                    }
4284                    if self.returning.is_some() {
4285                        break;
4286                    }
4287
4288                    // Execute step (use evaluate_arithmetic_expr for assignment support like i++)
4289                    if !step.is_empty() {
4290                        self.evaluate_arithmetic_expr(step);
4291                    }
4292                }
4293                Ok(self.last_status)
4294            }
4295
4296            CompoundCommand::While { condition, body } => {
4297                loop {
4298                    for cmd in condition {
4299                        self.execute_command(cmd)?;
4300                        if self.breaking > 0 || self.returning.is_some() {
4301                            break;
4302                        }
4303                    }
4304
4305                    if self.last_status != 0 || self.breaking > 0 || self.returning.is_some() {
4306                        break;
4307                    }
4308
4309                    for cmd in body {
4310                        self.execute_command(cmd)?;
4311                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4312                            break;
4313                        }
4314                    }
4315
4316                    if self.continuing > 0 {
4317                        self.continuing -= 1;
4318                        if self.continuing > 0 {
4319                            break;
4320                        }
4321                        continue;
4322                    }
4323                    if self.breaking > 0 {
4324                        self.breaking -= 1;
4325                        break;
4326                    }
4327                }
4328                Ok(self.last_status)
4329            }
4330
4331            CompoundCommand::Until { condition, body } => {
4332                loop {
4333                    for cmd in condition {
4334                        self.execute_command(cmd)?;
4335                        if self.breaking > 0 || self.returning.is_some() {
4336                            break;
4337                        }
4338                    }
4339
4340                    if self.last_status == 0 || self.breaking > 0 || self.returning.is_some() {
4341                        break;
4342                    }
4343
4344                    for cmd in body {
4345                        self.execute_command(cmd)?;
4346                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4347                            break;
4348                        }
4349                    }
4350
4351                    if self.continuing > 0 {
4352                        self.continuing -= 1;
4353                        if self.continuing > 0 {
4354                            break;
4355                        }
4356                        continue;
4357                    }
4358                    if self.breaking > 0 {
4359                        self.breaking -= 1;
4360                        break;
4361                    }
4362                }
4363                Ok(self.last_status)
4364            }
4365
4366            CompoundCommand::Case { word, cases } => {
4367                let value = self.expand_word(word);
4368
4369                for (patterns, body, term) in cases {
4370                    for pattern in patterns {
4371                        let pat = self.expand_word(pattern);
4372                        if self.matches_pattern(&value, &pat) {
4373                            for cmd in body {
4374                                self.execute_command(cmd)?;
4375                            }
4376
4377                            match term {
4378                                CaseTerminator::Break => return Ok(self.last_status),
4379                                CaseTerminator::Fallthrough => {
4380                                    // Continue to next case body
4381                                }
4382                                CaseTerminator::Continue => {
4383                                    // Continue pattern matching
4384                                    break;
4385                                }
4386                            }
4387                        }
4388                    }
4389                }
4390
4391                Ok(self.last_status)
4392            }
4393
4394            CompoundCommand::Select { var, words, body } => {
4395                // Simplified: just use first word
4396                if let Some(words) = words {
4397                    if let Some(first) = words.first() {
4398                        let val = self.expand_word(first);
4399                        env::set_var(var, &val);
4400                        for cmd in body {
4401                            self.execute_command(cmd)?;
4402                        }
4403                    }
4404                }
4405                Ok(self.last_status)
4406            }
4407
4408            CompoundCommand::Repeat { count, body } => {
4409                let n: i64 = self
4410                    .expand_word(&ShellWord::Literal(count.clone()))
4411                    .parse()
4412                    .unwrap_or(0);
4413
4414                for _ in 0..n {
4415                    for cmd in body {
4416                        self.execute_command(cmd)?;
4417                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
4418                            break;
4419                        }
4420                    }
4421
4422                    if self.continuing > 0 {
4423                        self.continuing -= 1;
4424                        if self.continuing > 0 {
4425                            break;
4426                        }
4427                        continue;
4428                    }
4429                    if self.breaking > 0 {
4430                        self.breaking -= 1;
4431                        break;
4432                    }
4433                    if self.returning.is_some() {
4434                        break;
4435                    }
4436                }
4437
4438                Ok(self.last_status)
4439            }
4440
4441            CompoundCommand::Try {
4442                try_body,
4443                always_body,
4444            } => {
4445                // Port of exectry() from Src/loop.c
4446                // The :try clause
4447                for cmd in try_body {
4448                    if let Err(_e) = self.execute_command(cmd) {
4449                        break;
4450                    }
4451                    if self.returning.is_some() {
4452                        break;
4453                    }
4454                }
4455
4456                // endval = lastval ? lastval : errflag
4457                let endval = self.last_status;
4458
4459                // Save and reset control flow flags for the always clause
4460                let save_returning = self.returning.take();
4461                let save_breaking = self.breaking;
4462                let save_continuing = self.continuing;
4463                self.breaking = 0;
4464                self.continuing = 0;
4465
4466                // The always clause — executes unconditionally
4467                for cmd in always_body {
4468                    let _ = self.execute_command(cmd);
4469                }
4470
4471                // Restore control flow: C uses "if (!retflag) retflag = save"
4472                // i.e. always block's flags take precedence if set
4473                if self.returning.is_none() {
4474                    self.returning = save_returning;
4475                }
4476                if self.breaking == 0 {
4477                    self.breaking = save_breaking;
4478                }
4479                if self.continuing == 0 {
4480                    self.continuing = save_continuing;
4481                }
4482
4483                self.last_status = endval;
4484                Ok(endval)
4485            }
4486
4487            CompoundCommand::Cond(expr) => {
4488                let result = self.eval_cond_expr(expr);
4489                self.last_status = if result { 0 } else { 1 };
4490                Ok(self.last_status)
4491            }
4492
4493            CompoundCommand::Arith(expr) => {
4494                // Evaluate arithmetic expression and set variables
4495                let result = self.evaluate_arithmetic_expr(expr);
4496                // (( )) returns 0 if result is non-zero, 1 if result is zero
4497                self.last_status = if result != 0 { 0 } else { 1 };
4498                Ok(self.last_status)
4499            }
4500
4501            CompoundCommand::Coproc { name, body } => {
4502                // Create pipes for stdin and stdout
4503                let (stdin_read, stdin_write) =
4504                    os_pipe::pipe().map_err(|e| format!("Cannot create pipe: {}", e))?;
4505                let (stdout_read, stdout_write) =
4506                    os_pipe::pipe().map_err(|e| format!("Cannot create pipe: {}", e))?;
4507
4508                // Get the command to run
4509                let cmd_str = match body.as_ref() {
4510                    ShellCommand::Simple(simple) => simple
4511                        .words
4512                        .iter()
4513                        .map(|w| self.expand_word(w))
4514                        .collect::<Vec<_>>()
4515                        .join(" "),
4516                    ShellCommand::Compound(CompoundCommand::BraceGroup(_cmds)) => {
4517                        // Just run as a subshell with the commands
4518                        // For simplicity, we'll use bash -c
4519                        "bash -c 'true'".to_string()
4520                    }
4521                    _ => "true".to_string(),
4522                };
4523
4524                // Fork and run the command in background with redirected stdin/stdout
4525                let parts: Vec<&str> = cmd_str.split_whitespace().collect();
4526                if parts.is_empty() {
4527                    return Ok(0);
4528                }
4529
4530                let mut command = Command::new(parts[0]);
4531                if parts.len() > 1 {
4532                    command.args(&parts[1..]);
4533                }
4534
4535                use std::os::unix::io::{FromRawFd, IntoRawFd};
4536
4537                command.stdin(unsafe { Stdio::from_raw_fd(stdin_read.into_raw_fd()) });
4538                command.stdout(unsafe { Stdio::from_raw_fd(stdout_write.into_raw_fd()) });
4539
4540                match command.spawn() {
4541                    Ok(child) => {
4542                        let pid = child.id();
4543                        let coproc_name = name.clone().unwrap_or_else(|| "COPROC".to_string());
4544
4545                        // Store file descriptors in environment-like variables
4546                        // COPROC[0] = read from coproc (stdout_read)
4547                        // COPROC[1] = write to coproc (stdin_write)
4548                        let read_fd = stdout_read.into_raw_fd();
4549                        let write_fd = stdin_write.into_raw_fd();
4550
4551                        self.arrays.insert(
4552                            coproc_name.clone(),
4553                            vec![read_fd.to_string(), write_fd.to_string()],
4554                        );
4555
4556                        // Also store PID
4557                        self.variables
4558                            .insert(format!("{}_PID", coproc_name), pid.to_string());
4559
4560                        let cmd_str_clone = cmd_str.clone();
4561                        self.jobs.add_job(child, cmd_str_clone, JobState::Running);
4562
4563                        Ok(0)
4564                    }
4565                    Err(e) => {
4566                        if e.kind() == io::ErrorKind::NotFound {
4567                            eprintln!("zshrs: command not found: {}", parts[0]);
4568                            Ok(127)
4569                        } else {
4570                            Err(format!("zshrs: coproc: {}: {}", parts[0], e))
4571                        }
4572                    }
4573                }
4574            }
4575
4576            CompoundCommand::WithRedirects(cmd, redirects) => {
4577                // Execute the command with redirects applied
4578                let mut saved_fds: Vec<(i32, i32)> = Vec::new();
4579
4580                // Set up redirects
4581                for redirect in redirects {
4582                    let fd = redirect.fd.unwrap_or(match redirect.op {
4583                        RedirectOp::Read
4584                        | RedirectOp::HereDoc
4585                        | RedirectOp::HereString
4586                        | RedirectOp::ReadWrite => 0,
4587                        _ => 1,
4588                    });
4589
4590                    let target = self.expand_word(&redirect.target);
4591
4592                    match redirect.op {
4593                        RedirectOp::Write | RedirectOp::Clobber => {
4594                            use std::os::unix::io::IntoRawFd;
4595                            let saved = unsafe { libc::dup(fd) };
4596                            if saved >= 0 {
4597                                saved_fds.push((fd, saved));
4598                            }
4599                            if let Ok(file) = std::fs::File::create(&target) {
4600                                let new_fd = file.into_raw_fd();
4601                                unsafe {
4602                                    libc::dup2(new_fd, fd);
4603                                }
4604                                unsafe {
4605                                    libc::close(new_fd);
4606                                }
4607                            }
4608                        }
4609                        RedirectOp::Append => {
4610                            use std::os::unix::io::IntoRawFd;
4611                            let saved = unsafe { libc::dup(fd) };
4612                            if saved >= 0 {
4613                                saved_fds.push((fd, saved));
4614                            }
4615                            if let Ok(file) = std::fs::OpenOptions::new()
4616                                .create(true)
4617                                .append(true)
4618                                .open(&target)
4619                            {
4620                                let new_fd = file.into_raw_fd();
4621                                unsafe {
4622                                    libc::dup2(new_fd, fd);
4623                                }
4624                                unsafe {
4625                                    libc::close(new_fd);
4626                                }
4627                            }
4628                        }
4629                        RedirectOp::Read => {
4630                            use std::os::unix::io::IntoRawFd;
4631                            let saved = unsafe { libc::dup(fd) };
4632                            if saved >= 0 {
4633                                saved_fds.push((fd, saved));
4634                            }
4635                            if let Ok(file) = std::fs::File::open(&target) {
4636                                let new_fd = file.into_raw_fd();
4637                                unsafe {
4638                                    libc::dup2(new_fd, fd);
4639                                }
4640                                unsafe {
4641                                    libc::close(new_fd);
4642                                }
4643                            }
4644                        }
4645                        RedirectOp::DupWrite | RedirectOp::DupRead => {
4646                            if let Ok(target_fd) = target.parse::<i32>() {
4647                                let saved = unsafe { libc::dup(fd) };
4648                                if saved >= 0 {
4649                                    saved_fds.push((fd, saved));
4650                                }
4651                                unsafe {
4652                                    libc::dup2(target_fd, fd);
4653                                }
4654                            }
4655                        }
4656                        _ => {}
4657                    }
4658                }
4659
4660                // Execute the inner command
4661                let result = self.execute_command(cmd);
4662
4663                // Restore saved fds
4664                for (fd, saved) in saved_fds.into_iter().rev() {
4665                    unsafe {
4666                        libc::dup2(saved, fd);
4667                        libc::close(saved);
4668                    }
4669                }
4670
4671                result
4672            }
4673        }
4674    }
4675
4676    /// Expand a word with brace and glob expansion (for command arguments)
4677    #[tracing::instrument(level = "trace", skip_all)]
4678    fn expand_word_glob(&mut self, word: &ShellWord) -> Vec<String> {
4679        match word {
4680            ShellWord::SingleQuoted(s) => vec![s.clone()],
4681            ShellWord::DoubleQuoted(parts) => {
4682                // Double quotes prevent glob and brace expansion
4683                vec![parts.iter().map(|p| self.expand_word(p)).collect()]
4684            }
4685            _ => {
4686                let expanded = self.expand_word(word);
4687
4688                // First do brace expansion
4689                let brace_expanded = self.expand_braces(&expanded);
4690
4691                // Then glob expansion on each result (unless noglob is set)
4692                let noglob = self.options.get("noglob").copied().unwrap_or(false)
4693                    || self.options.get("GLOB").map(|v| !v).unwrap_or(false);
4694                brace_expanded
4695                    .into_iter()
4696                    .flat_map(|s| {
4697                        if !noglob
4698                            && (s.contains('*')
4699                                || s.contains('?')
4700                                || s.contains('[')
4701                                || self.has_extglob_pattern(&s))
4702                        {
4703                            self.expand_glob(&s)
4704                        } else {
4705                            vec![s]
4706                        }
4707                    })
4708                    .collect()
4709            }
4710        }
4711    }
4712
4713    /// Expand brace patterns like {a,b,c} and {1..10}
4714    fn expand_braces(&self, s: &str) -> Vec<String> {
4715        // Find a brace pattern
4716        let mut depth = 0;
4717        let mut brace_start = None;
4718
4719        for (i, c) in s.char_indices() {
4720            match c {
4721                '{' => {
4722                    if depth == 0 {
4723                        brace_start = Some(i);
4724                    }
4725                    depth += 1;
4726                }
4727                '}' => {
4728                    depth -= 1;
4729                    if depth == 0 {
4730                        if let Some(start) = brace_start {
4731                            let prefix = &s[..start];
4732                            let content = &s[start + 1..i];
4733                            let suffix = &s[i + 1..];
4734
4735                            // Check if this is a sequence {a..b} or a list {a,b,c}
4736                            let expansions = if content.contains("..") {
4737                                self.expand_brace_sequence(content)
4738                            } else if content.contains(',') {
4739                                self.expand_brace_list(content)
4740                            } else {
4741                                // Not a valid brace expansion, return as-is
4742                                return vec![s.to_string()];
4743                            };
4744
4745                            // Combine prefix, expansions, and suffix
4746                            let mut results = Vec::new();
4747                            for exp in expansions {
4748                                let combined = format!("{}{}{}", prefix, exp, suffix);
4749                                // Recursively expand any remaining braces
4750                                results.extend(self.expand_braces(&combined));
4751                            }
4752                            return results;
4753                        }
4754                    }
4755                }
4756                _ => {}
4757            }
4758        }
4759
4760        // No brace expansion found
4761        vec![s.to_string()]
4762    }
4763
4764    /// Expand comma-separated brace list like {a,b,c}
4765    fn expand_brace_list(&self, content: &str) -> Vec<String> {
4766        // Split by comma, but respect nested braces
4767        let mut parts = Vec::new();
4768        let mut current = String::new();
4769        let mut depth = 0;
4770
4771        for c in content.chars() {
4772            match c {
4773                '{' => {
4774                    depth += 1;
4775                    current.push(c);
4776                }
4777                '}' => {
4778                    depth -= 1;
4779                    current.push(c);
4780                }
4781                ',' if depth == 0 => {
4782                    parts.push(current.clone());
4783                    current.clear();
4784                }
4785                _ => current.push(c),
4786            }
4787        }
4788        parts.push(current);
4789
4790        parts
4791    }
4792
4793    /// Expand sequence brace pattern like {1..10} or {a..z}
4794    fn expand_brace_sequence(&self, content: &str) -> Vec<String> {
4795        let parts: Vec<&str> = content.splitn(3, "..").collect();
4796        if parts.len() < 2 {
4797            return vec![content.to_string()];
4798        }
4799
4800        let start = parts[0];
4801        let end = parts[1];
4802        let step: i64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
4803
4804        // Try numeric sequence
4805        if let (Ok(start_num), Ok(end_num)) = (start.parse::<i64>(), end.parse::<i64>()) {
4806            let mut results = Vec::new();
4807            if start_num <= end_num {
4808                let mut i = start_num;
4809                while i <= end_num {
4810                    results.push(i.to_string());
4811                    i += step;
4812                }
4813            } else {
4814                let mut i = start_num;
4815                while i >= end_num {
4816                    results.push(i.to_string());
4817                    i -= step;
4818                }
4819            }
4820            return results;
4821        }
4822
4823        // Try character sequence
4824        if start.len() == 1 && end.len() == 1 {
4825            let start_char = start.chars().next().unwrap();
4826            let end_char = end.chars().next().unwrap();
4827            let mut results = Vec::new();
4828
4829            if start_char <= end_char {
4830                let mut c = start_char;
4831                while c <= end_char {
4832                    results.push(c.to_string());
4833                    c = (c as u8 + step as u8) as char;
4834                    if c as u8 > end_char as u8 {
4835                        break;
4836                    }
4837                }
4838            } else {
4839                let mut c = start_char;
4840                while c >= end_char {
4841                    results.push(c.to_string());
4842                    if (c as u8) < step as u8 {
4843                        break;
4844                    }
4845                    c = (c as u8 - step as u8) as char;
4846                }
4847            }
4848            return results;
4849        }
4850
4851        vec![content.to_string()]
4852    }
4853
4854    /// Expand glob pattern to matching files
4855    fn expand_glob(&self, pattern: &str) -> Vec<String> {
4856        // Check for zsh glob qualifiers at end: *(.) *(/) *(@) etc.
4857        let (glob_pattern, qualifiers) = self.parse_glob_qualifiers(pattern);
4858
4859        // Check for extended glob patterns: ?(pat), *(pat), +(pat), @(pat), !(pat)
4860        if self.has_extglob_pattern(&glob_pattern) {
4861            let expanded = self.expand_extglob(&glob_pattern);
4862            return self.filter_by_qualifiers(expanded, &qualifiers);
4863        }
4864
4865        let nullglob = self.options.get("nullglob").copied().unwrap_or(false);
4866        let dotglob = self.options.get("dotglob").copied().unwrap_or(false);
4867        let nocaseglob = self.options.get("nocaseglob").copied().unwrap_or(false);
4868
4869        // Parallel recursive glob: when pattern contains **/ we split the
4870        // directory walk across worker pool threads — one thread per top-level
4871        // subdirectory.  zsh does this single-threaded via fork+exec which is
4872        // why `echo **/*.rs` is painfully slow on large trees.
4873        let expanded = if glob_pattern.contains("**/") {
4874            self.expand_glob_parallel(&glob_pattern, dotglob, nocaseglob)
4875        } else {
4876            let options = glob::MatchOptions {
4877                case_sensitive: !nocaseglob,
4878                require_literal_separator: false,
4879                require_literal_leading_dot: !dotglob,
4880            };
4881            match glob::glob_with(&glob_pattern, options) {
4882                Ok(paths) => paths
4883                    .filter_map(|p| p.ok())
4884                    .map(|p| p.to_string_lossy().to_string())
4885                    .collect(),
4886                Err(_) => vec![],
4887            }
4888        };
4889
4890        let mut expanded = self.filter_by_qualifiers(expanded, &qualifiers);
4891        expanded.sort();
4892
4893        if expanded.is_empty() {
4894            if nullglob {
4895                vec![]
4896            } else {
4897                vec![pattern.to_string()]
4898            }
4899        } else {
4900            expanded
4901        }
4902    }
4903
4904    /// Parallel recursive glob using the worker pool.
4905    ///
4906    /// Splits `base/**/file_pattern` into per-subdirectory walks, each
4907    /// running on a pool thread via walkdir.  Results merge via channel.
4908    /// This is why `echo **/*.rs` will be 5-10x faster than zsh.
4909    fn expand_glob_parallel(&self, pattern: &str, dotglob: bool, nocaseglob: bool) -> Vec<String> {
4910        use walkdir::WalkDir;
4911
4912        // Split pattern at the first **/ into (base_dir, file_glob)
4913        // e.g. "src/**/*.rs" → ("src", "*.rs")
4914        //      "**/*.rs"     → (".", "*.rs")
4915        let (base, file_glob) = if let Some(pos) = pattern.find("**/") {
4916            let base = if pos == 0 {
4917                "."
4918            } else {
4919                &pattern[..pos.saturating_sub(1)]
4920            };
4921            let rest = &pattern[pos + 3..]; // skip "**/", get "*.rs" or "foo/**/*.rs"
4922            (base.to_string(), rest.to_string())
4923        } else {
4924            return vec![];
4925        };
4926
4927        // If file_glob itself contains **/, fall back to single-threaded glob
4928        // (nested recursive patterns are rare, not worth the complexity)
4929        if file_glob.contains("**/") {
4930            let options = glob::MatchOptions {
4931                case_sensitive: !nocaseglob,
4932                require_literal_separator: false,
4933                require_literal_leading_dot: !dotglob,
4934            };
4935            return match glob::glob_with(pattern, options) {
4936                Ok(paths) => paths
4937                    .filter_map(|p| p.ok())
4938                    .map(|p| p.to_string_lossy().to_string())
4939                    .collect(),
4940                Err(_) => vec![],
4941            };
4942        }
4943
4944        // Build the glob::Pattern for matching filenames
4945        let match_opts = glob::MatchOptions {
4946            case_sensitive: !nocaseglob,
4947            require_literal_separator: false,
4948            require_literal_leading_dot: !dotglob,
4949        };
4950        let file_pat = match glob::Pattern::new(&file_glob) {
4951            Ok(p) => p,
4952            Err(_) => return vec![],
4953        };
4954
4955        // Enumerate top-level entries in base dir to fan out across workers
4956        let top_entries: Vec<std::path::PathBuf> = match std::fs::read_dir(&base) {
4957            Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.path()).collect(),
4958            Err(_) => return vec![],
4959        };
4960
4961        // Also check files directly in base (not in subdirs)
4962        let mut results: Vec<String> = Vec::new();
4963        for entry in &top_entries {
4964            if entry.is_file() || entry.is_symlink() {
4965                if let Some(name) = entry.file_name().and_then(|n| n.to_str()) {
4966                    if file_pat.matches_with(name, match_opts) {
4967                        results.push(entry.to_string_lossy().to_string());
4968                    }
4969                }
4970            }
4971        }
4972
4973        // Fan out subdirectory walks to worker pool
4974        let subdirs: Vec<std::path::PathBuf> = top_entries
4975            .into_iter()
4976            .filter(|p| p.is_dir())
4977            .filter(|p| {
4978                dotglob
4979                    || !p
4980                        .file_name()
4981                        .and_then(|n| n.to_str())
4982                        .map(|n| n.starts_with('.'))
4983                        .unwrap_or(false)
4984            })
4985            .collect();
4986
4987        if subdirs.is_empty() {
4988            return results;
4989        }
4990
4991        let (tx, rx) = std::sync::mpsc::channel::<Vec<String>>();
4992
4993        for subdir in &subdirs {
4994            let tx = tx.clone();
4995            let subdir = subdir.clone();
4996            let file_pat = file_pat.clone();
4997            let skip_dot = !dotglob;
4998            self.worker_pool.submit(move || {
4999                let mut matches = Vec::new();
5000                let walker = WalkDir::new(&subdir)
5001                    .follow_links(false)
5002                    .into_iter()
5003                    .filter_entry(move |e| {
5004                        // Skip hidden dirs if !dotglob
5005                        if skip_dot {
5006                            if let Some(name) = e.file_name().to_str() {
5007                                if name.starts_with('.') && e.depth() > 0 {
5008                                    return false;
5009                                }
5010                            }
5011                        }
5012                        true
5013                    });
5014                for entry in walker.filter_map(|e| e.ok()) {
5015                    if entry.file_type().is_file() || entry.file_type().is_symlink() {
5016                        if let Some(name) = entry.file_name().to_str() {
5017                            if file_pat.matches_with(name, match_opts) {
5018                                matches.push(entry.path().to_string_lossy().to_string());
5019                            }
5020                        }
5021                    }
5022                }
5023                let _ = tx.send(matches);
5024            });
5025        }
5026
5027        // Drop our sender so rx knows when all workers are done
5028        drop(tx);
5029
5030        // Collect results from all workers
5031        for batch in rx {
5032            results.extend(batch);
5033        }
5034
5035        results
5036    }
5037
5038    /// Parse zsh glob qualifiers from the end of a pattern
5039    /// Returns (pattern_without_qualifiers, qualifiers_string)
5040    fn parse_glob_qualifiers(&self, pattern: &str) -> (String, String) {
5041        // Check if pattern ends with (...) that looks like qualifiers
5042        // Qualifiers are single chars like . / @ * % or combinations
5043        if !pattern.ends_with(')') {
5044            return (pattern.to_string(), String::new());
5045        }
5046
5047        // Find matching opening paren
5048        let chars: Vec<char> = pattern.chars().collect();
5049        let mut depth = 0;
5050        let mut qual_start = None;
5051
5052        for i in (0..chars.len()).rev() {
5053            match chars[i] {
5054                ')' => depth += 1,
5055                '(' => {
5056                    depth -= 1;
5057                    if depth == 0 {
5058                        qual_start = Some(i);
5059                        break;
5060                    }
5061                }
5062                _ => {}
5063            }
5064        }
5065
5066        if let Some(start) = qual_start {
5067            let qual_content: String = chars[start + 1..chars.len() - 1].iter().collect();
5068
5069            // Check if this looks like glob qualifiers (not extglob)
5070            // Qualifiers are things like: . / @ * % r w x ^ - etc.
5071            // Extglob would have | inside
5072            if !qual_content.contains('|') && self.looks_like_glob_qualifiers(&qual_content) {
5073                let base_pattern: String = chars[..start].iter().collect();
5074                return (base_pattern, qual_content);
5075            }
5076        }
5077
5078        (pattern.to_string(), String::new())
5079    }
5080
5081    /// Check if string looks like glob qualifiers
5082    fn looks_like_glob_qualifiers(&self, s: &str) -> bool {
5083        if s.is_empty() {
5084            return false;
5085        }
5086        // Valid qualifier chars: . / @ = p * % r w x A I E R W X s S t ^ - + :
5087        // Also numbers for depth limits, and things like [1,5] for ranges
5088        let valid_chars = "./@=p*%brwxAIERWXsStfedDLNnMmcaou^-+:0123456789,[]FT";
5089        s.chars()
5090            .all(|c| valid_chars.contains(c) || c.is_whitespace())
5091    }
5092
5093    /// Filter file list by glob qualifiers
5094    /// Prefetch file metadata in parallel across the worker pool.
5095    /// Returns a map from path → (metadata, symlink_metadata).
5096    /// Each batch of files is stat'd on a pool thread.
5097    fn prefetch_metadata(
5098        &self,
5099        files: &[String],
5100    ) -> HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)> {
5101        if files.len() < 32 {
5102            // Small list — serial stat is faster than channel overhead
5103            return files
5104                .iter()
5105                .map(|f| {
5106                    let meta = std::fs::metadata(f).ok();
5107                    let symlink_meta = std::fs::symlink_metadata(f).ok();
5108                    (f.clone(), (meta, symlink_meta))
5109                })
5110                .collect();
5111        }
5112
5113        let pool_size = self.worker_pool.size();
5114        let chunk_size = (files.len() + pool_size - 1) / pool_size;
5115        let (tx, rx) = std::sync::mpsc::channel();
5116
5117        for chunk in files.chunks(chunk_size) {
5118            let tx = tx.clone();
5119            let chunk: Vec<String> = chunk.to_vec();
5120            self.worker_pool.submit(move || {
5121                let batch: Vec<(
5122                    String,
5123                    (Option<std::fs::Metadata>, Option<std::fs::Metadata>),
5124                )> = chunk
5125                    .into_iter()
5126                    .map(|f| {
5127                        let meta = std::fs::metadata(&f).ok();
5128                        let symlink_meta = std::fs::symlink_metadata(&f).ok();
5129                        (f, (meta, symlink_meta))
5130                    })
5131                    .collect();
5132                let _ = tx.send(batch);
5133            });
5134        }
5135        drop(tx);
5136
5137        let mut map = HashMap::with_capacity(files.len());
5138        for batch in rx {
5139            for (path, metas) in batch {
5140                map.insert(path, metas);
5141            }
5142        }
5143        map
5144    }
5145
5146    fn filter_by_qualifiers(&self, files: Vec<String>, qualifiers: &str) -> Vec<String> {
5147        if qualifiers.is_empty() {
5148            return files;
5149        }
5150
5151        // Parallel metadata prefetch — all stat syscalls happen on pool threads,
5152        // then filter/sort uses cached metadata with zero syscalls.
5153        let meta_cache = self.prefetch_metadata(&files);
5154
5155        let mut result = files;
5156        let mut negate = false;
5157        let mut chars = qualifiers.chars().peekable();
5158
5159        while let Some(c) = chars.next() {
5160            match c {
5161                // Negation
5162                '^' => negate = !negate,
5163
5164                // File types — all use prefetched metadata cache
5165                '.' => {
5166                    result = result
5167                        .into_iter()
5168                        .filter(|f| {
5169                            let is_file = meta_cache
5170                                .get(f)
5171                                .and_then(|(m, _)| m.as_ref())
5172                                .map(|m| m.is_file())
5173                                .unwrap_or(false);
5174                            if negate {
5175                                !is_file
5176                            } else {
5177                                is_file
5178                            }
5179                        })
5180                        .collect();
5181                    negate = false;
5182                }
5183                '/' => {
5184                    result = result
5185                        .into_iter()
5186                        .filter(|f| {
5187                            let is_dir = meta_cache
5188                                .get(f)
5189                                .and_then(|(m, _)| m.as_ref())
5190                                .map(|m| m.is_dir())
5191                                .unwrap_or(false);
5192                            if negate {
5193                                !is_dir
5194                            } else {
5195                                is_dir
5196                            }
5197                        })
5198                        .collect();
5199                    negate = false;
5200                }
5201                '@' => {
5202                    result = result
5203                        .into_iter()
5204                        .filter(|f| {
5205                            let is_link = meta_cache
5206                                .get(f)
5207                                .and_then(|(_, sm)| sm.as_ref())
5208                                .map(|m| m.file_type().is_symlink())
5209                                .unwrap_or(false);
5210                            if negate {
5211                                !is_link
5212                            } else {
5213                                is_link
5214                            }
5215                        })
5216                        .collect();
5217                    negate = false;
5218                }
5219                '=' => {
5220                    // Sockets
5221                    use std::os::unix::fs::FileTypeExt;
5222                    result = result
5223                        .into_iter()
5224                        .filter(|f| {
5225                            let is_socket = meta_cache
5226                                .get(f)
5227                                .and_then(|(_, sm)| sm.as_ref())
5228                                .map(|m| m.file_type().is_socket())
5229                                .unwrap_or(false);
5230                            if negate {
5231                                !is_socket
5232                            } else {
5233                                is_socket
5234                            }
5235                        })
5236                        .collect();
5237                    negate = false;
5238                }
5239                'p' => {
5240                    // Named pipes (FIFOs)
5241                    use std::os::unix::fs::FileTypeExt;
5242                    result = result
5243                        .into_iter()
5244                        .filter(|f| {
5245                            let is_fifo = meta_cache
5246                                .get(f)
5247                                .and_then(|(_, sm)| sm.as_ref())
5248                                .map(|m| m.file_type().is_fifo())
5249                                .unwrap_or(false);
5250                            if negate {
5251                                !is_fifo
5252                            } else {
5253                                is_fifo
5254                            }
5255                        })
5256                        .collect();
5257                    negate = false;
5258                }
5259                '*' => {
5260                    // Executable files
5261                    use std::os::unix::fs::PermissionsExt;
5262                    result = result
5263                        .into_iter()
5264                        .filter(|f| {
5265                            let is_exec = meta_cache
5266                                .get(f)
5267                                .and_then(|(m, _)| m.as_ref())
5268                                .map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
5269                                .unwrap_or(false);
5270                            if negate {
5271                                !is_exec
5272                            } else {
5273                                is_exec
5274                            }
5275                        })
5276                        .collect();
5277                    negate = false;
5278                }
5279                '%' => {
5280                    // Device files
5281                    use std::os::unix::fs::FileTypeExt;
5282                    let next = chars.peek().copied();
5283                    result = result
5284                        .into_iter()
5285                        .filter(|f| {
5286                            let is_device = meta_cache
5287                                .get(f)
5288                                .and_then(|(_, sm)| sm.as_ref())
5289                                .map(|m| match next {
5290                                    Some('b') => m.file_type().is_block_device(),
5291                                    Some('c') => m.file_type().is_char_device(),
5292                                    _ => {
5293                                        m.file_type().is_block_device()
5294                                            || m.file_type().is_char_device()
5295                                    }
5296                                })
5297                                .unwrap_or(false);
5298                            if negate {
5299                                !is_device
5300                            } else {
5301                                is_device
5302                            }
5303                        })
5304                        .collect();
5305                    if next == Some('b') || next == Some('c') {
5306                        chars.next();
5307                    }
5308                    negate = false;
5309                }
5310
5311                // Permission qualifiers — all use prefetched metadata cache
5312                'r' => {
5313                    result = self.filter_by_permission(result, 0o400, negate, &meta_cache);
5314                    negate = false;
5315                }
5316                'w' => {
5317                    result = self.filter_by_permission(result, 0o200, negate, &meta_cache);
5318                    negate = false;
5319                }
5320                'x' => {
5321                    result = self.filter_by_permission(result, 0o100, negate, &meta_cache);
5322                    negate = false;
5323                }
5324                'A' => {
5325                    result = self.filter_by_permission(result, 0o040, negate, &meta_cache);
5326                    negate = false;
5327                }
5328                'I' => {
5329                    result = self.filter_by_permission(result, 0o020, negate, &meta_cache);
5330                    negate = false;
5331                }
5332                'E' => {
5333                    result = self.filter_by_permission(result, 0o010, negate, &meta_cache);
5334                    negate = false;
5335                }
5336                'R' => {
5337                    result = self.filter_by_permission(result, 0o004, negate, &meta_cache);
5338                    negate = false;
5339                }
5340                'W' => {
5341                    result = self.filter_by_permission(result, 0o002, negate, &meta_cache);
5342                    negate = false;
5343                }
5344                'X' => {
5345                    result = self.filter_by_permission(result, 0o001, negate, &meta_cache);
5346                    negate = false;
5347                }
5348                's' => {
5349                    result = self.filter_by_permission(result, 0o4000, negate, &meta_cache);
5350                    negate = false;
5351                }
5352                'S' => {
5353                    result = self.filter_by_permission(result, 0o2000, negate, &meta_cache);
5354                    negate = false;
5355                }
5356                't' => {
5357                    result = self.filter_by_permission(result, 0o1000, negate, &meta_cache);
5358                    negate = false;
5359                }
5360
5361                // Full/empty directories
5362                'F' => {
5363                    // Non-empty directories
5364                    result = result
5365                        .into_iter()
5366                        .filter(|f| {
5367                            let path = std::path::Path::new(f);
5368                            let is_nonempty = path.is_dir()
5369                                && std::fs::read_dir(path)
5370                                    .map(|mut d| d.next().is_some())
5371                                    .unwrap_or(false);
5372                            if negate {
5373                                !is_nonempty
5374                            } else {
5375                                is_nonempty
5376                            }
5377                        })
5378                        .collect();
5379                    negate = false;
5380                }
5381
5382                // Ownership — uses prefetched metadata cache
5383                'U' => {
5384                    // Owned by effective UID
5385                    let euid = unsafe { libc::geteuid() };
5386                    result = result
5387                        .into_iter()
5388                        .filter(|f| {
5389                            use std::os::unix::fs::MetadataExt;
5390                            let is_owned = meta_cache
5391                                .get(f)
5392                                .and_then(|(m, _)| m.as_ref())
5393                                .map(|m| m.uid() == euid)
5394                                .unwrap_or(false);
5395                            if negate {
5396                                !is_owned
5397                            } else {
5398                                is_owned
5399                            }
5400                        })
5401                        .collect();
5402                    negate = false;
5403                }
5404                'G' => {
5405                    // Owned by effective GID
5406                    let egid = unsafe { libc::getegid() };
5407                    result = result
5408                        .into_iter()
5409                        .filter(|f| {
5410                            use std::os::unix::fs::MetadataExt;
5411                            let is_owned = meta_cache
5412                                .get(f)
5413                                .and_then(|(m, _)| m.as_ref())
5414                                .map(|m| m.gid() == egid)
5415                                .unwrap_or(false);
5416                            if negate {
5417                                !is_owned
5418                            } else {
5419                                is_owned
5420                            }
5421                        })
5422                        .collect();
5423                    negate = false;
5424                }
5425
5426                // Sorting modifiers
5427                'o' => {
5428                    // Sort by name (ascending) - already default
5429                    if chars.peek() == Some(&'n') {
5430                        chars.next();
5431                        // Sort by name
5432                        result.sort();
5433                    } else if chars.peek() == Some(&'L') {
5434                        chars.next();
5435                        // Sort by size — uses prefetched metadata
5436                        result.sort_by_key(|f| {
5437                            meta_cache
5438                                .get(f)
5439                                .and_then(|(m, _)| m.as_ref())
5440                                .map(|m| m.len())
5441                                .unwrap_or(0)
5442                        });
5443                    } else if chars.peek() == Some(&'m') {
5444                        chars.next();
5445                        // Sort by modification time — uses prefetched metadata
5446                        result.sort_by_key(|f| {
5447                            meta_cache
5448                                .get(f)
5449                                .and_then(|(m, _)| m.as_ref())
5450                                .and_then(|m| m.modified().ok())
5451                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5452                        });
5453                    } else if chars.peek() == Some(&'a') {
5454                        chars.next();
5455                        // Sort by access time — uses prefetched metadata
5456                        result.sort_by_key(|f| {
5457                            meta_cache
5458                                .get(f)
5459                                .and_then(|(m, _)| m.as_ref())
5460                                .and_then(|m| m.accessed().ok())
5461                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5462                        });
5463                    }
5464                }
5465                'O' => {
5466                    // Reverse sort — uses prefetched metadata
5467                    if chars.peek() == Some(&'n') {
5468                        chars.next();
5469                        result.sort();
5470                        result.reverse();
5471                    } else if chars.peek() == Some(&'L') {
5472                        chars.next();
5473                        result.sort_by_key(|f| {
5474                            meta_cache
5475                                .get(f)
5476                                .and_then(|(m, _)| m.as_ref())
5477                                .map(|m| m.len())
5478                                .unwrap_or(0)
5479                        });
5480                        result.reverse();
5481                    } else if chars.peek() == Some(&'m') {
5482                        chars.next();
5483                        result.sort_by_key(|f| {
5484                            meta_cache
5485                                .get(f)
5486                                .and_then(|(m, _)| m.as_ref())
5487                                .and_then(|m| m.modified().ok())
5488                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5489                        });
5490                        result.reverse();
5491                    } else {
5492                        // Just reverse current order
5493                        result.reverse();
5494                    }
5495                }
5496
5497                // Subscript range [n] or [n,m]
5498                '[' => {
5499                    let mut range_str = String::new();
5500                    while let Some(&ch) = chars.peek() {
5501                        if ch == ']' {
5502                            chars.next();
5503                            break;
5504                        }
5505                        range_str.push(chars.next().unwrap());
5506                    }
5507
5508                    if let Some((start, end)) = self.parse_subscript_range(&range_str, result.len())
5509                    {
5510                        result = result.into_iter().skip(start).take(end - start).collect();
5511                    }
5512                }
5513
5514                // Depth limit (for **/)
5515                'D' => {
5516                    // Include dotfiles (handled by dotglob)
5517                }
5518                'N' => {
5519                    // Nullglob for this pattern
5520                }
5521
5522                // Unknown qualifier - ignore
5523                _ => {}
5524            }
5525        }
5526
5527        result
5528    }
5529
5530    /// Filter files by permission bits — uses prefetched metadata cache
5531    fn filter_by_permission(
5532        &self,
5533        files: Vec<String>,
5534        mode: u32,
5535        negate: bool,
5536        meta_cache: &HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)>,
5537    ) -> Vec<String> {
5538        use std::os::unix::fs::PermissionsExt;
5539        files
5540            .into_iter()
5541            .filter(|f| {
5542                let has_perm = meta_cache
5543                    .get(f)
5544                    .and_then(|(m, _)| m.as_ref())
5545                    .map(|m| (m.permissions().mode() & mode) != 0)
5546                    .unwrap_or(false);
5547                if negate {
5548                    !has_perm
5549                } else {
5550                    has_perm
5551                }
5552            })
5553            .collect()
5554    }
5555
5556    /// Parse subscript range like "1" or "1,5" or "-1" or "1,-1"
5557    fn parse_subscript_range(&self, s: &str, len: usize) -> Option<(usize, usize)> {
5558        if s.is_empty() || len == 0 {
5559            return None;
5560        }
5561
5562        let parts: Vec<&str> = s.split(',').collect();
5563
5564        let parse_idx = |idx_str: &str| -> Option<usize> {
5565            let idx: i64 = idx_str.trim().parse().ok()?;
5566            if idx < 0 {
5567                // Negative index from end
5568                let abs = (-idx) as usize;
5569                if abs > len {
5570                    None
5571                } else {
5572                    Some(len - abs)
5573                }
5574            } else if idx == 0 {
5575                Some(0)
5576            } else {
5577                // 1-indexed
5578                Some((idx as usize).saturating_sub(1).min(len))
5579            }
5580        };
5581
5582        match parts.len() {
5583            1 => {
5584                // Single element [n]
5585                let idx = parse_idx(parts[0])?;
5586                Some((idx, idx + 1))
5587            }
5588            2 => {
5589                // Range [n,m]
5590                let start = parse_idx(parts[0])?;
5591                let end = parse_idx(parts[1])?.saturating_add(1);
5592                Some((start.min(end), start.max(end)))
5593            }
5594            _ => None,
5595        }
5596    }
5597
5598    /// Check if pattern contains extended glob syntax
5599    fn has_extglob_pattern(&self, pattern: &str) -> bool {
5600        let chars: Vec<char> = pattern.chars().collect();
5601        for i in 0..chars.len().saturating_sub(1) {
5602            if (chars[i] == '?'
5603                || chars[i] == '*'
5604                || chars[i] == '+'
5605                || chars[i] == '@'
5606                || chars[i] == '!')
5607                && chars[i + 1] == '('
5608            {
5609                return true;
5610            }
5611        }
5612        false
5613    }
5614
5615    /// Convert extended glob pattern to regex
5616    fn extglob_to_regex(&self, pattern: &str) -> String {
5617        let mut regex = String::from("^");
5618        let chars: Vec<char> = pattern.chars().collect();
5619        let mut i = 0;
5620
5621        while i < chars.len() {
5622            let c = chars[i];
5623
5624            // Check for extglob patterns
5625            if i + 1 < chars.len() && chars[i + 1] == '(' {
5626                match c {
5627                    '?' => {
5628                        // ?(pattern) - zero or one occurrence
5629                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5630                        let inner_regex = self.extglob_inner_to_regex(&inner);
5631                        regex.push_str(&format!("({})?", inner_regex));
5632                        i = end + 1;
5633                        continue;
5634                    }
5635                    '*' => {
5636                        // *(pattern) - zero or more occurrences
5637                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5638                        let inner_regex = self.extglob_inner_to_regex(&inner);
5639                        regex.push_str(&format!("({})*", inner_regex));
5640                        i = end + 1;
5641                        continue;
5642                    }
5643                    '+' => {
5644                        // +(pattern) - one or more occurrences
5645                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5646                        let inner_regex = self.extglob_inner_to_regex(&inner);
5647                        regex.push_str(&format!("({})+", inner_regex));
5648                        i = end + 1;
5649                        continue;
5650                    }
5651                    '@' => {
5652                        // @(pattern) - exactly one occurrence
5653                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
5654                        let inner_regex = self.extglob_inner_to_regex(&inner);
5655                        regex.push_str(&format!("({})", inner_regex));
5656                        i = end + 1;
5657                        continue;
5658                    }
5659                    '!' => {
5660                        // !(pattern) - handled specially in expand_extglob
5661                        // Just skip this extglob for regex, will do manual filtering
5662                        let (_, end) = self.extract_extglob_inner(&chars, i + 2);
5663                        regex.push_str(".*"); // Match anything, we filter later
5664                        i = end + 1;
5665                        continue;
5666                    }
5667                    _ => {}
5668                }
5669            }
5670
5671            // Handle regular glob characters
5672            match c {
5673                '*' => regex.push_str(".*"),
5674                '?' => regex.push('.'),
5675                '.' => regex.push_str("\\."),
5676                '[' => {
5677                    regex.push('[');
5678                    i += 1;
5679                    while i < chars.len() && chars[i] != ']' {
5680                        if chars[i] == '!' && regex.ends_with('[') {
5681                            regex.push('^');
5682                        } else {
5683                            regex.push(chars[i]);
5684                        }
5685                        i += 1;
5686                    }
5687                    regex.push(']');
5688                }
5689                '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
5690                    regex.push('\\');
5691                    regex.push(c);
5692                }
5693                _ => regex.push(c),
5694            }
5695            i += 1;
5696        }
5697
5698        regex.push('$');
5699        regex
5700    }
5701
5702    /// Extract the inner part of an extglob pattern (until closing paren)
5703    fn extract_extglob_inner(&self, chars: &[char], start: usize) -> (String, usize) {
5704        let mut inner = String::new();
5705        let mut depth = 1;
5706        let mut i = start;
5707
5708        while i < chars.len() && depth > 0 {
5709            if chars[i] == '(' {
5710                depth += 1;
5711            } else if chars[i] == ')' {
5712                depth -= 1;
5713                if depth == 0 {
5714                    return (inner, i);
5715                }
5716            }
5717            inner.push(chars[i]);
5718            i += 1;
5719        }
5720
5721        (inner, i)
5722    }
5723
5724    /// Convert the inner part of extglob (handles | for alternation)
5725    fn extglob_inner_to_regex(&self, inner: &str) -> String {
5726        // Split by | and convert each alternative
5727        let alternatives: Vec<String> = inner
5728            .split('|')
5729            .map(|alt| {
5730                let mut result = String::new();
5731                for c in alt.chars() {
5732                    match c {
5733                        '*' => result.push_str(".*"),
5734                        '?' => result.push('.'),
5735                        '.' => result.push_str("\\."),
5736                        '^' | '$' | '(' | ')' | '{' | '}' | '\\' => {
5737                            result.push('\\');
5738                            result.push(c);
5739                        }
5740                        _ => result.push(c),
5741                    }
5742                }
5743                result
5744            })
5745            .collect();
5746
5747        alternatives.join("|")
5748    }
5749
5750    /// Expand extended glob pattern
5751    fn expand_extglob(&self, pattern: &str) -> Vec<String> {
5752        // Determine directory to search
5753        let (search_dir, file_pattern) = if let Some(last_slash) = pattern.rfind('/') {
5754            (&pattern[..last_slash], &pattern[last_slash + 1..])
5755        } else {
5756            (".", pattern)
5757        };
5758
5759        // Check for !(pattern) - negative matching
5760        if let Some((neg_pat, suffix)) = self.extract_neg_extglob(file_pattern) {
5761            return self.expand_neg_extglob(search_dir, &neg_pat, &suffix, pattern);
5762        }
5763
5764        // Convert file pattern to regex for positive extglob
5765        let regex_str = self.extglob_to_regex(file_pattern);
5766
5767        let re = match cached_regex(&regex_str) {
5768            Some(r) => r,
5769            None => return vec![pattern.to_string()],
5770        };
5771
5772        let mut results = Vec::new();
5773
5774        if let Ok(entries) = std::fs::read_dir(search_dir) {
5775            for entry in entries.flatten() {
5776                let name = entry.file_name().to_string_lossy().to_string();
5777                // Skip hidden files unless pattern starts with .
5778                if name.starts_with('.') && !file_pattern.starts_with('.') {
5779                    continue;
5780                }
5781
5782                if re.is_match(&name) {
5783                    let full_path = if search_dir == "." {
5784                        name
5785                    } else {
5786                        format!("{}/{}", search_dir, name)
5787                    };
5788                    results.push(full_path);
5789                }
5790            }
5791        }
5792
5793        if results.is_empty() {
5794            vec![pattern.to_string()]
5795        } else {
5796            results.sort();
5797            results
5798        }
5799    }
5800
5801    /// Handle !(pattern) negative extglob expansion
5802    fn expand_neg_extglob(
5803        &self,
5804        search_dir: &str,
5805        neg_pat: &str,
5806        suffix: &str,
5807        original_pattern: &str,
5808    ) -> Vec<String> {
5809        let mut results = Vec::new();
5810
5811        if let Ok(entries) = std::fs::read_dir(search_dir) {
5812            for entry in entries.flatten() {
5813                let name = entry.file_name().to_string_lossy().to_string();
5814                // Skip hidden files
5815                if name.starts_with('.') {
5816                    continue;
5817                }
5818
5819                // File must end with suffix
5820                if !name.ends_with(suffix) {
5821                    continue;
5822                }
5823
5824                let basename = &name[..name.len() - suffix.len()];
5825                // Check if basename matches any negated alternative
5826                let alts: Vec<&str> = neg_pat.split('|').collect();
5827                let matches_neg = alts.iter().any(|alt| {
5828                    if alt.contains('*') || alt.contains('?') {
5829                        let alt_re = self.extglob_inner_to_regex(alt);
5830                        let full_pattern = format!("^{}$", alt_re);
5831                        if let Some(r) = cached_regex(&full_pattern) {
5832                            r.is_match(basename)
5833                        } else {
5834                            *alt == basename
5835                        }
5836                    } else {
5837                        *alt == basename
5838                    }
5839                });
5840
5841                if !matches_neg {
5842                    let full_path = if search_dir == "." {
5843                        name
5844                    } else {
5845                        format!("{}/{}", search_dir, name)
5846                    };
5847                    results.push(full_path);
5848                }
5849            }
5850        }
5851
5852        if results.is_empty() {
5853            vec![original_pattern.to_string()]
5854        } else {
5855            results.sort();
5856            results
5857        }
5858    }
5859
5860    /// Extract !(pattern) info from file pattern, returns (inner_pattern, suffix)
5861    fn extract_neg_extglob(&self, pattern: &str) -> Option<(String, String)> {
5862        let chars: Vec<char> = pattern.chars().collect();
5863        if chars.len() >= 3 && chars[0] == '!' && chars[1] == '(' {
5864            let mut depth = 1;
5865            let mut i = 2;
5866            while i < chars.len() && depth > 0 {
5867                if chars[i] == '(' {
5868                    depth += 1;
5869                } else if chars[i] == ')' {
5870                    depth -= 1;
5871                }
5872                i += 1;
5873            }
5874            if depth == 0 {
5875                let inner: String = chars[2..i - 1].iter().collect();
5876                let suffix: String = chars[i..].iter().collect();
5877                return Some((inner, suffix));
5878            }
5879        }
5880        None
5881    }
5882
5883    /// Expand a word with word splitting (for contexts like `for x in $words`)
5884    fn expand_word_split(&mut self, word: &ShellWord) -> Vec<String> {
5885        match word {
5886            ShellWord::Literal(s) => {
5887                // First do brace expansion, then variable expansion on each result
5888                let brace_expanded = self.expand_braces(s);
5889                brace_expanded
5890                    .into_iter()
5891                    .flat_map(|item| self.expand_string_split(&item))
5892                    .collect()
5893            }
5894            ShellWord::SingleQuoted(s) => vec![s.clone()],
5895            ShellWord::DoubleQuoted(parts) => {
5896                // Double quotes prevent word splitting
5897                vec![parts.iter().map(|p| self.expand_word(p)).collect()]
5898            }
5899            ShellWord::Variable(name) => {
5900                let val = env::var(name).unwrap_or_default();
5901                self.split_words(&val)
5902            }
5903            ShellWord::VariableBraced(name, modifier) => {
5904                let val = env::var(name).ok();
5905                let expanded = self.apply_var_modifier(name, val, modifier.as_deref());
5906                self.split_words(&expanded)
5907            }
5908            ShellWord::ArrayVar(name, index) => {
5909                let idx_str = self.expand_word(index);
5910                if idx_str == "@" || idx_str == "*" {
5911                    // ${arr[@]} returns each element as separate word
5912                    self.arrays.get(name).cloned().unwrap_or_default()
5913                } else {
5914                    vec![self.expand_array_access(name, index)]
5915                }
5916            }
5917            ShellWord::Glob(pattern) => match glob::glob(pattern) {
5918                Ok(paths) => {
5919                    let expanded: Vec<String> = paths
5920                        .filter_map(|p| p.ok())
5921                        .map(|p| p.to_string_lossy().to_string())
5922                        .collect();
5923                    if expanded.is_empty() {
5924                        vec![pattern.clone()]
5925                    } else {
5926                        expanded
5927                    }
5928                }
5929                Err(_) => vec![pattern.clone()],
5930            },
5931            ShellWord::CommandSub(_) => {
5932                // Command substitution results must be word-split for array context
5933                let val = self.expand_word(word);
5934                self.split_words(&val)
5935            }
5936            ShellWord::Concat(parts) => {
5937                // Concat in split context — expand and split the result
5938                let val = self.expand_concat_parallel(parts);
5939                self.split_words(&val)
5940            }
5941            _ => vec![self.expand_word(word)],
5942        }
5943    }
5944
5945    /// Expand string with word splitting - returns Vec for array expansions
5946    fn expand_string_split(&mut self, s: &str) -> Vec<String> {
5947        let mut results: Vec<String> = Vec::new();
5948        let mut current = String::new();
5949        let mut chars = s.chars().peekable();
5950
5951        while let Some(c) = chars.next() {
5952            if c == '$' {
5953                if chars.peek() == Some(&'{') {
5954                    chars.next(); // consume '{'
5955                    let mut brace_content = String::new();
5956                    let mut depth = 1;
5957                    while let Some(ch) = chars.next() {
5958                        if ch == '{' {
5959                            depth += 1;
5960                            brace_content.push(ch);
5961                        } else if ch == '}' {
5962                            depth -= 1;
5963                            if depth == 0 {
5964                                break;
5965                            }
5966                            brace_content.push(ch);
5967                        } else {
5968                            brace_content.push(ch);
5969                        }
5970                    }
5971
5972                    // Check if this is an array expansion ${arr[@]} or ${arr[*]}
5973                    if let Some(bracket_start) = brace_content.find('[') {
5974                        let var_name = &brace_content[..bracket_start];
5975                        let bracket_content = &brace_content[bracket_start + 1..];
5976                        if let Some(bracket_end) = bracket_content.find(']') {
5977                            let index = &bracket_content[..bracket_end];
5978                            if (index == "@" || index == "*")
5979                                && bracket_end + 1 == bracket_content.len()
5980                            {
5981                                // This is ${arr[@]} - expand to separate elements
5982                                if !current.is_empty() {
5983                                    results.push(current.clone());
5984                                    current.clear();
5985                                }
5986                                if let Some(arr) = self.arrays.get(var_name) {
5987                                    results.extend(arr.clone());
5988                                }
5989                                continue;
5990                            }
5991                        }
5992                    }
5993
5994                    // Not an array expansion, use normal expansion
5995                    current.push_str(&self.expand_braced_variable(&brace_content));
5996                } else {
5997                    // Simple variable like $var
5998                    let mut var_name = String::new();
5999                    while let Some(&ch) = chars.peek() {
6000                        if ch.is_alphanumeric() || ch == '_' {
6001                            var_name.push(chars.next().unwrap());
6002                        } else {
6003                            break;
6004                        }
6005                    }
6006                    let val = self.get_variable(&var_name);
6007                    // Split this variable's value
6008                    if !current.is_empty() {
6009                        results.push(current.clone());
6010                        current.clear();
6011                    }
6012                    results.extend(self.split_words(&val));
6013                }
6014            } else {
6015                current.push(c);
6016            }
6017        }
6018
6019        if !current.is_empty() {
6020            results.push(current);
6021        }
6022
6023        if results.is_empty() {
6024            results.push(String::new());
6025        }
6026
6027        results
6028    }
6029
6030    /// Split a string into words based on IFS
6031    fn split_words(&self, s: &str) -> Vec<String> {
6032        let ifs = self
6033            .variables
6034            .get("IFS")
6035            .cloned()
6036            .or_else(|| env::var("IFS").ok())
6037            .unwrap_or_else(|| " \t\n".to_string());
6038
6039        if ifs.is_empty() {
6040            return vec![s.to_string()];
6041        }
6042
6043        s.split(|c: char| ifs.contains(c))
6044            .filter(|s| !s.is_empty())
6045            .map(|s| s.to_string())
6046            .collect()
6047    }
6048
6049    #[tracing::instrument(level = "trace", skip_all)]
6050    fn expand_word(&mut self, word: &ShellWord) -> String {
6051        match word {
6052            ShellWord::Literal(s) => {
6053                let expanded = self.expand_string(s);
6054                // Don't glob-expand here, that's done in expand_word_glob
6055                expanded
6056            }
6057            ShellWord::SingleQuoted(s) => s.clone(),
6058            ShellWord::DoubleQuoted(parts) => parts.iter().map(|p| self.expand_word(p)).collect(),
6059            ShellWord::Variable(name) => self.get_variable(name),
6060            ShellWord::VariableBraced(name, modifier) => {
6061                let val = env::var(name).ok();
6062                self.apply_var_modifier(name, val, modifier.as_deref())
6063            }
6064            ShellWord::Tilde(user) => {
6065                if let Some(u) = user {
6066                    // ~user expansion (simplified)
6067                    format!("/home/{}", u)
6068                } else {
6069                    dirs::home_dir()
6070                        .map(|p| p.to_string_lossy().to_string())
6071                        .unwrap_or_else(|| "~".to_string())
6072                }
6073            }
6074            ShellWord::Glob(pattern) => {
6075                // Expand glob
6076                match glob::glob(pattern) {
6077                    Ok(paths) => {
6078                        let expanded: Vec<String> = paths
6079                            .filter_map(|p| p.ok())
6080                            .map(|p| p.to_string_lossy().to_string())
6081                            .collect();
6082                        if expanded.is_empty() {
6083                            pattern.clone()
6084                        } else {
6085                            expanded.join(" ")
6086                        }
6087                    }
6088                    Err(_) => pattern.clone(),
6089                }
6090            }
6091            ShellWord::Concat(parts) => self.expand_concat_parallel(parts),
6092            ShellWord::CommandSub(cmd) => self.execute_command_substitution(cmd),
6093            ShellWord::ProcessSubIn(cmd) => self.execute_process_sub_in(cmd),
6094            ShellWord::ProcessSubOut(cmd) => self.execute_process_sub_out(cmd),
6095            ShellWord::ArithSub(expr) => self.evaluate_arithmetic(expr),
6096            ShellWord::ArrayVar(name, index) => self.expand_array_access(name, index),
6097            ShellWord::ArrayLiteral(elements) => elements
6098                .iter()
6099                .map(|e| self.expand_word(e))
6100                .collect::<Vec<_>>()
6101                .join(" "),
6102        }
6103    }
6104
6105    /// Pre-launch external command substitutions from a word list onto the worker pool.
6106    /// Returns a Vec aligned with `words` — Some(receiver) for pre-launched externals, None otherwise.
6107    fn preflight_command_subs(
6108        &mut self,
6109        words: &[ShellWord],
6110    ) -> Vec<Option<crossbeam_channel::Receiver<String>>> {
6111        use crate::parser::ShellWord;
6112        use std::process::{Command, Stdio};
6113
6114        let mut receivers = Vec::with_capacity(words.len());
6115
6116        // Count external command subs — don't bother with pool overhead for just one
6117        let external_count = words
6118            .iter()
6119            .filter(|w| {
6120                if let ShellWord::CommandSub(cmd) = w {
6121                    if let ShellCommand::Simple(simple) = cmd.as_ref() {
6122                        if let Some(first) = simple.words.first() {
6123                            let name = self.expand_word(first);
6124                            return !self.functions.contains_key(&name) && !self.is_builtin(&name);
6125                        }
6126                    }
6127                }
6128                false
6129            })
6130            .count();
6131
6132        if external_count < 2 {
6133            // Not worth parallelizing — fall through to sequential
6134            return vec![None; words.len()];
6135        }
6136
6137        for word in words {
6138            if let ShellWord::CommandSub(cmd) = word {
6139                if let ShellCommand::Simple(simple) = cmd.as_ref() {
6140                    let first = simple.words.first().map(|w| self.expand_word(w));
6141                    if let Some(ref name) = first {
6142                        if !self.functions.contains_key(name) && !self.is_builtin(name) {
6143                            let expanded: Vec<String> =
6144                                simple.words.iter().map(|w| self.expand_word(w)).collect();
6145                            let rx = self.worker_pool.submit_with_result(move || {
6146                                let output = Command::new(&expanded[0])
6147                                    .args(&expanded[1..])
6148                                    .stdout(Stdio::piped())
6149                                    .stderr(Stdio::inherit())
6150                                    .output();
6151                                match output {
6152                                    Ok(out) => String::from_utf8_lossy(&out.stdout)
6153                                        .trim_end_matches('\n')
6154                                        .to_string(),
6155                                    Err(_) => String::new(),
6156                                }
6157                            });
6158                            receivers.push(Some(rx));
6159                            continue;
6160                        }
6161                    }
6162                }
6163            }
6164            receivers.push(None);
6165        }
6166
6167        receivers
6168    }
6169
6170    /// Expand a Concat word list, launching external command substitutions in parallel.
6171    /// Internal subs (builtins/functions) still run sequentially on the main thread.
6172    fn expand_concat_parallel(&mut self, parts: &[ShellWord]) -> String {
6173        use crate::parser::ShellWord;
6174        use std::process::{Command, Stdio};
6175
6176        // Phase 1: identify external command subs and pre-launch them
6177        let mut preflight: Vec<Option<crossbeam_channel::Receiver<String>>> =
6178            Vec::with_capacity(parts.len());
6179
6180        for part in parts {
6181            if let ShellWord::CommandSub(cmd) = part {
6182                if let ShellCommand::Simple(simple) = cmd.as_ref() {
6183                    let first = simple.words.first().map(|w| self.expand_word(w));
6184                    if let Some(ref name) = first {
6185                        if !self.functions.contains_key(name) && !self.is_builtin(name) {
6186                            // External command — pre-launch on background thread
6187                            let words: Vec<String> =
6188                                simple.words.iter().map(|w| self.expand_word(w)).collect();
6189                            let rx = self.worker_pool.submit_with_result(move || {
6190                                let output = Command::new(&words[0])
6191                                    .args(&words[1..])
6192                                    .stdout(Stdio::piped())
6193                                    .stderr(Stdio::inherit())
6194                                    .output();
6195                                match output {
6196                                    Ok(out) => String::from_utf8_lossy(&out.stdout)
6197                                        .trim_end_matches('\n')
6198                                        .to_string(),
6199                                    Err(_) => String::new(),
6200                                }
6201                            });
6202                            preflight.push(Some(rx));
6203                            continue;
6204                        }
6205                    }
6206                }
6207            }
6208            preflight.push(None); // not pre-launched
6209        }
6210
6211        // Phase 2: collect results in order, using pre-launched receivers where available
6212        let mut result = String::new();
6213        for (i, part) in parts.iter().enumerate() {
6214            if let Some(rx) = preflight[i].take() {
6215                // Pre-launched external command sub — collect result
6216                result.push_str(&rx.recv().unwrap_or_default());
6217            } else {
6218                // Everything else — expand sequentially (may be internal sub, variable, literal)
6219                result.push_str(&self.expand_word(part));
6220            }
6221        }
6222        result
6223    }
6224
6225    fn expand_braced_variable(&mut self, content: &str) -> String {
6226        // Handle nested expansion: ${${inner}[subscript]} or ${${inner}modifier}
6227        if content.starts_with("${") {
6228            // Find matching closing brace for inner expansion
6229            let mut depth = 0;
6230            let mut inner_end = 0;
6231            for (i, c) in content.char_indices() {
6232                match c {
6233                    '{' => depth += 1,
6234                    '}' => {
6235                        depth -= 1;
6236                        if depth == 0 {
6237                            inner_end = i;
6238                            break;
6239                        }
6240                    }
6241                    _ => {}
6242                }
6243            }
6244
6245            if inner_end > 0 {
6246                // Expand the inner ${...}
6247                let inner_content = &content[2..inner_end];
6248                let inner_result = self.expand_braced_variable(inner_content);
6249
6250                // Check for subscript or modifier after the inner expansion
6251                let rest = &content[inner_end + 1..];
6252                if rest.starts_with('[') {
6253                    // Apply subscript to result: ${${...}[idx]}
6254                    if let Some(bracket_end) = rest.find(']') {
6255                        let index = &rest[1..bracket_end];
6256                        if let Ok(idx) = index.parse::<i64>() {
6257                            let chars: Vec<char> = inner_result.chars().collect();
6258                            let actual_idx = if idx < 0 {
6259                                (chars.len() as i64 + idx).max(0) as usize
6260                            } else if idx > 0 {
6261                                (idx - 1) as usize
6262                            } else {
6263                                0
6264                            };
6265                            return chars
6266                                .get(actual_idx)
6267                                .map(|c| c.to_string())
6268                                .unwrap_or_default();
6269                        }
6270                    }
6271                }
6272
6273                return inner_result;
6274            }
6275        }
6276
6277        // Handle zsh-style parameter expansion flags ${(flags)var}
6278        if content.starts_with('(') {
6279            if let Some(close_paren) = content.find(')') {
6280                let flags_str = &content[1..close_paren];
6281                let rest = &content[close_paren + 1..];
6282                let flags = self.parse_zsh_flags(flags_str);
6283
6284                // Check for (M) match flag
6285                let has_match_flag = flags.iter().any(|f| matches!(f, ZshParamFlag::Match));
6286
6287                // Handle ${(M)var:#pattern} - pattern filter with flags
6288                if let Some(filter_pos) = rest.find(":#") {
6289                    let var_name = &rest[..filter_pos];
6290                    let pattern = &rest[filter_pos + 2..];
6291
6292                    // Array path: filter each element against pattern
6293                    if let Some(arr) = self.arrays.get(var_name).cloned() {
6294                        let filtered: Vec<String> = if arr.len() >= 1000 {
6295                            tracing::trace!(
6296                                count = arr.len(),
6297                                pattern,
6298                                "using parallel filter (rayon) for large array"
6299                            );
6300                            use rayon::prelude::*;
6301                            let pattern = pattern.to_string();
6302                            arr.into_par_iter()
6303                                .filter(|elem| {
6304                                    let m = Self::glob_match_static(elem, &pattern);
6305                                    if has_match_flag {
6306                                        m
6307                                    } else {
6308                                        !m
6309                                    }
6310                                })
6311                                .collect()
6312                        } else {
6313                            arr.into_iter()
6314                                .filter(|elem| {
6315                                    let m = self.glob_match(elem, pattern);
6316                                    if has_match_flag {
6317                                        m
6318                                    } else {
6319                                        !m
6320                                    }
6321                                })
6322                                .collect()
6323                        };
6324                        return filtered.join(" ");
6325                    }
6326
6327                    // Scalar path: original behavior
6328                    let val = self.get_variable(var_name);
6329                    let matches = self.glob_match(&val, pattern);
6330
6331                    return if has_match_flag {
6332                        if matches {
6333                            val
6334                        } else {
6335                            String::new()
6336                        }
6337                    } else {
6338                        if matches {
6339                            String::new()
6340                        } else {
6341                            val
6342                        }
6343                    };
6344                }
6345
6346                // Handle ${(%):-%n} style - empty var with default after flags
6347                // rest could be ":-%n" or ":-default" or "var:-default" or just "var"
6348                let (var_name, default_val) = if rest.starts_with(":-") {
6349                    // Empty variable name with default: ${(%):-default}
6350                    ("", Some(&rest[2..]))
6351                } else if let Some(pos) = rest.find(":-") {
6352                    // Variable with default: ${(%)var:-default}
6353                    (&rest[..pos], Some(&rest[pos + 2..]))
6354                } else if rest.starts_with(':') {
6355                    // Just ":" means empty var name, no default
6356                    ("", None)
6357                } else {
6358                    // Normal variable reference
6359                    let vn = rest
6360                        .split(|c: char| !c.is_alphanumeric() && c != '_')
6361                        .next()
6362                        .unwrap_or("");
6363                    (vn, None)
6364                };
6365
6366                let mut val = self.get_variable(var_name);
6367
6368                // Use default if variable is empty
6369                if val.is_empty() {
6370                    if let Some(def) = default_val {
6371                        // Expand the default value (handles $var and other expansions)
6372                        val = self.expand_string(def);
6373                    }
6374                }
6375
6376                // Apply flags in order
6377                for flag in &flags {
6378                    val = self.apply_zsh_param_flag(&val, var_name, flag);
6379                }
6380                return val;
6381            }
6382        }
6383
6384        // Handle ${#arr[@]} - array length
6385        if content.starts_with('#') {
6386            let rest = &content[1..];
6387            if let Some(bracket_start) = rest.find('[') {
6388                let var_name = &rest[..bracket_start];
6389                let bracket_content = &rest[bracket_start + 1..];
6390                if let Some(bracket_end) = bracket_content.find(']') {
6391                    let index = &bracket_content[..bracket_end];
6392                    if index == "@" || index == "*" {
6393                        // ${#arr[@]} - return array length
6394                        return self
6395                            .arrays
6396                            .get(var_name)
6397                            .map(|arr| arr.len().to_string())
6398                            .unwrap_or_else(|| "0".to_string());
6399                    }
6400                }
6401            }
6402            // ${#arr} - if rest is an array name, return array length
6403            if self.arrays.contains_key(rest) {
6404                return self
6405                    .arrays
6406                    .get(rest)
6407                    .map(|arr| arr.len().to_string())
6408                    .unwrap_or_else(|| "0".to_string());
6409            }
6410            // ${#assoc} - if rest is an assoc array name, return assoc length
6411            if self.assoc_arrays.contains_key(rest) {
6412                return self
6413                    .assoc_arrays
6414                    .get(rest)
6415                    .map(|h| h.len().to_string())
6416                    .unwrap_or_else(|| "0".to_string());
6417            }
6418            // ${#var} - string length
6419            let val = self.get_variable(rest);
6420            return val.len().to_string();
6421        }
6422
6423        // Handle ${+var} and ${+arr[key]} - test if variable/element is set (returns 1 if set, 0 if not)
6424        if content.starts_with('+') {
6425            let rest = &content[1..];
6426
6427            // Check for array/assoc access: ${+arr[key]}
6428            if let Some(bracket_start) = rest.find('[') {
6429                let var_name = &rest[..bracket_start];
6430                let bracket_content = &rest[bracket_start + 1..];
6431                if let Some(bracket_end) = bracket_content.find(']') {
6432                    let key = &bracket_content[..bracket_end];
6433
6434                    // Check special arrays first
6435                    if let Some(val) = self.get_special_array_value(var_name, key) {
6436                        return if val.is_empty() {
6437                            "0".to_string()
6438                        } else {
6439                            "1".to_string()
6440                        };
6441                    }
6442
6443                    // Check user assoc arrays
6444                    if self.assoc_arrays.contains_key(var_name) {
6445                        let expanded_key = self.expand_string(key);
6446                        let has_key = self
6447                            .assoc_arrays
6448                            .get(var_name)
6449                            .map(|a| a.contains_key(&expanded_key))
6450                            .unwrap_or(false);
6451                        return if has_key {
6452                            "1".to_string()
6453                        } else {
6454                            "0".to_string()
6455                        };
6456                    }
6457
6458                    // Check regular arrays
6459                    if let Some(arr) = self.arrays.get(var_name) {
6460                        if let Ok(idx) = key.parse::<usize>() {
6461                            let actual_idx = if idx > 0 { idx - 1 } else { 0 };
6462                            return if arr.get(actual_idx).is_some() {
6463                                "1".to_string()
6464                            } else {
6465                                "0".to_string()
6466                            };
6467                        }
6468                    }
6469
6470                    return "0".to_string();
6471                }
6472            }
6473
6474            // Simple variable: ${+var}
6475            let is_set = self.variables.contains_key(rest)
6476                || self.arrays.contains_key(rest)
6477                || self.assoc_arrays.contains_key(rest)
6478                || std::env::var(rest).is_ok()
6479                || self.functions.contains_key(rest);
6480            return if is_set {
6481                "1".to_string()
6482            } else {
6483                "0".to_string()
6484            };
6485        }
6486
6487        // Handle ${arr[idx]} or ${assoc[key]}
6488        if let Some(bracket_start) = content.find('[') {
6489            let var_name = &content[..bracket_start];
6490            let bracket_content = &content[bracket_start + 1..];
6491            if let Some(bracket_end) = bracket_content.find(']') {
6492                let index = &bracket_content[..bracket_end];
6493
6494                // Check for zsh/parameter special associative arrays (options, commands, etc.)
6495                if let Some(val) = self.get_special_array_value(var_name, index) {
6496                    return val;
6497                }
6498
6499                // Check if it's a user-defined associative array
6500                if self.assoc_arrays.contains_key(var_name) {
6501                    if index == "@" || index == "*" {
6502                        // ${assoc[@]} - return all values
6503                        return self
6504                            .assoc_arrays
6505                            .get(var_name)
6506                            .map(|a| a.values().cloned().collect::<Vec<_>>().join(" "))
6507                            .unwrap_or_default();
6508                    } else {
6509                        // ${assoc[key]} - return value for key
6510                        let key = self.expand_string(index);
6511                        return self
6512                            .assoc_arrays
6513                            .get(var_name)
6514                            .and_then(|a| a.get(&key).cloned())
6515                            .unwrap_or_default();
6516                    }
6517                }
6518
6519                // Regular indexed array
6520                if index == "@" || index == "*" {
6521                    // ${arr[@]} - return all elements
6522                    return self
6523                        .arrays
6524                        .get(var_name)
6525                        .map(|arr| arr.join(" "))
6526                        .unwrap_or_default();
6527                }
6528
6529                // Use the ported subscript module for comprehensive index parsing
6530                use crate::subscript::{
6531                    get_array_by_subscript, get_array_element_by_subscript, getindex,
6532                };
6533                let ksh_arrays = self.options.get("ksh_arrays").copied().unwrap_or(false);
6534
6535                if let Ok(v) = getindex(index, false, ksh_arrays) {
6536                    // Check if it's an array first
6537                    if let Some(arr) = self.arrays.get(var_name) {
6538                        if v.is_all() {
6539                            return arr.join(" ");
6540                        }
6541                        // Check if this is a range (comma in subscript) vs single element
6542                        // For a single element, v.end == v.start + 1 after adjustment
6543                        // But for negative single indices, we need to handle specially
6544                        let is_range = index.contains(',');
6545                        if is_range {
6546                            // Range: ${arr[2,4]} returns elements 2 through 4
6547                            return get_array_by_subscript(arr, &v, ksh_arrays).join(" ");
6548                        } else {
6549                            // Single element (including negative indices like -1)
6550                            return get_array_element_by_subscript(arr, &v, ksh_arrays)
6551                                .unwrap_or_default();
6552                        }
6553                    }
6554
6555                    // Not an array - treat as string subscripting
6556                    let val = self.get_variable(var_name);
6557                    if !val.is_empty() {
6558                        let chars: Vec<char> = val.chars().collect();
6559                        let idx = v.start;
6560                        let actual_idx = if idx < 0 {
6561                            (chars.len() as i64 + idx).max(0) as usize
6562                        } else if idx > 0 {
6563                            (idx - 1) as usize // zsh is 1-indexed
6564                        } else {
6565                            0
6566                        };
6567
6568                        if v.end > v.start + 1 {
6569                            // String range
6570                            let end_idx = if v.end < 0 {
6571                                (chars.len() as i64 + v.end + 1).max(0) as usize
6572                            } else {
6573                                v.end as usize
6574                            };
6575                            let end_idx = end_idx.min(chars.len());
6576                            return chars[actual_idx..end_idx].iter().collect();
6577                        } else {
6578                            return chars
6579                                .get(actual_idx)
6580                                .map(|c| c.to_string())
6581                                .unwrap_or_default();
6582                        }
6583                    }
6584                    return String::new();
6585                }
6586
6587                // Non-numeric index on non-assoc - return empty
6588                return String::new();
6589            }
6590        }
6591
6592        // Handle ${var:-default}, ${var:=default}, ${var:?error}, ${var:+alternate}
6593        if let Some(colon_pos) = content.find(':') {
6594            let var_name = &content[..colon_pos];
6595            let rest = &content[colon_pos + 1..];
6596            let val = self.get_variable(var_name);
6597            let val_opt = if val.is_empty() {
6598                None
6599            } else {
6600                Some(val.clone())
6601            };
6602
6603            if rest.starts_with('-') {
6604                // ${var:-default}
6605                return match val_opt {
6606                    Some(v) if !v.is_empty() => v,
6607                    _ => self.expand_string(&rest[1..]),
6608                };
6609            } else if rest.starts_with('=') {
6610                // ${var:=default}
6611                return match val_opt {
6612                    Some(v) if !v.is_empty() => v,
6613                    _ => {
6614                        let default = self.expand_string(&rest[1..]);
6615                        self.variables.insert(var_name.to_string(), default.clone());
6616                        default
6617                    }
6618                };
6619            } else if rest.starts_with('?') {
6620                // ${var:?error}
6621                return match val_opt {
6622                    Some(v) if !v.is_empty() => v,
6623                    _ => {
6624                        let msg = self.expand_string(&rest[1..]);
6625                        eprintln!("zshrs: {}: {}", var_name, msg);
6626                        String::new()
6627                    }
6628                };
6629            } else if rest.starts_with('+') {
6630                // ${var:+alternate}
6631                return match val_opt {
6632                    Some(v) if !v.is_empty() => self.expand_string(&rest[1..]),
6633                    _ => String::new(),
6634                };
6635            } else if rest.starts_with('#') {
6636                // ${var:#pattern} - filter: remove elements matching pattern
6637                // With (M) flag, keep only matching elements
6638                let pattern = &rest[1..];
6639                // For scalars, return empty if matches, value if not
6640                if self.glob_match(&val, pattern) {
6641                    return String::new();
6642                } else {
6643                    return val;
6644                }
6645            } else if self.is_history_modifier(rest) {
6646                // Handle history-style modifiers: :A, :h, :t, :r, :e, :l, :u, :q, :Q
6647                // These can be chained: ${var:A:h:h}
6648                return self.apply_history_modifiers(&val, rest);
6649            } else if rest
6650                .chars()
6651                .next()
6652                .map(|c| c.is_ascii_digit() || c == '-')
6653                .unwrap_or(false)
6654            {
6655                // ${var:offset} or ${var:offset:length}
6656                let parts: Vec<&str> = rest.splitn(2, ':').collect();
6657                let offset: i64 = parts[0].parse().unwrap_or(0);
6658                let length: Option<usize> = parts.get(1).and_then(|s| s.parse().ok());
6659
6660                let start = if offset < 0 {
6661                    (val.len() as i64 + offset).max(0) as usize
6662                } else {
6663                    (offset as usize).min(val.len())
6664                };
6665
6666                return if let Some(len) = length {
6667                    val.chars().skip(start).take(len).collect()
6668                } else {
6669                    val.chars().skip(start).collect()
6670                };
6671            }
6672        }
6673
6674        // Handle ${var/pattern/replacement} and ${var//pattern/replacement}
6675        // Only if the part before / is a valid variable name
6676        if let Some(slash_pos) = content.find('/') {
6677            let var_name = &content[..slash_pos];
6678            // Variable names must start with letter/underscore and contain only alnum/_
6679            if !var_name.is_empty()
6680                && var_name
6681                    .chars()
6682                    .next()
6683                    .map(|c| c.is_alphabetic() || c == '_')
6684                    .unwrap_or(false)
6685                && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
6686            {
6687                let rest = &content[slash_pos + 1..];
6688                let val = self.get_variable(var_name);
6689
6690                let replace_all = rest.starts_with('/');
6691                let rest = if replace_all { &rest[1..] } else { rest };
6692
6693                let parts: Vec<&str> = rest.splitn(2, '/').collect();
6694                let pattern = parts.get(0).unwrap_or(&"");
6695                let replacement = parts.get(1).unwrap_or(&"");
6696
6697                return if replace_all {
6698                    val.replace(pattern, replacement)
6699                } else {
6700                    val.replacen(pattern, replacement, 1)
6701                };
6702            }
6703        }
6704
6705        // Handle ${var#pattern} and ${var##pattern} - remove prefix
6706        // But only if the # is not at the start (which would be length)
6707        if let Some(hash_pos) = content.find('#') {
6708            if hash_pos > 0 {
6709                let var_name = &content[..hash_pos];
6710                // Make sure var_name looks like a valid variable name
6711                if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
6712                    let rest = &content[hash_pos + 1..];
6713                    let val = self.get_variable(var_name);
6714
6715                    let long = rest.starts_with('#');
6716                    let pattern = if long { &rest[1..] } else { rest };
6717
6718                    // Convert shell glob pattern to regex-style for matching prefixes
6719                    let pattern_regex = regex::escape(pattern)
6720                        .replace(r"\*", ".*")
6721                        .replace(r"\?", ".");
6722                    let full_pattern = format!("^{}", pattern_regex);
6723
6724                    if let Some(re) = cached_regex(&full_pattern) {
6725                        if long {
6726                            // Remove longest prefix match - find all matches and use the longest
6727                            let mut longest_end = 0;
6728                            for m in re.find_iter(&val) {
6729                                if m.end() > longest_end {
6730                                    longest_end = m.end();
6731                                }
6732                            }
6733                            if longest_end > 0 {
6734                                return val[longest_end..].to_string();
6735                            }
6736                        } else {
6737                            // Remove shortest prefix match
6738                            if let Some(m) = re.find(&val) {
6739                                return val[m.end()..].to_string();
6740                            }
6741                        }
6742                    }
6743                    return val;
6744                }
6745            }
6746        }
6747
6748        // Handle ${var%pattern} and ${var%%pattern} - remove suffix
6749        if let Some(pct_pos) = content.find('%') {
6750            if pct_pos > 0 {
6751                let var_name = &content[..pct_pos];
6752                if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
6753                    let rest = &content[pct_pos + 1..];
6754                    let val = self.get_variable(var_name);
6755
6756                    let long = rest.starts_with('%');
6757                    let pattern = if long { &rest[1..] } else { rest };
6758
6759                    // Use glob pattern matching for suffix removal
6760                    if let Ok(glob) = glob::Pattern::new(pattern) {
6761                        if long {
6762                            // Remove longest suffix match - find earliest matching position
6763                            for i in 0..=val.len() {
6764                                if glob.matches(&val[i..]) {
6765                                    return val[..i].to_string();
6766                                }
6767                            }
6768                        } else {
6769                            // Remove shortest suffix match - find latest matching position
6770                            for i in (0..=val.len()).rev() {
6771                                if glob.matches(&val[i..]) {
6772                                    return val[..i].to_string();
6773                                }
6774                            }
6775                        }
6776                    }
6777                    return val;
6778                }
6779            }
6780        }
6781
6782        // Handle ${var^} and ${var^^} - uppercase
6783        if let Some(caret_pos) = content.find('^') {
6784            let var_name = &content[..caret_pos];
6785            let val = self.get_variable(var_name);
6786            let all = content[caret_pos + 1..].starts_with('^');
6787
6788            return if all {
6789                val.to_uppercase()
6790            } else {
6791                let mut chars = val.chars();
6792                match chars.next() {
6793                    Some(first) => first.to_uppercase().to_string() + chars.as_str(),
6794                    None => String::new(),
6795                }
6796            };
6797        }
6798
6799        // Handle ${var,} and ${var,,} - lowercase
6800        if let Some(comma_pos) = content.find(',') {
6801            let var_name = &content[..comma_pos];
6802            let val = self.get_variable(var_name);
6803            let all = content[comma_pos + 1..].starts_with(',');
6804
6805            return if all {
6806                val.to_lowercase()
6807            } else {
6808                let mut chars = val.chars();
6809                match chars.next() {
6810                    Some(first) => first.to_lowercase().to_string() + chars.as_str(),
6811                    None => String::new(),
6812                }
6813            };
6814        }
6815
6816        // Handle ${!prefix*} and ${!prefix@} - expand to variable names with prefix
6817        if content.starts_with('!') {
6818            let rest = &content[1..];
6819            if rest.ends_with('*') || rest.ends_with('@') {
6820                let prefix = &rest[..rest.len() - 1];
6821                let mut matches: Vec<String> = self
6822                    .variables
6823                    .keys()
6824                    .filter(|k| k.starts_with(prefix))
6825                    .cloned()
6826                    .collect();
6827                // Also check arrays
6828                for k in self.arrays.keys() {
6829                    if k.starts_with(prefix) && !matches.contains(k) {
6830                        matches.push(k.clone());
6831                    }
6832                }
6833                matches.sort();
6834                return matches.join(" ");
6835            }
6836
6837            // ${!var} - indirect expansion
6838            let var_name = self.get_variable(rest);
6839            return self.get_variable(&var_name);
6840        }
6841
6842        // Default: just get the variable
6843        self.get_variable(content)
6844    }
6845
6846    fn expand_array_access(&mut self, name: &str, index: &ShellWord) -> String {
6847        use crate::subscript::{get_array_by_subscript, get_array_element_by_subscript, getindex};
6848
6849        let idx_str = self.expand_word(index);
6850        let ksh_arrays = self.options.get("ksh_arrays").copied().unwrap_or(false);
6851
6852        // Use the ported subscript module for index parsing
6853        match getindex(&idx_str, false, ksh_arrays) {
6854            Ok(v) => {
6855                if let Some(arr) = self.arrays.get(name) {
6856                    if v.is_all() {
6857                        arr.join(" ")
6858                    } else if v.start == v.end - 1 {
6859                        // Single element
6860                        get_array_element_by_subscript(arr, &v, ksh_arrays).unwrap_or_default()
6861                    } else {
6862                        // Range
6863                        get_array_by_subscript(arr, &v, ksh_arrays).join(" ")
6864                    }
6865                } else {
6866                    String::new()
6867                }
6868            }
6869            Err(_) => String::new(),
6870        }
6871    }
6872
6873    #[tracing::instrument(level = "trace", skip_all)]
6874    fn expand_string(&mut self, s: &str) -> String {
6875        let mut result = String::new();
6876        let mut chars = s.chars().peekable();
6877
6878        while let Some(c) = chars.next() {
6879            // \x00 prefix marks chars from single quotes - keep them literal
6880            if c == '\x00' {
6881                if let Some(literal_char) = chars.next() {
6882                    result.push(literal_char);
6883                }
6884                continue;
6885            }
6886            if c == '$' {
6887                if chars.peek() == Some(&'(') {
6888                    chars.next(); // consume '('
6889
6890                    // Check for $(( )) arithmetic
6891                    if chars.peek() == Some(&'(') {
6892                        chars.next(); // consume second '('
6893                        let expr = Self::collect_until_double_paren(&mut chars);
6894                        result.push_str(&self.evaluate_arithmetic(&expr));
6895                    } else {
6896                        // Command substitution $(...)
6897                        let cmd_str = Self::collect_until_paren(&mut chars);
6898                        result.push_str(&self.run_command_substitution(&cmd_str));
6899                    }
6900                } else if chars.peek() == Some(&'{') {
6901                    chars.next();
6902                    // Collect the full braced expression including brackets
6903                    let mut brace_content = String::new();
6904                    let mut depth = 1;
6905                    while let Some(c) = chars.next() {
6906                        if c == '{' {
6907                            depth += 1;
6908                            brace_content.push(c);
6909                        } else if c == '}' {
6910                            depth -= 1;
6911                            if depth == 0 {
6912                                break;
6913                            }
6914                            brace_content.push(c);
6915                        } else {
6916                            brace_content.push(c);
6917                        }
6918                    }
6919                    result.push_str(&self.expand_braced_variable(&brace_content));
6920                } else {
6921                    // Check for single-char special vars first: $$, $!, $-
6922                    if matches!(chars.peek(), Some(&'$') | Some(&'!') | Some(&'-')) {
6923                        let sc = chars.next().unwrap();
6924                        result.push_str(&self.get_variable(&sc.to_string()));
6925                        continue;
6926                    }
6927                    // $#name → ${#name} (string/array length)
6928                    if chars.peek() == Some(&'#') {
6929                        let mut peek_iter = chars.clone();
6930                        peek_iter.next(); // skip #
6931                        if peek_iter
6932                            .peek()
6933                            .map(|c| c.is_alphabetic() || *c == '_')
6934                            .unwrap_or(false)
6935                        {
6936                            chars.next(); // consume #
6937                            let mut name = String::new();
6938                            while let Some(&c) = chars.peek() {
6939                                if c.is_alphanumeric() || c == '_' {
6940                                    name.push(chars.next().unwrap());
6941                                } else {
6942                                    break;
6943                                }
6944                            }
6945                            // Return length of variable or array
6946                            let len = if let Some(arr) = self.arrays.get(&name) {
6947                                arr.len()
6948                            } else {
6949                                self.get_variable(&name).len()
6950                            };
6951                            result.push_str(&len.to_string());
6952                            continue;
6953                        }
6954                    }
6955                    let mut var_name = String::new();
6956                    while let Some(&c) = chars.peek() {
6957                        if c.is_alphanumeric()
6958                            || c == '_'
6959                            || c == '@'
6960                            || c == '*'
6961                            || c == '#'
6962                            || c == '?'
6963                        {
6964                            var_name.push(chars.next().unwrap());
6965                            // Handle single-char special vars
6966                            if matches!(
6967                                var_name.as_str(),
6968                                "@" | "*"
6969                                    | "#"
6970                                    | "?"
6971                                    | "$"
6972                                    | "!"
6973                                    | "-"
6974                                    | "0"
6975                                    | "1"
6976                                    | "2"
6977                                    | "3"
6978                                    | "4"
6979                                    | "5"
6980                                    | "6"
6981                                    | "7"
6982                                    | "8"
6983                                    | "9"
6984                            ) {
6985                                break;
6986                            }
6987                        } else {
6988                            break;
6989                        }
6990                    }
6991                    result.push_str(&self.get_variable(&var_name));
6992                }
6993            } else if c == '`' {
6994                // Backtick command substitution
6995                let cmd_str: String = chars.by_ref().take_while(|&c| c != '`').collect();
6996                result.push_str(&self.run_command_substitution(&cmd_str));
6997            } else if c == '<' && chars.peek() == Some(&'(') {
6998                // Process substitution <(cmd)
6999                chars.next(); // consume '('
7000                let cmd_str = Self::collect_until_paren(&mut chars);
7001                result.push_str(&self.run_process_sub_in(&cmd_str));
7002            } else if c == '>' && chars.peek() == Some(&'(') {
7003                // Process substitution >(cmd)
7004                chars.next(); // consume '('
7005                let cmd_str = Self::collect_until_paren(&mut chars);
7006                result.push_str(&self.run_process_sub_out(&cmd_str));
7007            } else if c == '~' && result.is_empty() {
7008                if let Some(home) = dirs::home_dir() {
7009                    result.push_str(&home.to_string_lossy());
7010                } else {
7011                    result.push(c);
7012                }
7013            } else {
7014                result.push(c);
7015            }
7016        }
7017
7018        result
7019    }
7020
7021    fn collect_until_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
7022        let mut result = String::new();
7023        let mut depth = 1;
7024
7025        while let Some(c) = chars.next() {
7026            if c == '(' {
7027                depth += 1;
7028                result.push(c);
7029            } else if c == ')' {
7030                depth -= 1;
7031                if depth == 0 {
7032                    break;
7033                }
7034                result.push(c);
7035            } else {
7036                result.push(c);
7037            }
7038        }
7039
7040        result
7041    }
7042
7043    fn collect_until_double_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
7044        let mut result = String::new();
7045        let mut arith_depth = 1; // Tracks $(( ... )) nesting
7046        let mut paren_depth = 0; // Tracks ( ... ) nesting within expression
7047
7048        while let Some(c) = chars.next() {
7049            if c == '(' {
7050                if paren_depth == 0 && chars.peek() == Some(&'(') {
7051                    // Nested $(( - but we need to see if it's really another arithmetic
7052                    // For simplicity, track inner parens
7053                    paren_depth += 1;
7054                    result.push(c);
7055                } else {
7056                    paren_depth += 1;
7057                    result.push(c);
7058                }
7059            } else if c == ')' {
7060                if paren_depth > 0 {
7061                    // Inside nested parens, just close one level
7062                    paren_depth -= 1;
7063                    result.push(c);
7064                } else if chars.peek() == Some(&')') {
7065                    // At top level and seeing )) - this closes our arithmetic
7066                    chars.next();
7067                    arith_depth -= 1;
7068                    if arith_depth == 0 {
7069                        break;
7070                    }
7071                    result.push_str("))");
7072                } else {
7073                    // Single ) at top level - shouldn't happen in valid expression
7074                    result.push(c);
7075                }
7076            } else {
7077                result.push(c);
7078            }
7079        }
7080
7081        result
7082    }
7083
7084    fn run_process_sub_in(&mut self, cmd_str: &str) -> String {
7085        use std::fs;
7086        use std::process::Stdio;
7087
7088        // Parse the command
7089        let mut parser = ShellParser::new(cmd_str);
7090        let commands = match parser.parse_script() {
7091            Ok(cmds) => cmds,
7092            Err(_) => return String::new(),
7093        };
7094
7095        // Create a unique FIFO in temp directory
7096        let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
7097        let fifo_counter = self.process_sub_counter;
7098        self.process_sub_counter += 1;
7099        let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
7100
7101        // Remove if exists, then create FIFO
7102        let _ = fs::remove_file(&fifo_path);
7103        if let Err(_) = nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU) {
7104            return String::new();
7105        }
7106
7107        // Spawn command that writes to the FIFO
7108        let fifo_clone = fifo_path.clone();
7109        if let Some(cmd) = commands.first() {
7110            if let ShellCommand::Simple(simple) = cmd {
7111                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7112                if !words.is_empty() {
7113                    let cmd_name = words[0].clone();
7114                    let args: Vec<String> = words[1..].to_vec();
7115
7116                    self.worker_pool.submit(move || {
7117                        // Open FIFO for writing (will block until reader connects)
7118                        if let Ok(fifo) = fs::OpenOptions::new().write(true).open(&fifo_clone) {
7119                            let _ = Command::new(&cmd_name)
7120                                .args(&args)
7121                                .stdout(fifo)
7122                                .stderr(Stdio::inherit())
7123                                .status();
7124                        }
7125                        // Clean up FIFO after command completes
7126                        let _ = fs::remove_file(&fifo_clone);
7127                    });
7128                }
7129            }
7130        }
7131
7132        fifo_path
7133    }
7134
7135    fn run_process_sub_out(&mut self, cmd_str: &str) -> String {
7136        use std::fs;
7137        use std::process::Stdio;
7138
7139        // Parse the command
7140        let mut parser = ShellParser::new(cmd_str);
7141        let commands = match parser.parse_script() {
7142            Ok(cmds) => cmds,
7143            Err(_) => return String::new(),
7144        };
7145
7146        // Create a unique FIFO in temp directory
7147        let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
7148        let fifo_counter = self.process_sub_counter;
7149        self.process_sub_counter += 1;
7150        let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
7151
7152        // Remove if exists, then create FIFO
7153        let _ = fs::remove_file(&fifo_path);
7154        if let Err(_) = nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU) {
7155            return String::new();
7156        }
7157
7158        // Spawn command that reads from the FIFO
7159        let fifo_clone = fifo_path.clone();
7160        if let Some(cmd) = commands.first() {
7161            if let ShellCommand::Simple(simple) = cmd {
7162                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7163                if !words.is_empty() {
7164                    let cmd_name = words[0].clone();
7165                    let args: Vec<String> = words[1..].to_vec();
7166
7167                    self.worker_pool.submit(move || {
7168                        // Open FIFO for reading (will block until writer connects)
7169                        if let Ok(fifo) = fs::File::open(&fifo_clone) {
7170                            let _ = Command::new(&cmd_name)
7171                                .args(&args)
7172                                .stdin(fifo)
7173                                .stdout(Stdio::inherit())
7174                                .stderr(Stdio::inherit())
7175                                .status();
7176                        }
7177                        // Clean up FIFO after command completes
7178                        let _ = fs::remove_file(&fifo_clone);
7179                    });
7180                }
7181            }
7182        }
7183
7184        fifo_path
7185    }
7186
7187    fn run_command_substitution(&mut self, cmd_str: &str) -> String {
7188        use std::process::Stdio;
7189
7190        // Port of getoutput() from Src/exec.c:
7191        // C zsh forks, redirects stdout to a pipe, executes via execode(),
7192        // and the parent reads back the output.  We achieve the same by
7193        // capturing stdout through an in-process pipe.
7194
7195        let mut parser = ShellParser::new(cmd_str);
7196        let commands = match parser.parse_script() {
7197            Ok(cmds) => cmds,
7198            Err(_) => return String::new(),
7199        };
7200
7201        if commands.is_empty() {
7202            return String::new();
7203        }
7204
7205        // Check if this is a simple external-only command (no builtins/functions)
7206        // so we can use the fast path of spawning a child process.
7207        let is_internal = if let ShellCommand::Simple(simple) = &commands[0] {
7208            let first = simple.words.first().map(|w| self.expand_word(w));
7209            if let Some(ref name) = first {
7210                self.functions.contains_key(name) || self.is_builtin(name)
7211            } else {
7212                true
7213            }
7214        } else {
7215            true // compound commands are always internal
7216        };
7217
7218        if is_internal {
7219            // Internal execution: capture stdout via a pipe (no fork)
7220            let (read_fd, write_fd) = {
7221                let mut fds = [0i32; 2];
7222                if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
7223                    return String::new();
7224                }
7225                (fds[0], fds[1])
7226            };
7227
7228            // Save original stdout and redirect to our pipe
7229            let saved_stdout = unsafe { libc::dup(1) };
7230            unsafe {
7231                libc::dup2(write_fd, 1);
7232            }
7233            unsafe {
7234                libc::close(write_fd);
7235            }
7236
7237            // Execute all commands
7238            for cmd in &commands {
7239                let _ = self.execute_command(cmd);
7240            }
7241
7242            // Flush stdout so buffered output goes to pipe
7243            use std::io::Write;
7244            let _ = io::stdout().flush();
7245
7246            // Restore stdout
7247            unsafe {
7248                libc::dup2(saved_stdout, 1);
7249            }
7250            unsafe {
7251                libc::close(saved_stdout);
7252            }
7253
7254            // Read captured output
7255            use std::os::unix::io::FromRawFd;
7256            let mut output = String::new();
7257            let read_file = unsafe { std::fs::File::from_raw_fd(read_fd) };
7258            use std::io::Read;
7259            let _ = std::io::BufReader::new(read_file).read_to_string(&mut output);
7260
7261            output.trim_end_matches('\n').to_string()
7262        } else {
7263            // External command: spawn child and capture stdout
7264            if let ShellCommand::Simple(simple) = &commands[0] {
7265                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7266                if words.is_empty() {
7267                    return String::new();
7268                }
7269
7270                let output = Command::new(&words[0])
7271                    .args(&words[1..])
7272                    .stdout(Stdio::piped())
7273                    .stderr(Stdio::inherit())
7274                    .output();
7275
7276                match output {
7277                    Ok(out) => String::from_utf8_lossy(&out.stdout)
7278                        .trim_end_matches('\n')
7279                        .to_string(),
7280                    Err(_) => String::new(),
7281                }
7282            } else {
7283                String::new()
7284            }
7285        }
7286    }
7287
7288    /// Process substitution <(cmd) - returns FIFO path
7289    fn execute_process_sub_in(&mut self, cmd: &ShellCommand) -> String {
7290        if let ShellCommand::Simple(simple) = cmd {
7291            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7292            let cmd_str = words.join(" ");
7293            self.run_process_sub_in(&cmd_str)
7294        } else {
7295            String::new()
7296        }
7297    }
7298
7299    /// Process substitution >(cmd) - returns FIFO path
7300    fn execute_process_sub_out(&mut self, cmd: &ShellCommand) -> String {
7301        if let ShellCommand::Simple(simple) = cmd {
7302            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7303            let cmd_str = words.join(" ");
7304            self.run_process_sub_out(&cmd_str)
7305        } else {
7306            String::new()
7307        }
7308    }
7309
7310    /// Get value from zsh/parameter special arrays (options, commands, functions, etc.)
7311    /// Returns Some(value) if this is a special array access, None otherwise
7312    fn get_special_array_value(&self, array_name: &str, key: &str) -> Option<String> {
7313        match array_name {
7314            // === SHELL OPTIONS ===
7315            "options" => {
7316                if key == "@" || key == "*" {
7317                    // Return all options as "name=on/off" pairs
7318                    let opts: Vec<String> = self
7319                        .options
7320                        .iter()
7321                        .map(|(k, v)| format!("{}={}", k, if *v { "on" } else { "off" }))
7322                        .collect();
7323                    return Some(opts.join(" "));
7324                }
7325                let opt_name = key.to_lowercase().replace('_', "");
7326                let is_on = self.options.get(&opt_name).copied().unwrap_or(false);
7327                Some(if is_on {
7328                    "on".to_string()
7329                } else {
7330                    "off".to_string()
7331                })
7332            }
7333
7334            // === ALIASES ===
7335            "aliases" => {
7336                if key == "@" || key == "*" {
7337                    let vals: Vec<String> = self.aliases.values().cloned().collect();
7338                    return Some(vals.join(" "));
7339                }
7340                Some(self.aliases.get(key).cloned().unwrap_or_default())
7341            }
7342            "galiases" => {
7343                if key == "@" || key == "*" {
7344                    let vals: Vec<String> = self.global_aliases.values().cloned().collect();
7345                    return Some(vals.join(" "));
7346                }
7347                Some(self.global_aliases.get(key).cloned().unwrap_or_default())
7348            }
7349            "saliases" => {
7350                if key == "@" || key == "*" {
7351                    let vals: Vec<String> = self.suffix_aliases.values().cloned().collect();
7352                    return Some(vals.join(" "));
7353                }
7354                Some(self.suffix_aliases.get(key).cloned().unwrap_or_default())
7355            }
7356
7357            // === FUNCTIONS ===
7358            "functions" => {
7359                if key == "@" || key == "*" {
7360                    let names: Vec<String> = self.functions.keys().cloned().collect();
7361                    return Some(names.join(" "));
7362                }
7363                if let Some(body) = self.functions.get(key) {
7364                    Some(format!("{:?}", body))
7365                } else {
7366                    Some(String::new())
7367                }
7368            }
7369            "functions_source" => {
7370                // We don't track source locations, return empty
7371                Some(String::new())
7372            }
7373
7374            // === COMMANDS (command hash table) ===
7375            "commands" => {
7376                if key == "@" || key == "*" {
7377                    return Some(String::new()); // Would need to enumerate PATH
7378                }
7379                // Look up command in PATH
7380                if let Some(path) = self.find_in_path(key) {
7381                    Some(path)
7382                } else {
7383                    Some(String::new())
7384                }
7385            }
7386
7387            // === BUILTINS ===
7388            "builtins" => {
7389                let builtins = Self::get_builtin_names();
7390                if key == "@" || key == "*" {
7391                    return Some(builtins.join(" "));
7392                }
7393                if builtins.contains(&key) {
7394                    Some("defined".to_string())
7395                } else {
7396                    Some(String::new())
7397                }
7398            }
7399
7400            // === PARAMETERS ===
7401            "parameters" => {
7402                if key == "@" || key == "*" {
7403                    let mut names: Vec<String> = self.variables.keys().cloned().collect();
7404                    names.extend(self.arrays.keys().cloned());
7405                    names.extend(self.assoc_arrays.keys().cloned());
7406                    return Some(names.join(" "));
7407                }
7408                // Return type of parameter
7409                if self.assoc_arrays.contains_key(key) {
7410                    Some("association".to_string())
7411                } else if self.arrays.contains_key(key) {
7412                    Some("array".to_string())
7413                } else if self.variables.contains_key(key) || std::env::var(key).is_ok() {
7414                    Some("scalar".to_string())
7415                } else {
7416                    Some(String::new())
7417                }
7418            }
7419
7420            // === NAMED DIRECTORIES ===
7421            "nameddirs" => {
7422                if key == "@" || key == "*" {
7423                    let vals: Vec<String> = self
7424                        .named_dirs
7425                        .values()
7426                        .map(|p| p.display().to_string())
7427                        .collect();
7428                    return Some(vals.join(" "));
7429                }
7430                Some(
7431                    self.named_dirs
7432                        .get(key)
7433                        .map(|p| p.display().to_string())
7434                        .unwrap_or_default(),
7435                )
7436            }
7437
7438            // === USER DIRECTORIES ===
7439            "userdirs" => {
7440                if key == "@" || key == "*" {
7441                    return Some(String::new());
7442                }
7443                // Get home directory for user
7444                #[cfg(unix)]
7445                {
7446                    use std::ffi::CString;
7447                    if let Ok(name) = CString::new(key) {
7448                        unsafe {
7449                            let pwd = libc::getpwnam(name.as_ptr());
7450                            if !pwd.is_null() {
7451                                let dir = std::ffi::CStr::from_ptr((*pwd).pw_dir);
7452                                return Some(dir.to_string_lossy().to_string());
7453                            }
7454                        }
7455                    }
7456                }
7457                Some(String::new())
7458            }
7459
7460            // === USER GROUPS ===
7461            "usergroups" => {
7462                if key == "@" || key == "*" {
7463                    return Some(String::new());
7464                }
7465                // Get GID for group name
7466                #[cfg(unix)]
7467                {
7468                    use std::ffi::CString;
7469                    if let Ok(name) = CString::new(key) {
7470                        unsafe {
7471                            let grp = libc::getgrnam(name.as_ptr());
7472                            if !grp.is_null() {
7473                                return Some((*grp).gr_gid.to_string());
7474                            }
7475                        }
7476                    }
7477                }
7478                Some(String::new())
7479            }
7480
7481            // === DIRECTORY STACK ===
7482            "dirstack" => {
7483                if key == "@" || key == "*" {
7484                    let dirs: Vec<String> = self
7485                        .dir_stack
7486                        .iter()
7487                        .map(|p| p.display().to_string())
7488                        .collect();
7489                    return Some(dirs.join(" "));
7490                }
7491                if let Ok(idx) = key.parse::<usize>() {
7492                    Some(
7493                        self.dir_stack
7494                            .get(idx)
7495                            .map(|p| p.display().to_string())
7496                            .unwrap_or_default(),
7497                    )
7498                } else {
7499                    Some(String::new())
7500                }
7501            }
7502
7503            // === JOBS ===
7504            "jobstates" => {
7505                if key == "@" || key == "*" {
7506                    let states: Vec<String> = self
7507                        .jobs
7508                        .iter()
7509                        .map(|(id, job)| format!("{}:{:?}", id, job.state))
7510                        .collect();
7511                    return Some(states.join(" "));
7512                }
7513                if let Ok(id) = key.parse::<usize>() {
7514                    if let Some(job) = self.jobs.get(id) {
7515                        return Some(format!("{:?}", job.state));
7516                    }
7517                }
7518                Some(String::new())
7519            }
7520            "jobtexts" => {
7521                if key == "@" || key == "*" {
7522                    let texts: Vec<String> = self
7523                        .jobs
7524                        .iter()
7525                        .map(|(_, job)| job.command.clone())
7526                        .collect();
7527                    return Some(texts.join(" "));
7528                }
7529                if let Ok(id) = key.parse::<usize>() {
7530                    if let Some(job) = self.jobs.get(id) {
7531                        return Some(job.command.clone());
7532                    }
7533                }
7534                Some(String::new())
7535            }
7536            "jobdirs" => {
7537                // We don't track job directories separately - return current dir
7538                if key == "@" || key == "*" {
7539                    return Some(String::new());
7540                }
7541                Some(String::new())
7542            }
7543
7544            // === HISTORY ===
7545            "history" => {
7546                if key == "@" || key == "*" {
7547                    // Return recent history
7548                    if let Some(ref engine) = self.history {
7549                        if let Ok(entries) = engine.recent(100) {
7550                            let cmds: Vec<String> =
7551                                entries.iter().map(|e| e.command.clone()).collect();
7552                            return Some(cmds.join("\n"));
7553                        }
7554                    }
7555                    return Some(String::new());
7556                }
7557                if let Ok(num) = key.parse::<usize>() {
7558                    if let Some(ref engine) = self.history {
7559                        if let Ok(Some(entry)) = engine.get_by_offset(num.saturating_sub(1)) {
7560                            return Some(entry.command);
7561                        }
7562                    }
7563                }
7564                Some(String::new())
7565            }
7566            "historywords" => {
7567                // Array of words from history - simplified
7568                Some(String::new())
7569            }
7570
7571            // === MODULES ===
7572            "modules" => {
7573                // zshrs doesn't have loadable modules like zsh
7574                // Return empty or fake "loaded" for common modules
7575                if key == "@" || key == "*" {
7576                    return Some("zsh/parameter zsh/zutil".to_string());
7577                }
7578                match key {
7579                    "zsh/parameter" | "zsh/zutil" | "zsh/complete" | "zsh/complist" => {
7580                        Some("loaded".to_string())
7581                    }
7582                    _ => Some(String::new()),
7583                }
7584            }
7585
7586            // === RESERVED WORDS ===
7587            "reswords" => {
7588                let reswords = [
7589                    "do",
7590                    "done",
7591                    "esac",
7592                    "then",
7593                    "elif",
7594                    "else",
7595                    "fi",
7596                    "for",
7597                    "case",
7598                    "if",
7599                    "while",
7600                    "function",
7601                    "repeat",
7602                    "time",
7603                    "until",
7604                    "select",
7605                    "coproc",
7606                    "nocorrect",
7607                    "foreach",
7608                    "end",
7609                    "in",
7610                ];
7611                if key == "@" || key == "*" {
7612                    return Some(reswords.join(" "));
7613                }
7614                if let Ok(idx) = key.parse::<usize>() {
7615                    Some(reswords.get(idx).map(|s| s.to_string()).unwrap_or_default())
7616                } else {
7617                    Some(String::new())
7618                }
7619            }
7620
7621            // === PATCHARS (characters with special meaning in patterns) ===
7622            "patchars" => {
7623                let patchars = ["?", "*", "[", "]", "^", "#", "~", "(", ")", "|"];
7624                if key == "@" || key == "*" {
7625                    return Some(patchars.join(" "));
7626                }
7627                if let Ok(idx) = key.parse::<usize>() {
7628                    Some(patchars.get(idx).map(|s| s.to_string()).unwrap_or_default())
7629                } else {
7630                    Some(String::new())
7631                }
7632            }
7633
7634            // === FUNCTION CALL STACK ===
7635            "funcstack" | "functrace" | "funcfiletrace" | "funcsourcetrace" => {
7636                // Would need call stack tracking - return empty for now
7637                Some(String::new())
7638            }
7639
7640            // === DISABLED VARIANTS (dis_*) ===
7641            "dis_aliases"
7642            | "dis_galiases"
7643            | "dis_saliases"
7644            | "dis_functions"
7645            | "dis_functions_source"
7646            | "dis_builtins"
7647            | "dis_reswords"
7648            | "dis_patchars" => {
7649                // We don't track disabled items - return empty
7650                Some(String::new())
7651            }
7652
7653            // Not a special array
7654            _ => None,
7655        }
7656    }
7657
7658    /// Get list of all builtin command names
7659    fn get_builtin_names() -> Vec<&'static str> {
7660        vec![
7661            ".",
7662            ":",
7663            "[",
7664            "alias",
7665            "autoload",
7666            "bg",
7667            "bind",
7668            "bindkey",
7669            "break",
7670            "builtin",
7671            "bye",
7672            "caller",
7673            "cd",
7674            "cdreplay",
7675            "chdir",
7676            "clone",
7677            "command",
7678            "compadd",
7679            "comparguments",
7680            "compcall",
7681            "compctl",
7682            "compdef",
7683            "compdescribe",
7684            "compfiles",
7685            "compgen",
7686            "compgroups",
7687            "compinit",
7688            "complete",
7689            "compopt",
7690            "compquote",
7691            "compset",
7692            "comptags",
7693            "comptry",
7694            "compvalues",
7695            "continue",
7696            "coproc",
7697            "declare",
7698            "dirs",
7699            "disable",
7700            "disown",
7701            "echo",
7702            "echotc",
7703            "echoti",
7704            "emulate",
7705            "enable",
7706            "eval",
7707            "exec",
7708            "exit",
7709            "export",
7710            "false",
7711            "fc",
7712            "fg",
7713            "float",
7714            "functions",
7715            "getln",
7716            "getopts",
7717            "hash",
7718            "help",
7719            "history",
7720            "integer",
7721            "jobs",
7722            "kill",
7723            "let",
7724            "limit",
7725            "local",
7726            "log",
7727            "logout",
7728            "mapfile",
7729            "noglob",
7730            "popd",
7731            "print",
7732            "printf",
7733            "private",
7734            "prompt",
7735            "promptinit",
7736            "pushd",
7737            "pushln",
7738            "pwd",
7739            "r",
7740            "read",
7741            "readarray",
7742            "readonly",
7743            "rehash",
7744            "return",
7745            "sched",
7746            "set",
7747            "setopt",
7748            "shift",
7749            "shopt",
7750            "source",
7751            "stat",
7752            "strftime",
7753            "suspend",
7754            "test",
7755            "times",
7756            "trap",
7757            "true",
7758            "ttyctl",
7759            "type",
7760            "typeset",
7761            "ulimit",
7762            "umask",
7763            "unalias",
7764            "unfunction",
7765            "unhash",
7766            "unlimit",
7767            "unset",
7768            "unsetopt",
7769            "vared",
7770            "wait",
7771            "whence",
7772            "where",
7773            "which",
7774            "zcompile",
7775            "zcurses",
7776            "zformat",
7777            "zle",
7778            "zmodload",
7779            "zparseopts",
7780            "zprof",
7781            "zpty",
7782            "zregexparse",
7783            "zsocket",
7784            "zstyle",
7785            "ztcp",
7786            "add-zsh-hook",
7787        ]
7788    }
7789
7790    fn get_variable(&self, name: &str) -> String {
7791        // Handle special parameters
7792        match name {
7793            "" => String::new(), // Empty name returns empty
7794            "$" => std::process::id().to_string(),
7795            "@" | "*" => self.positional_params.join(" "),
7796            "#" => self.positional_params.len().to_string(),
7797            "?" => self.last_status.to_string(),
7798            "0" => self
7799                .variables
7800                .get("0")
7801                .cloned()
7802                .unwrap_or_else(|| env::args().next().unwrap_or_default()),
7803            n if !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) => {
7804                let idx: usize = n.parse().unwrap_or(0);
7805                if idx == 0 {
7806                    env::args().next().unwrap_or_default()
7807                } else {
7808                    self.positional_params
7809                        .get(idx - 1)
7810                        .cloned()
7811                        .unwrap_or_default()
7812                }
7813            }
7814            _ => {
7815                // Check local variables first, then arrays, then env
7816                self.variables
7817                    .get(name)
7818                    .cloned()
7819                    .or_else(|| {
7820                        // In zsh, $arr expands to space-joined array elements
7821                        self.arrays.get(name).map(|a| a.join(" "))
7822                    })
7823                    .or_else(|| env::var(name).ok())
7824                    .unwrap_or_default()
7825            }
7826        }
7827    }
7828
7829    fn apply_var_modifier(
7830        &mut self,
7831        name: &str,
7832        val: Option<String>,
7833        modifier: Option<&VarModifier>,
7834    ) -> String {
7835        match modifier {
7836            None => val.unwrap_or_default(),
7837
7838            // ${var:-word} - use default value
7839            Some(VarModifier::Default(word)) => match &val {
7840                Some(v) if !v.is_empty() => v.clone(),
7841                _ => self.expand_word(word),
7842            },
7843
7844            // ${var:=word} - assign default value
7845            Some(VarModifier::DefaultAssign(word)) => match &val {
7846                Some(v) if !v.is_empty() => v.clone(),
7847                _ => self.expand_word(word),
7848            },
7849
7850            // ${var:?word} - error if null or unset
7851            Some(VarModifier::Error(word)) => match &val {
7852                Some(v) if !v.is_empty() => v.clone(),
7853                _ => {
7854                    let msg = self.expand_word(word);
7855                    eprintln!("zshrs: {}", msg);
7856                    String::new()
7857                }
7858            },
7859
7860            // ${var:+word} - use alternate value
7861            Some(VarModifier::Alternate(word)) => match &val {
7862                Some(v) if !v.is_empty() => self.expand_word(word),
7863                _ => String::new(),
7864            },
7865
7866            // ${#var} - string length
7867            Some(VarModifier::Length) => val
7868                .map(|v| v.len().to_string())
7869                .unwrap_or_else(|| "0".to_string()),
7870
7871            // ${var:offset} or ${var:offset:length} - substring
7872            Some(VarModifier::Substring(offset, length)) => {
7873                let v = val.unwrap_or_default();
7874                let start = if *offset < 0 {
7875                    (v.len() as i64 + offset).max(0) as usize
7876                } else {
7877                    (*offset as usize).min(v.len())
7878                };
7879
7880                if let Some(len) = length {
7881                    let len = (*len as usize).min(v.len().saturating_sub(start));
7882                    v.chars().skip(start).take(len).collect()
7883                } else {
7884                    v.chars().skip(start).collect()
7885                }
7886            }
7887
7888            // ${var#pattern} - remove shortest prefix
7889            Some(VarModifier::RemovePrefix(pattern)) => {
7890                let v = val.unwrap_or_default();
7891                let pat = self.expand_word(pattern);
7892                if v.starts_with(&pat) {
7893                    v[pat.len()..].to_string()
7894                } else {
7895                    v
7896                }
7897            }
7898
7899            // ${var##pattern} - remove longest prefix
7900            Some(VarModifier::RemovePrefixLong(pattern)) => {
7901                let v = val.unwrap_or_default();
7902                let pat = self.expand_word(pattern);
7903                // For glob patterns, find longest match from start
7904                if let Ok(glob) = glob::Pattern::new(&pat) {
7905                    for i in (0..=v.len()).rev() {
7906                        if glob.matches(&v[..i]) {
7907                            return v[i..].to_string();
7908                        }
7909                    }
7910                }
7911                v
7912            }
7913
7914            // ${var%pattern} - remove shortest suffix
7915            Some(VarModifier::RemoveSuffix(pattern)) => {
7916                let v = val.unwrap_or_default();
7917                let pat = self.expand_word(pattern);
7918                // For glob patterns, find shortest match from end
7919                if let Ok(glob) = glob::Pattern::new(&pat) {
7920                    for i in (0..=v.len()).rev() {
7921                        if glob.matches(&v[i..]) {
7922                            return v[..i].to_string();
7923                        }
7924                    }
7925                } else if v.ends_with(&pat) {
7926                    return v[..v.len() - pat.len()].to_string();
7927                }
7928                v
7929            }
7930
7931            // ${var%%pattern} - remove longest suffix
7932            Some(VarModifier::RemoveSuffixLong(pattern)) => {
7933                let v = val.unwrap_or_default();
7934                let pat = self.expand_word(pattern);
7935                // For glob patterns, find longest match from end
7936                if let Ok(glob) = glob::Pattern::new(&pat) {
7937                    for i in 0..=v.len() {
7938                        if glob.matches(&v[i..]) {
7939                            return v[..i].to_string();
7940                        }
7941                    }
7942                }
7943                v
7944            }
7945
7946            // ${var/pattern/replacement} - replace first match
7947            Some(VarModifier::Replace(pattern, replacement)) => {
7948                let v = val.unwrap_or_default();
7949                let pat = self.expand_word(pattern);
7950                let repl = self.expand_word(replacement);
7951                v.replacen(&pat, &repl, 1)
7952            }
7953
7954            // ${var//pattern/replacement} - replace all matches
7955            Some(VarModifier::ReplaceAll(pattern, replacement)) => {
7956                let v = val.unwrap_or_default();
7957                let pat = self.expand_word(pattern);
7958                let repl = self.expand_word(replacement);
7959                v.replace(&pat, &repl)
7960            }
7961
7962            // ${var^} or ${var^^} - uppercase
7963            Some(VarModifier::Upper) => val.map(|v| v.to_uppercase()).unwrap_or_default(),
7964
7965            // ${var,} or ${var,,} - lowercase
7966            Some(VarModifier::Lower) => val.map(|v| v.to_lowercase()).unwrap_or_default(),
7967
7968            // ${(flags)var} - zsh parameter expansion flags
7969            Some(VarModifier::ZshFlags(flags)) => {
7970                let mut result = val.unwrap_or_default();
7971                for flag in flags {
7972                    result = self.apply_zsh_param_flag(&result, name, flag);
7973                }
7974                result
7975            }
7976
7977            // Array-related modifiers are handled elsewhere
7978            Some(VarModifier::ArrayLength)
7979            | Some(VarModifier::ArrayIndex(_))
7980            | Some(VarModifier::ArrayAll) => val.unwrap_or_default(),
7981        }
7982    }
7983
7984    /// Check if a string starts with history modifier characters
7985    fn is_history_modifier(&self, s: &str) -> bool {
7986        if s.is_empty() {
7987            return false;
7988        }
7989        let first = s.chars().next().unwrap();
7990        matches!(
7991            first,
7992            'A' | 'a' | 'h' | 't' | 'r' | 'e' | 'l' | 'u' | 'q' | 'Q' | 'P'
7993        )
7994    }
7995
7996    /// Apply zsh history-style modifiers to a value
7997    /// Modifiers can be chained: :A:h:h
7998    fn apply_history_modifiers(&self, val: &str, modifiers: &str) -> String {
7999        let mut result = val.to_string();
8000        let mut chars = modifiers.chars().peekable();
8001
8002        while let Some(c) = chars.next() {
8003            match c {
8004                ':' => continue,
8005                'A' => {
8006                    if let Ok(abs) = std::fs::canonicalize(&result) {
8007                        result = abs.to_string_lossy().to_string();
8008                    } else if !result.starts_with('/') {
8009                        if let Ok(cwd) = std::env::current_dir() {
8010                            result = cwd.join(&result).to_string_lossy().to_string();
8011                        }
8012                    }
8013                }
8014                'a' => {
8015                    if !result.starts_with('/') {
8016                        if let Ok(cwd) = std::env::current_dir() {
8017                            result = cwd.join(&result).to_string_lossy().to_string();
8018                        }
8019                    }
8020                }
8021                'h' => {
8022                    if let Some(pos) = result.rfind('/') {
8023                        if pos == 0 {
8024                            result = "/".to_string();
8025                        } else {
8026                            result = result[..pos].to_string();
8027                        }
8028                    } else {
8029                        result = ".".to_string();
8030                    }
8031                }
8032                't' => {
8033                    if let Some(pos) = result.rfind('/') {
8034                        result = result[pos + 1..].to_string();
8035                    }
8036                }
8037                'r' => {
8038                    if let Some(dot_pos) = result.rfind('.') {
8039                        let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
8040                        if dot_pos > slash_pos {
8041                            result = result[..dot_pos].to_string();
8042                        }
8043                    }
8044                }
8045                'e' => {
8046                    if let Some(dot_pos) = result.rfind('.') {
8047                        let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
8048                        if dot_pos > slash_pos {
8049                            result = result[dot_pos + 1..].to_string();
8050                        } else {
8051                            result = String::new();
8052                        }
8053                    } else {
8054                        result = String::new();
8055                    }
8056                }
8057                'l' => result = result.to_lowercase(),
8058                'u' => result = result.to_uppercase(),
8059                'q' => result = format!("'{}'", result.replace('\'', "'\\''")),
8060                'Q' => {
8061                    if result.starts_with('\'') && result.ends_with('\'') && result.len() >= 2 {
8062                        result = result[1..result.len() - 1].to_string();
8063                    } else if result.starts_with('"') && result.ends_with('"') && result.len() >= 2
8064                    {
8065                        result = result[1..result.len() - 1].to_string();
8066                    }
8067                }
8068                'P' => {
8069                    if let Ok(real) = std::fs::canonicalize(&result) {
8070                        result = real.to_string_lossy().to_string();
8071                    }
8072                }
8073                _ => break,
8074            }
8075        }
8076        result
8077    }
8078
8079    /// Parse zsh parameter expansion flags from a string like "L", "U", "j:,:"
8080    fn parse_zsh_flags(&self, s: &str) -> Vec<ZshParamFlag> {
8081        let mut flags = Vec::new();
8082        let mut chars = s.chars().peekable();
8083
8084        while let Some(c) = chars.next() {
8085            match c {
8086                'L' => flags.push(ZshParamFlag::Lower),
8087                'U' => flags.push(ZshParamFlag::Upper),
8088                'C' => flags.push(ZshParamFlag::Capitalize),
8089                'j' => {
8090                    // j<delim>sep<delim> — join with separator (delim can be any char)
8091                    if let Some(&delim) = chars.peek() {
8092                        chars.next(); // consume delimiter char
8093                        let mut sep = String::new();
8094                        while let Some(&ch) = chars.peek() {
8095                            if ch == delim {
8096                                chars.next();
8097                                break;
8098                            }
8099                            sep.push(chars.next().unwrap());
8100                        }
8101                        flags.push(ZshParamFlag::Join(sep));
8102                    }
8103                }
8104                'F' => flags.push(ZshParamFlag::JoinNewline),
8105                's' => {
8106                    // s:sep: - split on separator
8107                    if chars.peek() == Some(&':') {
8108                        chars.next();
8109                        let mut sep = String::new();
8110                        while let Some(&ch) = chars.peek() {
8111                            if ch == ':' {
8112                                chars.next();
8113                                break;
8114                            }
8115                            sep.push(chars.next().unwrap());
8116                        }
8117                        flags.push(ZshParamFlag::Split(sep));
8118                    }
8119                }
8120                'f' => flags.push(ZshParamFlag::SplitLines),
8121                'z' => flags.push(ZshParamFlag::SplitWords),
8122                't' => flags.push(ZshParamFlag::Type),
8123                'w' => flags.push(ZshParamFlag::Words),
8124                'b' => flags.push(ZshParamFlag::QuoteBackslash),
8125                'q' => {
8126                    if chars.peek() == Some(&'q') {
8127                        chars.next();
8128                        flags.push(ZshParamFlag::DoubleQuote);
8129                    } else {
8130                        flags.push(ZshParamFlag::Quote);
8131                    }
8132                }
8133                'u' => flags.push(ZshParamFlag::Unique),
8134                'O' => flags.push(ZshParamFlag::Reverse),
8135                'o' => flags.push(ZshParamFlag::Sort),
8136                'n' => flags.push(ZshParamFlag::NumericSort),
8137                'a' => flags.push(ZshParamFlag::IndexSort),
8138                'k' => flags.push(ZshParamFlag::Keys),
8139                'v' => flags.push(ZshParamFlag::Values),
8140                '#' => flags.push(ZshParamFlag::Length),
8141                'c' => flags.push(ZshParamFlag::CountChars),
8142                'e' => flags.push(ZshParamFlag::Expand),
8143                '%' => {
8144                    if chars.peek() == Some(&'%') {
8145                        chars.next();
8146                        flags.push(ZshParamFlag::PromptExpandFull);
8147                    } else {
8148                        flags.push(ZshParamFlag::PromptExpand);
8149                    }
8150                }
8151                'V' => flags.push(ZshParamFlag::Visible),
8152                'D' => flags.push(ZshParamFlag::Directory),
8153                'M' => flags.push(ZshParamFlag::Match),
8154                'R' => flags.push(ZshParamFlag::Remove),
8155                'S' => flags.push(ZshParamFlag::Subscript),
8156                'P' => flags.push(ZshParamFlag::Parameter),
8157                '~' => flags.push(ZshParamFlag::Glob),
8158                'l' => {
8159                    // l:len:fill: - pad left
8160                    if chars.peek() == Some(&':') {
8161                        chars.next();
8162                        let mut len_str = String::new();
8163                        while let Some(&ch) = chars.peek() {
8164                            if ch == ':' {
8165                                chars.next();
8166                                break;
8167                            }
8168                            len_str.push(chars.next().unwrap());
8169                        }
8170                        let mut fill = ' ';
8171                        if let Some(&ch) = chars.peek() {
8172                            if ch != ':' {
8173                                fill = chars.next().unwrap();
8174                                if chars.peek() == Some(&':') {
8175                                    chars.next();
8176                                }
8177                            }
8178                        }
8179                        if let Ok(len) = len_str.parse() {
8180                            flags.push(ZshParamFlag::PadLeft(len, fill));
8181                        }
8182                    }
8183                }
8184                'r' => {
8185                    // r:len:fill: - pad right
8186                    if chars.peek() == Some(&':') {
8187                        chars.next();
8188                        let mut len_str = String::new();
8189                        while let Some(&ch) = chars.peek() {
8190                            if ch == ':' {
8191                                chars.next();
8192                                break;
8193                            }
8194                            len_str.push(chars.next().unwrap());
8195                        }
8196                        let mut fill = ' ';
8197                        if let Some(&ch) = chars.peek() {
8198                            if ch != ':' {
8199                                fill = chars.next().unwrap();
8200                                if chars.peek() == Some(&':') {
8201                                    chars.next();
8202                                }
8203                            }
8204                        }
8205                        if let Ok(len) = len_str.parse() {
8206                            flags.push(ZshParamFlag::PadRight(len, fill));
8207                        }
8208                    }
8209                }
8210                'm' => {
8211                    // Width for padding - parse number if present
8212                    let mut width_str = String::new();
8213                    while let Some(&ch) = chars.peek() {
8214                        if ch.is_ascii_digit() {
8215                            width_str.push(chars.next().unwrap());
8216                        } else {
8217                            break;
8218                        }
8219                    }
8220                    if let Ok(w) = width_str.parse() {
8221                        flags.push(ZshParamFlag::Width(w));
8222                    }
8223                }
8224                _ => {}
8225            }
8226        }
8227        flags
8228    }
8229
8230    /// Apply a single zsh parameter expansion flag
8231    fn apply_zsh_param_flag(&self, val: &str, name: &str, flag: &ZshParamFlag) -> String {
8232        match flag {
8233            ZshParamFlag::Lower => val.to_lowercase(),
8234            ZshParamFlag::Upper => val.to_uppercase(),
8235            ZshParamFlag::Capitalize => val
8236                .split_whitespace()
8237                .map(|word| {
8238                    let mut c = word.chars();
8239                    match c.next() {
8240                        None => String::new(),
8241                        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
8242                    }
8243                })
8244                .collect::<Vec<_>>()
8245                .join(" "),
8246            ZshParamFlag::Join(sep) => {
8247                if let Some(arr) = self.arrays.get(name) {
8248                    arr.join(sep)
8249                } else {
8250                    val.to_string()
8251                }
8252            }
8253            ZshParamFlag::Split(sep) => val.split(sep).collect::<Vec<_>>().join(" "),
8254            ZshParamFlag::SplitLines => val.lines().collect::<Vec<_>>().join(" "),
8255            ZshParamFlag::Type => {
8256                if self.arrays.contains_key(name) {
8257                    "array".to_string()
8258                } else if self.assoc_arrays.contains_key(name) {
8259                    "association".to_string()
8260                } else if self.functions.contains_key(name) {
8261                    "function".to_string()
8262                } else if std::env::var(name).is_ok() || self.variables.contains_key(name) {
8263                    "scalar".to_string()
8264                } else {
8265                    "".to_string()
8266                }
8267            }
8268            ZshParamFlag::Words => val.split_whitespace().collect::<Vec<_>>().join(" "),
8269            ZshParamFlag::Quote => format!("'{}'", val.replace('\'', "'\\''")),
8270            ZshParamFlag::DoubleQuote => format!("\"{}\"", val.replace('"', "\\\"")),
8271            ZshParamFlag::Unique => {
8272                // Unique preserves first-occurrence order, so parallel doesn't help.
8273                // For 1000+ elements, pre-allocate the HashSet for less rehashing.
8274                let words: Vec<&str> = val.split_whitespace().collect();
8275                let mut seen = std::collections::HashSet::with_capacity(if words.len() >= 1000 {
8276                    words.len()
8277                } else {
8278                    0
8279                });
8280                if words.len() >= 1000 {
8281                    tracing::trace!(
8282                        count = words.len(),
8283                        "unique on large array ({} elements)",
8284                        words.len()
8285                    );
8286                }
8287                words
8288                    .into_iter()
8289                    .filter(|s| seen.insert(*s))
8290                    .collect::<Vec<_>>()
8291                    .join(" ")
8292            }
8293            ZshParamFlag::Reverse => {
8294                // (O) flag: reverse sort (sort descending)
8295                let mut words: Vec<&str> = val.split_whitespace().collect();
8296                if words.len() >= 1000 {
8297                    tracing::trace!(
8298                        count = words.len(),
8299                        "using parallel reverse sort (rayon) for large array"
8300                    );
8301                    use rayon::prelude::*;
8302                    words.par_sort_unstable_by(|a, b| b.cmp(a));
8303                } else {
8304                    words.sort_unstable_by(|a, b| b.cmp(a));
8305                }
8306                words.join(" ")
8307            }
8308            ZshParamFlag::Sort => {
8309                let mut words: Vec<&str> = val.split_whitespace().collect();
8310                if words.len() >= 1000 {
8311                    tracing::trace!(
8312                        count = words.len(),
8313                        "using parallel sort (rayon) for large array"
8314                    );
8315                    use rayon::prelude::*;
8316                    words.par_sort_unstable();
8317                } else {
8318                    words.sort_unstable();
8319                }
8320                words.join(" ")
8321            }
8322            ZshParamFlag::NumericSort => {
8323                let mut words: Vec<&str> = val.split_whitespace().collect();
8324                let cmp = |a: &&str, b: &&str| {
8325                    let na: i64 = a.parse().unwrap_or(0);
8326                    let nb: i64 = b.parse().unwrap_or(0);
8327                    na.cmp(&nb)
8328                };
8329                if words.len() >= 1000 {
8330                    tracing::trace!(
8331                        count = words.len(),
8332                        "using parallel numeric sort (rayon) for large array"
8333                    );
8334                    use rayon::prelude::*;
8335                    words.par_sort_unstable_by(cmp);
8336                } else {
8337                    words.sort_unstable_by(cmp);
8338                }
8339                words.join(" ")
8340            }
8341            ZshParamFlag::Keys => {
8342                if let Some(assoc) = self.assoc_arrays.get(name) {
8343                    assoc.keys().cloned().collect::<Vec<_>>().join(" ")
8344                } else {
8345                    String::new()
8346                }
8347            }
8348            ZshParamFlag::Values => {
8349                if let Some(assoc) = self.assoc_arrays.get(name) {
8350                    assoc.values().cloned().collect::<Vec<_>>().join(" ")
8351                } else {
8352                    val.to_string()
8353                }
8354            }
8355            ZshParamFlag::Length => val.len().to_string(),
8356            ZshParamFlag::Head(n) => val
8357                .split_whitespace()
8358                .take(*n)
8359                .collect::<Vec<_>>()
8360                .join(" "),
8361            ZshParamFlag::Tail(n) => {
8362                let words: Vec<&str> = val.split_whitespace().collect();
8363                if words.len() > *n {
8364                    words[words.len() - n..].join(" ")
8365                } else {
8366                    val.to_string()
8367                }
8368            }
8369            ZshParamFlag::JoinNewline => {
8370                if let Some(arr) = self.arrays.get(name) {
8371                    arr.join("\n")
8372                } else {
8373                    val.to_string()
8374                }
8375            }
8376            ZshParamFlag::SplitWords => {
8377                // Shell-style word splitting
8378                val.split_whitespace().collect::<Vec<_>>().join(" ")
8379            }
8380            ZshParamFlag::QuoteBackslash => {
8381                // Quote special pattern chars with backslashes
8382                let mut result = String::new();
8383                for c in val.chars() {
8384                    if "\\*?[]{}()".contains(c) {
8385                        result.push('\\');
8386                    }
8387                    result.push(c);
8388                }
8389                result
8390            }
8391            ZshParamFlag::IndexSort => {
8392                // Array index order - just return as-is (default)
8393                val.to_string()
8394            }
8395            ZshParamFlag::CountChars => {
8396                // Count total characters
8397                val.chars().count().to_string()
8398            }
8399            ZshParamFlag::Expand => {
8400                // Would need mutable self to do expansions
8401                val.to_string()
8402            }
8403            ZshParamFlag::PromptExpand => {
8404                // Expand prompt escapes
8405                self.expand_prompt_string(val)
8406            }
8407            ZshParamFlag::PromptExpandFull => {
8408                // Full prompt expansion
8409                self.expand_prompt_string(val)
8410            }
8411            ZshParamFlag::Visible => {
8412                // Make non-printable characters visible
8413                val.chars()
8414                    .map(|c| {
8415                        if c.is_control() {
8416                            format!("^{}", (c as u8 + 64) as char)
8417                        } else {
8418                            c.to_string()
8419                        }
8420                    })
8421                    .collect()
8422            }
8423            ZshParamFlag::Directory => {
8424                // Substitute leading directory with ~ if it's home
8425                if let Some(home) = dirs::home_dir() {
8426                    let home_str = home.to_string_lossy();
8427                    if val.starts_with(home_str.as_ref()) {
8428                        format!("~{}", &val[home_str.len()..])
8429                    } else {
8430                        val.to_string()
8431                    }
8432                } else {
8433                    val.to_string()
8434                }
8435            }
8436            ZshParamFlag::PadLeft(len, fill) => {
8437                if val.len() >= *len {
8438                    val.to_string()
8439                } else {
8440                    let padding: String = std::iter::repeat(*fill).take(len - val.len()).collect();
8441                    format!("{}{}", padding, val)
8442                }
8443            }
8444            ZshParamFlag::PadRight(len, fill) => {
8445                if val.len() >= *len {
8446                    val.to_string()
8447                } else {
8448                    let padding: String = std::iter::repeat(*fill).take(len - val.len()).collect();
8449                    format!("{}{}", val, padding)
8450                }
8451            }
8452            ZshParamFlag::Width(_) => {
8453                // Width modifier - used with padding, just return value
8454                val.to_string()
8455            }
8456            ZshParamFlag::Match => {
8457                // Match flag - used with pattern operations, just pass through
8458                // Actual matching is handled in the pattern operations below
8459                val.to_string()
8460            }
8461            ZshParamFlag::Remove => {
8462                // Remove flag - complement of Match
8463                val.to_string()
8464            }
8465            ZshParamFlag::Subscript => {
8466                // Subscript scanning
8467                val.to_string()
8468            }
8469            ZshParamFlag::Parameter => {
8470                // Parameter indirection - treat val as parameter name
8471                self.get_variable(val)
8472            }
8473            ZshParamFlag::Glob => {
8474                // Glob patterns in pattern matching
8475                val.to_string()
8476            }
8477        }
8478    }
8479
8480    /// Expand prompt escape sequences using the full prompt module
8481    fn expand_prompt_string(&self, s: &str) -> String {
8482        let ctx = self.build_prompt_context();
8483        expand_prompt(s, &ctx)
8484    }
8485
8486    /// Build a PromptContext from current executor state
8487    fn build_prompt_context(&self) -> PromptContext {
8488        let pwd = env::current_dir()
8489            .map(|p| p.to_string_lossy().to_string())
8490            .unwrap_or_else(|_| "/".to_string());
8491
8492        let home = env::var("HOME").unwrap_or_default();
8493
8494        let user = env::var("USER")
8495            .or_else(|_| env::var("LOGNAME"))
8496            .unwrap_or_else(|_| "user".to_string());
8497
8498        let host = hostname::get()
8499            .map(|h| h.to_string_lossy().to_string())
8500            .unwrap_or_else(|_| "localhost".to_string());
8501
8502        let host_short = host.split('.').next().unwrap_or(&host).to_string();
8503
8504        let shlvl = env::var("SHLVL")
8505            .ok()
8506            .and_then(|s| s.parse().ok())
8507            .unwrap_or(1);
8508
8509        PromptContext {
8510            pwd,
8511            home,
8512            user,
8513            host,
8514            host_short,
8515            tty: String::new(),
8516            lastval: self.last_status,
8517            histnum: self
8518                .history
8519                .as_ref()
8520                .and_then(|h| h.count().ok())
8521                .unwrap_or(1),
8522            shlvl,
8523            num_jobs: self.jobs.list().len() as i32,
8524            is_root: unsafe { libc::geteuid() } == 0,
8525            cmd_stack: Vec::new(),
8526            psvar: self.get_psvar(),
8527            term_width: self.get_term_width(),
8528            lineno: 1,
8529        }
8530    }
8531
8532    fn get_psvar(&self) -> Vec<String> {
8533        if let Some(arr) = self.arrays.get("psvar") {
8534            arr.clone()
8535        } else {
8536            Vec::new()
8537        }
8538    }
8539
8540    fn get_term_width(&self) -> usize {
8541        env::var("COLUMNS")
8542            .ok()
8543            .and_then(|s| s.parse().ok())
8544            .unwrap_or(80)
8545    }
8546
8547    /// Execute a command and capture its output (command substitution)
8548    fn execute_command_substitution(&mut self, cmd: &ShellCommand) -> String {
8549        match self.execute_command_capture(cmd) {
8550            Ok(output) => output.trim_end_matches('\n').to_string(),
8551            Err(_) => String::new(),
8552        }
8553    }
8554
8555    /// Execute a command and capture its stdout
8556    fn execute_command_capture(&mut self, cmd: &ShellCommand) -> Result<String, String> {
8557        // For simple commands, we can use Command directly
8558        if let ShellCommand::Simple(simple) = cmd {
8559            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
8560            if words.is_empty() {
8561                return Ok(String::new());
8562            }
8563
8564            let cmd_name = &words[0];
8565            let args = &words[1..];
8566
8567            // Handle some builtins that can return values
8568            match cmd_name.as_str() {
8569                "echo" => {
8570                    let output = args.join(" ");
8571                    return Ok(format!("{}\n", output));
8572                }
8573                "printf" => {
8574                    if !args.is_empty() {
8575                        // Simple printf - just format string with args
8576                        let format = &args[0];
8577                        let result = if args.len() > 1 {
8578                            // Very basic: just handle %s
8579                            let mut out = format.clone();
8580                            for (i, arg) in args[1..].iter().enumerate() {
8581                                out = out.replacen("%s", arg, 1);
8582                                out = out.replacen(&format!("${}", i + 1), arg, 1);
8583                            }
8584                            out
8585                        } else {
8586                            format.clone()
8587                        };
8588                        return Ok(result);
8589                    }
8590                    return Ok(String::new());
8591                }
8592                "pwd" => {
8593                    return Ok(env::current_dir()
8594                        .map(|p| format!("{}\n", p.display()))
8595                        .unwrap_or_default());
8596                }
8597                _ => {}
8598            }
8599
8600            // External command - capture its output
8601            let output = Command::new(cmd_name)
8602                .args(args)
8603                .stdout(Stdio::piped())
8604                .stderr(Stdio::inherit())
8605                .output();
8606
8607            match output {
8608                Ok(output) => {
8609                    self.last_status = output.status.code().unwrap_or(1);
8610                    Ok(String::from_utf8_lossy(&output.stdout).to_string())
8611                }
8612                Err(e) => {
8613                    self.last_status = 127;
8614                    Err(format!("{}: {}", cmd_name, e))
8615                }
8616            }
8617        } else if let ShellCommand::Pipeline(cmds, _negated) = cmd {
8618            // For pipelines, execute and capture output of the last command
8619            // This is simplified - proper implementation would pipe between all
8620            if let Some(last) = cmds.last() {
8621                return self.execute_command_capture(last);
8622            }
8623            Ok(String::new())
8624        } else {
8625            // For compound commands, execute them and return empty
8626            // (complex case - could be expanded later)
8627            let _ = self.execute_command(cmd);
8628            Ok(String::new())
8629        }
8630    }
8631
8632    /// Evaluate arithmetic expression using the full math module
8633    fn evaluate_arithmetic(&mut self, expr: &str) -> String {
8634        let expr = self.expand_string(expr);
8635        let force_float = self.options.get("forcefloat").copied().unwrap_or(false);
8636        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
8637        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
8638
8639        let mut evaluator = MathEval::new(&expr)
8640            .with_string_variables(&self.variables)
8641            .with_force_float(force_float)
8642            .with_c_precedences(c_prec)
8643            .with_octal_zeroes(octal);
8644
8645        match evaluator.evaluate() {
8646            Ok(result) => {
8647                for (k, v) in evaluator.extract_string_variables() {
8648                    self.variables.insert(k.clone(), v.clone());
8649                    env::set_var(&k, &v);
8650                }
8651                match result {
8652                    crate::math::MathNum::Integer(i) => i.to_string(),
8653                    crate::math::MathNum::Float(f) => {
8654                        if f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
8655                            (f as i64).to_string()
8656                        } else {
8657                            f.to_string()
8658                        }
8659                    }
8660                    crate::math::MathNum::Unset => "0".to_string(),
8661                }
8662            }
8663            Err(_) => "0".to_string(),
8664        }
8665    }
8666
8667    fn eval_arith_expr(&mut self, expr: &str) -> i64 {
8668        let expr_expanded = self.expand_string(expr);
8669        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
8670        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
8671
8672        let mut evaluator = MathEval::new(&expr_expanded)
8673            .with_string_variables(&self.variables)
8674            .with_c_precedences(c_prec)
8675            .with_octal_zeroes(octal);
8676
8677        match evaluator.evaluate() {
8678            Ok(result) => {
8679                for (k, v) in evaluator.extract_string_variables() {
8680                    self.variables.insert(k.clone(), v.clone());
8681                    env::set_var(&k, &v);
8682                }
8683                result.to_int()
8684            }
8685            Err(_) => 0,
8686        }
8687    }
8688
8689    fn eval_arith_expr_float(&mut self, expr: &str) -> f64 {
8690        let expr_expanded = self.expand_string(expr);
8691        let force_float = self.options.get("forcefloat").copied().unwrap_or(false);
8692        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
8693        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
8694
8695        let mut evaluator = MathEval::new(&expr_expanded)
8696            .with_string_variables(&self.variables)
8697            .with_force_float(force_float)
8698            .with_c_precedences(c_prec)
8699            .with_octal_zeroes(octal);
8700
8701        match evaluator.evaluate() {
8702            Ok(result) => {
8703                for (k, v) in evaluator.extract_string_variables() {
8704                    self.variables.insert(k.clone(), v.clone());
8705                    env::set_var(&k, &v);
8706                }
8707                result.to_float()
8708            }
8709            Err(_) => 0.0,
8710        }
8711    }
8712
8713    fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
8714        // Simple glob matching
8715        if pattern == "*" {
8716            return true;
8717        }
8718        if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
8719            // Use glob matching for wildcards and character classes
8720            glob::Pattern::new(pattern)
8721                .map(|p| p.matches(value))
8722                .unwrap_or(false)
8723        } else {
8724            value == pattern
8725        }
8726    }
8727
8728    fn eval_cond_expr(&mut self, expr: &CondExpr) -> bool {
8729        match expr {
8730            CondExpr::FileExists(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8731            CondExpr::FileRegular(w) => std::path::Path::new(&self.expand_word(w)).is_file(),
8732            CondExpr::FileDirectory(w) => std::path::Path::new(&self.expand_word(w)).is_dir(),
8733            CondExpr::FileSymlink(w) => std::path::Path::new(&self.expand_word(w)).is_symlink(),
8734            CondExpr::FileReadable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8735            CondExpr::FileWritable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8736            CondExpr::FileExecutable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
8737            CondExpr::FileNonEmpty(w) => std::fs::metadata(&self.expand_word(w))
8738                .map(|m| m.len() > 0)
8739                .unwrap_or(false),
8740            CondExpr::StringEmpty(w) => self.expand_word(w).is_empty(),
8741            CondExpr::StringNonEmpty(w) => !self.expand_word(w).is_empty(),
8742            CondExpr::StringEqual(a, b) => {
8743                let left = self.expand_word(a);
8744                let right = self.expand_word(b);
8745                // In [[ ]], == does glob pattern matching on the right side
8746                if right.contains('*') || right.contains('?') || right.contains('[') {
8747                    crate::glob::pattern_match(&right, &left, true, true)
8748                } else {
8749                    left == right
8750                }
8751            }
8752            CondExpr::StringNotEqual(a, b) => {
8753                let left = self.expand_word(a);
8754                let right = self.expand_word(b);
8755                if right.contains('*') || right.contains('?') || right.contains('[') {
8756                    !crate::glob::pattern_match(&right, &left, true, true)
8757                } else {
8758                    left != right
8759                }
8760            }
8761            CondExpr::StringMatch(a, b) => {
8762                let val = self.expand_word(a);
8763                let pattern = self.expand_word(b);
8764                if let Some(re) = cached_regex(&pattern) {
8765                    if let Some(caps) = re.captures(&val) {
8766                        // Set $MATCH to the full match
8767                        if let Some(m) = caps.get(0) {
8768                            self.variables
8769                                .insert("MATCH".to_string(), m.as_str().to_string());
8770                        }
8771                        // Set $match array with capture groups
8772                        let mut match_arr = Vec::new();
8773                        for i in 1..caps.len() {
8774                            if let Some(g) = caps.get(i) {
8775                                match_arr.push(g.as_str().to_string());
8776                            }
8777                        }
8778                        if !match_arr.is_empty() {
8779                            self.arrays.insert("match".to_string(), match_arr);
8780                        }
8781                        true
8782                    } else {
8783                        self.variables.remove("MATCH");
8784                        self.arrays.remove("match");
8785                        false
8786                    }
8787                } else {
8788                    false
8789                }
8790            }
8791            CondExpr::StringLess(a, b) => self.expand_word(a) < self.expand_word(b),
8792            CondExpr::StringGreater(a, b) => self.expand_word(a) > self.expand_word(b),
8793            CondExpr::NumEqual(a, b) => {
8794                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8795                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8796                a_val == b_val
8797            }
8798            CondExpr::NumNotEqual(a, b) => {
8799                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8800                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8801                a_val != b_val
8802            }
8803            CondExpr::NumLess(a, b) => {
8804                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8805                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8806                a_val < b_val
8807            }
8808            CondExpr::NumLessEqual(a, b) => {
8809                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8810                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8811                a_val <= b_val
8812            }
8813            CondExpr::NumGreater(a, b) => {
8814                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8815                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8816                a_val > b_val
8817            }
8818            CondExpr::NumGreaterEqual(a, b) => {
8819                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
8820                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
8821                a_val >= b_val
8822            }
8823            CondExpr::Not(inner) => !self.eval_cond_expr(inner),
8824            CondExpr::And(a, b) => self.eval_cond_expr(a) && self.eval_cond_expr(b),
8825            CondExpr::Or(a, b) => self.eval_cond_expr(a) || self.eval_cond_expr(b),
8826        }
8827    }
8828
8829    // Builtins
8830    // Ported from zsh/Src/builtin.c
8831
8832    /// cd builtin - change directory
8833    /// Ported from zsh/Src/builtin.c bin_cd() lines 839-859, cd_get_dest() lines 864-957,
8834    /// cd_do_chdir() lines 967-1081, cd_try_chdir() lines 1116-1181
8835    fn builtin_cd(&mut self, args: &[String]) -> i32 {
8836        // cd [ -qsLP ] [ arg ]
8837        // cd [ -qsLP ] old new
8838        // cd [ -qsLP ] {+|-}n
8839        let mut quiet = false;
8840        let mut use_cdpath = false;
8841        let mut logical = true; // -L is default
8842        let mut positional_args: Vec<&str> = Vec::new();
8843
8844        for arg in args {
8845            if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
8846                // Check if it's a stack index like -2
8847                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
8848                    positional_args.push(arg);
8849                    continue;
8850                }
8851                for ch in arg[1..].chars() {
8852                    match ch {
8853                        'q' => quiet = true,
8854                        's' => use_cdpath = true,
8855                        'L' => logical = true,
8856                        'P' => logical = false,
8857                        _ => {
8858                            eprintln!("cd: bad option: -{}", ch);
8859                            return 1;
8860                        }
8861                    }
8862                }
8863            } else if arg.starts_with('+')
8864                && arg.len() > 1
8865                && arg[1..].chars().all(|c| c.is_ascii_digit())
8866            {
8867                // Stack index like +2
8868                positional_args.push(arg);
8869            } else {
8870                positional_args.push(arg);
8871            }
8872        }
8873
8874        // Handle cd old new (substitution)
8875        if positional_args.len() == 2 {
8876            if let Ok(cwd) = env::current_dir() {
8877                let cwd_str = cwd.to_string_lossy();
8878                let old = positional_args[0];
8879                let new = positional_args[1];
8880                if cwd_str.contains(old) {
8881                    let new_path = cwd_str.replace(old, new);
8882                    if !quiet {
8883                        println!("{}", new_path);
8884                    }
8885                    positional_args = vec![];
8886                    return self.do_cd(&new_path, quiet, use_cdpath, logical);
8887                }
8888            }
8889        }
8890
8891        let path_arg = positional_args.first().map(|s| *s).unwrap_or("~");
8892
8893        // Handle stack indices
8894        if path_arg.starts_with('+') || path_arg.starts_with('-') {
8895            if let Ok(n) = path_arg[1..].parse::<usize>() {
8896                let idx = if path_arg.starts_with('+') {
8897                    n
8898                } else {
8899                    self.dir_stack.len().saturating_sub(n)
8900                };
8901                if let Some(dir) = self.dir_stack.get(idx) {
8902                    let dir_path = dir.to_string_lossy().to_string();
8903                    return self.do_cd(&dir_path, quiet, use_cdpath, logical);
8904                } else {
8905                    eprintln!("cd: no such entry in dir stack");
8906                    return 1;
8907                }
8908            }
8909        }
8910
8911        self.do_cd(path_arg, quiet, use_cdpath, logical)
8912    }
8913
8914    fn do_cd(&mut self, path_arg: &str, quiet: bool, use_cdpath: bool, physical: bool) -> i32 {
8915        let path = if path_arg == "~" || path_arg.is_empty() {
8916            dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
8917        } else if path_arg.starts_with("~/") {
8918            dirs::home_dir()
8919                .unwrap_or_else(|| PathBuf::from("."))
8920                .join(&path_arg[2..])
8921        } else if path_arg == "-" {
8922            if let Ok(oldpwd) = env::var("OLDPWD") {
8923                if !quiet {
8924                    println!("{}", oldpwd);
8925                }
8926                PathBuf::from(oldpwd)
8927            } else {
8928                eprintln!("cd: OLDPWD not set");
8929                return 1;
8930            }
8931        } else if use_cdpath && !path_arg.starts_with('/') && !path_arg.starts_with('.') {
8932            // Search CDPATH
8933            let cdpath = env::var("CDPATH").unwrap_or_default();
8934            let mut found = None;
8935            for dir in cdpath.split(':') {
8936                let candidate = if dir.is_empty() {
8937                    PathBuf::from(path_arg)
8938                } else {
8939                    PathBuf::from(dir).join(path_arg)
8940                };
8941                if candidate.is_dir() {
8942                    found = Some(candidate);
8943                    break;
8944                }
8945            }
8946            found.unwrap_or_else(|| PathBuf::from(path_arg))
8947        } else {
8948            PathBuf::from(path_arg)
8949        };
8950
8951        if let Ok(cwd) = env::current_dir() {
8952            env::set_var("OLDPWD", &cwd);
8953        }
8954
8955        // Resolve symlinks if -P (physical)
8956        let target = if !physical {
8957            if let Ok(resolved) = path.canonicalize() {
8958                resolved
8959            } else {
8960                path.clone()
8961            }
8962        } else {
8963            path.clone()
8964        };
8965
8966        match env::set_current_dir(&target) {
8967            Ok(_) => {
8968                if let Ok(cwd) = env::current_dir() {
8969                    env::set_var("PWD", &cwd);
8970                    self.variables
8971                        .insert("PWD".to_string(), cwd.to_string_lossy().to_string());
8972                }
8973                0
8974            }
8975            Err(e) => {
8976                eprintln!("cd: {}: {}", path.display(), e);
8977                1
8978            }
8979        }
8980    }
8981
8982    fn builtin_pwd(&mut self, _redirects: &[Redirect]) -> i32 {
8983        match env::current_dir() {
8984            Ok(path) => {
8985                println!("{}", path.display());
8986                0
8987            }
8988            Err(e) => {
8989                eprintln!("pwd: {}", e);
8990                1
8991            }
8992        }
8993    }
8994
8995    fn builtin_echo(&mut self, args: &[String], _redirects: &[Redirect]) -> i32 {
8996        let mut newline = true;
8997        let mut interpret_escapes = false;
8998        let mut start = 0;
8999
9000        for (i, arg) in args.iter().enumerate() {
9001            match arg.as_str() {
9002                "-n" => {
9003                    newline = false;
9004                    start = i + 1;
9005                }
9006                "-e" => {
9007                    interpret_escapes = true;
9008                    start = i + 1;
9009                }
9010                "-E" => {
9011                    interpret_escapes = false;
9012                    start = i + 1;
9013                }
9014                _ => break,
9015            }
9016        }
9017
9018        let output = args[start..].join(" ");
9019        if interpret_escapes {
9020            print!("{}", output.replace("\\n", "\n").replace("\\t", "\t"));
9021        } else {
9022            print!("{}", output);
9023        }
9024
9025        if newline {
9026            println!();
9027        }
9028        0
9029    }
9030
9031    fn builtin_export(&mut self, args: &[String]) -> i32 {
9032        for arg in args {
9033            if let Some((key, value)) = arg.split_once('=') {
9034                self.variables.insert(key.to_string(), value.to_string());
9035                env::set_var(key, value);
9036            } else {
9037                // export VAR (no value) — mark existing var as exported
9038                let val = self.get_variable(arg);
9039                env::set_var(arg, &val);
9040            }
9041        }
9042        0
9043    }
9044
9045    fn builtin_unset(&mut self, args: &[String]) -> i32 {
9046        for arg in args {
9047            env::remove_var(arg);
9048            self.variables.remove(arg);
9049        }
9050        0
9051    }
9052
9053    fn builtin_source(&mut self, args: &[String]) -> i32 {
9054        if args.is_empty() {
9055            eprintln!("source: filename argument required");
9056            return 1;
9057        }
9058
9059        let path = &args[0];
9060
9061        // Resolve to absolute path
9062        let abs_path = if path.starts_with('/') {
9063            path.clone()
9064        } else if path.starts_with("~/") {
9065            if let Some(home) = dirs::home_dir() {
9066                home.join(&path[2..]).to_string_lossy().to_string()
9067            } else {
9068                path.clone()
9069            }
9070        } else {
9071            std::env::current_dir()
9072                .map(|cwd| cwd.join(path).to_string_lossy().to_string())
9073                .unwrap_or_else(|_| path.clone())
9074        };
9075
9076        // Save current $0 and set to the sourced file path
9077        let saved_zero = self.variables.get("0").cloned();
9078        self.variables.insert("0".to_string(), abs_path.clone());
9079
9080        let result;
9081
9082        if self.posix_mode {
9083            // --- POSIX mode: plain read + execute, no SQLite, no caching, no threads ---
9084            result = match std::fs::read_to_string(&abs_path) {
9085                Ok(content) => match self.execute_script(&content) {
9086                    Ok(status) => status,
9087                    Err(e) => {
9088                        eprintln!("source: {}: {}", path, e);
9089                        1
9090                    }
9091                },
9092                Err(e) => {
9093                    eprintln!("source: {}: {}", path, e);
9094                    1
9095                }
9096            };
9097        } else {
9098            // --- zshrs/zsh mode: plugin cache + AST cache + worker pool ---
9099            let file_path = std::path::Path::new(&abs_path);
9100
9101            // Check plugin cache for side-effect replay
9102            if let Some(ref cache) = self.plugin_cache {
9103                if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(file_path) {
9104                    if let Some(plugin_id) = cache.check(&abs_path, mt_s, mt_ns) {
9105                        if let Ok(delta) = cache.load(plugin_id) {
9106                            let t0 = std::time::Instant::now();
9107                            self.replay_plugin_delta(&delta);
9108                            tracing::info!(
9109                                path = %abs_path,
9110                                replay_us = t0.elapsed().as_micros() as u64,
9111                                funcs = delta.functions.len(),
9112                                aliases = delta.aliases.len(),
9113                                vars = delta.variables.len() + delta.exports.len(),
9114                                "source: cache hit, replayed"
9115                            );
9116                            // Restore $0
9117                            if let Some(z) = saved_zero {
9118                                self.variables.insert("0".to_string(), z);
9119                            } else {
9120                                self.variables.remove("0");
9121                            }
9122                            return 0;
9123                        }
9124                    }
9125                }
9126            }
9127
9128            // Cache miss — snapshot, execute via AST-cached path, diff, async store
9129            let snapshot = self.snapshot_state();
9130            let t0 = std::time::Instant::now();
9131            tracing::debug!(path = %abs_path, "source: cache miss, executing via AST-cached path");
9132            result = match self.execute_script_file(&abs_path) {
9133                Ok(status) => status,
9134                Err(e) => {
9135                    tracing::warn!(path = %abs_path, error = %e, "source: execution failed");
9136                    eprintln!("source: {}: {}", path, e);
9137                    1
9138                }
9139            };
9140            let source_ms = t0.elapsed().as_millis() as u64;
9141
9142            // Async-store delta to plugin cache on worker pool
9143            if result == 0 {
9144                if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(file_path) {
9145                    let delta = self.diff_state(&snapshot);
9146                    let store_path = abs_path.clone();
9147                    tracing::info!(
9148                        path = %abs_path, source_ms,
9149                        funcs = delta.functions.len(),
9150                        aliases = delta.aliases.len(),
9151                        vars = delta.variables.len() + delta.exports.len(),
9152                        "source: caching delta on worker"
9153                    );
9154                    let cache_db_path = crate::plugin_cache::default_cache_path();
9155                    self.worker_pool.submit(move || {
9156                        match crate::plugin_cache::PluginCache::open(&cache_db_path) {
9157                            Ok(cache) => {
9158                                if let Err(e) = cache.store(&store_path, mt_s, mt_ns, source_ms, &delta) {
9159                                    tracing::error!(path = %store_path, error = %e, "plugin_cache: store failed");
9160                                } else {
9161                                    tracing::debug!(path = %store_path, "plugin_cache: stored");
9162                                }
9163                            }
9164                            Err(e) => tracing::error!(error = %e, "plugin_cache: open for write failed"),
9165                        }
9166                    });
9167                }
9168            }
9169        }
9170
9171        // Handle return from sourced script
9172        let final_result = if let Some(ret) = self.returning.take() {
9173            ret
9174        } else {
9175            result
9176        };
9177
9178        // Restore $0
9179        if let Some(z) = saved_zero {
9180            self.variables.insert("0".to_string(), z);
9181        } else {
9182            self.variables.remove("0");
9183        }
9184
9185        final_result
9186    }
9187
9188    /// Snapshot executor state before sourcing a plugin (for delta computation).
9189    fn snapshot_state(&self) -> PluginSnapshot {
9190        PluginSnapshot {
9191            functions: self.functions.keys().cloned().collect(),
9192            aliases: self.aliases.keys().cloned().collect(),
9193            global_aliases: self.global_aliases.keys().cloned().collect(),
9194            suffix_aliases: self.suffix_aliases.keys().cloned().collect(),
9195            variables: self.variables.clone(),
9196            arrays: self.arrays.keys().cloned().collect(),
9197            assoc_arrays: self.assoc_arrays.keys().cloned().collect(),
9198            fpath: self.fpath.clone(),
9199            options: self.options.clone(),
9200            hooks: self.hook_functions.clone(),
9201            autoloads: self.autoload_pending.keys().cloned().collect(),
9202        }
9203    }
9204
9205    /// Compute the delta between current state and a previous snapshot.
9206    fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
9207        use crate::plugin_cache::{AliasKind, PluginDelta};
9208        let mut delta = PluginDelta::default();
9209
9210        // New functions — serialize AST to bincode for instant replay
9211        for (name, body) in &self.functions {
9212            if !snap.functions.contains(name) {
9213                if let Ok(bytes) = bincode::serialize(body) {
9214                    delta.functions.push((name.clone(), bytes));
9215                }
9216            }
9217        }
9218
9219        // New aliases
9220        for (name, value) in &self.aliases {
9221            if !snap.aliases.contains(name) {
9222                delta
9223                    .aliases
9224                    .push((name.clone(), value.clone(), AliasKind::Regular));
9225            }
9226        }
9227        for (name, value) in &self.global_aliases {
9228            if !snap.global_aliases.contains(name) {
9229                delta
9230                    .aliases
9231                    .push((name.clone(), value.clone(), AliasKind::Global));
9232            }
9233        }
9234        for (name, value) in &self.suffix_aliases {
9235            if !snap.suffix_aliases.contains(name) {
9236                delta
9237                    .aliases
9238                    .push((name.clone(), value.clone(), AliasKind::Suffix));
9239            }
9240        }
9241
9242        // New/changed variables
9243        for (name, value) in &self.variables {
9244            if name == "0" {
9245                continue;
9246            } // skip $0 (we set it ourselves)
9247            match snap.variables.get(name) {
9248                Some(old) if old == value => {} // unchanged
9249                _ => {
9250                    // Check if it's also exported
9251                    if env::var(name).ok().as_ref() == Some(value) {
9252                        delta.exports.push((name.clone(), value.clone()));
9253                    } else {
9254                        delta.variables.push((name.clone(), value.clone()));
9255                    }
9256                }
9257            }
9258        }
9259
9260        // New arrays
9261        for (name, values) in &self.arrays {
9262            if !snap.arrays.contains(name) {
9263                delta.arrays.push((name.clone(), values.clone()));
9264            }
9265        }
9266
9267        // New fpath entries
9268        for p in &self.fpath {
9269            if !snap.fpath.contains(p) {
9270                delta.fpath_additions.push(p.to_string_lossy().to_string());
9271            }
9272        }
9273
9274        // Changed options
9275        for (name, value) in &self.options {
9276            match snap.options.get(name) {
9277                Some(old) if old == value => {}
9278                _ => delta.options_changed.push((name.clone(), *value)),
9279            }
9280        }
9281
9282        // New hooks
9283        for (hook, funcs) in &self.hook_functions {
9284            let old_funcs = snap.hooks.get(hook);
9285            for f in funcs {
9286                let is_new = old_funcs.map_or(true, |old| !old.contains(f));
9287                if is_new {
9288                    delta.hooks.push((hook.clone(), f.clone()));
9289                }
9290            }
9291        }
9292
9293        // New autoloads
9294        for (name, flags) in &self.autoload_pending {
9295            if !snap.autoloads.contains(name) {
9296                delta.autoloads.push((name.clone(), format!("{:?}", flags)));
9297            }
9298        }
9299
9300        delta
9301    }
9302
9303    /// Replay a cached plugin delta into the executor state.
9304    fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
9305        use crate::plugin_cache::AliasKind;
9306
9307        // Aliases
9308        for (name, value, kind) in &delta.aliases {
9309            match kind {
9310                AliasKind::Regular => {
9311                    self.aliases.insert(name.clone(), value.clone());
9312                }
9313                AliasKind::Global => {
9314                    self.global_aliases.insert(name.clone(), value.clone());
9315                }
9316                AliasKind::Suffix => {
9317                    self.suffix_aliases.insert(name.clone(), value.clone());
9318                }
9319            }
9320        }
9321
9322        // Variables
9323        for (name, value) in &delta.variables {
9324            self.variables.insert(name.clone(), value.clone());
9325        }
9326
9327        // Exports (set in both variables and process env)
9328        for (name, value) in &delta.exports {
9329            self.variables.insert(name.clone(), value.clone());
9330            env::set_var(name, value);
9331        }
9332
9333        // Arrays
9334        for (name, values) in &delta.arrays {
9335            self.arrays.insert(name.clone(), values.clone());
9336        }
9337
9338        // Fpath additions
9339        for p in &delta.fpath_additions {
9340            let pb = PathBuf::from(p);
9341            if !self.fpath.contains(&pb) {
9342                self.fpath.push(pb);
9343            }
9344        }
9345
9346        // Completions
9347        for (cmd, func) in &delta.completions {
9348            if let Some(ref mut comps) = self.assoc_arrays.get_mut("_comps") {
9349                comps.insert(cmd.clone(), func.clone());
9350            }
9351        }
9352
9353        // Options
9354        for (name, enabled) in &delta.options_changed {
9355            self.options.insert(name.clone(), *enabled);
9356        }
9357
9358        // Hooks
9359        for (hook, func) in &delta.hooks {
9360            self.hook_functions
9361                .entry(hook.clone())
9362                .or_insert_with(Vec::new)
9363                .push(func.clone());
9364        }
9365
9366        // Functions — deserialize bincode bytecode blobs directly into self.functions
9367        for (name, bytes) in &delta.functions {
9368            if let Ok(ast) = bincode::deserialize::<crate::parser::ShellCommand>(bytes) {
9369                self.functions.insert(name.clone(), ast);
9370            }
9371        }
9372    }
9373
9374    fn builtin_exit(&mut self, args: &[String]) -> i32 {
9375        let code = args
9376            .first()
9377            .and_then(|s| s.parse::<i32>().ok())
9378            .unwrap_or(self.last_status);
9379        std::process::exit(code);
9380    }
9381
9382    fn builtin_return(&mut self, args: &[String]) -> i32 {
9383        let status = args
9384            .first()
9385            .and_then(|s| s.parse::<i32>().ok())
9386            .unwrap_or(self.last_status);
9387        self.returning = Some(status);
9388        status
9389    }
9390
9391    fn builtin_test(&mut self, args: &[String]) -> i32 {
9392        if args.is_empty() {
9393            return 1;
9394        }
9395
9396        // Strip trailing "]" when called as `[`
9397        let args: Vec<&str> = args
9398            .iter()
9399            .map(|s| s.as_str())
9400            .filter(|&s| s != "]")
9401            .collect();
9402
9403        // Prefetch metadata for all file paths in the expression — one stat() per unique path
9404        // instead of one stat() per test flag. Avoids 7 serial stat()s for -r -w -x -g -k -u -s.
9405        let mut meta_cache: HashMap<String, Option<std::fs::Metadata>> = HashMap::new();
9406        for arg in &args {
9407            if !arg.starts_with('-') && !arg.starts_with('!') && *arg != "(" && *arg != ")" {
9408                let path_str = arg.to_string();
9409                if !meta_cache.contains_key(&path_str) {
9410                    meta_cache.insert(path_str, std::fs::metadata(arg).ok());
9411                }
9412            }
9413        }
9414
9415        // Helper closure: get metadata from cache or fetch
9416        let get_meta = |path: &str| -> Option<std::fs::Metadata> {
9417            meta_cache
9418                .get(path)
9419                .cloned()
9420                .unwrap_or_else(|| std::fs::metadata(path).ok())
9421        };
9422
9423        match args.as_slice() {
9424            // String tests
9425            ["-z", s] => {
9426                if s.is_empty() {
9427                    0
9428                } else {
9429                    1
9430                }
9431            }
9432            ["-n", s] => {
9433                if !s.is_empty() {
9434                    0
9435                } else {
9436                    1
9437                }
9438            }
9439
9440            // File existence/type tests
9441            ["-a", path] | ["-e", path] => {
9442                if std::path::Path::new(path).exists() {
9443                    0
9444                } else {
9445                    1
9446                }
9447            }
9448            ["-f", path] => {
9449                if std::path::Path::new(path).is_file() {
9450                    0
9451                } else {
9452                    1
9453                }
9454            }
9455            ["-d", path] => {
9456                if std::path::Path::new(path).is_dir() {
9457                    0
9458                } else {
9459                    1
9460                }
9461            }
9462            ["-b", path] => {
9463                use std::os::unix::fs::FileTypeExt;
9464                if std::fs::symlink_metadata(path)
9465                    .map(|m| m.file_type().is_block_device())
9466                    .unwrap_or(false)
9467                {
9468                    0
9469                } else {
9470                    1
9471                }
9472            }
9473            ["-c", path] => {
9474                use std::os::unix::fs::FileTypeExt;
9475                if std::fs::symlink_metadata(path)
9476                    .map(|m| m.file_type().is_char_device())
9477                    .unwrap_or(false)
9478                {
9479                    0
9480                } else {
9481                    1
9482                }
9483            }
9484            ["-p", path] => {
9485                use std::os::unix::fs::FileTypeExt;
9486                if std::fs::symlink_metadata(path)
9487                    .map(|m| m.file_type().is_fifo())
9488                    .unwrap_or(false)
9489                {
9490                    0
9491                } else {
9492                    1
9493                }
9494            }
9495            ["-S", path] => {
9496                use std::os::unix::fs::FileTypeExt;
9497                if std::fs::symlink_metadata(path)
9498                    .map(|m| m.file_type().is_socket())
9499                    .unwrap_or(false)
9500                {
9501                    0
9502                } else {
9503                    1
9504                }
9505            }
9506            ["-h", path] | ["-L", path] => {
9507                if std::path::Path::new(path).is_symlink() {
9508                    0
9509                } else {
9510                    1
9511                }
9512            }
9513
9514            // File permission tests — all use prefetched metadata (one stat per path)
9515            ["-r", path] => {
9516                use std::os::unix::fs::MetadataExt;
9517                if let Some(meta) = get_meta(path) {
9518                    let mode = meta.mode();
9519                    let uid = unsafe { libc::geteuid() };
9520                    let gid = unsafe { libc::getegid() };
9521                    let readable = if meta.uid() == uid {
9522                        mode & 0o400 != 0
9523                    } else if meta.gid() == gid {
9524                        mode & 0o040 != 0
9525                    } else {
9526                        mode & 0o004 != 0
9527                    };
9528                    if readable {
9529                        0
9530                    } else {
9531                        1
9532                    }
9533                } else {
9534                    1
9535                }
9536            }
9537            ["-w", path] => {
9538                use std::os::unix::fs::MetadataExt;
9539                if let Some(meta) = get_meta(path) {
9540                    let mode = meta.mode();
9541                    let uid = unsafe { libc::geteuid() };
9542                    let gid = unsafe { libc::getegid() };
9543                    let writable = if meta.uid() == uid {
9544                        mode & 0o200 != 0
9545                    } else if meta.gid() == gid {
9546                        mode & 0o020 != 0
9547                    } else {
9548                        mode & 0o002 != 0
9549                    };
9550                    if writable {
9551                        0
9552                    } else {
9553                        1
9554                    }
9555                } else {
9556                    1
9557                }
9558            }
9559            ["-x", path] => {
9560                use std::os::unix::fs::MetadataExt;
9561                if let Some(meta) = get_meta(path) {
9562                    let mode = meta.mode();
9563                    let uid = unsafe { libc::geteuid() };
9564                    let gid = unsafe { libc::getegid() };
9565                    let executable = if meta.uid() == uid {
9566                        mode & 0o100 != 0
9567                    } else if meta.gid() == gid {
9568                        mode & 0o010 != 0
9569                    } else {
9570                        mode & 0o001 != 0
9571                    };
9572                    if executable {
9573                        0
9574                    } else {
9575                        1
9576                    }
9577                } else {
9578                    1
9579                }
9580            }
9581
9582            // Special permission bits — prefetched metadata
9583            ["-g", path] => {
9584                use std::os::unix::fs::MetadataExt;
9585                if get_meta(path)
9586                    .map(|m| m.mode() & 0o2000 != 0)
9587                    .unwrap_or(false)
9588                {
9589                    0
9590                } else {
9591                    1
9592                }
9593            }
9594            ["-k", path] => {
9595                use std::os::unix::fs::MetadataExt;
9596                if get_meta(path)
9597                    .map(|m| m.mode() & 0o1000 != 0)
9598                    .unwrap_or(false)
9599                {
9600                    0
9601                } else {
9602                    1
9603                }
9604            }
9605            ["-u", path] => {
9606                use std::os::unix::fs::MetadataExt;
9607                if get_meta(path)
9608                    .map(|m| m.mode() & 0o4000 != 0)
9609                    .unwrap_or(false)
9610                {
9611                    0
9612                } else {
9613                    1
9614                }
9615            }
9616
9617            // File size — prefetched metadata
9618            ["-s", path] => {
9619                if get_meta(path).map(|m| m.len() > 0).unwrap_or(false) {
9620                    0
9621                } else {
9622                    1
9623                }
9624            }
9625
9626            // Ownership — prefetched metadata
9627            ["-O", path] => {
9628                use std::os::unix::fs::MetadataExt;
9629                if get_meta(path)
9630                    .map(|m| m.uid() == unsafe { libc::geteuid() })
9631                    .unwrap_or(false)
9632                {
9633                    0
9634                } else {
9635                    1
9636                }
9637            }
9638            ["-G", path] => {
9639                use std::os::unix::fs::MetadataExt;
9640                if get_meta(path)
9641                    .map(|m| m.gid() == unsafe { libc::getegid() })
9642                    .unwrap_or(false)
9643                {
9644                    0
9645                } else {
9646                    1
9647                }
9648            }
9649
9650            // File times — prefetched metadata
9651            ["-N", path] => {
9652                use std::os::unix::fs::MetadataExt;
9653                if let Some(meta) = get_meta(path) {
9654                    if meta.mtime() > meta.atime() {
9655                        0
9656                    } else {
9657                        1
9658                    }
9659                } else {
9660                    1
9661                }
9662            }
9663
9664            // Terminal test
9665            ["-t", fd] => {
9666                if let Ok(fd_num) = fd.parse::<i32>() {
9667                    if unsafe { libc::isatty(fd_num) } == 1 {
9668                        0
9669                    } else {
9670                        1
9671                    }
9672                } else {
9673                    1
9674                }
9675            }
9676
9677            // Variable test
9678            ["-v", varname] => {
9679                if self.variables.contains_key(*varname) || std::env::var(varname).is_ok() {
9680                    0
9681                } else {
9682                    1
9683                }
9684            }
9685
9686            // Option test
9687            ["-o", opt] => {
9688                let (name, _) = Self::normalize_option_name(opt);
9689                if self.options.get(&name).copied().unwrap_or(false) {
9690                    0
9691                } else {
9692                    1
9693                }
9694            }
9695
9696            // String comparisons
9697            [a, "=", b] | [a, "==", b] => {
9698                if a == b {
9699                    0
9700                } else {
9701                    1
9702                }
9703            }
9704            [a, "!=", b] => {
9705                if a != b {
9706                    0
9707                } else {
9708                    1
9709                }
9710            }
9711            [a, "<", b] => {
9712                if *a < *b {
9713                    0
9714                } else {
9715                    1
9716                }
9717            }
9718            [a, ">", b] => {
9719                if *a > *b {
9720                    0
9721                } else {
9722                    1
9723                }
9724            }
9725
9726            // Numeric comparisons
9727            [a, "-eq", b] => {
9728                let a: i64 = a.parse().unwrap_or(0);
9729                let b: i64 = b.parse().unwrap_or(0);
9730                if a == b {
9731                    0
9732                } else {
9733                    1
9734                }
9735            }
9736            [a, "-ne", b] => {
9737                let a: i64 = a.parse().unwrap_or(0);
9738                let b: i64 = b.parse().unwrap_or(0);
9739                if a != b {
9740                    0
9741                } else {
9742                    1
9743                }
9744            }
9745            [a, "-lt", b] => {
9746                let a: i64 = a.parse().unwrap_or(0);
9747                let b: i64 = b.parse().unwrap_or(0);
9748                if a < b {
9749                    0
9750                } else {
9751                    1
9752                }
9753            }
9754            [a, "-le", b] => {
9755                let a: i64 = a.parse().unwrap_or(0);
9756                let b: i64 = b.parse().unwrap_or(0);
9757                if a <= b {
9758                    0
9759                } else {
9760                    1
9761                }
9762            }
9763            [a, "-gt", b] => {
9764                let a: i64 = a.parse().unwrap_or(0);
9765                let b: i64 = b.parse().unwrap_or(0);
9766                if a > b {
9767                    0
9768                } else {
9769                    1
9770                }
9771            }
9772            [a, "-ge", b] => {
9773                let a: i64 = a.parse().unwrap_or(0);
9774                let b: i64 = b.parse().unwrap_or(0);
9775                if a >= b {
9776                    0
9777                } else {
9778                    1
9779                }
9780            }
9781
9782            // File comparisons
9783            [f1, "-nt", f2] => {
9784                let m1 = std::fs::metadata(f1).and_then(|m| m.modified()).ok();
9785                let m2 = std::fs::metadata(f2).and_then(|m| m.modified()).ok();
9786                match (m1, m2) {
9787                    (Some(t1), Some(t2)) => {
9788                        if t1 > t2 {
9789                            0
9790                        } else {
9791                            1
9792                        }
9793                    }
9794                    (Some(_), None) => 0,
9795                    _ => 1,
9796                }
9797            }
9798            [f1, "-ot", f2] => {
9799                let m1 = std::fs::metadata(f1).and_then(|m| m.modified()).ok();
9800                let m2 = std::fs::metadata(f2).and_then(|m| m.modified()).ok();
9801                match (m1, m2) {
9802                    (Some(t1), Some(t2)) => {
9803                        if t1 < t2 {
9804                            0
9805                        } else {
9806                            1
9807                        }
9808                    }
9809                    (None, Some(_)) => 0,
9810                    _ => 1,
9811                }
9812            }
9813            [f1, "-ef", f2] => {
9814                use std::os::unix::fs::MetadataExt;
9815                let m1 = std::fs::metadata(f1).ok();
9816                let m2 = std::fs::metadata(f2).ok();
9817                match (m1, m2) {
9818                    (Some(a), Some(b)) => {
9819                        if a.dev() == b.dev() && a.ino() == b.ino() {
9820                            0
9821                        } else {
9822                            1
9823                        }
9824                    }
9825                    _ => 1,
9826                }
9827            }
9828
9829            // Single string test
9830            [s] => {
9831                if !s.is_empty() {
9832                    0
9833                } else {
9834                    1
9835                }
9836            }
9837
9838            _ => 1,
9839        }
9840    }
9841
9842    fn builtin_local(&mut self, args: &[String]) -> i32 {
9843        self.builtin_typeset(args)
9844    }
9845
9846    fn builtin_declare(&mut self, args: &[String]) -> i32 {
9847        self.builtin_typeset(args)
9848    }
9849
9850    fn builtin_typeset(&mut self, args: &[String]) -> i32 {
9851        // Save old values when inside a function scope (local variable support).
9852        // Restored by call_function on function exit.
9853        if self.local_scope_depth > 0 {
9854            for arg in args {
9855                if arg.starts_with('-') || arg.starts_with('+') {
9856                    continue;
9857                }
9858                let name = arg.split('=').next().unwrap_or(arg);
9859                if !name.is_empty() {
9860                    let old_val = self.variables.get(name).cloned();
9861                    self.local_save_stack.push((name.to_string(), old_val));
9862                }
9863            }
9864        }
9865
9866        // typeset [ {+|-}AHUaghlmrtux ] [ {+|-}EFLRZip [ n ] ]
9867        //         [ + ] [ name[=value] ... ]
9868        // typeset -T [ {+|-}Urux ] [ {+|-}LRZp [ n ] ] SCALAR[=value] array
9869        // typeset -f [ {+|-}TUkmtuz ] [ + ] [ name ... ]
9870
9871        let mut is_array = false; // -a
9872        let mut is_assoc = false; // -A
9873        let mut is_export = false; // -x
9874        let mut is_integer = false; // -i
9875        let mut is_readonly = false; // -r
9876        let mut is_lower = false; // -l
9877        let mut is_upper = false; // -u
9878        let mut is_left_pad = false; // -L
9879        let mut is_right_pad = false; // -R
9880        let mut is_zero_pad = false; // -Z
9881        let mut is_float = false; // -F
9882        let mut is_float_exp = false; // -E
9883        let mut is_function = false; // -f
9884        let mut is_global = false; // -g
9885        let mut is_tied = false; // -T
9886        let mut is_hidden = false; // -H
9887        let mut is_hide_val = false; // -h
9888        let mut is_trace = false; // -t
9889        let mut print_mode = false; // -p
9890        let mut pattern_match = false; // -m
9891        let mut list_mode = false; // no args: list all
9892        let mut plus_mode = false; // +x etc: remove attribute
9893        let mut width: Option<usize> = None;
9894        let mut precision: Option<usize> = None;
9895        let mut var_args: Vec<String> = Vec::new();
9896
9897        let mut i = 0;
9898        while i < args.len() {
9899            let arg = &args[i];
9900
9901            if arg == "--" {
9902                i += 1;
9903                while i < args.len() {
9904                    var_args.push(args[i].clone());
9905                    i += 1;
9906                }
9907                break;
9908            }
9909
9910            if arg == "+" {
9911                plus_mode = true;
9912                i += 1;
9913                continue;
9914            }
9915
9916            if arg.starts_with('+') && arg.len() > 1 {
9917                plus_mode = true;
9918                for c in arg[1..].chars() {
9919                    match c {
9920                        'a' => is_array = false,
9921                        'A' => is_assoc = false,
9922                        'x' => is_export = false,
9923                        'i' => is_integer = false,
9924                        'r' => is_readonly = false,
9925                        'l' => is_lower = false,
9926                        'u' => is_upper = false,
9927                        'L' => is_left_pad = false,
9928                        'R' => is_right_pad = false,
9929                        'Z' => is_zero_pad = false,
9930                        'F' => is_float = false,
9931                        'E' => is_float_exp = false,
9932                        'f' => is_function = false,
9933                        'g' => is_global = false,
9934                        'T' => is_tied = false,
9935                        'H' => is_hidden = false,
9936                        'h' => is_hide_val = false,
9937                        't' => is_trace = false,
9938                        'p' => print_mode = false,
9939                        'm' => pattern_match = false,
9940                        _ => {}
9941                    }
9942                }
9943            } else if arg.starts_with('-') && arg.len() > 1 {
9944                let mut chars = arg[1..].chars().peekable();
9945                while let Some(c) = chars.next() {
9946                    match c {
9947                        'a' => is_array = true,
9948                        'A' => is_assoc = true,
9949                        'x' => is_export = true,
9950                        'i' => is_integer = true,
9951                        'r' => is_readonly = true,
9952                        'l' => is_lower = true,
9953                        'u' => is_upper = true,
9954                        'L' => {
9955                            is_left_pad = true;
9956                            // Check for width
9957                            let rest: String = chars.clone().collect();
9958                            if !rest.is_empty()
9959                                && rest
9960                                    .chars()
9961                                    .next()
9962                                    .map(|c| c.is_ascii_digit())
9963                                    .unwrap_or(false)
9964                            {
9965                                let num: String =
9966                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9967                                width = num.parse().ok();
9968                            }
9969                        }
9970                        'R' => {
9971                            is_right_pad = true;
9972                            let rest: String = chars.clone().collect();
9973                            if !rest.is_empty()
9974                                && rest
9975                                    .chars()
9976                                    .next()
9977                                    .map(|c| c.is_ascii_digit())
9978                                    .unwrap_or(false)
9979                            {
9980                                let num: String =
9981                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9982                                width = num.parse().ok();
9983                            }
9984                        }
9985                        'Z' => {
9986                            is_zero_pad = true;
9987                            let rest: String = chars.clone().collect();
9988                            if !rest.is_empty()
9989                                && rest
9990                                    .chars()
9991                                    .next()
9992                                    .map(|c| c.is_ascii_digit())
9993                                    .unwrap_or(false)
9994                            {
9995                                let num: String =
9996                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
9997                                width = num.parse().ok();
9998                            }
9999                        }
10000                        'F' => {
10001                            is_float = true;
10002                            let rest: String = chars.clone().collect();
10003                            if !rest.is_empty()
10004                                && rest
10005                                    .chars()
10006                                    .next()
10007                                    .map(|c| c.is_ascii_digit())
10008                                    .unwrap_or(false)
10009                            {
10010                                let num: String =
10011                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
10012                                precision = num.parse().ok();
10013                            }
10014                        }
10015                        'E' => {
10016                            is_float_exp = true;
10017                            let rest: String = chars.clone().collect();
10018                            if !rest.is_empty()
10019                                && rest
10020                                    .chars()
10021                                    .next()
10022                                    .map(|c| c.is_ascii_digit())
10023                                    .unwrap_or(false)
10024                            {
10025                                let num: String =
10026                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
10027                                precision = num.parse().ok();
10028                            }
10029                        }
10030                        'f' => is_function = true,
10031                        'g' => is_global = true,
10032                        'T' => is_tied = true,
10033                        'H' => is_hidden = true,
10034                        'h' => is_hide_val = true,
10035                        't' => is_trace = true,
10036                        'p' => print_mode = true,
10037                        'm' => pattern_match = true,
10038                        _ => {}
10039                    }
10040                }
10041            } else {
10042                var_args.push(arg.clone());
10043            }
10044            i += 1;
10045        }
10046
10047        let _ = is_global;
10048        let _ = is_tied;
10049        let _ = is_hidden;
10050        let _ = is_hide_val;
10051        let _ = is_trace;
10052        let _ = pattern_match;
10053        let _ = precision;
10054
10055        // If -f (function mode) with no args, list functions
10056        if is_function && var_args.is_empty() {
10057            let mut func_names: Vec<_> = self.functions.keys().cloned().collect();
10058            func_names.sort();
10059            for name in &func_names {
10060                if let Some(func) = self.functions.get(name) {
10061                    if print_mode {
10062                        let body = crate::text::getpermtext(func);
10063                        println!("{} () {{\n\t{}\n}}", name, body.trim());
10064                    } else {
10065                        let body = crate::text::getpermtext(func);
10066                        println!("{} () {{\n\t{}\n}}", name, body.trim());
10067                    }
10068                }
10069            }
10070            return 0;
10071        }
10072
10073        // If -f with args, just show those functions
10074        if is_function {
10075            for name in &var_args {
10076                if let Some(func) = self.functions.get(name) {
10077                    if print_mode {
10078                        let body = crate::text::getpermtext(func);
10079                        println!("{} () {{\n\t{}\n}}", name, body.trim());
10080                    } else {
10081                        let body = crate::text::getpermtext(func);
10082                        println!("{} () {{\n\t{}\n}}", name, body.trim());
10083                    }
10084                }
10085            }
10086            return 0;
10087        }
10088
10089        // No args: list all variables with attributes
10090        if var_args.is_empty() {
10091            list_mode = true;
10092        }
10093
10094        if list_mode {
10095            let mut sorted_names: Vec<_> = self.variables.keys().cloned().collect();
10096            sorted_names.sort();
10097            for name in &sorted_names {
10098                let val = self.variables.get(name).cloned().unwrap_or_default();
10099                let mut attrs = String::new();
10100                if is_export || env::var(name).is_ok() {
10101                    attrs.push('x');
10102                }
10103                let is_arr = self.arrays.contains_key(name);
10104                let is_hash = self.assoc_arrays.contains_key(name);
10105                if is_arr {
10106                    attrs.push('a');
10107                }
10108                if is_hash {
10109                    attrs.push('A');
10110                }
10111                if print_mode {
10112                    // typeset -p: output re-executable code with values
10113                    let prefix = if attrs.is_empty() {
10114                        "typeset".to_string()
10115                    } else {
10116                        format!("typeset -{}", attrs)
10117                    };
10118                    if is_hash {
10119                        if let Some(assoc) = self.assoc_arrays.get(name) {
10120                            let mut pairs: Vec<_> = assoc.iter().collect();
10121                            pairs.sort_by_key(|(k, _)| (*k).clone());
10122                            let formatted: Vec<String> = pairs
10123                                .iter()
10124                                .map(|(k, v)| {
10125                                    format!("[{}]={}", shell_quote_value(k), shell_quote_value(v))
10126                                })
10127                                .collect();
10128                            println!("{} {}=( {} )", prefix, name, formatted.join(" "));
10129                        }
10130                    } else if is_arr {
10131                        if let Some(arr) = self.arrays.get(name) {
10132                            let formatted: Vec<String> =
10133                                arr.iter().map(|v| shell_quote_value(v)).collect();
10134                            println!("{} {}=( {} )", prefix, name, formatted.join(" "));
10135                        }
10136                    } else {
10137                        println!("{} {}={}", prefix, name, shell_quote_value(&val));
10138                    }
10139                } else if is_hide_val {
10140                    println!("{}={}", name, "*".repeat(val.len().min(8)));
10141                } else {
10142                    println!("{}={}", name, val);
10143                }
10144            }
10145            return 0;
10146        }
10147
10148        // Process variable assignments
10149        for arg in var_args {
10150            // Check if this starts an array assignment: "name=(" or "name=(value"
10151            if let Some(eq_pos) = arg.find('=') {
10152                let name = &arg[..eq_pos];
10153                let rest = &arg[eq_pos + 1..];
10154
10155                if rest.starts_with('(') {
10156                    // Array assignment - collect all elements until we find ')'
10157                    let mut elements = Vec::new();
10158                    let current = rest[1..].to_string(); // skip '('
10159
10160                    // Check if closing ) is in this arg
10161                    if let Some(close_pos) = current.find(')') {
10162                        let content = &current[..close_pos];
10163                        if !content.is_empty() {
10164                            elements.extend(content.split_whitespace().map(|s| s.to_string()));
10165                        }
10166                    } else {
10167                        // Single arg with just elements
10168                        if !current.is_empty() {
10169                            let trimmed = current.trim_end_matches(')');
10170                            elements.extend(trimmed.split_whitespace().map(|s| s.to_string()));
10171                        }
10172                    }
10173
10174                    // Set array variable
10175                    if is_assoc {
10176                        let mut assoc = std::collections::HashMap::new();
10177                        let mut iter = elements.iter();
10178                        while let Some(key) = iter.next() {
10179                            if let Some(val) = iter.next() {
10180                                assoc.insert(key.clone(), val.clone());
10181                            }
10182                        }
10183                        self.assoc_arrays.insert(name.to_string(), assoc);
10184                    } else {
10185                        self.arrays.insert(name.to_string(), elements);
10186                    }
10187                    self.variables.insert(name.to_string(), String::new());
10188                } else {
10189                    // Regular assignment - apply transformations
10190                    let mut value = rest.to_string();
10191
10192                    if is_integer {
10193                        // Force integer evaluation
10194                        value = self.evaluate_arithmetic(&value).to_string();
10195                    }
10196                    if is_lower {
10197                        value = value.to_lowercase();
10198                    }
10199                    if is_upper {
10200                        value = value.to_uppercase();
10201                    }
10202                    if let Some(w) = width {
10203                        if is_left_pad {
10204                            value = format!("{:<width$}", value, width = w);
10205                            value.truncate(w);
10206                        } else if is_right_pad || is_zero_pad {
10207                            let pad_char = if is_zero_pad { '0' } else { ' ' };
10208                            if value.len() < w {
10209                                value = format!(
10210                                    "{}{}",
10211                                    pad_char.to_string().repeat(w - value.len()),
10212                                    value
10213                                );
10214                            }
10215                            if value.len() > w {
10216                                value = value[value.len() - w..].to_string();
10217                            }
10218                        }
10219                    }
10220                    if is_float || is_float_exp {
10221                        if let Ok(f) = value.parse::<f64>() {
10222                            let prec = precision.unwrap_or(10);
10223                            value = if is_float_exp {
10224                                format!("{:.prec$e}", f, prec = prec)
10225                            } else {
10226                                format!("{:.prec$}", f, prec = prec)
10227                            };
10228                        }
10229                    }
10230
10231                    self.variables.insert(name.to_string(), value.clone());
10232
10233                    if is_export {
10234                        env::set_var(name, &value);
10235                    }
10236                }
10237            } else if is_array || is_assoc {
10238                // Just declaring the variable
10239                if is_assoc {
10240                    self.assoc_arrays
10241                        .insert(arg.clone(), std::collections::HashMap::new());
10242                } else {
10243                    self.arrays.insert(arg.clone(), Vec::new());
10244                }
10245                self.variables.insert(arg.clone(), String::new());
10246            } else {
10247                self.variables.insert(arg.clone(), String::new());
10248                if is_export {
10249                    env::set_var(&arg, "");
10250                }
10251            }
10252
10253            // Apply readonly flag — must come after the variable is set
10254            if is_readonly {
10255                let name = if let Some(eq_pos) = arg.find('=') {
10256                    arg[..eq_pos].to_string()
10257                } else {
10258                    arg.clone()
10259                };
10260                self.readonly_vars.insert(name);
10261            }
10262        }
10263        0
10264    }
10265
10266    fn builtin_read(&mut self, args: &[String]) -> i32 {
10267        // read [ -rszpqAclneE ] [ -t timeout ] [ -d delim ] [ -k [ num ] ] [ -u fd ]
10268        //      [ name[?prompt] ] [ name ... ]
10269        use std::io::{BufRead, Read as IoRead};
10270
10271        let mut raw_mode = false; // -r: don't interpret backslash escapes
10272        let mut silent = false; // -s: don't echo input
10273        let mut to_history = false; // -z: read from history stack
10274        let mut prompt_str: Option<String> = None; // -p prompt
10275        let mut use_array = false; // -A: read into array
10276        let mut timeout: Option<u64> = None; // -t timeout in seconds
10277        let mut delimiter = '\n'; // -d delim
10278        let mut nchars: Option<usize> = None; // -k num: read exactly num chars
10279        let mut fd = 0; // -u fd: read from fd
10280        let mut quiet = false; // -q: test only, don't assign
10281        let mut var_names: Vec<String> = Vec::new();
10282
10283        let mut i = 0;
10284        while i < args.len() {
10285            let arg = &args[i];
10286
10287            if arg == "--" {
10288                i += 1;
10289                while i < args.len() {
10290                    var_names.push(args[i].clone());
10291                    i += 1;
10292                }
10293                break;
10294            }
10295
10296            if arg.starts_with('-') && arg.len() > 1 {
10297                let mut chars = arg[1..].chars().peekable();
10298                while let Some(ch) = chars.next() {
10299                    match ch {
10300                        'r' => raw_mode = true,
10301                        's' => silent = true,
10302                        'z' => to_history = true,
10303                        'A' => use_array = true,
10304                        'c' | 'l' | 'n' | 'e' | 'E' => {} // TODO
10305                        'q' => quiet = true,
10306                        't' => {
10307                            let rest: String = chars.collect();
10308                            if !rest.is_empty() {
10309                                timeout = rest.parse().ok();
10310                            } else {
10311                                i += 1;
10312                                if i < args.len() {
10313                                    timeout = args[i].parse().ok();
10314                                }
10315                            }
10316                            break;
10317                        }
10318                        'd' => {
10319                            let rest: String = chars.collect();
10320                            if !rest.is_empty() {
10321                                delimiter = rest.chars().next().unwrap_or('\n');
10322                            } else {
10323                                i += 1;
10324                                if i < args.len() {
10325                                    delimiter = args[i].chars().next().unwrap_or('\n');
10326                                }
10327                            }
10328                            break;
10329                        }
10330                        'k' => {
10331                            let rest: String = chars.collect();
10332                            if !rest.is_empty() {
10333                                nchars = Some(rest.parse().unwrap_or(1));
10334                            } else if i + 1 < args.len()
10335                                && args[i + 1].chars().all(|c| c.is_ascii_digit())
10336                            {
10337                                i += 1;
10338                                nchars = Some(args[i].parse().unwrap_or(1));
10339                            } else {
10340                                nchars = Some(1);
10341                            }
10342                            break;
10343                        }
10344                        'u' => {
10345                            let rest: String = chars.collect();
10346                            if !rest.is_empty() {
10347                                fd = rest.parse().unwrap_or(0);
10348                            } else {
10349                                i += 1;
10350                                if i < args.len() {
10351                                    fd = args[i].parse().unwrap_or(0);
10352                                }
10353                            }
10354                            break;
10355                        }
10356                        'p' => {
10357                            let rest: String = chars.collect();
10358                            if !rest.is_empty() {
10359                                prompt_str = Some(rest);
10360                            } else {
10361                                i += 1;
10362                                if i < args.len() {
10363                                    prompt_str = Some(args[i].clone());
10364                                }
10365                            }
10366                            break;
10367                        }
10368                        _ => {}
10369                    }
10370                }
10371            } else {
10372                if let Some(pos) = arg.find('?') {
10373                    var_names.push(arg[..pos].to_string());
10374                    prompt_str = Some(arg[pos + 1..].to_string());
10375                } else {
10376                    var_names.push(arg.clone());
10377                }
10378            }
10379            i += 1;
10380        }
10381
10382        if var_names.is_empty() {
10383            var_names.push("REPLY".to_string());
10384        }
10385
10386        if let Some(ref p) = prompt_str {
10387            eprint!("{}", p);
10388            let _ = std::io::stderr().flush();
10389        }
10390
10391        let _ = to_history;
10392        let _ = fd;
10393        let _ = silent;
10394
10395        let input = if let Some(n) = nchars {
10396            let mut buf = vec![0u8; n];
10397            let stdin = io::stdin();
10398            if let Some(_t) = timeout {
10399                // TODO: proper timeout
10400            }
10401            match stdin.lock().read_exact(&mut buf) {
10402                Ok(_) => String::from_utf8_lossy(&buf).to_string(),
10403                Err(_) => return 1,
10404            }
10405        } else {
10406            let stdin = io::stdin();
10407            let mut input = String::new();
10408            if delimiter == '\n' {
10409                match stdin.lock().read_line(&mut input) {
10410                    Ok(0) => return 1,
10411                    Ok(_) => {}
10412                    Err(_) => return 1,
10413                }
10414            } else {
10415                let mut byte = [0u8; 1];
10416                loop {
10417                    match stdin.lock().read_exact(&mut byte) {
10418                        Ok(_) => {
10419                            let c = byte[0] as char;
10420                            if c == delimiter {
10421                                break;
10422                            }
10423                            input.push(c);
10424                        }
10425                        Err(_) => break,
10426                    }
10427                }
10428            }
10429            input
10430                .trim_end_matches('\n')
10431                .trim_end_matches('\r')
10432                .to_string()
10433        };
10434
10435        let processed = if raw_mode {
10436            input
10437        } else {
10438            input.replace("\\\n", "")
10439        };
10440
10441        if quiet {
10442            return if processed.is_empty() { 1 } else { 0 };
10443        }
10444
10445        if use_array {
10446            let var = &var_names[0];
10447            let words: Vec<String> = processed.split_whitespace().map(String::from).collect();
10448            self.arrays.insert(var.clone(), words);
10449        } else if var_names.len() == 1 {
10450            let var = &var_names[0];
10451            env::set_var(var, &processed);
10452            self.variables.insert(var.clone(), processed);
10453        } else {
10454            let ifs = self
10455                .variables
10456                .get("IFS")
10457                .map(|s| s.as_str())
10458                .unwrap_or(" \t\n");
10459            let words: Vec<&str> = processed
10460                .split(|c| ifs.contains(c))
10461                .filter(|s| !s.is_empty())
10462                .collect();
10463
10464            for (j, var) in var_names.iter().enumerate() {
10465                if j < words.len() {
10466                    if j == var_names.len() - 1 && words.len() > var_names.len() {
10467                        let remaining = words[j..].join(" ");
10468                        env::set_var(var, &remaining);
10469                        self.variables.insert(var.clone(), remaining);
10470                    } else {
10471                        env::set_var(var, words[j]);
10472                        self.variables.insert(var.clone(), words[j].to_string());
10473                    }
10474                } else {
10475                    env::set_var(var, "");
10476                    self.variables.insert(var.clone(), String::new());
10477                }
10478            }
10479        }
10480
10481        0
10482    }
10483
10484    fn builtin_shift(&mut self, args: &[String]) -> i32 {
10485        // shift [ -p ] [ n ] [ name ... ]
10486        // -p: shift from end instead of beginning (pop)
10487        // n: number of elements to shift (default 1)
10488        // name: array names to shift (default: shift positional parameters)
10489
10490        let mut from_end = false;
10491        let mut count = 1usize;
10492        let mut array_names: Vec<String> = Vec::new();
10493
10494        let mut i = 0;
10495        while i < args.len() {
10496            let arg = &args[i];
10497            if arg == "-p" {
10498                from_end = true;
10499            } else if arg.chars().all(|c| c.is_ascii_digit()) {
10500                count = arg.parse().unwrap_or(1);
10501            } else {
10502                array_names.push(arg.clone());
10503            }
10504            i += 1;
10505        }
10506
10507        if array_names.is_empty() {
10508            // Shift positional parameters
10509            if from_end {
10510                for _ in 0..count {
10511                    if !self.positional_params.is_empty() {
10512                        self.positional_params.pop();
10513                    }
10514                }
10515            } else {
10516                for _ in 0..count.min(self.positional_params.len()) {
10517                    self.positional_params.remove(0);
10518                }
10519            }
10520        } else {
10521            // Shift specified arrays
10522            for name in array_names {
10523                if let Some(arr) = self.arrays.get_mut(&name) {
10524                    if from_end {
10525                        for _ in 0..count {
10526                            if !arr.is_empty() {
10527                                arr.pop();
10528                            }
10529                        }
10530                    } else {
10531                        for _ in 0..count {
10532                            if !arr.is_empty() {
10533                                arr.remove(0);
10534                            }
10535                        }
10536                    }
10537                }
10538            }
10539        }
10540
10541        0
10542    }
10543
10544    #[tracing::instrument(level = "debug", skip(self))]
10545    fn builtin_eval(&mut self, args: &[String]) -> i32 {
10546        let code = args.join(" ");
10547        match self.execute_script(&code) {
10548            Ok(status) => status,
10549            Err(e) => {
10550                eprintln!("eval: {}", e);
10551                1
10552            }
10553        }
10554    }
10555
10556    fn builtin_autoload(&mut self, args: &[String]) -> i32 {
10557        // Parse options like zsh: -U (no alias), -z (zsh style), -k (ksh style),
10558        // -X (execute now), -x (export), -r (resolve), -R (resolve recurse),
10559        // -t (trace), -T (trace local), -W (warn nested), -d (use calling dir)
10560        let mut functions = Vec::new();
10561        let mut no_alias = false; // -U
10562        let mut zsh_style = false; // -z
10563        let mut ksh_style = false; // -k
10564        let mut execute_now = false; // -X
10565        let mut resolve = false; // -r
10566        let mut trace = false; // -t
10567        let mut use_caller_dir = false; // -d
10568        let _list_mode = false;
10569
10570        let mut i = 0;
10571        while i < args.len() {
10572            let arg = &args[i];
10573
10574            if arg == "--" {
10575                i += 1;
10576                break;
10577            }
10578
10579            if arg.starts_with('+') {
10580                let flags = &arg[1..];
10581                for c in flags.chars() {
10582                    match c {
10583                        'U' => no_alias = false,
10584                        'z' => zsh_style = false,
10585                        'k' => ksh_style = false,
10586                        't' => trace = false,
10587                        'd' => use_caller_dir = false,
10588                        _ => {}
10589                    }
10590                }
10591            } else if arg.starts_with('-') {
10592                let flags = &arg[1..];
10593                if flags.is_empty() {
10594                    // Just "-" means end of options
10595                    i += 1;
10596                    break;
10597                }
10598                for c in flags.chars() {
10599                    match c {
10600                        'U' => no_alias = true,
10601                        'z' => zsh_style = true,
10602                        'k' => ksh_style = true,
10603                        'X' => execute_now = true,
10604                        'r' | 'R' => resolve = true,
10605                        't' => trace = true,
10606                        'T' => {} // trace local
10607                        'W' => {} // warn nested
10608                        'd' => use_caller_dir = true,
10609                        'w' => {} // wordcode
10610                        'm' => {} // pattern match
10611                        _ => {}
10612                    }
10613                }
10614            } else {
10615                functions.push(arg.clone());
10616            }
10617            i += 1;
10618        }
10619
10620        // Collect remaining args as function names
10621        while i < args.len() {
10622            functions.push(args[i].clone());
10623            i += 1;
10624        }
10625
10626        // If no functions specified, list autoloaded functions
10627        if functions.is_empty() && !execute_now {
10628            for (name, _) in &self.autoload_pending {
10629                if no_alias && zsh_style {
10630                    println!("autoload -Uz {}", name);
10631                } else if no_alias {
10632                    println!("autoload -U {}", name);
10633                } else {
10634                    println!("autoload {}", name);
10635                }
10636            }
10637            return 0;
10638        }
10639
10640        // Handle -X: load and execute function immediately (called from stub)
10641        // When a stub function calls `builtin autoload -Xz`, we load the real function
10642        // and then need to execute it with the original arguments
10643        if execute_now {
10644            for func_name in &functions {
10645                // Load the function from fpath
10646                if let Some(loaded) = self.load_autoload_function(func_name) {
10647                    // Extract body from FunctionDef
10648                    let body = match loaded {
10649                        ShellCommand::FunctionDef(_, body) => (*body).clone(),
10650                        other => other,
10651                    };
10652                    // Replace the stub with the real function
10653                    self.functions.insert(func_name.clone(), body);
10654                    // Remove from pending
10655                    self.autoload_pending.remove(func_name);
10656                } else {
10657                    eprintln!(
10658                        "autoload: {}: function definition file not found",
10659                        func_name
10660                    );
10661                    return 1;
10662                }
10663            }
10664            return 0;
10665        }
10666
10667        // Register functions for autoload - create stub functions
10668        for func_name in &functions {
10669            // Store autoload metadata
10670            let mut flags = AutoloadFlags::empty();
10671            if no_alias {
10672                flags |= AutoloadFlags::NO_ALIAS;
10673            }
10674            if zsh_style {
10675                flags |= AutoloadFlags::ZSH_STYLE;
10676            }
10677            if ksh_style {
10678                flags |= AutoloadFlags::KSH_STYLE;
10679            }
10680            if trace {
10681                flags |= AutoloadFlags::TRACE;
10682            }
10683            if use_caller_dir {
10684                flags |= AutoloadFlags::USE_CALLER_DIR;
10685            }
10686
10687            self.autoload_pending.insert(func_name.clone(), flags);
10688
10689            // Create a stub function: `builtin autoload -Xz funcname && funcname "$@"`
10690            // When called, this loads the real function and re-calls it
10691            let autoload_opts = if zsh_style && no_alias {
10692                "-XUz"
10693            } else if zsh_style {
10694                "-Xz"
10695            } else if no_alias {
10696                "-XU"
10697            } else {
10698                "-X"
10699            };
10700
10701            // The stub: builtin autoload -Xz funcname && funcname "$@"
10702            let stub = ShellCommand::List(vec![
10703                (
10704                    ShellCommand::Simple(SimpleCommand {
10705                        assignments: vec![],
10706                        words: vec![
10707                            ShellWord::Literal("builtin".to_string()),
10708                            ShellWord::Literal("autoload".to_string()),
10709                            ShellWord::Literal(autoload_opts.to_string()),
10710                            ShellWord::Literal(func_name.clone()),
10711                        ],
10712                        redirects: vec![],
10713                    }),
10714                    ListOp::And,
10715                ),
10716                (
10717                    ShellCommand::Simple(SimpleCommand {
10718                        assignments: vec![],
10719                        words: vec![
10720                            ShellWord::Literal(func_name.clone()),
10721                            ShellWord::DoubleQuoted(vec![ShellWord::Variable("@".to_string())]),
10722                        ],
10723                        redirects: vec![],
10724                    }),
10725                    ListOp::Semi,
10726                ),
10727            ]);
10728
10729            self.functions.insert(func_name.clone(), stub);
10730
10731            // If -r or -R, resolve the path now to verify it exists
10732            if resolve {
10733                if self.find_function_file(func_name).is_none() {
10734                    eprintln!(
10735                        "autoload: {}: function definition file not found",
10736                        func_name
10737                    );
10738                }
10739            }
10740        }
10741
10742        // Batch pre-resolution: when multiple autoloads are registered at once
10743        // (common during .zshrc processing), dispatch fpath lookups in parallel
10744        // across the worker pool to pre-read function files into the OS page cache.
10745        if functions.len() >= 4 && !resolve && !execute_now {
10746            let fpath_dirs: Vec<PathBuf> = self.fpath.clone();
10747            let names: Vec<String> = functions.clone();
10748            let pool = std::sync::Arc::clone(&self.worker_pool);
10749
10750            tracing::debug!(
10751                count = names.len(),
10752                fpath_dirs = fpath_dirs.len(),
10753                "batch autoload: pre-resolving fpath lookups on worker pool"
10754            );
10755
10756            // Submit resolution tasks — each worker scans fpath for a subset of names.
10757            // Results are cached in a shared map for later use by load_autoload_function.
10758            let resolved = std::sync::Arc::new(parking_lot::Mutex::new(
10759                HashMap::<String, PathBuf>::with_capacity(names.len()),
10760            ));
10761
10762            for name in names {
10763                let dirs = fpath_dirs.clone();
10764                let resolved = std::sync::Arc::clone(&resolved);
10765                pool.submit(move || {
10766                    for dir in &dirs {
10767                        let path = dir.join(&name);
10768                        if path.exists() && path.is_file() {
10769                            // Pre-read to warm OS page cache (the read result is discarded,
10770                            // but the pages stay in the kernel buffer cache)
10771                            let _ = std::fs::read(&path);
10772                            resolved.lock().insert(name.clone(), path);
10773                            tracing::trace!(func = %name, "autoload batch: pre-resolved");
10774                            break;
10775                        }
10776                    }
10777                });
10778            }
10779        }
10780
10781        0
10782    }
10783
10784    /// Find a function file in fpath
10785    fn find_function_file(&self, name: &str) -> Option<PathBuf> {
10786        for dir in &self.fpath {
10787            let path = dir.join(name);
10788            if path.exists() && path.is_file() {
10789                return Some(path);
10790            }
10791        }
10792        None
10793    }
10794
10795    /// Load an autoloaded function from fpath - reads file and parses it
10796    fn load_autoload_function(&mut self, name: &str) -> Option<ShellCommand> {
10797        // FAST PATH: Try SQLite cache first (no filesystem access)
10798        // Skip in zsh_compat mode - use traditional fpath scanning only
10799        if !self.zsh_compat {
10800            if let Some(ref cache) = self.compsys_cache {
10801                // FASTEST: try cached bytecodes (skip lex+parse+compile entirely)
10802                if let Ok(Some(bc_blob)) = cache.get_autoload_bytecode(name) {
10803                    // Try fusevm::Chunk first (new format — actual bytecodes)
10804                    if let Ok(chunk) = bincode::deserialize::<fusevm::Chunk>(&bc_blob) {
10805                        if !chunk.ops.is_empty() {
10806                            tracing::trace!(
10807                                name,
10808                                bytes = bc_blob.len(),
10809                                ops = chunk.ops.len(),
10810                                "autoload: bytecode cache hit → VM"
10811                            );
10812                            // Execute directly on fusevm — no parse, no compile
10813                            let mut vm = fusevm::VM::new(chunk);
10814                            let _ = vm.run();
10815                            self.last_status = vm.last_status;
10816                            // Return a no-op so the caller doesn't try to execute again
10817                            return Some(ShellCommand::Simple(crate::parser::SimpleCommand {
10818                                assignments: Vec::new(),
10819                                words: Vec::new(),
10820                                redirects: Vec::new(),
10821                            }));
10822                        }
10823                    }
10824                    // Fallback: try legacy Vec<ShellCommand> format (migration)
10825                    if let Ok(commands) = bincode::deserialize::<Vec<ShellCommand>>(&bc_blob) {
10826                        if !commands.is_empty() {
10827                            tracing::trace!(
10828                                name,
10829                                bytes = bc_blob.len(),
10830                                "autoload: legacy AST cache hit"
10831                            );
10832                            return Some(self.wrap_autoload_commands(name, commands));
10833                        }
10834                    }
10835                }
10836
10837                // FAST: cached source text — parse + compile (still no filesystem access)
10838                if let Ok(Some(body)) = cache.get_autoload_body(name) {
10839                    let mut parser = ShellParser::new(&body);
10840                    if let Ok(commands) = parser.parse_script() {
10841                        if !commands.is_empty() {
10842                            // Compile to bytecodes and cache for next time
10843                            let compiler = crate::shell_compiler::ShellCompiler::new();
10844                            let chunk = compiler.compile(&commands);
10845                            if let Ok(blob) = bincode::serialize(&chunk) {
10846                                let _ = cache.set_autoload_bytecode(name, &blob);
10847                                tracing::trace!(
10848                                    name,
10849                                    bytes = blob.len(),
10850                                    "autoload: bytecodes compiled and cached"
10851                                );
10852                            }
10853                            return Some(self.wrap_autoload_commands(name, commands));
10854                        }
10855                    }
10856                }
10857            }
10858        }
10859
10860        // SLOW PATH: Try ZWC cache (but skip if we're reloading an existing function)
10861        if !self.functions.contains_key(name) {
10862            // Try to load from ZWC files
10863            for dir in &self.fpath.clone() {
10864                // Try dir.zwc (e.g., /path/to/src.zwc for /path/to/src)
10865                let zwc_path = dir.with_extension("zwc");
10866                if zwc_path.exists() {
10867                    // Function name in directory ZWC includes path prefix
10868                    let prefixed_name = format!(
10869                        "{}/{}",
10870                        dir.file_name().and_then(|n| n.to_str()).unwrap_or(""),
10871                        name
10872                    );
10873                    if let Some(func) = self.load_function_from_zwc(&zwc_path, &prefixed_name) {
10874                        return Some(func);
10875                    }
10876                    // Also try without prefix
10877                    if let Some(func) = self.load_function_from_zwc(&zwc_path, name) {
10878                        return Some(func);
10879                    }
10880                }
10881                // Try individual function.zwc
10882                let func_zwc = dir.join(format!("{}.zwc", name));
10883                if func_zwc.exists() {
10884                    if let Some(func) = self.load_function_from_zwc(&func_zwc, name) {
10885                        return Some(func);
10886                    }
10887                }
10888            }
10889        }
10890
10891        // SLOWEST PATH: Find the function file in fpath
10892        let path = self.find_function_file(name)?;
10893
10894        // Read the file
10895        let content = std::fs::read_to_string(&path).ok()?;
10896
10897        // Parse the content
10898        let mut parser = ShellParser::new(&content);
10899
10900        if let Ok(commands) = parser.parse_script() {
10901            if commands.is_empty() {
10902                return None;
10903            }
10904
10905            // Check if it's a single function definition for this name (ksh style)
10906            if commands.len() == 1 {
10907                if let ShellCommand::FunctionDef(ref fn_name, _) = commands[0] {
10908                    if fn_name == name {
10909                        return Some(commands[0].clone());
10910                    }
10911                }
10912            }
10913
10914            // Otherwise, the file contents become the function body (zsh style)
10915            // Wrap all commands in a List
10916            let body = if commands.len() == 1 {
10917                commands.into_iter().next().unwrap()
10918            } else {
10919                // Convert to List with Semi separators
10920                let list_cmds: Vec<(ShellCommand, ListOp)> =
10921                    commands.into_iter().map(|c| (c, ListOp::Semi)).collect();
10922                ShellCommand::List(list_cmds)
10923            };
10924
10925            return Some(ShellCommand::FunctionDef(name.to_string(), Box::new(body)));
10926        }
10927
10928        None
10929    }
10930
10931    /// Convert parsed commands into a FunctionDef, handling ksh vs zsh style.
10932    fn wrap_autoload_commands(&self, name: &str, commands: Vec<ShellCommand>) -> ShellCommand {
10933        // ksh style: file contains a single function definition for this name
10934        if commands.len() == 1 {
10935            if let ShellCommand::FunctionDef(ref fn_name, _) = commands[0] {
10936                if fn_name == name {
10937                    return commands.into_iter().next().unwrap();
10938                }
10939            }
10940        }
10941        // zsh style: file body IS the function body
10942        let body = if commands.len() == 1 {
10943            commands.into_iter().next().unwrap()
10944        } else {
10945            let list_cmds: Vec<(ShellCommand, ListOp)> =
10946                commands.into_iter().map(|c| (c, ListOp::Semi)).collect();
10947            ShellCommand::List(list_cmds)
10948        };
10949        ShellCommand::FunctionDef(name.to_string(), Box::new(body))
10950    }
10951
10952    /// Check if a function is autoload pending and load it if so
10953    pub fn maybe_autoload(&mut self, name: &str) -> bool {
10954        if self.autoload_pending.contains_key(name) {
10955            if let Some(func) = self.load_autoload_function(name) {
10956                // For FunctionDef, extract the body and store it
10957                let to_store = match func {
10958                    ShellCommand::FunctionDef(_, body) => (*body).clone(),
10959                    other => other,
10960                };
10961                self.functions.insert(name.to_string(), to_store);
10962                self.autoload_pending.remove(name);
10963                return true;
10964            }
10965        }
10966        false
10967    }
10968
10969    fn builtin_jobs(&mut self, args: &[String]) -> i32 {
10970        // jobs [ -dlprsZ ] [ job ... ]
10971        // -l: long format (show PID)
10972        // -p: print process group IDs only
10973        // -d: show directory from which job was started
10974        // -r: show running jobs only
10975        // -s: show stopped jobs only
10976        // -Z: set process name (not relevant here)
10977
10978        let mut long_format = false;
10979        let mut pids_only = false;
10980        let mut show_dir = false;
10981        let mut running_only = false;
10982        let mut stopped_only = false;
10983        let mut job_ids: Vec<usize> = Vec::new();
10984
10985        for arg in args {
10986            if arg.starts_with('-') {
10987                for c in arg[1..].chars() {
10988                    match c {
10989                        'l' => long_format = true,
10990                        'p' => pids_only = true,
10991                        'd' => show_dir = true,
10992                        'r' => running_only = true,
10993                        's' => stopped_only = true,
10994                        'Z' => {} // ignore
10995                        _ => {}
10996                    }
10997                }
10998            } else if arg.starts_with('%') {
10999                if let Ok(id) = arg[1..].parse::<usize>() {
11000                    job_ids.push(id);
11001                }
11002            } else if let Ok(id) = arg.parse::<usize>() {
11003                job_ids.push(id);
11004            }
11005        }
11006
11007        // Reap finished jobs first
11008        for job in self.jobs.reap_finished() {
11009            if !running_only && !stopped_only {
11010                if pids_only {
11011                    println!("{}", job.pid);
11012                } else {
11013                    println!("[{}]  Done                    {}", job.id, job.command);
11014                }
11015            }
11016        }
11017
11018        // List jobs (optionally filtered)
11019        for job in self.jobs.list() {
11020            // Filter by specific job IDs if provided
11021            if !job_ids.is_empty() && !job_ids.contains(&job.id) {
11022                continue;
11023            }
11024
11025            // Filter by state
11026            if running_only && job.state != JobState::Running {
11027                continue;
11028            }
11029            if stopped_only && job.state != JobState::Stopped {
11030                continue;
11031            }
11032
11033            if pids_only {
11034                println!("{}", job.pid);
11035                continue;
11036            }
11037
11038            let marker = if job.is_current { "+" } else { "-" };
11039            let state = match job.state {
11040                JobState::Running => "running",
11041                JobState::Stopped => "suspended",
11042                JobState::Done => "done",
11043            };
11044
11045            if long_format {
11046                println!(
11047                    "[{}]{} {:6} {}  {}",
11048                    job.id, marker, job.pid, state, job.command
11049                );
11050            } else {
11051                println!("[{}]{} {}  {}", job.id, marker, state, job.command);
11052            }
11053
11054            if show_dir {
11055                if let Ok(cwd) = env::current_dir() {
11056                    println!("    (pwd: {})", cwd.display());
11057                }
11058            }
11059        }
11060        0
11061    }
11062
11063    fn builtin_fg(&mut self, args: &[String]) -> i32 {
11064        let job_id = if let Some(arg) = args.first() {
11065            // Parse %N or just N
11066            let s = arg.trim_start_matches('%');
11067            match s.parse::<usize>() {
11068                Ok(id) => Some(id),
11069                Err(_) => {
11070                    eprintln!("fg: {}: no such job", arg);
11071                    return 1;
11072                }
11073            }
11074        } else {
11075            self.jobs.current().map(|j| j.id)
11076        };
11077
11078        let Some(id) = job_id else {
11079            eprintln!("fg: no current job");
11080            return 1;
11081        };
11082
11083        let Some(job) = self.jobs.get(id) else {
11084            eprintln!("fg: %{}: no such job", id);
11085            return 1;
11086        };
11087
11088        let pid = job.pid;
11089        let cmd = job.command.clone();
11090        println!("{}", cmd);
11091
11092        // Continue the job
11093        if let Err(e) = continue_job(pid) {
11094            eprintln!("fg: {}", e);
11095            return 1;
11096        }
11097
11098        // Wait for it
11099        match wait_for_job(pid) {
11100            Ok(status) => {
11101                self.jobs.remove(id);
11102                status
11103            }
11104            Err(e) => {
11105                eprintln!("fg: {}", e);
11106                1
11107            }
11108        }
11109    }
11110
11111    fn builtin_bg(&mut self, args: &[String]) -> i32 {
11112        let job_id = if let Some(arg) = args.first() {
11113            let s = arg.trim_start_matches('%');
11114            match s.parse::<usize>() {
11115                Ok(id) => Some(id),
11116                Err(_) => {
11117                    eprintln!("bg: {}: no such job", arg);
11118                    return 1;
11119                }
11120            }
11121        } else {
11122            self.jobs.current().map(|j| j.id)
11123        };
11124
11125        let Some(id) = job_id else {
11126            eprintln!("bg: no current job");
11127            return 1;
11128        };
11129
11130        let Some(job) = self.jobs.get_mut(id) else {
11131            eprintln!("bg: %{}: no such job", id);
11132            return 1;
11133        };
11134
11135        let pid = job.pid;
11136        let cmd = job.command.clone();
11137
11138        if let Err(e) = continue_job(pid) {
11139            eprintln!("bg: {}", e);
11140            return 1;
11141        }
11142
11143        job.state = JobState::Running;
11144        println!("[{}] {} &", id, cmd);
11145        0
11146    }
11147
11148    fn builtin_kill(&mut self, args: &[String]) -> i32 {
11149        // kill [ -s signal_name | -n signal_number | -sig ] job ...
11150        // kill -l [ sig ... ]
11151        use crate::jobs::send_signal;
11152        use nix::sys::signal::Signal;
11153
11154        if args.is_empty() {
11155            eprintln!("kill: usage: kill [-s signal | -n num | -sig] pid ...");
11156            eprintln!("       kill -l [sig ...]");
11157            return 1;
11158        }
11159
11160        // Signal name/number mapping
11161        let signal_map: &[(&str, i32, Signal)] = &[
11162            ("HUP", 1, Signal::SIGHUP),
11163            ("INT", 2, Signal::SIGINT),
11164            ("QUIT", 3, Signal::SIGQUIT),
11165            ("ILL", 4, Signal::SIGILL),
11166            ("TRAP", 5, Signal::SIGTRAP),
11167            ("ABRT", 6, Signal::SIGABRT),
11168            ("BUS", 7, Signal::SIGBUS),
11169            ("FPE", 8, Signal::SIGFPE),
11170            ("KILL", 9, Signal::SIGKILL),
11171            ("USR1", 10, Signal::SIGUSR1),
11172            ("SEGV", 11, Signal::SIGSEGV),
11173            ("USR2", 12, Signal::SIGUSR2),
11174            ("PIPE", 13, Signal::SIGPIPE),
11175            ("ALRM", 14, Signal::SIGALRM),
11176            ("TERM", 15, Signal::SIGTERM),
11177            ("CHLD", 17, Signal::SIGCHLD),
11178            ("CONT", 18, Signal::SIGCONT),
11179            ("STOP", 19, Signal::SIGSTOP),
11180            ("TSTP", 20, Signal::SIGTSTP),
11181            ("TTIN", 21, Signal::SIGTTIN),
11182            ("TTOU", 22, Signal::SIGTTOU),
11183            ("URG", 23, Signal::SIGURG),
11184            ("XCPU", 24, Signal::SIGXCPU),
11185            ("XFSZ", 25, Signal::SIGXFSZ),
11186            ("VTALRM", 26, Signal::SIGVTALRM),
11187            ("PROF", 27, Signal::SIGPROF),
11188            ("WINCH", 28, Signal::SIGWINCH),
11189            ("IO", 29, Signal::SIGIO),
11190            ("SYS", 31, Signal::SIGSYS),
11191        ];
11192
11193        let mut sig = Signal::SIGTERM;
11194        let mut pids: Vec<String> = Vec::new();
11195        let mut list_mode = false;
11196        let mut list_args: Vec<String> = Vec::new();
11197
11198        let mut i = 0;
11199        while i < args.len() {
11200            let arg = &args[i];
11201
11202            if arg == "-l" || arg == "-L" {
11203                list_mode = true;
11204                // Remaining args are signal numbers to translate
11205                list_args = args[i + 1..].to_vec();
11206                break;
11207            } else if arg == "-s" {
11208                // -s signal_name
11209                i += 1;
11210                if i >= args.len() {
11211                    eprintln!("kill: -s requires an argument");
11212                    return 1;
11213                }
11214                let sig_name = args[i].to_uppercase();
11215                let sig_name = sig_name.strip_prefix("SIG").unwrap_or(&sig_name);
11216                if let Some((_, _, s)) = signal_map.iter().find(|(name, _, _)| *name == sig_name) {
11217                    sig = *s;
11218                } else {
11219                    eprintln!("kill: invalid signal: {}", args[i]);
11220                    return 1;
11221                }
11222            } else if arg == "-n" {
11223                // -n signal_number
11224                i += 1;
11225                if i >= args.len() {
11226                    eprintln!("kill: -n requires an argument");
11227                    return 1;
11228                }
11229                let num: i32 = match args[i].parse() {
11230                    Ok(n) => n,
11231                    Err(_) => {
11232                        eprintln!("kill: invalid signal number: {}", args[i]);
11233                        return 1;
11234                    }
11235                };
11236                if let Some((_, _, s)) = signal_map.iter().find(|(_, n, _)| *n == num) {
11237                    sig = *s;
11238                } else {
11239                    eprintln!("kill: invalid signal number: {}", num);
11240                    return 1;
11241                }
11242            } else if arg.starts_with('-') && arg.len() > 1 {
11243                // -SIGNAL or -NUM
11244                let sig_str = &arg[1..];
11245                let sig_upper = sig_str.to_uppercase();
11246                let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
11247
11248                // Try as number first
11249                if let Ok(num) = sig_str.parse::<i32>() {
11250                    if let Some((_, _, s)) = signal_map.iter().find(|(_, n, _)| *n == num) {
11251                        sig = *s;
11252                    } else {
11253                        eprintln!("kill: invalid signal: {}", arg);
11254                        return 1;
11255                    }
11256                } else if let Some((_, _, s)) =
11257                    signal_map.iter().find(|(name, _, _)| *name == sig_name)
11258                {
11259                    sig = *s;
11260                } else {
11261                    eprintln!("kill: invalid signal: {}", arg);
11262                    return 1;
11263                }
11264            } else {
11265                pids.push(arg.clone());
11266            }
11267            i += 1;
11268        }
11269
11270        // Handle -l (list signals)
11271        if list_mode {
11272            if list_args.is_empty() {
11273                // List all signals
11274                for (name, num, _) in signal_map {
11275                    println!("{:2}) SIG{}", num, name);
11276                }
11277            } else {
11278                // Translate signal numbers to names or vice versa
11279                for arg in &list_args {
11280                    if let Ok(num) = arg.parse::<i32>() {
11281                        // Number -> name
11282                        if let Some((name, _, _)) = signal_map.iter().find(|(_, n, _)| *n == num) {
11283                            println!("{}", name);
11284                        } else {
11285                            eprintln!("kill: unknown signal: {}", num);
11286                        }
11287                    } else {
11288                        // Name -> number
11289                        let sig_upper = arg.to_uppercase();
11290                        let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
11291                        if let Some((_, num, _)) =
11292                            signal_map.iter().find(|(name, _, _)| *name == sig_name)
11293                        {
11294                            println!("{}", num);
11295                        } else {
11296                            eprintln!("kill: unknown signal: {}", arg);
11297                        }
11298                    }
11299                }
11300            }
11301            return 0;
11302        }
11303
11304        if pids.is_empty() {
11305            eprintln!("kill: usage: kill [-s signal | -n num | -sig] pid ...");
11306            return 1;
11307        }
11308
11309        let mut status = 0;
11310        for arg in &pids {
11311            // Handle %job syntax
11312            if arg.starts_with('%') {
11313                let id: usize = match arg[1..].parse() {
11314                    Ok(id) => id,
11315                    Err(_) => {
11316                        eprintln!("kill: {}: no such job", arg);
11317                        status = 1;
11318                        continue;
11319                    }
11320                };
11321                if let Some(job) = self.jobs.get(id) {
11322                    if let Err(e) = send_signal(job.pid, sig) {
11323                        eprintln!("kill: {}", e);
11324                        status = 1;
11325                    }
11326                } else {
11327                    eprintln!("kill: {}: no such job", arg);
11328                    status = 1;
11329                }
11330            } else {
11331                // Direct PID
11332                let pid: u32 = match arg.parse() {
11333                    Ok(p) => p,
11334                    Err(_) => {
11335                        eprintln!("kill: {}: invalid pid", arg);
11336                        status = 1;
11337                        continue;
11338                    }
11339                };
11340                if let Err(e) = send_signal(pid as i32, sig) {
11341                    eprintln!("kill: {}", e);
11342                    status = 1;
11343                }
11344            }
11345        }
11346        status
11347    }
11348
11349    fn builtin_disown(&mut self, args: &[String]) -> i32 {
11350        if args.is_empty() {
11351            // Disown current job
11352            if let Some(job) = self.jobs.current() {
11353                let id = job.id;
11354                self.jobs.remove(id);
11355            }
11356            return 0;
11357        }
11358
11359        for arg in args {
11360            let s = arg.trim_start_matches('%');
11361            if let Ok(id) = s.parse::<usize>() {
11362                self.jobs.remove(id);
11363            } else {
11364                eprintln!("disown: {}: no such job", arg);
11365            }
11366        }
11367        0
11368    }
11369
11370    fn builtin_wait(&mut self, args: &[String]) -> i32 {
11371        if args.is_empty() {
11372            // Wait for all jobs
11373            let ids: Vec<usize> = self.jobs.list().iter().map(|j| j.id).collect();
11374            for id in ids {
11375                if let Some(mut job) = self.jobs.remove(id) {
11376                    if let Some(ref mut child) = job.child {
11377                        let _ = wait_for_child(child);
11378                    }
11379                }
11380            }
11381            return 0;
11382        }
11383
11384        let mut status = 0;
11385        for arg in args {
11386            if arg.starts_with('%') {
11387                let id: usize = match arg[1..].parse() {
11388                    Ok(id) => id,
11389                    Err(_) => {
11390                        eprintln!("wait: {}: no such job", arg);
11391                        status = 127;
11392                        continue;
11393                    }
11394                };
11395                if let Some(mut job) = self.jobs.remove(id) {
11396                    if let Some(ref mut child) = job.child {
11397                        match wait_for_child(child) {
11398                            Ok(s) => status = s,
11399                            Err(e) => {
11400                                eprintln!("wait: {}", e);
11401                                status = 127;
11402                            }
11403                        }
11404                    }
11405                } else {
11406                    eprintln!("wait: {}: no such job", arg);
11407                    status = 127;
11408                }
11409            } else {
11410                let pid: u32 = match arg.parse() {
11411                    Ok(p) => p,
11412                    Err(_) => {
11413                        eprintln!("wait: {}: invalid pid", arg);
11414                        status = 127;
11415                        continue;
11416                    }
11417                };
11418                match wait_for_job(pid as i32) {
11419                    Ok(s) => status = s,
11420                    Err(e) => {
11421                        eprintln!("wait: {}", e);
11422                        status = 127;
11423                    }
11424                }
11425            }
11426        }
11427        status
11428    }
11429
11430    fn builtin_suspend(&self, args: &[String]) -> i32 {
11431        let mut force = false;
11432        for arg in args {
11433            if arg == "-f" {
11434                force = true;
11435            }
11436        }
11437
11438        #[cfg(unix)]
11439        {
11440            use nix::sys::signal::{kill, Signal};
11441            use nix::unistd::getppid;
11442
11443            // Check if we're a login shell (parent is init/PID 1)
11444            let ppid = getppid();
11445            if !force && ppid == nix::unistd::Pid::from_raw(1) {
11446                eprintln!("suspend: cannot suspend a login shell");
11447                return 1;
11448            }
11449
11450            // Send SIGTSTP to ourselves
11451            let pid = nix::unistd::getpid();
11452            if let Err(e) = kill(pid, Signal::SIGTSTP) {
11453                eprintln!("suspend: {}", e);
11454                return 1;
11455            }
11456            0
11457        }
11458
11459        #[cfg(not(unix))]
11460        {
11461            eprintln!("suspend: not supported on this platform");
11462            1
11463        }
11464    }
11465}
11466
11467impl Default for ShellExecutor {
11468    fn default() -> Self {
11469        Self::new()
11470    }
11471}
11472
11473#[cfg(test)]
11474mod tests {
11475    use super::*;
11476
11477    #[test]
11478    fn test_simple_echo() {
11479        let mut exec = ShellExecutor::new();
11480        let status = exec.execute_script("true").unwrap();
11481        assert_eq!(status, 0);
11482    }
11483
11484    #[test]
11485    fn test_if_true() {
11486        let mut exec = ShellExecutor::new();
11487        let status = exec.execute_script("if true; then true; fi").unwrap();
11488        assert_eq!(status, 0);
11489    }
11490
11491    #[test]
11492    fn test_if_false() {
11493        let mut exec = ShellExecutor::new();
11494        let status = exec
11495            .execute_script("if false; then true; else false; fi")
11496            .unwrap();
11497        assert_eq!(status, 1);
11498    }
11499
11500    #[test]
11501    fn test_for_loop() {
11502        let mut exec = ShellExecutor::new();
11503        exec.execute_script("for i in a b c; do true; done")
11504            .unwrap();
11505        assert_eq!(exec.last_status, 0);
11506    }
11507
11508    #[test]
11509    fn test_and_list() {
11510        let mut exec = ShellExecutor::new();
11511        let status = exec.execute_script("true && true").unwrap();
11512        assert_eq!(status, 0);
11513
11514        let status = exec.execute_script("true && false").unwrap();
11515        assert_eq!(status, 1);
11516    }
11517
11518    #[test]
11519    fn test_or_list() {
11520        let mut exec = ShellExecutor::new();
11521        let status = exec.execute_script("false || true").unwrap();
11522        assert_eq!(status, 0);
11523    }
11524}
11525
11526impl ShellExecutor {
11527    fn builtin_history(&self, args: &[String]) -> i32 {
11528        let Some(ref engine) = self.history else {
11529            eprintln!("history: history engine not available");
11530            return 1;
11531        };
11532
11533        // Parse options
11534        let mut count = 20usize;
11535        let mut show_all = false;
11536        let mut search_query = None;
11537
11538        let mut i = 0;
11539        while i < args.len() {
11540            match args[i].as_str() {
11541                "-c" | "--clear" => {
11542                    // Clear history - need mutable access
11543                    eprintln!("history: clear not supported in this mode");
11544                    return 1;
11545                }
11546                "-a" | "--all" => show_all = true,
11547                "-n" => {
11548                    if i + 1 < args.len() {
11549                        i += 1;
11550                        count = args[i].parse().unwrap_or(20);
11551                    }
11552                }
11553                s if s.starts_with('-') && s[1..].chars().all(|c| c.is_ascii_digit()) => {
11554                    count = s[1..].parse().unwrap_or(20);
11555                }
11556                s if s.chars().all(|c| c.is_ascii_digit()) => {
11557                    count = s.parse().unwrap_or(20);
11558                }
11559                s => {
11560                    search_query = Some(s.to_string());
11561                }
11562            }
11563            i += 1;
11564        }
11565
11566        if show_all {
11567            count = 10000;
11568        }
11569
11570        let entries = if let Some(ref q) = search_query {
11571            engine.search(q, count)
11572        } else {
11573            engine.recent(count)
11574        };
11575
11576        match entries {
11577            Ok(entries) => {
11578                // Print in chronological order (reverse the results since recent() is newest-first)
11579                for entry in entries.into_iter().rev() {
11580                    println!("{:>6}  {}", entry.id, entry.command);
11581                }
11582                0
11583            }
11584            Err(e) => {
11585                eprintln!("history: {}", e);
11586                1
11587            }
11588        }
11589    }
11590
11591    /// fc builtin - fix command (history manipulation)
11592    /// Ported from zsh/Src/builtin.c bin_fc() lines 1426-1700
11593    /// Options: -l (list), -n (no numbers), -r (reverse), -d/-f/-E/-i/-t (time formats),
11594    /// -D (duration), -e editor, -m pattern, -R/-W/-A (read/write/append history file),
11595    /// -p/-P (push/pop history stack), -I (skip old), -L (local), -s (substitute)
11596    fn builtin_fc(&mut self, args: &[String]) -> i32 {
11597        let Some(ref engine) = self.history else {
11598            eprintln!("fc: history engine not available");
11599            return 1;
11600        };
11601
11602        // Parse options
11603        let mut list_mode = false;
11604        let mut no_numbers = false;
11605        let mut reverse = false;
11606        let mut show_time = false;
11607        let mut show_duration = false;
11608        let mut editor: Option<String> = None;
11609        let mut read_file = false;
11610        let mut write_file = false;
11611        let mut append_file = false;
11612        let mut substitute_mode = false;
11613        let mut positional: Vec<&str> = Vec::new();
11614        let mut substitutions: Vec<(String, String)> = Vec::new();
11615
11616        let mut i = 0;
11617        while i < args.len() {
11618            let arg = &args[i];
11619            if arg == "--" {
11620                i += 1;
11621                while i < args.len() {
11622                    positional.push(&args[i]);
11623                    i += 1;
11624                }
11625                break;
11626            }
11627            if arg.starts_with('-') && arg.len() > 1 {
11628                let chars: Vec<char> = arg[1..].chars().collect();
11629                let mut j = 0;
11630                while j < chars.len() {
11631                    match chars[j] {
11632                        'l' => list_mode = true,
11633                        'n' => no_numbers = true,
11634                        'r' => reverse = true,
11635                        'd' | 'f' | 'E' | 'i' => show_time = true,
11636                        'D' => show_duration = true,
11637                        'R' => read_file = true,
11638                        'W' => write_file = true,
11639                        'A' => append_file = true,
11640                        's' => substitute_mode = true,
11641                        'e' => {
11642                            if j + 1 < chars.len() {
11643                                editor = Some(chars[j + 1..].iter().collect());
11644                                break;
11645                            } else {
11646                                i += 1;
11647                                if i < args.len() {
11648                                    editor = Some(args[i].clone());
11649                                }
11650                            }
11651                        }
11652                        't' => {
11653                            show_time = true;
11654                            if j + 1 < chars.len() {
11655                                break;
11656                            } else {
11657                                i += 1;
11658                            }
11659                        }
11660                        'p' | 'P' | 'a' | 'I' | 'L' | 'm' => {} // Handled but no-op for now
11661                        _ => {
11662                            if chars[j].is_ascii_digit() {
11663                                positional.push(arg);
11664                                break;
11665                            }
11666                        }
11667                    }
11668                    j += 1;
11669                }
11670            } else if arg.contains('=') && !list_mode {
11671                if let Some((old, new)) = arg.split_once('=') {
11672                    substitutions.push((old.to_string(), new.to_string()));
11673                }
11674            } else {
11675                positional.push(arg);
11676            }
11677            i += 1;
11678        }
11679
11680        // Handle file operations (read/write/append)
11681        // Note: HistoryEngine uses SQLite, so file ops are simplified
11682        if read_file || write_file || append_file {
11683            let filename = positional.first().map(|s| *s).unwrap_or("~/.zsh_history");
11684            let path = if filename.starts_with("~/") {
11685                dirs::home_dir()
11686                    .map(|h| h.join(&filename[2..]))
11687                    .unwrap_or_else(|| std::path::PathBuf::from(filename))
11688            } else {
11689                std::path::PathBuf::from(filename)
11690            };
11691
11692            if read_file {
11693                // Read plain text history file and import
11694                if let Ok(contents) = std::fs::read_to_string(&path) {
11695                    for line in contents.lines() {
11696                        if !line.is_empty() && !line.starts_with('#') && !line.starts_with(':') {
11697                            let _ = engine.add(line, None);
11698                        }
11699                    }
11700                } else {
11701                    eprintln!("fc: cannot read {}", path.display());
11702                    return 1;
11703                }
11704            } else if write_file || append_file {
11705                // Export history to plain text file
11706                let mode = if append_file {
11707                    std::fs::OpenOptions::new()
11708                        .create(true)
11709                        .append(true)
11710                        .open(&path)
11711                } else {
11712                    std::fs::File::create(&path)
11713                };
11714                match mode {
11715                    Ok(mut file) => {
11716                        use std::io::Write;
11717                        if let Ok(entries) = engine.recent(10000) {
11718                            for entry in entries.iter().rev() {
11719                                let _ = writeln!(file, ": {}:0;{}", entry.timestamp, entry.command);
11720                            }
11721                        }
11722                    }
11723                    Err(e) => {
11724                        eprintln!("fc: cannot write {}: {}", path.display(), e);
11725                        return 1;
11726                    }
11727                }
11728            }
11729            return 0;
11730        }
11731
11732        // List mode (fc -l)
11733        if list_mode || args.is_empty() {
11734            let (first, last) = match positional.len() {
11735                0 => (-16i64, -1i64),
11736                1 => {
11737                    let n = positional[0].parse::<i64>().unwrap_or(-16);
11738                    (n, -1)
11739                }
11740                _ => {
11741                    let f = positional[0].parse::<i64>().unwrap_or(-16);
11742                    let l = positional[1].parse::<i64>().unwrap_or(-1);
11743                    (f, l)
11744                }
11745            };
11746
11747            let count = if first < 0 { (-first) as usize } else { 16 };
11748            match engine.recent(count.max(100)) {
11749                Ok(mut entries) => {
11750                    if reverse {
11751                        entries.reverse();
11752                    }
11753                    for entry in entries.iter().rev().take(count) {
11754                        if no_numbers {
11755                            println!("{}", entry.command);
11756                        } else if show_time {
11757                            println!(
11758                                "{:>6}  {:>10}  {}",
11759                                entry.id, entry.timestamp, entry.command
11760                            );
11761                        } else if show_duration {
11762                            println!(
11763                                "{:>6}  {:>5}  {}",
11764                                entry.id,
11765                                entry.duration_ms.unwrap_or(0),
11766                                entry.command
11767                            );
11768                        } else {
11769                            println!("{:>6}  {}", entry.id, entry.command);
11770                        }
11771                    }
11772                    0
11773                }
11774                Err(e) => {
11775                    eprintln!("fc: {}", e);
11776                    1
11777                }
11778            }
11779        } else if substitute_mode || !substitutions.is_empty() {
11780            // Substitution mode: fc -s old=new
11781            match engine.get_by_offset(0) {
11782                Ok(Some(entry)) => {
11783                    let mut cmd = entry.command.clone();
11784                    for (old, new) in &substitutions {
11785                        cmd = cmd.replace(old, new);
11786                    }
11787                    println!("{}", cmd);
11788                    self.execute_script(&cmd).unwrap_or(1)
11789                }
11790                Ok(None) => {
11791                    eprintln!("fc: no command to re-execute");
11792                    1
11793                }
11794                Err(e) => {
11795                    eprintln!("fc: {}", e);
11796                    1
11797                }
11798            }
11799        } else if editor.as_deref() == Some("-") {
11800            // fc -e -: re-execute last command without editor
11801            match engine.get_by_offset(0) {
11802                Ok(Some(entry)) => {
11803                    println!("{}", entry.command);
11804                    self.execute_script(&entry.command).unwrap_or(1)
11805                }
11806                Ok(None) => {
11807                    eprintln!("fc: no command to re-execute");
11808                    1
11809                }
11810                Err(e) => {
11811                    eprintln!("fc: {}", e);
11812                    1
11813                }
11814            }
11815        } else if let Some(arg) = positional.first() {
11816            if arg.starts_with('-') || arg.starts_with('+') {
11817                // fc -N or fc +N: re-execute Nth command
11818                let n: usize = arg[1..].parse().unwrap_or(1);
11819                let offset = if arg.starts_with('-') { n - 1 } else { n };
11820                match engine.get_by_offset(offset) {
11821                    Ok(Some(entry)) => {
11822                        println!("{}", entry.command);
11823                        self.execute_script(&entry.command).unwrap_or(1)
11824                    }
11825                    Ok(None) => {
11826                        eprintln!("fc: event not found");
11827                        1
11828                    }
11829                    Err(e) => {
11830                        eprintln!("fc: {}", e);
11831                        1
11832                    }
11833                }
11834            } else {
11835                // Try to find command by prefix
11836                match engine.search_prefix(arg, 1) {
11837                    Ok(entries) if !entries.is_empty() => {
11838                        println!("{}", entries[0].command);
11839                        self.execute_script(&entries[0].command).unwrap_or(1)
11840                    }
11841                    Ok(_) => {
11842                        eprintln!("fc: event not found: {}", arg);
11843                        1
11844                    }
11845                    Err(e) => {
11846                        eprintln!("fc: {}", e);
11847                        1
11848                    }
11849                }
11850            }
11851        } else {
11852            // Default: edit and execute last command
11853            match engine.get_by_offset(0) {
11854                Ok(Some(entry)) => {
11855                    println!("{}", entry.command);
11856                    self.execute_script(&entry.command).unwrap_or(1)
11857                }
11858                Ok(None) => {
11859                    eprintln!("fc: no command to re-execute");
11860                    1
11861                }
11862                Err(e) => {
11863                    eprintln!("fc: {}", e);
11864                    1
11865                }
11866            }
11867        }
11868    }
11869
11870    fn builtin_trap(&mut self, args: &[String]) -> i32 {
11871        if args.is_empty() {
11872            // List all traps
11873            for (sig, action) in &self.traps {
11874                println!("trap -- '{}' {}", action, sig);
11875            }
11876            return 0;
11877        }
11878
11879        // trap -l: list signal names
11880        if args.len() == 1 && args[0] == "-l" {
11881            let signals = [
11882                "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
11883                "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN",
11884                "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS",
11885            ];
11886            for (i, sig) in signals.iter().enumerate() {
11887                print!("{:2}) SIG{:<8}", i + 1, sig);
11888                if (i + 1) % 5 == 0 {
11889                    println!();
11890                }
11891            }
11892            println!();
11893            return 0;
11894        }
11895
11896        // trap -p [sigspec...]: print trap commands
11897        if args.len() >= 1 && args[0] == "-p" {
11898            let signals = if args.len() > 1 {
11899                &args[1..]
11900            } else {
11901                &[] as &[String]
11902            };
11903            if signals.is_empty() {
11904                for (sig, action) in &self.traps {
11905                    println!("trap -- '{}' {}", action, sig);
11906                }
11907            } else {
11908                for sig in signals {
11909                    if let Some(action) = self.traps.get(sig) {
11910                        println!("trap -- '{}' {}", action, sig);
11911                    }
11912                }
11913            }
11914            return 0;
11915        }
11916
11917        // trap '' signal: reset to default
11918        // trap action signal...: set trap
11919        // trap signal: print current action for signal
11920        if args.len() == 1 {
11921            // Print trap for this signal
11922            let sig = &args[0];
11923            if let Some(action) = self.traps.get(sig) {
11924                println!("trap -- '{}' {}", action, sig);
11925            }
11926            return 0;
11927        }
11928
11929        let action = &args[0];
11930        let signals = &args[1..];
11931
11932        for sig in signals {
11933            let sig_upper = sig.to_uppercase();
11934            let sig_name = if sig_upper.starts_with("SIG") {
11935                sig_upper[3..].to_string()
11936            } else {
11937                sig_upper.clone()
11938            };
11939
11940            if action.is_empty() || action == "-" {
11941                // Reset to default
11942                self.traps.remove(&sig_name);
11943            } else {
11944                self.traps.insert(sig_name, action.clone());
11945            }
11946        }
11947
11948        0
11949    }
11950
11951    /// Execute trap handlers for a signal
11952    pub fn run_trap(&mut self, signal: &str) {
11953        if let Some(action) = self.traps.get(signal).cloned() {
11954            let _ = self.execute_script(&action);
11955        }
11956    }
11957
11958    fn builtin_alias(&mut self, args: &[String]) -> i32 {
11959        // alias [ {+|-}gmrsL ] [ name[=value] ... ]
11960        // -g: global alias (expanded anywhere in command line)
11961        // -s: suffix alias (file.ext expands to "handler file.ext")
11962        // -r: regular alias (default)
11963        // -m: pattern match mode
11964        // -L: list in form suitable for reinput
11965        // +g/+s/+r: print aliases of that type
11966
11967        let mut is_global = false;
11968        let mut is_suffix = false;
11969        let mut list_form = false;
11970        let mut pattern_match = false;
11971        let mut print_global = false;
11972        let mut print_suffix = false;
11973        let mut print_regular = false;
11974        let mut positional_args = Vec::new();
11975
11976        let mut i = 0;
11977        while i < args.len() {
11978            let arg = &args[i];
11979            if arg.starts_with('+') && arg.len() > 1 {
11980                // +g, +s, +r: print aliases of that type
11981                for ch in arg[1..].chars() {
11982                    match ch {
11983                        'g' => print_global = true,
11984                        's' => print_suffix = true,
11985                        'r' => print_regular = true,
11986                        'L' => list_form = true,
11987                        'm' => pattern_match = true,
11988                        _ => {}
11989                    }
11990                }
11991            } else if arg.starts_with('-') && arg != "-" {
11992                for ch in arg[1..].chars() {
11993                    match ch {
11994                        'g' => is_global = true,
11995                        's' => is_suffix = true,
11996                        'L' => list_form = true,
11997                        'm' => pattern_match = true,
11998                        'r' => {} // regular alias (default)
11999                        _ => {
12000                            eprintln!("zshrs: alias: bad option: -{}", ch);
12001                            return 1;
12002                        }
12003                    }
12004                }
12005            } else {
12006                positional_args.push(arg.clone());
12007            }
12008            i += 1;
12009        }
12010
12011        // If +g/+s/+r used, list those types
12012        if print_global || print_suffix || print_regular {
12013            if print_regular {
12014                for (name, value) in &self.aliases {
12015                    if list_form {
12016                        println!("alias {}='{}'", name, value);
12017                    } else {
12018                        println!("{}='{}'", name, value);
12019                    }
12020                }
12021            }
12022            if print_global {
12023                for (name, value) in &self.global_aliases {
12024                    if list_form {
12025                        println!("alias -g {}='{}'", name, value);
12026                    } else {
12027                        println!("{}='{}'", name, value);
12028                    }
12029                }
12030            }
12031            if print_suffix {
12032                for (name, value) in &self.suffix_aliases {
12033                    if list_form {
12034                        println!("alias -s {}='{}'", name, value);
12035                    } else {
12036                        println!("{}='{}'", name, value);
12037                    }
12038                }
12039            }
12040            return 0;
12041        }
12042
12043        if positional_args.is_empty() {
12044            // List aliases
12045            let prefix = if is_suffix {
12046                "alias -s "
12047            } else if is_global {
12048                "alias -g "
12049            } else {
12050                "alias "
12051            };
12052            let alias_map: Vec<(String, String)> = if is_suffix {
12053                self.suffix_aliases
12054                    .iter()
12055                    .map(|(k, v)| (k.clone(), v.clone()))
12056                    .collect()
12057            } else if is_global {
12058                self.global_aliases
12059                    .iter()
12060                    .map(|(k, v)| (k.clone(), v.clone()))
12061                    .collect()
12062            } else {
12063                self.aliases
12064                    .iter()
12065                    .map(|(k, v)| (k.clone(), v.clone()))
12066                    .collect()
12067            };
12068            for (name, value) in alias_map {
12069                if list_form {
12070                    println!("{}{}='{}'", prefix, name, value);
12071                } else {
12072                    println!("{}='{}'", name, value);
12073                }
12074            }
12075            return 0;
12076        }
12077
12078        for arg in &positional_args {
12079            if let Some(eq_pos) = arg.find('=') {
12080                // Define alias: name=value
12081                let name = &arg[..eq_pos];
12082                let value = &arg[eq_pos + 1..];
12083                if is_suffix {
12084                    self.suffix_aliases
12085                        .insert(name.to_string(), value.to_string());
12086                } else if is_global {
12087                    self.global_aliases
12088                        .insert(name.to_string(), value.to_string());
12089                } else {
12090                    self.aliases.insert(name.to_string(), value.to_string());
12091                }
12092            } else if pattern_match {
12093                // -m: pattern match mode - list matching aliases
12094                let pattern = arg.replace("*", ".*").replace("?", ".");
12095                let re = regex::Regex::new(&format!("^{}$", pattern));
12096
12097                let alias_map: &HashMap<String, String> = if is_suffix {
12098                    &self.suffix_aliases
12099                } else if is_global {
12100                    &self.global_aliases
12101                } else {
12102                    &self.aliases
12103                };
12104
12105                let prefix = if is_suffix {
12106                    "alias -s "
12107                } else if is_global {
12108                    "alias -g "
12109                } else {
12110                    "alias "
12111                };
12112
12113                for (name, value) in alias_map {
12114                    let matches = if let Ok(ref r) = re {
12115                        r.is_match(name)
12116                    } else {
12117                        name.contains(arg.as_str())
12118                    };
12119                    if matches {
12120                        if list_form {
12121                            println!("{}{}='{}'", prefix, name, value);
12122                        } else {
12123                            println!("{}='{}'", name, value);
12124                        }
12125                    }
12126                }
12127            } else {
12128                // Print alias - look up directly without holding borrow
12129                let value = if is_suffix {
12130                    self.suffix_aliases.get(arg.as_str()).cloned()
12131                } else if is_global {
12132                    self.global_aliases.get(arg.as_str()).cloned()
12133                } else {
12134                    self.aliases.get(arg.as_str()).cloned()
12135                };
12136                if let Some(v) = value {
12137                    println!("{}='{}'", arg, v);
12138                } else {
12139                    eprintln!("zshrs: alias: {}: not found", arg);
12140                    return 1;
12141                }
12142            }
12143        }
12144        0
12145    }
12146
12147    fn builtin_unalias(&mut self, args: &[String]) -> i32 {
12148        if args.is_empty() {
12149            eprintln!("zshrs: unalias: usage: unalias [-agsm] name [name ...]");
12150            return 1;
12151        }
12152
12153        let mut is_global = false;
12154        let mut is_suffix = false;
12155        let mut remove_all = false;
12156        let mut positional_args = Vec::new();
12157
12158        for arg in args {
12159            if arg.starts_with('-') && arg != "-" {
12160                for ch in arg[1..].chars() {
12161                    match ch {
12162                        'a' => remove_all = true,
12163                        'g' => is_global = true,
12164                        's' => is_suffix = true,
12165                        'm' => {} // pattern match, ignore for now
12166                        _ => {
12167                            eprintln!("zshrs: unalias: bad option: -{}", ch);
12168                            return 1;
12169                        }
12170                    }
12171                }
12172            } else {
12173                positional_args.push(arg.clone());
12174            }
12175        }
12176
12177        if remove_all {
12178            if is_suffix {
12179                self.suffix_aliases.clear();
12180            } else if is_global {
12181                self.global_aliases.clear();
12182            } else {
12183                // -a without -g/-s clears all three
12184                self.aliases.clear();
12185                self.global_aliases.clear();
12186                self.suffix_aliases.clear();
12187            }
12188            return 0;
12189        }
12190
12191        if positional_args.is_empty() {
12192            eprintln!("zshrs: unalias: usage: unalias [-agsm] name [name ...]");
12193            return 1;
12194        }
12195
12196        for name in positional_args {
12197            let removed = if is_suffix {
12198                self.suffix_aliases.remove(&name).is_some()
12199            } else if is_global {
12200                self.global_aliases.remove(&name).is_some()
12201            } else {
12202                self.aliases.remove(&name).is_some()
12203            };
12204            if !removed {
12205                eprintln!("zshrs: unalias: {}: not found", name);
12206                return 1;
12207            }
12208        }
12209        0
12210    }
12211
12212    fn builtin_set(&mut self, args: &[String]) -> i32 {
12213        if args.is_empty() {
12214            // List all variables and their values (zsh behavior)
12215            let mut vars: Vec<_> = self.variables.iter().collect();
12216            vars.sort_by_key(|(k, _)| *k);
12217            for (k, v) in vars {
12218                println!("{}={}", k, shell_quote(v));
12219            }
12220            // Also print arrays
12221            let mut arrs: Vec<_> = self.arrays.iter().collect();
12222            arrs.sort_by_key(|(k, _)| *k);
12223            for (k, v) in arrs {
12224                let quoted: Vec<String> = v.iter().map(|s| shell_quote(s)).collect();
12225                println!("{}=( {} )", k, quoted.join(" "));
12226            }
12227            return 0;
12228        }
12229
12230        // Check for "+" alone - print just variable names
12231        if args.len() == 1 && args[0] == "+" {
12232            let mut names: Vec<_> = self.variables.keys().collect();
12233            names.extend(self.arrays.keys());
12234            names.sort();
12235            names.dedup();
12236            for name in names {
12237                println!("{}", name);
12238            }
12239            return 0;
12240        }
12241
12242        let mut iter = args.iter().peekable();
12243        let mut set_array: Option<bool> = None; // Some(true) = -A, Some(false) = +A
12244        let mut array_name: Option<String> = None;
12245        let mut sort_asc = false;
12246        let mut sort_desc = false;
12247
12248        while let Some(arg) = iter.next() {
12249            match arg.as_str() {
12250                "-o" => {
12251                    // -o with no arg: print all options in "option on/off" format
12252                    if iter.peek().is_none()
12253                        || iter
12254                            .peek()
12255                            .map(|s| s.starts_with('-') || s.starts_with('+'))
12256                            .unwrap_or(false)
12257                    {
12258                        self.print_options_table();
12259                        continue;
12260                    }
12261                    if let Some(opt) = iter.next() {
12262                        let (name, enable) = Self::normalize_option_name(opt);
12263                        self.options.insert(name, enable);
12264                    }
12265                }
12266                "+o" => {
12267                    // +o with no arg: print options in re-entrant format
12268                    if iter.peek().is_none()
12269                        || iter
12270                            .peek()
12271                            .map(|s| s.starts_with('-') || s.starts_with('+'))
12272                            .unwrap_or(false)
12273                    {
12274                        self.print_options_reentrant();
12275                        continue;
12276                    }
12277                    if let Some(opt) = iter.next() {
12278                        let (name, enable) = Self::normalize_option_name(opt);
12279                        self.options.insert(name, !enable);
12280                    }
12281                }
12282                "-A" => {
12283                    set_array = Some(true);
12284                    if let Some(name) = iter.next() {
12285                        if !name.starts_with('-') && !name.starts_with('+') {
12286                            array_name = Some(name.clone());
12287                        }
12288                    }
12289                    if array_name.is_none() {
12290                        // Print all arrays with values
12291                        let mut arrs: Vec<_> = self.arrays.iter().collect();
12292                        arrs.sort_by_key(|(k, _)| *k);
12293                        for (k, v) in arrs {
12294                            let quoted: Vec<String> = v.iter().map(|s| shell_quote(s)).collect();
12295                            println!("{}=( {} )", k, quoted.join(" "));
12296                        }
12297                        return 0;
12298                    }
12299                }
12300                "+A" => {
12301                    set_array = Some(false);
12302                    if let Some(name) = iter.next() {
12303                        if !name.starts_with('-') && !name.starts_with('+') {
12304                            array_name = Some(name.clone());
12305                        }
12306                    }
12307                    if array_name.is_none() {
12308                        // Print array names only
12309                        let mut names: Vec<_> = self.arrays.keys().collect();
12310                        names.sort();
12311                        for name in names {
12312                            println!("{}", name);
12313                        }
12314                        return 0;
12315                    }
12316                }
12317                "-s" => sort_asc = true,
12318                "+s" => sort_desc = true,
12319                "-e" => {
12320                    self.options.insert("errexit".to_string(), true);
12321                }
12322                "+e" => {
12323                    self.options.insert("errexit".to_string(), false);
12324                }
12325                "-x" => {
12326                    self.options.insert("xtrace".to_string(), true);
12327                }
12328                "+x" => {
12329                    self.options.insert("xtrace".to_string(), false);
12330                }
12331                "-u" => {
12332                    self.options.insert("nounset".to_string(), true);
12333                }
12334                "+u" => {
12335                    self.options.insert("nounset".to_string(), false);
12336                }
12337                "-v" => {
12338                    self.options.insert("verbose".to_string(), true);
12339                }
12340                "+v" => {
12341                    self.options.insert("verbose".to_string(), false);
12342                }
12343                "-n" => {
12344                    self.options.insert("exec".to_string(), false);
12345                }
12346                "+n" => {
12347                    self.options.insert("exec".to_string(), true);
12348                }
12349                "-f" => {
12350                    self.options.insert("glob".to_string(), false);
12351                }
12352                "+f" => {
12353                    self.options.insert("glob".to_string(), true);
12354                }
12355                "-m" => {
12356                    self.options.insert("monitor".to_string(), true);
12357                }
12358                "+m" => {
12359                    self.options.insert("monitor".to_string(), false);
12360                }
12361                "-C" => {
12362                    self.options.insert("clobber".to_string(), false);
12363                }
12364                "+C" => {
12365                    self.options.insert("clobber".to_string(), true);
12366                }
12367                "-b" => {
12368                    self.options.insert("notify".to_string(), true);
12369                }
12370                "+b" => {
12371                    self.options.insert("notify".to_string(), false);
12372                }
12373                "--" => {
12374                    let remaining: Vec<String> = iter.cloned().collect();
12375                    if let Some(ref name) = array_name {
12376                        let mut values = remaining;
12377                        if sort_asc {
12378                            values.sort();
12379                        } else if sort_desc {
12380                            values.sort();
12381                            values.reverse();
12382                        }
12383                        if set_array == Some(true) {
12384                            self.arrays.insert(name.clone(), values);
12385                        } else {
12386                            // +A: replace initial elements
12387                            let arr = self.arrays.entry(name.clone()).or_default();
12388                            for (i, v) in values.into_iter().enumerate() {
12389                                if i < arr.len() {
12390                                    arr[i] = v;
12391                                } else {
12392                                    arr.push(v);
12393                                }
12394                            }
12395                        }
12396                    } else if remaining.is_empty() {
12397                        // "set --" with nothing after unsets positional params
12398                        self.positional_params.clear();
12399                    } else {
12400                        let mut values = remaining;
12401                        if sort_asc {
12402                            values.sort();
12403                        } else if sort_desc {
12404                            values.sort();
12405                            values.reverse();
12406                        }
12407                        self.positional_params = values;
12408                    }
12409                    return 0;
12410                }
12411                _ => {
12412                    // Handle single-letter options like -ex (multiple options)
12413                    if arg.starts_with('-') && arg.len() > 1 {
12414                        for c in arg[1..].chars() {
12415                            match c {
12416                                'e' => {
12417                                    self.options.insert("errexit".to_string(), true);
12418                                }
12419                                'x' => {
12420                                    self.options.insert("xtrace".to_string(), true);
12421                                }
12422                                'u' => {
12423                                    self.options.insert("nounset".to_string(), true);
12424                                }
12425                                'v' => {
12426                                    self.options.insert("verbose".to_string(), true);
12427                                }
12428                                'n' => {
12429                                    self.options.insert("exec".to_string(), false);
12430                                }
12431                                'f' => {
12432                                    self.options.insert("glob".to_string(), false);
12433                                }
12434                                'm' => {
12435                                    self.options.insert("monitor".to_string(), true);
12436                                }
12437                                'C' => {
12438                                    self.options.insert("clobber".to_string(), false);
12439                                }
12440                                'b' => {
12441                                    self.options.insert("notify".to_string(), true);
12442                                }
12443                                _ => {
12444                                    eprintln!("zshrs: set: -{}: invalid option", c);
12445                                    return 1;
12446                                }
12447                            }
12448                        }
12449                        continue;
12450                    }
12451                    if arg.starts_with('+') && arg.len() > 1 {
12452                        for c in arg[1..].chars() {
12453                            match c {
12454                                'e' => {
12455                                    self.options.insert("errexit".to_string(), false);
12456                                }
12457                                'x' => {
12458                                    self.options.insert("xtrace".to_string(), false);
12459                                }
12460                                'u' => {
12461                                    self.options.insert("nounset".to_string(), false);
12462                                }
12463                                'v' => {
12464                                    self.options.insert("verbose".to_string(), false);
12465                                }
12466                                'n' => {
12467                                    self.options.insert("exec".to_string(), true);
12468                                }
12469                                'f' => {
12470                                    self.options.insert("glob".to_string(), true);
12471                                }
12472                                'm' => {
12473                                    self.options.insert("monitor".to_string(), false);
12474                                }
12475                                'C' => {
12476                                    self.options.insert("clobber".to_string(), true);
12477                                }
12478                                'b' => {
12479                                    self.options.insert("notify".to_string(), false);
12480                                }
12481                                _ => {
12482                                    eprintln!("zshrs: set: +{}: invalid option", c);
12483                                    return 1;
12484                                }
12485                            }
12486                        }
12487                        continue;
12488                    }
12489                    // Treat as positional params
12490                    let mut values: Vec<String> =
12491                        std::iter::once(arg.clone()).chain(iter.cloned()).collect();
12492                    if sort_asc {
12493                        values.sort();
12494                    } else if sort_desc {
12495                        values.sort();
12496                        values.reverse();
12497                    }
12498                    if let Some(ref name) = array_name {
12499                        if set_array == Some(true) {
12500                            self.arrays.insert(name.clone(), values);
12501                        } else {
12502                            let arr = self.arrays.entry(name.clone()).or_default();
12503                            for (i, v) in values.into_iter().enumerate() {
12504                                if i < arr.len() {
12505                                    arr[i] = v;
12506                                } else {
12507                                    arr.push(v);
12508                                }
12509                            }
12510                        }
12511                    } else {
12512                        self.positional_params = values;
12513                    }
12514                    return 0;
12515                }
12516            }
12517        }
12518        0
12519    }
12520
12521    fn default_on_options() -> &'static [&'static str] {
12522        &[
12523            "aliases",
12524            "alwayslastprompt",
12525            "appendhistory",
12526            "autolist",
12527            "automenu",
12528            "autoparamkeys",
12529            "autoparamslash",
12530            "autoremoveslash",
12531            "badpattern",
12532            "banghist",
12533            "bareglobqual",
12534            "beep",
12535            "bgnice",
12536            "caseglob",
12537            "casematch",
12538            "checkjobs",
12539            "checkrunningjobs",
12540            "clobber",
12541            "debugbeforecmd",
12542            "equals",
12543            "evallineno",
12544            "exec",
12545            "flowcontrol",
12546            "functionargzero",
12547            "glob",
12548            "globalexport",
12549            "globalrcs",
12550            "hashcmds",
12551            "hashdirs",
12552            "hashlistall",
12553            "histbeep",
12554            "histsavebycopy",
12555            "hup",
12556            "interactive",
12557            "listambiguous",
12558            "listbeep",
12559            "listtypes",
12560            "monitor",
12561            "multibyte",
12562            "multifuncdef",
12563            "multios",
12564            "nomatch",
12565            "notify",
12566            "promptcr",
12567            "promptpercent",
12568            "promptsp",
12569            "rcs",
12570            "shinstdin",
12571            "shortloops",
12572            "unset",
12573            "zle",
12574        ]
12575    }
12576
12577    fn print_options_table(&self) {
12578        let mut opts: Vec<_> = Self::all_zsh_options().to_vec();
12579        opts.sort();
12580        let defaults_on = Self::default_on_options();
12581        for &opt in &opts {
12582            let enabled = self.options.get(opt).copied().unwrap_or(false);
12583            let is_default_on = defaults_on.contains(&opt);
12584            // zsh format: for default-ON options, show "noOPTION off" when on, "noOPTION on" when off
12585            // for default-OFF options, show "OPTION off" when off, "OPTION on" when on
12586            let (display_name, display_state) = if is_default_on {
12587                (format!("no{}", opt), if enabled { "off" } else { "on" })
12588            } else {
12589                (opt.to_string(), if enabled { "on" } else { "off" })
12590            };
12591            println!("{:<22}{}", display_name, display_state);
12592        }
12593    }
12594
12595    fn print_options_reentrant(&self) {
12596        let mut opts: Vec<_> = Self::all_zsh_options().to_vec();
12597        opts.sort();
12598        let defaults_on = Self::default_on_options();
12599        for &opt in &opts {
12600            let enabled = self.options.get(opt).copied().unwrap_or(false);
12601            let is_default_on = defaults_on.contains(&opt);
12602            // zsh format: use noOPTION for default-on options
12603            let (display_name, use_minus) = if is_default_on {
12604                (format!("no{}", opt), !enabled)
12605            } else {
12606                (opt.to_string(), enabled)
12607            };
12608            if use_minus {
12609                println!("set -o {}", display_name);
12610            } else {
12611                println!("set +o {}", display_name);
12612            }
12613        }
12614    }
12615
12616    /// caller - display call stack (bash)
12617    fn builtin_caller(&self, args: &[String]) -> i32 {
12618        let depth: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
12619        // In a real implementation, we'd track the call stack
12620        // For now, show basic info
12621        if depth == 0 {
12622            println!("1 main");
12623        } else {
12624            println!("{} main", depth);
12625        }
12626        0
12627    }
12628
12629    /// doctor - diagnostic report of shell health, caches, and performance
12630    fn builtin_doctor(&self, _args: &[String]) -> i32 {
12631        let green = |s: &str| format!("\x1b[32m{}\x1b[0m", s);
12632        let red = |s: &str| format!("\x1b[31m{}\x1b[0m", s);
12633        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
12634        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
12635        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
12636
12637        println!("{}", bold("zshrs doctor"));
12638        println!("{}", dim(&"=".repeat(60)));
12639        println!();
12640
12641        // --- Environment ---
12642        println!("{}", bold("Environment"));
12643        println!("  version:    zshrs {}", env!("CARGO_PKG_VERSION"));
12644        println!("  pid:        {}", std::process::id());
12645        let cwd = env::current_dir()
12646            .map(|p| p.to_string_lossy().to_string())
12647            .unwrap_or_else(|_| "?".to_string());
12648        println!("  cwd:        {}", cwd);
12649        println!(
12650            "  shell:      {}",
12651            env::var("SHELL").unwrap_or_else(|_| "?".to_string())
12652        );
12653        println!("  pool size:  {}", self.worker_pool.size());
12654        println!(
12655            "  pool done:  {} tasks completed",
12656            self.worker_pool.completed()
12657        );
12658        println!("  pool queue: {} pending", self.worker_pool.queue_depth());
12659        println!();
12660
12661        // --- Config ---
12662        println!("{}", bold("Config"));
12663        let config_path = crate::config::config_path();
12664        if config_path.exists() {
12665            println!("  {}  {}", green("*"), config_path.display());
12666        } else {
12667            println!(
12668                "  {}  {} {}",
12669                dim("-"),
12670                config_path.display(),
12671                dim("(using defaults)")
12672            );
12673        }
12674        println!();
12675
12676        // --- PATH ---
12677        println!("{}", bold("PATH"));
12678        let path_var = env::var("PATH").unwrap_or_default();
12679        let path_dirs: Vec<&str> = path_var.split(':').filter(|s| !s.is_empty()).collect();
12680        let path_ok = path_dirs
12681            .iter()
12682            .filter(|d| std::path::Path::new(d).is_dir())
12683            .count();
12684        let path_missing = path_dirs.len() - path_ok;
12685        println!(
12686            "  directories: {} total, {} {}, {} {}",
12687            path_dirs.len(),
12688            path_ok,
12689            green("valid"),
12690            path_missing,
12691            if path_missing > 0 {
12692                red("missing")
12693            } else {
12694                green("missing")
12695            },
12696        );
12697        println!("  hash table:  {} entries", self.command_hash.len());
12698        println!();
12699
12700        // --- FPATH ---
12701        println!("{}", bold("FPATH"));
12702        println!("  directories: {}", self.fpath.len());
12703        let fpath_ok = self.fpath.iter().filter(|d| d.is_dir()).count();
12704        let fpath_missing = self.fpath.len() - fpath_ok;
12705        if fpath_missing > 0 {
12706            println!("  {} {} missing fpath directories", red("!"), fpath_missing);
12707        }
12708        println!("  functions:   {} loaded", self.functions.len());
12709        println!("  autoload:    {} pending", self.autoload_pending.len());
12710        println!();
12711
12712        // --- SQLite Caches ---
12713        println!("{}", bold("SQLite Caches"));
12714        if let Some(ref engine) = self.history {
12715            let count = engine.count().unwrap_or(0);
12716            println!("  history:     {} entries  {}", count, green("OK"));
12717        } else {
12718            println!("  history:     {}", yellow("not initialized"));
12719        }
12720
12721        if let Some(ref cache) = self.compsys_cache {
12722            let count = compsys::cache_entry_count(cache);
12723            println!("  compsys:     {} completions  {}", count, green("OK"));
12724
12725            // Check bytecode blob coverage
12726            if let Ok(missing) = cache.get_autoloads_missing_bytecode() {
12727                if missing.is_empty() {
12728                    println!(
12729                        "  bytecode cache:   {}",
12730                        green("all functions compiled to bytecode")
12731                    );
12732                } else {
12733                    println!(
12734                        "  bytecode cache:   {} functions {}",
12735                        missing.len(),
12736                        yellow("missing bytecode blobs")
12737                    );
12738                }
12739            }
12740        } else {
12741            println!("  compsys:     {}", yellow("no cache"));
12742        }
12743
12744        if let Some(ref cache) = self.plugin_cache {
12745            let (plugins, functions) = cache.stats();
12746            println!(
12747                "  plugins:     {} plugins, {} cached functions  {}",
12748                plugins,
12749                functions,
12750                green("OK")
12751            );
12752        } else {
12753            println!("  plugins:     {}", yellow("no cache"));
12754        }
12755        println!();
12756
12757        // --- Shell State ---
12758        println!("{}", bold("Shell State"));
12759        println!("  aliases:     {}", self.aliases.len());
12760        println!("  global:      {} aliases", self.global_aliases.len());
12761        println!("  suffix:      {} aliases", self.suffix_aliases.len());
12762        println!("  variables:   {}", self.variables.len());
12763        println!("  arrays:      {}", self.arrays.len());
12764        println!("  assoc:       {}", self.assoc_arrays.len());
12765        println!(
12766            "  options:     {} set",
12767            self.options.iter().filter(|(_, v)| **v).count()
12768        );
12769        println!("  traps:       {} active", self.traps.len());
12770        println!(
12771            "  hooks:       {} registered",
12772            self.hook_functions.values().map(|v| v.len()).sum::<usize>()
12773        );
12774        println!();
12775
12776        // --- Log ---
12777        println!("{}", bold("Log"));
12778        let log_path = crate::log::log_path();
12779        if log_path.exists() {
12780            let size = std::fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
12781            println!("  {}  {} bytes", log_path.display(), size);
12782        } else {
12783            println!("  {}", dim("no log file yet"));
12784        }
12785        println!();
12786
12787        // --- Profiling ---
12788        println!("{}", bold("Profiling"));
12789        println!(
12790            "  chrome tracing: {}",
12791            if crate::log::profiling_enabled() {
12792                green("enabled")
12793            } else {
12794                dim("disabled")
12795            }
12796        );
12797        println!(
12798            "  flamegraph:     {}",
12799            if crate::log::flamegraph_enabled() {
12800                green("enabled")
12801            } else {
12802                dim("disabled")
12803            }
12804        );
12805        println!(
12806            "  prometheus:     {}",
12807            if crate::log::prometheus_enabled() {
12808                green("enabled")
12809            } else {
12810                dim("disabled")
12811            }
12812        );
12813        println!();
12814
12815        0
12816    }
12817
12818    /// dbview — browse zshrs SQLite cache tables without SQL.
12819    ///
12820    /// Usage:
12821    ///   dbview                      — list all tables and row counts
12822    ///   dbview autoloads             — dump autoloads table (name, source, body len, ast len)
12823    ///   dbview autoloads _git        — show single row by name
12824    ///   dbview comps                 — dump comps table
12825    ///   dbview history               — recent history entries
12826    ///   dbview history <pattern>     — search history
12827    ///   dbview plugins               — plugin cache entries
12828    ///   dbview executables            — PATH executables cache
12829    ///   dbview <table> --count       — just the count
12830    fn builtin_dbview(&self, args: &[String]) -> i32 {
12831        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
12832        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
12833        let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
12834        let green = |s: &str| format!("\x1b[32m{}\x1b[0m", s);
12835        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
12836
12837        if args.is_empty() {
12838            // List all tables with row counts
12839            println!("{}", bold("zshrs SQLite caches"));
12840            println!();
12841
12842            if let Some(ref cache) = self.compsys_cache {
12843                println!("  {} {}", bold("compsys.db"), dim("(completion cache)"));
12844                if let Ok(n) = cache.count_table("autoloads") {
12845                    let bc_count = cache
12846                        .count_table_where("autoloads", "bytecode IS NOT NULL")
12847                        .unwrap_or(0);
12848                    println!("    autoloads:    {:>6} rows  ({} compiled)", n, bc_count);
12849                }
12850                if let Ok(n) = cache.count_table("comps") {
12851                    println!("    comps:        {:>6} rows", n);
12852                }
12853                if let Ok(n) = cache.count_table("services") {
12854                    println!("    services:     {:>6} rows", n);
12855                }
12856                if let Ok(n) = cache.count_table("patcomps") {
12857                    println!("    patcomps:     {:>6} rows", n);
12858                }
12859                if let Ok(n) = cache.count_table("executables") {
12860                    println!("    executables:  {:>6} rows", n);
12861                }
12862                if let Ok(n) = cache.count_table("zstyles") {
12863                    println!("    zstyles:      {:>6} rows", n);
12864                }
12865                println!();
12866            }
12867
12868            if let Some(ref engine) = self.history {
12869                println!("  {} {}", bold("history.db"), dim("(command history)"));
12870                if let Ok(n) = engine.count() {
12871                    println!("    entries:      {:>6} rows", n);
12872                }
12873                println!();
12874            }
12875
12876            if let Some(ref cache) = self.plugin_cache {
12877                let (plugins, functions) = cache.stats();
12878                println!("  {} {}", bold("plugins.db"), dim("(plugin source cache)"));
12879                println!("    plugins:      {:>6} rows", plugins);
12880                println!("    functions:    {:>6} rows", functions);
12881                println!();
12882            }
12883
12884            println!("  Usage: {} <table> [name] [--count]", cyan("dbview"));
12885            return 0;
12886        }
12887
12888        let table = args[0].as_str();
12889        let filter = args.get(1).map(|s| s.as_str());
12890        let count_only = args.iter().any(|a| a == "--count" || a == "-c");
12891
12892        match table {
12893            "autoloads" => {
12894                let Some(ref cache) = self.compsys_cache else {
12895                    eprintln!("dbview: no compsys cache");
12896                    return 1;
12897                };
12898
12899                if count_only {
12900                    let n = cache.count_table("autoloads").unwrap_or(0);
12901                    println!("{}", n);
12902                    return 0;
12903                }
12904
12905                if let Some(name) = filter {
12906                    // Single row lookup
12907                    match cache.get_autoload(name) {
12908                        Ok(Some(stub)) => {
12909                            println!("{}", bold(&format!("autoload: {}", name)));
12910                            println!("  source:   {}", stub.source);
12911                            println!(
12912                                "  body:     {} bytes",
12913                                stub.body.as_ref().map(|b| b.len()).unwrap_or(0)
12914                            );
12915                            match cache.get_autoload_bytecode(name) {
12916                                Ok(Some(blob)) => {
12917                                    println!("  bytecode: {} {} bytes", green("YES"), blob.len())
12918                                }
12919                                _ => println!("  bytecode: {}", yellow("NULL")),
12920                            }
12921                            // Show first few lines of body
12922                            if let Some(ref body) = stub.body {
12923                                println!("  preview:");
12924                                for (i, line) in body.lines().take(10).enumerate() {
12925                                    println!("    {:>3}: {}", i + 1, dim(line));
12926                                }
12927                                let total = body.lines().count();
12928                                if total > 10 {
12929                                    println!("    {} ({} more lines)", dim("..."), total - 10);
12930                                }
12931                            }
12932                        }
12933                        _ => {
12934                            eprintln!("dbview: autoload '{}' not found", name);
12935                            return 1;
12936                        }
12937                    }
12938                    return 0;
12939                }
12940
12941                // Dump all autoloads
12942                let conn = &cache.conn();
12943                match conn.prepare("SELECT name, source, length(body), length(bytecode) FROM autoloads ORDER BY name LIMIT 200") {
12944                    Ok(mut stmt) => {
12945                        let rows = stmt.query_map([], |row| {
12946                            Ok((
12947                                row.get::<_, String>(0)?,
12948                                row.get::<_, String>(1)?,
12949                                row.get::<_, Option<i64>>(2)?,
12950                                row.get::<_, Option<i64>>(3)?,
12951                            ))
12952                        });
12953                        if let Ok(rows) = rows {
12954                            println!("{:<40} {:>8} {:>8}  {}", bold("NAME"), bold("BODY"), bold("BYTECODE"), bold("SOURCE"));
12955                            let mut count = 0;
12956                            for row in rows.flatten() {
12957                                let (name, source, body_len, ast_len) = row;
12958                                let ast_str = match ast_len {
12959                                    Some(n) => green(&format!("{:>8}", n)),
12960                                    None => yellow(&format!("{:>8}", "NULL")),
12961                                };
12962                                let body_str = match body_len {
12963                                    Some(n) => format!("{:>8}", n),
12964                                    None => dim("NULL").to_string(),
12965                                };
12966                                // Truncate source path for display
12967                                let src_short = if source.len() > 50 {
12968                                    format!("...{}", &source[source.len() - 47..])
12969                                } else {
12970                                    source
12971                                };
12972                                println!("{:<40} {} {}  {}", name, body_str, ast_str, dim(&src_short));
12973                                count += 1;
12974                            }
12975                            println!("\n{} rows shown (LIMIT 200)", count);
12976                        }
12977                    }
12978                    Err(e) => {
12979                        eprintln!("dbview: query failed: {}", e);
12980                        return 1;
12981                    }
12982                }
12983            }
12984
12985            "comps" => {
12986                let Some(ref cache) = self.compsys_cache else {
12987                    eprintln!("dbview: no compsys cache");
12988                    return 1;
12989                };
12990                if count_only {
12991                    println!("{}", cache.count_table("comps").unwrap_or(0));
12992                    return 0;
12993                }
12994                let conn = cache.conn();
12995                let query = if let Some(pat) = filter {
12996                    format!("SELECT command, function FROM comps WHERE command LIKE '%{}%' ORDER BY command LIMIT 100", pat)
12997                } else {
12998                    "SELECT command, function FROM comps ORDER BY command LIMIT 100".to_string()
12999                };
13000                match conn.prepare(&query) {
13001                    Ok(mut stmt) => {
13002                        println!("{:<40} {}", bold("COMMAND"), bold("FUNCTION"));
13003                        let rows = stmt.query_map([], |row| {
13004                            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
13005                        });
13006                        if let Ok(rows) = rows {
13007                            for row in rows.flatten() {
13008                                println!("{:<40} {}", row.0, cyan(&row.1));
13009                            }
13010                        }
13011                    }
13012                    Err(e) => {
13013                        eprintln!("dbview: {}", e);
13014                        return 1;
13015                    }
13016                }
13017            }
13018
13019            "executables" => {
13020                let Some(ref cache) = self.compsys_cache else {
13021                    eprintln!("dbview: no compsys cache");
13022                    return 1;
13023                };
13024                if count_only {
13025                    println!("{}", cache.count_table("executables").unwrap_or(0));
13026                    return 0;
13027                }
13028                let conn = cache.conn();
13029                let query = if let Some(pat) = filter {
13030                    format!("SELECT name, path FROM executables WHERE name LIKE '%{}%' ORDER BY name LIMIT 100", pat)
13031                } else {
13032                    "SELECT name, path FROM executables ORDER BY name LIMIT 100".to_string()
13033                };
13034                match conn.prepare(&query) {
13035                    Ok(mut stmt) => {
13036                        println!("{:<30} {}", bold("NAME"), bold("PATH"));
13037                        let rows = stmt.query_map([], |row| {
13038                            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
13039                        });
13040                        if let Ok(rows) = rows {
13041                            for row in rows.flatten() {
13042                                println!("{:<30} {}", row.0, dim(&row.1));
13043                            }
13044                        }
13045                    }
13046                    Err(e) => {
13047                        eprintln!("dbview: {}", e);
13048                        return 1;
13049                    }
13050                }
13051            }
13052
13053            "history" => {
13054                let Some(ref engine) = self.history else {
13055                    eprintln!("dbview: no history engine");
13056                    return 1;
13057                };
13058                if count_only {
13059                    println!("{}", engine.count().unwrap_or(0));
13060                    return 0;
13061                }
13062                if let Some(pat) = filter {
13063                    if let Ok(entries) = engine.search(pat, 20) {
13064                        for e in entries {
13065                            println!(
13066                                "  {} {} {}",
13067                                dim(&e.timestamp.to_string()),
13068                                cyan(&e.command),
13069                                dim(&format!("[{}]", e.exit_code.unwrap_or(0)))
13070                            );
13071                        }
13072                    }
13073                } else if let Ok(entries) = engine.recent(20) {
13074                    for e in entries {
13075                        println!(
13076                            "  {} {} {}",
13077                            dim(&e.timestamp.to_string()),
13078                            cyan(&e.command),
13079                            dim(&format!("[{}]", e.exit_code.unwrap_or(0)))
13080                        );
13081                    }
13082                }
13083            }
13084
13085            "plugins" => {
13086                let Some(ref cache) = self.plugin_cache else {
13087                    eprintln!("dbview: no plugin cache");
13088                    return 1;
13089                };
13090                let (plugins, functions) = cache.stats();
13091                println!("{} plugins, {} cached functions", plugins, functions);
13092            }
13093
13094            _ => {
13095                eprintln!("dbview: unknown table '{}'. Available: autoloads, comps, executables, history, plugins", table);
13096                return 1;
13097            }
13098        }
13099
13100        0
13101    }
13102
13103    /// profile — in-process command profiling with nanosecond accuracy.
13104    ///
13105    /// Unlike `time` (which measures one command) or `zprof` (which only
13106    /// profiles function calls), `profile` traces every execute_command,
13107    /// expansion, glob, and builtin dispatch inside the block.
13108    ///
13109    /// Usage:
13110    ///   profile { commands }     — profile a block
13111    ///   profile -s 'script'     — profile a script string
13112    ///   profile -f func         — profile a function call
13113    ///   profile --clear         — clear accumulated profile data
13114    ///   profile --dump          — show accumulated profile data
13115    fn builtin_profile(&mut self, args: &[String]) -> i32 {
13116        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
13117        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
13118        let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
13119        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
13120
13121        if args.is_empty() {
13122            println!("Usage: profile {{ commands }}");
13123            println!("       profile -s 'script string'");
13124            println!("       profile -f function_name [args...]");
13125            println!("       profile --clear");
13126            println!("       profile --dump");
13127            return 0;
13128        }
13129
13130        if args[0] == "--clear" {
13131            self.profiler = crate::zprof::Profiler::new();
13132            println!("profile data cleared");
13133            return 0;
13134        }
13135
13136        if args[0] == "--dump" {
13137            let (_, output) = crate::zprof::builtin_zprof(
13138                &mut self.profiler,
13139                &crate::zprof::ZprofOptions { clear: false },
13140            );
13141            if !output.is_empty() {
13142                print!("{}", output);
13143            } else {
13144                println!("{}", dim("no profile data"));
13145            }
13146            return 0;
13147        }
13148
13149        // Determine what to profile
13150        let code = if args[0] == "-s" {
13151            // profile -s 'script string'
13152            if args.len() < 2 {
13153                eprintln!("profile: -s requires a script string");
13154                return 1;
13155            }
13156            args[1..].join(" ")
13157        } else if args[0] == "-f" {
13158            // profile -f func_name [args...]
13159            if args.len() < 2 {
13160                eprintln!("profile: -f requires a function name");
13161                return 1;
13162            }
13163            args[1..].join(" ")
13164        } else {
13165            // profile { commands } — args is the block body
13166            args.join(" ")
13167        };
13168
13169        // Enable profiling, run, collect results
13170        let was_enabled = self.profiling_enabled;
13171        self.profiling_enabled = true;
13172        self.profiler = crate::zprof::Profiler::new(); // fresh data for this run
13173
13174        let t0 = std::time::Instant::now();
13175        let result = self.execute_script(&code);
13176        let elapsed = t0.elapsed();
13177        let status = match result {
13178            Ok(s) => s,
13179            Err(e) => {
13180                eprintln!("profile: {}", e);
13181                1
13182            }
13183        };
13184
13185        // Collect timing data
13186        println!();
13187        println!("{}", bold("profile results"));
13188        println!("{}", dim(&"─".repeat(60)));
13189        let dur_str = if elapsed.as_secs() > 0 {
13190            format!("{:.3}s", elapsed.as_secs_f64())
13191        } else if elapsed.as_millis() > 0 {
13192            format!("{:.3}ms", elapsed.as_secs_f64() * 1000.0)
13193        } else {
13194            format!("{:.1}µs", elapsed.as_secs_f64() * 1_000_000.0)
13195        };
13196        println!("  total:     {}", cyan(&dur_str));
13197        println!("  status:    {}", status);
13198        println!();
13199
13200        // Show function-level breakdown from profiler
13201        let (_, output) = crate::zprof::builtin_zprof(
13202            &mut self.profiler,
13203            &crate::zprof::ZprofOptions { clear: false },
13204        );
13205        if !output.is_empty() {
13206            println!("{}", bold("function breakdown"));
13207            print!("{}", output);
13208        }
13209
13210        // Per-command breakdown from tracing (if tracing is at debug level)
13211        println!();
13212        println!(
13213            "  {} set ZSHRS_LOG=trace for per-command tracing",
13214            yellow("tip:")
13215        );
13216        println!(
13217            "  {} output: {}",
13218            dim("log"),
13219            dim(&crate::log::log_path().display().to_string())
13220        );
13221
13222        self.profiling_enabled = was_enabled;
13223        status
13224    }
13225
13226    // ═══════════════════════════════════════════════════════════════════
13227    // AOP INTERCEPT — the killer builtin
13228    // ═══════════════════════════════════════════════════════════════════
13229
13230    /// Check intercepts for a command. Returns Some(result) if an around
13231    /// advice fully handled the command, None to proceed normally.
13232    fn run_intercepts(
13233        &mut self,
13234        cmd_name: &str,
13235        full_cmd: &str,
13236        args: &[String],
13237    ) -> Option<Result<i32, String>> {
13238        // Collect matching intercepts (clone to avoid borrow issues)
13239        let matching: Vec<Intercept> = self
13240            .intercepts
13241            .iter()
13242            .filter(|i| intercept_matches(&i.pattern, cmd_name, full_cmd))
13243            .cloned()
13244            .collect();
13245
13246        if matching.is_empty() {
13247            return None;
13248        }
13249
13250        // Set INTERCEPT_NAME and INTERCEPT_ARGS for advice code
13251        self.variables
13252            .insert("INTERCEPT_NAME".to_string(), cmd_name.to_string());
13253        self.variables
13254            .insert("INTERCEPT_ARGS".to_string(), args.join(" "));
13255        self.variables
13256            .insert("INTERCEPT_CMD".to_string(), full_cmd.to_string());
13257
13258        // Run before advice
13259        for advice in matching
13260            .iter()
13261            .filter(|i| matches!(i.kind, AdviceKind::Before))
13262        {
13263            let _ = self.execute_advice(&advice.code);
13264        }
13265
13266        // Check for around advice — first match wins
13267        let around = matching
13268            .iter()
13269            .find(|i| matches!(i.kind, AdviceKind::Around));
13270
13271        let t0 = std::time::Instant::now();
13272
13273        let result = if let Some(advice) = around {
13274            // Around advice: set INTERCEPT_PROCEED flag, run advice code.
13275            // If advice calls `intercept_proceed`, the original command runs.
13276            self.variables
13277                .insert("__intercept_proceed".to_string(), "0".to_string());
13278            let advice_result = self.execute_advice(&advice.code);
13279
13280            // Check if intercept_proceed was called
13281            let proceeded = self
13282                .variables
13283                .get("__intercept_proceed")
13284                .map(|v| v == "1")
13285                .unwrap_or(false);
13286
13287            if proceeded {
13288                // The original command was already executed inside the advice
13289                advice_result
13290            } else {
13291                // Advice didn't call proceed — command was suppressed
13292                advice_result
13293            }
13294        } else {
13295            // No around advice — run the original command.
13296            // We return None to let the normal dispatch continue.
13297            // But we still need after advice to fire, so we can't return None here
13298            // if there are after advices. Run the command ourselves.
13299            let has_after = matching.iter().any(|i| matches!(i.kind, AdviceKind::After));
13300            if !has_after {
13301                // Only before advice, no after — let normal dispatch continue
13302                return None;
13303            }
13304
13305            // Has after advice — we must run the command and then run after advice
13306            self.run_original_command(cmd_name, args)
13307        };
13308
13309        let elapsed = t0.elapsed();
13310
13311        // Set timing variable for after advice
13312        let ms = elapsed.as_secs_f64() * 1000.0;
13313        self.variables
13314            .insert("INTERCEPT_MS".to_string(), format!("{:.3}", ms));
13315        self.variables
13316            .insert("INTERCEPT_US".to_string(), format!("{:.0}", ms * 1000.0));
13317
13318        // Run after advice
13319        for advice in matching
13320            .iter()
13321            .filter(|i| matches!(i.kind, AdviceKind::After))
13322        {
13323            let _ = self.execute_advice(&advice.code);
13324        }
13325
13326        // Clean up
13327        self.variables.remove("INTERCEPT_NAME");
13328        self.variables.remove("INTERCEPT_ARGS");
13329        self.variables.remove("INTERCEPT_CMD");
13330        self.variables.remove("INTERCEPT_MS");
13331        self.variables.remove("INTERCEPT_US");
13332        self.variables.remove("__intercept_proceed");
13333
13334        Some(result)
13335    }
13336
13337    /// Execute the original command (used by around/after intercept dispatch).
13338    /// Execute advice code — dispatches @ prefix to stryke (fat binary),
13339    /// everything else to the shell parser. No fork. Machine code speed.
13340    fn execute_advice(&mut self, code: &str) -> Result<i32, String> {
13341        let code = code.trim();
13342        if code.starts_with('@') {
13343            let stryke_code = code.trim_start_matches('@').trim();
13344            if let Some(status) = crate::try_stryke_dispatch(stryke_code) {
13345                self.last_status = status;
13346                return Ok(status);
13347            }
13348            // No stryke handler (thin binary) — fall through to shell
13349        }
13350        self.execute_script(code)
13351    }
13352
13353    fn run_original_command(&mut self, cmd_name: &str, args: &[String]) -> Result<i32, String> {
13354        // Try function
13355        if let Some(func) = self.functions.get(cmd_name).cloned() {
13356            return self.call_function(&func, args);
13357        }
13358        if self.maybe_autoload(cmd_name) {
13359            if let Some(func) = self.functions.get(cmd_name).cloned() {
13360                return self.call_function(&func, args);
13361            }
13362        }
13363        // External command
13364        self.execute_external(cmd_name, &args.to_vec(), &[])
13365    }
13366
13367    /// intercept builtin — register AOP advice on commands.
13368    ///
13369    /// Usage:
13370    ///   intercept before <pattern> { code }
13371    ///   intercept after <pattern> { code }
13372    ///   intercept around <pattern> { code }
13373    ///   intercept list                       — show all intercepts
13374    ///   intercept remove <id>                — remove by ID
13375    ///   intercept clear                      — remove all
13376    fn builtin_intercept(&mut self, args: &[String]) -> i32 {
13377        if args.is_empty() {
13378            println!("Usage: intercept <before|after|around> <pattern> {{ code }}");
13379            println!("       intercept list | remove <id> | clear");
13380            return 0;
13381        }
13382
13383        match args[0].as_str() {
13384            "list" => {
13385                if self.intercepts.is_empty() {
13386                    println!("no intercepts registered");
13387                } else {
13388                    let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
13389                    let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
13390                    println!(
13391                        "{:>4}  {:<8}  {:<20}  {}",
13392                        bold("ID"),
13393                        bold("KIND"),
13394                        bold("PATTERN"),
13395                        bold("CODE")
13396                    );
13397                    for i in &self.intercepts {
13398                        let kind = match i.kind {
13399                            AdviceKind::Before => "before",
13400                            AdviceKind::After => "after",
13401                            AdviceKind::Around => "around",
13402                        };
13403                        let code_preview = if i.code.len() > 40 {
13404                            format!("{}...", &i.code[..37])
13405                        } else {
13406                            i.code.clone()
13407                        };
13408                        println!(
13409                            "{:>4}  {:<8}  {:<20}  {}",
13410                            cyan(&i.id.to_string()),
13411                            kind,
13412                            i.pattern,
13413                            code_preview
13414                        );
13415                    }
13416                }
13417                0
13418            }
13419            "clear" => {
13420                let count = self.intercepts.len();
13421                self.intercepts.clear();
13422                println!("cleared {} intercepts", count);
13423                0
13424            }
13425            "remove" => {
13426                if args.len() < 2 {
13427                    eprintln!("intercept remove: requires ID");
13428                    return 1;
13429                }
13430                if let Ok(id) = args[1].parse::<u32>() {
13431                    let before = self.intercepts.len();
13432                    self.intercepts.retain(|i| i.id != id);
13433                    if self.intercepts.len() < before {
13434                        println!("removed intercept {}", id);
13435                        0
13436                    } else {
13437                        eprintln!("intercept: no intercept with ID {}", id);
13438                        1
13439                    }
13440                } else {
13441                    eprintln!("intercept remove: invalid ID");
13442                    1
13443                }
13444            }
13445            "before" | "after" | "around" => {
13446                let kind = match args[0].as_str() {
13447                    "before" => AdviceKind::Before,
13448                    "after" => AdviceKind::After,
13449                    "around" => AdviceKind::Around,
13450                    _ => unreachable!(),
13451                };
13452
13453                if args.len() < 3 {
13454                    eprintln!("intercept {}: requires <pattern> {{ code }}", args[0]);
13455                    return 1;
13456                }
13457
13458                let pattern = args[1].clone();
13459                // Join remaining args as the code (handles { code } or 'code')
13460                let code = args[2..].join(" ");
13461                // Strip surrounding braces if present
13462                let code = code.trim().to_string();
13463                let code = if code.starts_with('{') && code.ends_with('}') {
13464                    code[1..code.len() - 1].trim().to_string()
13465                } else {
13466                    code
13467                };
13468
13469                let id = self.intercepts.iter().map(|i| i.id).max().unwrap_or(0) + 1;
13470                self.intercepts.push(Intercept {
13471                    pattern,
13472                    kind: kind.clone(),
13473                    code: code.clone(),
13474                    id,
13475                });
13476
13477                let kind_str = match kind {
13478                    AdviceKind::Before => "before",
13479                    AdviceKind::After => "after",
13480                    AdviceKind::Around => "around",
13481                };
13482                println!(
13483                    "intercept #{}: {} {} → {}",
13484                    id,
13485                    kind_str,
13486                    self.intercepts.last().unwrap().pattern,
13487                    if code.len() > 50 {
13488                        format!("{}...", &code[..47])
13489                    } else {
13490                        code
13491                    }
13492                );
13493                0
13494            }
13495            _ => {
13496                eprintln!(
13497                    "intercept: unknown subcommand '{}'. Use before|after|around|list|remove|clear",
13498                    args[0]
13499                );
13500                1
13501            }
13502        }
13503    }
13504
13505    /// intercept_proceed — called from around advice to execute the original command.
13506    fn builtin_intercept_proceed(&mut self, _args: &[String]) -> i32 {
13507        self.variables
13508            .insert("__intercept_proceed".to_string(), "1".to_string());
13509        // Run the original command using saved INTERCEPT_NAME/INTERCEPT_ARGS
13510        let cmd_name = self
13511            .variables
13512            .get("INTERCEPT_NAME")
13513            .cloned()
13514            .unwrap_or_default();
13515        let args_str = self
13516            .variables
13517            .get("INTERCEPT_ARGS")
13518            .cloned()
13519            .unwrap_or_default();
13520        let args: Vec<String> = if args_str.is_empty() {
13521            Vec::new()
13522        } else {
13523            args_str.split_whitespace().map(|s| s.to_string()).collect()
13524        };
13525        match self.run_original_command(&cmd_name, &args) {
13526            Ok(status) => status,
13527            Err(e) => {
13528                eprintln!("intercept_proceed: {}", e);
13529                1
13530            }
13531        }
13532    }
13533
13534    // ═══════════════════════════════════════════════════════════════════
13535    // CONCURRENT PRIMITIVES — ship work to the worker pool from shell
13536    // No stryke dependency. Pure zshrs. Thin binary gets full parallelism.
13537    // ═══════════════════════════════════════════════════════════════════
13538
13539    /// async { cmd } — run command on worker pool, return job ID immediately.
13540    /// Output captured in background, retrieve with `await $id`.
13541    ///
13542    /// Usage:
13543    ///   id=$(async 'sleep 2; echo done')
13544    ///   ... do other work ...
13545    ///   result=$(await $id)
13546    fn builtin_async(&mut self, args: &[String]) -> i32 {
13547        if args.is_empty() {
13548            eprintln!("async: requires a command string");
13549            return 1;
13550        }
13551
13552        let code = args.join(" ");
13553        let id = self.next_async_id;
13554        self.next_async_id += 1;
13555
13556        let (tx, rx) = crossbeam_channel::bounded::<(i32, String)>(1);
13557        let pool = std::sync::Arc::clone(&self.worker_pool);
13558
13559        pool.submit(move || {
13560            // Execute in a subprocess to capture stdout
13561            use std::process::{Command, Stdio};
13562            let output = Command::new("sh")
13563                .args(["-c", &code])
13564                .stdout(Stdio::piped())
13565                .stderr(Stdio::inherit())
13566                .output();
13567            match output {
13568                Ok(out) => {
13569                    let stdout = String::from_utf8_lossy(&out.stdout).to_string();
13570                    let status = out.status.code().unwrap_or(1);
13571                    let _ = tx.send((status, stdout));
13572                }
13573                Err(_) => {
13574                    let _ = tx.send((127, String::new()));
13575                }
13576            }
13577        });
13578
13579        self.async_jobs.insert(id, rx);
13580        // Print the job ID so it can be captured: id=$(async 'cmd')
13581        println!("{}", id);
13582        0
13583    }
13584
13585    /// await $id — block until async job completes, print its stdout, return its status.
13586    ///
13587    /// Usage:
13588    ///   id=$(async 'expensive_command')
13589    ///   await $id    # blocks until done, prints output
13590    ///   echo $?      # exit status of the async command
13591    fn builtin_await(&mut self, args: &[String]) -> i32 {
13592        if args.is_empty() {
13593            eprintln!("await: requires a job ID");
13594            return 1;
13595        }
13596
13597        let id: u32 = match args[0].parse() {
13598            Ok(n) => n,
13599            Err(_) => {
13600                eprintln!("await: invalid job ID '{}'", args[0]);
13601                return 1;
13602            }
13603        };
13604
13605        let rx = match self.async_jobs.remove(&id) {
13606            Some(rx) => rx,
13607            None => {
13608                eprintln!("await: no async job with ID {}", id);
13609                return 1;
13610            }
13611        };
13612
13613        // Block until the job completes
13614        match rx.recv() {
13615            Ok((status, stdout)) => {
13616                if !stdout.is_empty() {
13617                    print!("{}", stdout);
13618                }
13619                self.last_status = status;
13620                status
13621            }
13622            Err(_) => {
13623                eprintln!("await: job {} died without result", id);
13624                1
13625            }
13626        }
13627    }
13628
13629    /// pmap 'cmd {}' arg1 arg2 arg3 — parallel map across worker pool.
13630    /// Runs `cmd` for each argument, replacing `{}` with the argument.
13631    /// Output is collected in order. Returns 0 if all succeed.
13632    ///
13633    /// Usage:
13634    ///   pmap 'gzip {}' *.log
13635    ///   pmap 'echo {}' a b c d
13636    ///   ls *.rs | pmap 'wc -l {}'
13637    fn builtin_pmap(&mut self, args: &[String]) -> i32 {
13638        if args.len() < 2 {
13639            eprintln!("pmap: requires 'command {{}}' followed by arguments");
13640            return 1;
13641        }
13642
13643        let template = &args[0];
13644        let items = &args[1..];
13645
13646        // Compile template once, execute for each item on VM — no forks
13647        let mut results: Vec<(i32, String)> = Vec::with_capacity(items.len());
13648        
13649        for item in items {
13650            let cmd = template.replace("{}", item);
13651            let mut parser = crate::parser::ShellParser::new(&cmd);
13652            match parser.parse_script() {
13653                Ok(commands) => {
13654                    let compiler = crate::shell_compiler::ShellCompiler::new();
13655                    let chunk = compiler.compile(&commands);
13656                    
13657                    // Capture stdout
13658                    let mut output = Vec::new();
13659                    let status = {
13660                        let mut vm = fusevm::VM::new(chunk);
13661                        register_builtins(&mut vm);
13662                        let _ctx = ExecutorContext::enter(self);
13663                        match vm.run() {
13664                            fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => vm.last_status,
13665                            fusevm::VMResult::Error(_) => 1,
13666                        }
13667                    };
13668                    results.push((status, String::from_utf8_lossy(&output).to_string()));
13669                }
13670                Err(e) => {
13671                    eprintln!("pmap: parse error: {}", e);
13672                    results.push((1, String::new()));
13673                }
13674            }
13675        }
13676
13677        let mut any_fail = false;
13678        for (status, stdout) in results {
13679            if !stdout.is_empty() {
13680                print!("{}", stdout);
13681            }
13682            if status != 0 {
13683                any_fail = true;
13684            }
13685        }
13686
13687        if any_fail { 1 } else { 0 }
13688    }
13689
13690    /// pgrep 'pattern' arg1 arg2 ... — parallel grep/filter across worker pool.
13691    /// Runs the pattern command for each argument, prints args where command succeeds.
13692    ///
13693    /// Usage:
13694    ///   pgrep 'test -f {}' /path/a /path/b /path/c
13695    ///   pgrep 'grep -q TODO {}' *.rs
13696    fn builtin_pgrep(&mut self, args: &[String]) -> i32 {
13697        if args.len() < 2 {
13698            eprintln!("pgrep: requires 'test_command {{}}' followed by arguments");
13699            return 1;
13700        }
13701
13702        let template = &args[0];
13703        let items = &args[1..];
13704
13705        // Compile and run on VM — no forks
13706        for item in items {
13707            let cmd = template.replace("{}", item);
13708            let mut parser = crate::parser::ShellParser::new(&cmd);
13709            if let Ok(commands) = parser.parse_script() {
13710                let compiler = crate::shell_compiler::ShellCompiler::new();
13711                let chunk = compiler.compile(&commands);
13712                
13713                let mut vm = fusevm::VM::new(chunk);
13714                register_builtins(&mut vm);
13715                let _ctx = ExecutorContext::enter(self);
13716                let status = match vm.run() {
13717                    fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => vm.last_status,
13718                    fusevm::VMResult::Error(_) => 1,
13719                };
13720                
13721                if status == 0 {
13722                    println!("{}", item);
13723                }
13724            }
13725        }
13726
13727        0
13728    }
13729
13730    /// peach 'cmd {}' arg1 arg2 ... — parallel for-each, no output ordering.
13731    /// Like pmap but doesn't collect output — fire-and-forget, print as completed.
13732    ///
13733    /// Usage:
13734    ///   peach 'convert {} {}.png' *.svg
13735    ///   peach 'rsync -a {} remote:{}' dir1 dir2 dir3
13736    fn builtin_peach(&mut self, args: &[String]) -> i32 {
13737        if args.len() < 2 {
13738            eprintln!("peach: requires 'command {{}}' followed by arguments");
13739            return 1;
13740        }
13741
13742        let template = &args[0];
13743        let items = &args[1..];
13744
13745        // Compile and run on VM — no forks, fire-and-forget style
13746        let mut any_fail = false;
13747        
13748        for item in items {
13749            let cmd = template.replace("{}", item);
13750            let mut parser = crate::parser::ShellParser::new(&cmd);
13751            if let Ok(commands) = parser.parse_script() {
13752                let compiler = crate::shell_compiler::ShellCompiler::new();
13753                let chunk = compiler.compile(&commands);
13754                
13755                let mut vm = fusevm::VM::new(chunk);
13756                register_builtins(&mut vm);
13757                let _ctx = ExecutorContext::enter(self);
13758                let status = match vm.run() {
13759                    fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => vm.last_status,
13760                    fusevm::VMResult::Error(_) => 1,
13761                };
13762                
13763                if status != 0 {
13764                    any_fail = true;
13765                }
13766            } else {
13767                any_fail = true;
13768            }
13769        }
13770
13771        if any_fail { 1 } else { 0 }
13772    }
13773
13774    /// barrier cmd1 ::: cmd2 ::: cmd3 — run commands in parallel, wait for ALL to complete.
13775    /// Returns the worst (highest) exit status.
13776    ///
13777    /// Usage:
13778    ///   barrier 'make -C proj1' ::: 'make -C proj2' ::: 'make -C proj3'
13779    ///   barrier 'npm test' ::: 'cargo test' ::: 'pytest'
13780    fn builtin_barrier(&mut self, args: &[String]) -> i32 {
13781        if args.is_empty() {
13782            eprintln!("barrier: requires commands separated by :::");
13783            return 1;
13784        }
13785
13786        // Split on ::: delimiter
13787        let mut commands: Vec<String> = Vec::new();
13788        let mut current = String::new();
13789        for arg in args {
13790            if arg == ":::" {
13791                if !current.is_empty() {
13792                    commands.push(current.trim().to_string());
13793                    current.clear();
13794                }
13795            } else {
13796                if !current.is_empty() {
13797                    current.push(' ');
13798                }
13799                current.push_str(arg);
13800            }
13801        }
13802        if !current.is_empty() {
13803            commands.push(current.trim().to_string());
13804        }
13805
13806        if commands.is_empty() {
13807            return 0;
13808        }
13809
13810        // Ship all to pool
13811        let mut receivers = Vec::with_capacity(commands.len());
13812        for cmd in &commands {
13813            let cmd = cmd.clone();
13814            let rx = self.worker_pool.submit_with_result(move || {
13815                use std::process::{Command, Stdio};
13816                Command::new("sh")
13817                    .args(["-c", &cmd])
13818                    .stdout(Stdio::inherit())
13819                    .stderr(Stdio::inherit())
13820                    .status()
13821                    .map(|s| s.code().unwrap_or(1))
13822                    .unwrap_or(127)
13823            });
13824            receivers.push(rx);
13825        }
13826
13827        // Wait for all — return worst status
13828        let mut worst = 0i32;
13829        for rx in receivers {
13830            if let Ok(status) = rx.recv() {
13831                if status > worst {
13832                    worst = status;
13833                }
13834            }
13835        }
13836
13837        self.last_status = worst;
13838        worst
13839    }
13840
13841    /// help - display help for builtins (bash)
13842    fn builtin_help(&self, args: &[String]) -> i32 {
13843        if args.is_empty() {
13844            println!("zshrs shell builtins:");
13845            println!("");
13846            println!("  alias, bg, bind, break, builtin, cd, command, continue,");
13847            println!("  declare, dirs, disown, echo, enable, eval, exec, exit,");
13848            println!("  export, false, fc, fg, getopts, hash, help, history,");
13849            println!("  jobs, kill, let, local, logout, popd, printf, pushd,");
13850            println!("  pwd, read, readonly, return, set, shift, shopt, source,");
13851            println!("  suspend, test, times, trap, true, type, typeset, ulimit,");
13852            println!("  umask, unalias, unset, wait, whence, where, which");
13853            println!("");
13854            println!("Type 'help name' for more information about 'name'.");
13855            return 0;
13856        }
13857
13858        let cmd = &args[0];
13859        match cmd.as_str() {
13860            "cd" => println!("cd: cd [-L|-P] [dir]\n    Change the shell working directory."),
13861            "echo" => println!("echo: echo [-neE] [arg ...]\n    Write arguments to standard output."),
13862            "export" => println!("export: export [-fn] [name[=value] ...]\n    Set export attribute for shell variables."),
13863            "alias" => println!("alias: alias [-p] [name[=value] ...]\n    Define or display aliases."),
13864            "history" => println!("history: history [-c] [-d offset] [n]\n    Display or manipulate the history list."),
13865            "jobs" => println!("jobs: jobs [-lnprs] [jobspec ...]\n    Display status of jobs."),
13866            "kill" => println!("kill: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n    Send a signal to a job."),
13867            "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."),
13868            "set" => println!("set: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]\n    Set or unset values of shell options and positional parameters."),
13869            "test" | "[" => println!("test: test [expr]\n    Evaluate conditional expression."),
13870            "type" => println!("type: type [-afptP] name [name ...]\n    Display information about command type."),
13871            _ => println!("{}: no help available", cmd),
13872        }
13873        0
13874    }
13875
13876    /// readarray/mapfile - read lines into array (bash)
13877    fn builtin_readarray(&mut self, args: &[String]) -> i32 {
13878        use std::io::{BufRead, BufReader};
13879
13880        let mut array_name = "MAPFILE".to_string();
13881        let mut delimiter = '\n';
13882        let mut count = 0usize; // 0 = unlimited
13883        let mut skip = 0usize;
13884        let mut strip_trailing = false;
13885        let mut callback: Option<String> = None;
13886        let mut callback_quantum = 0usize;
13887
13888        let mut i = 0;
13889        while i < args.len() {
13890            match args[i].as_str() {
13891                "-d" => {
13892                    i += 1;
13893                    if i < args.len() && !args[i].is_empty() {
13894                        delimiter = args[i].chars().next().unwrap_or('\n');
13895                    }
13896                }
13897                "-n" => {
13898                    i += 1;
13899                    if i < args.len() {
13900                        count = args[i].parse().unwrap_or(0);
13901                    }
13902                }
13903                "-O" => {
13904                    i += 1;
13905                    // Origin - start index (ignored, we always start at 0)
13906                }
13907                "-s" => {
13908                    i += 1;
13909                    if i < args.len() {
13910                        skip = args[i].parse().unwrap_or(0);
13911                    }
13912                }
13913                "-t" => strip_trailing = true,
13914                "-C" => {
13915                    i += 1;
13916                    if i < args.len() {
13917                        callback = Some(args[i].clone());
13918                    }
13919                }
13920                "-c" => {
13921                    i += 1;
13922                    if i < args.len() {
13923                        callback_quantum = args[i].parse().unwrap_or(5000);
13924                    }
13925                }
13926                "-u" => {
13927                    i += 1;
13928                    // fd - ignored, we read from stdin
13929                }
13930                s if !s.starts_with('-') => {
13931                    array_name = s.to_string();
13932                }
13933                _ => {}
13934            }
13935            i += 1;
13936        }
13937
13938        let stdin = std::io::stdin();
13939        let reader = BufReader::new(stdin.lock());
13940        let mut lines = Vec::new();
13941        let mut line_count = 0usize;
13942
13943        for line_result in reader.lines() {
13944            if let Ok(mut line) = line_result {
13945                line_count += 1;
13946
13947                if line_count <= skip {
13948                    continue;
13949                }
13950
13951                if strip_trailing {
13952                    while line.ends_with('\n') || line.ends_with('\r') {
13953                        line.pop();
13954                    }
13955                }
13956
13957                lines.push(line);
13958
13959                if count > 0 && lines.len() >= count {
13960                    break;
13961                }
13962            }
13963        }
13964
13965        self.arrays.insert(array_name, lines);
13966        let _ = (callback, callback_quantum);
13967        0
13968    }
13969
13970    fn builtin_shopt(&mut self, args: &[String]) -> i32 {
13971        if args.is_empty() {
13972            // List all shell options
13973            for (opt, val) in &self.options {
13974                println!("shopt {} {}", if *val { "-s" } else { "-u" }, opt);
13975            }
13976            return 0;
13977        }
13978
13979        let mut set = None;
13980        let mut opts = Vec::new();
13981
13982        for arg in args {
13983            match arg.as_str() {
13984                "-s" => set = Some(true),
13985                "-u" => set = Some(false),
13986                "-p" => {
13987                    // Print option status
13988                    for opt in &opts {
13989                        let val = self.options.get(opt).copied().unwrap_or(false);
13990                        println!("shopt {} {}", if val { "-s" } else { "-u" }, opt);
13991                    }
13992                    return 0;
13993                }
13994                _ => opts.push(arg.clone()),
13995            }
13996        }
13997
13998        if let Some(enable) = set {
13999            for opt in &opts {
14000                self.options.insert(opt.clone(), enable);
14001            }
14002        } else {
14003            // Query options
14004            for opt in &opts {
14005                let val = self.options.get(opt).copied().unwrap_or(false);
14006                println!("shopt {} {}", if val { "-s" } else { "-u" }, opt);
14007            }
14008        }
14009        0
14010    }
14011
14012    /// zsh-compatible setopt builtin
14013    fn builtin_setopt(&mut self, args: &[String]) -> i32 {
14014        if args.is_empty() {
14015            // List options that differ from compiled-in defaults (zsh behavior)
14016            // For default-ON options: show "noOPTION" if currently OFF
14017            // For default-OFF options: show "OPTION" if currently ON
14018            let defaults_on = Self::default_on_options();
14019            let mut diff_opts: Vec<String> = Vec::new();
14020
14021            for &opt in Self::all_zsh_options() {
14022                let enabled = self.options.get(opt).copied().unwrap_or(false);
14023                let is_default_on = defaults_on.contains(&opt);
14024
14025                if is_default_on && !enabled {
14026                    // Default ON but currently OFF -> show noOPTION
14027                    diff_opts.push(format!("no{}", opt));
14028                } else if !is_default_on && enabled {
14029                    // Default OFF but currently ON -> show OPTION
14030                    diff_opts.push(opt.to_string());
14031                }
14032            }
14033            diff_opts.sort();
14034            for opt in diff_opts {
14035                println!("{}", opt);
14036            }
14037            return 0;
14038        }
14039
14040        let mut use_pattern = false;
14041        let mut iter = args.iter().peekable();
14042
14043        while let Some(arg) = iter.next() {
14044            match arg.as_str() {
14045                "-m" => use_pattern = true,
14046                "-o" => {
14047                    // -o option_name: set option
14048                    if let Some(opt) = iter.next() {
14049                        let (name, enable) = Self::normalize_option_name(opt);
14050                        self.options.insert(name, enable);
14051                    }
14052                }
14053                "+o" => {
14054                    // +o option_name: unset option
14055                    if let Some(opt) = iter.next() {
14056                        let (name, enable) = Self::normalize_option_name(opt);
14057                        self.options.insert(name, !enable);
14058                    }
14059                }
14060                _ => {
14061                    if use_pattern {
14062                        // Match pattern against all options
14063                        for opt in Self::all_zsh_options() {
14064                            if Self::option_matches_pattern(opt, arg) {
14065                                self.options.insert(opt.to_string(), true);
14066                            }
14067                        }
14068                    } else {
14069                        let (name, enable) = Self::normalize_option_name(arg);
14070                        // Verify it's a valid option (zsh doesn't error on bad names in setopt)
14071                        self.options.insert(name, enable);
14072                    }
14073                }
14074            }
14075        }
14076        0
14077    }
14078
14079    /// zsh-compatible unsetopt builtin
14080    fn builtin_unsetopt(&mut self, args: &[String]) -> i32 {
14081        if args.is_empty() {
14082            // List all options in the format you'd pass to unsetopt to disable them
14083            // For default-ON options: show "noOPTION" (to turn it off)
14084            // For default-OFF options: show "OPTION" (already off, but this is what you'd type)
14085            let defaults_on = Self::default_on_options();
14086            let mut all_opts: Vec<String> = Vec::new();
14087
14088            for &opt in Self::all_zsh_options() {
14089                let is_default_on = defaults_on.contains(&opt);
14090                if is_default_on {
14091                    all_opts.push(format!("no{}", opt));
14092                } else {
14093                    all_opts.push(opt.to_string());
14094                }
14095            }
14096            all_opts.sort();
14097            for opt in all_opts {
14098                println!("{}", opt);
14099            }
14100            return 0;
14101        }
14102
14103        let mut use_pattern = false;
14104        let mut iter = args.iter().peekable();
14105
14106        while let Some(arg) = iter.next() {
14107            match arg.as_str() {
14108                "-m" => use_pattern = true,
14109                "-o" => {
14110                    // -o option_name: unset option
14111                    if let Some(opt) = iter.next() {
14112                        let (name, enable) = Self::normalize_option_name(opt);
14113                        self.options.insert(name, !enable);
14114                    }
14115                }
14116                "+o" => {
14117                    // +o option_name: set option (opposite in unsetopt)
14118                    if let Some(opt) = iter.next() {
14119                        let (name, enable) = Self::normalize_option_name(opt);
14120                        self.options.insert(name, enable);
14121                    }
14122                }
14123                _ => {
14124                    if use_pattern {
14125                        for opt in Self::all_zsh_options() {
14126                            if Self::option_matches_pattern(opt, arg) {
14127                                self.options.insert(opt.to_string(), false);
14128                            }
14129                        }
14130                    } else {
14131                        let (name, enable) = Self::normalize_option_name(arg);
14132                        // unsetopt turns OFF the option (or ON if "no" prefix)
14133                        self.options.insert(name, !enable);
14134                    }
14135                }
14136            }
14137        }
14138        0
14139    }
14140
14141    fn builtin_getopts(&mut self, args: &[String]) -> i32 {
14142        if args.len() < 2 {
14143            eprintln!("zshrs: getopts: usage: getopts optstring name [arg ...]");
14144            return 1;
14145        }
14146
14147        let optstring = &args[0];
14148        let varname = &args[1];
14149        let opt_args: Vec<&str> = if args.len() > 2 {
14150            args[2..].iter().map(|s| s.as_str()).collect()
14151        } else {
14152            self.positional_params.iter().map(|s| s.as_str()).collect()
14153        };
14154
14155        // Get current OPTIND
14156        let optind: usize = self
14157            .variables
14158            .get("OPTIND")
14159            .and_then(|s| s.parse().ok())
14160            .unwrap_or(1);
14161
14162        if optind > opt_args.len() {
14163            self.variables.insert(varname.to_string(), "?".to_string());
14164            return 1;
14165        }
14166
14167        let current_arg = opt_args[optind - 1];
14168
14169        if !current_arg.starts_with('-') || current_arg == "-" {
14170            self.variables.insert(varname.to_string(), "?".to_string());
14171            return 1;
14172        }
14173
14174        if current_arg == "--" {
14175            self.variables
14176                .insert("OPTIND".to_string(), (optind + 1).to_string());
14177            self.variables.insert(varname.to_string(), "?".to_string());
14178            return 1;
14179        }
14180
14181        // Get current option position within the argument
14182        let optpos: usize = self
14183            .variables
14184            .get("_OPTPOS")
14185            .and_then(|s| s.parse().ok())
14186            .unwrap_or(1);
14187
14188        let opt_char = current_arg.chars().nth(optpos);
14189
14190        if let Some(c) = opt_char {
14191            // Look up option in optstring
14192            let opt_idx = optstring.find(c);
14193
14194            match opt_idx {
14195                Some(idx) => {
14196                    // Check if option takes an argument
14197                    let takes_arg = optstring.chars().nth(idx + 1) == Some(':');
14198
14199                    if takes_arg {
14200                        // Get argument
14201                        let arg = if optpos + 1 < current_arg.len() {
14202                            // Argument is rest of current arg
14203                            current_arg[optpos + 1..].to_string()
14204                        } else if optind < opt_args.len() {
14205                            // Argument is next arg
14206                            self.variables
14207                                .insert("OPTIND".to_string(), (optind + 2).to_string());
14208                            self.variables.remove("_OPTPOS");
14209                            opt_args[optind].to_string()
14210                        } else {
14211                            // Missing argument
14212                            self.variables.insert(varname.to_string(), "?".to_string());
14213                            if !optstring.starts_with(':') {
14214                                eprintln!("zshrs: getopts: option requires an argument -- {}", c);
14215                            }
14216                            self.variables.insert("OPTARG".to_string(), c.to_string());
14217                            return 1;
14218                        };
14219
14220                        self.variables.insert("OPTARG".to_string(), arg);
14221                        self.variables
14222                            .insert("OPTIND".to_string(), (optind + 1).to_string());
14223                        self.variables.remove("_OPTPOS");
14224                    } else {
14225                        // No argument needed
14226                        if optpos + 1 < current_arg.len() {
14227                            // More options in this arg
14228                            self.variables
14229                                .insert("_OPTPOS".to_string(), (optpos + 1).to_string());
14230                        } else {
14231                            // Move to next arg
14232                            self.variables
14233                                .insert("OPTIND".to_string(), (optind + 1).to_string());
14234                            self.variables.remove("_OPTPOS");
14235                        }
14236                    }
14237
14238                    self.variables.insert(varname.to_string(), c.to_string());
14239                    0
14240                }
14241                None => {
14242                    // Unknown option
14243                    if !optstring.starts_with(':') {
14244                        eprintln!("zshrs: getopts: illegal option -- {}", c);
14245                    }
14246                    self.variables.insert(varname.to_string(), "?".to_string());
14247                    self.variables.insert("OPTARG".to_string(), c.to_string());
14248
14249                    // Advance to next option/arg
14250                    if optpos + 1 < current_arg.len() {
14251                        self.variables
14252                            .insert("_OPTPOS".to_string(), (optpos + 1).to_string());
14253                    } else {
14254                        self.variables
14255                            .insert("OPTIND".to_string(), (optind + 1).to_string());
14256                        self.variables.remove("_OPTPOS");
14257                    }
14258                    0
14259                }
14260            }
14261        } else {
14262            // No more options in current arg
14263            self.variables
14264                .insert("OPTIND".to_string(), (optind + 1).to_string());
14265            self.variables.remove("_OPTPOS");
14266            self.variables.insert(varname.to_string(), "?".to_string());
14267            1
14268        }
14269    }
14270
14271    fn builtin_type(&mut self, args: &[String]) -> i32 {
14272        if args.is_empty() {
14273            return 0;
14274        }
14275
14276        let mut show_all = false;
14277        let mut path_only = false;
14278        let mut silent = false;
14279        let mut show_type = false;
14280        let mut names = Vec::new();
14281
14282        let mut iter = args.iter();
14283        while let Some(arg) = iter.next() {
14284            if arg.starts_with('-') && arg.len() > 1 {
14285                for c in arg[1..].chars() {
14286                    match c {
14287                        'a' => show_all = true,
14288                        'p' => path_only = true,
14289                        'P' => path_only = true,
14290                        's' => silent = true,
14291                        't' => show_type = true,
14292                        'f' => {} // ignore functions (we still show them)
14293                        'w' => {} // like -t but different format
14294                        _ => {}
14295                    }
14296                }
14297            } else {
14298                names.push(arg.clone());
14299            }
14300        }
14301
14302        if names.is_empty() {
14303            return 0;
14304        }
14305
14306        let mut status = 0;
14307        for name in &names {
14308            let mut found_any = false;
14309
14310            // Check for alias (skip if -p)
14311            if !path_only && self.aliases.contains_key(name) {
14312                found_any = true;
14313                if !silent {
14314                    if show_type {
14315                        println!("alias");
14316                    } else {
14317                        println!(
14318                            "{} is aliased to `{}'",
14319                            name,
14320                            self.aliases.get(name).unwrap()
14321                        );
14322                    }
14323                }
14324                if !show_all {
14325                    continue;
14326                }
14327            }
14328
14329            // Check for function (skip if -p)
14330            if !path_only && self.functions.contains_key(name) {
14331                found_any = true;
14332                if !silent {
14333                    if show_type {
14334                        println!("function");
14335                    } else {
14336                        println!("{} is a shell function", name);
14337                    }
14338                }
14339                if !show_all {
14340                    continue;
14341                }
14342            }
14343
14344            // Check for builtin (skip if -p)
14345            if !path_only && (self.is_builtin(name) || name == ":" || name == "[") {
14346                found_any = true;
14347                if !silent {
14348                    if show_type {
14349                        println!("builtin");
14350                    } else {
14351                        println!("{} is a shell builtin", name);
14352                    }
14353                }
14354                if !show_all {
14355                    continue;
14356                }
14357            }
14358
14359            // Check for external command in PATH
14360            if let Ok(path_env) = std::env::var("PATH") {
14361                for dir in path_env.split(':') {
14362                    let full_path = format!("{}/{}", dir, name);
14363                    if std::path::Path::new(&full_path).exists() {
14364                        found_any = true;
14365                        if !silent {
14366                            if show_type {
14367                                println!("file");
14368                            } else {
14369                                println!("{} is {}", name, full_path);
14370                            }
14371                        }
14372                        if !show_all {
14373                            break;
14374                        }
14375                    }
14376                }
14377            }
14378
14379            if !found_any {
14380                if !silent {
14381                    eprintln!("zshrs: type: {}: not found", name);
14382                }
14383                status = 1;
14384            }
14385        }
14386        status
14387    }
14388
14389    fn builtin_hash(&mut self, args: &[String]) -> i32 {
14390        // hash [ -Ldfmrv ] [ name[=value] ] ...
14391        // hash -r clears the hash table
14392        // hash -d manages named directories
14393        // hash -f fills the table with all PATH commands
14394        // hash -m matches patterns
14395        // hash -v verbose
14396        // hash -L list in form suitable for reinput
14397
14398        let mut dir_mode = false;
14399        let mut rehash = false;
14400        let mut fill_all = false;
14401        let mut pattern_match = false;
14402        let mut verbose = false;
14403        let mut list_form = false;
14404        let mut names = Vec::new();
14405
14406        let mut i = 0;
14407        while i < args.len() {
14408            let arg = &args[i];
14409            if arg.starts_with('-') && arg.len() > 1 {
14410                for ch in arg[1..].chars() {
14411                    match ch {
14412                        'd' => dir_mode = true,
14413                        'r' => rehash = true,
14414                        'f' => fill_all = true,
14415                        'm' => pattern_match = true,
14416                        'v' => verbose = true,
14417                        'L' => list_form = true,
14418                        _ => {}
14419                    }
14420                }
14421            } else {
14422                names.push(arg.clone());
14423            }
14424            i += 1;
14425        }
14426
14427        // -r: clear hash table
14428        if rehash && !dir_mode && names.is_empty() {
14429            self.command_hash.clear();
14430            return 0;
14431        }
14432
14433        // -f: fill hash table with all commands in PATH
14434        if fill_all {
14435            if let Ok(path_var) = env::var("PATH") {
14436                for dir in path_var.split(':') {
14437                    if let Ok(entries) = std::fs::read_dir(dir) {
14438                        for entry in entries.flatten() {
14439                            if let Ok(ft) = entry.file_type() {
14440                                if ft.is_file() || ft.is_symlink() {
14441                                    if let Some(name) = entry.file_name().to_str() {
14442                                        let path = entry.path().to_string_lossy().to_string();
14443                                        self.command_hash.insert(name.to_string(), path);
14444                                    }
14445                                }
14446                            }
14447                        }
14448                    }
14449                }
14450            }
14451            return 0;
14452        }
14453
14454        if dir_mode {
14455            // Named directories mode (hash -d)
14456            if names.is_empty() {
14457                // List named directories
14458                for (name, path) in &self.named_dirs {
14459                    if list_form {
14460                        println!("hash -d {}={}", name, path.display());
14461                    } else if verbose {
14462                        println!("{}={}", name, path.display());
14463                    } else {
14464                        println!("{}={}", name, path.display());
14465                    }
14466                }
14467                return 0;
14468            }
14469
14470            if rehash {
14471                // Remove named directories
14472                if pattern_match {
14473                    // -m: pattern matching
14474                    let to_remove: Vec<String> = self
14475                        .named_dirs
14476                        .keys()
14477                        .filter(|k| {
14478                            names.iter().any(|pat| {
14479                                let pattern = pat.replace("*", ".*").replace("?", ".");
14480                                regex::Regex::new(&format!("^{}$", pattern))
14481                                    .map(|r| r.is_match(k))
14482                                    .unwrap_or(false)
14483                            })
14484                        })
14485                        .cloned()
14486                        .collect();
14487                    for name in to_remove {
14488                        self.named_dirs.remove(&name);
14489                    }
14490                } else {
14491                    for name in &names {
14492                        self.named_dirs.remove(name);
14493                    }
14494                }
14495                return 0;
14496            }
14497
14498            // Add named directories
14499            for name in &names {
14500                if let Some((n, p)) = name.split_once('=') {
14501                    self.add_named_dir(n, p);
14502                } else {
14503                    eprintln!("hash: -d: {} not in name=value format", name);
14504                    return 1;
14505                }
14506            }
14507            return 0;
14508        }
14509
14510        // Regular hash - command path lookup
14511        if names.is_empty() {
14512            // List all hashed commands
14513            for (name, path) in &self.command_hash {
14514                if list_form {
14515                    println!("hash {}={}", name, path);
14516                } else {
14517                    println!("{}={}", name, path);
14518                }
14519            }
14520            return 0;
14521        }
14522
14523        for name in &names {
14524            if let Some((cmd, path)) = name.split_once('=') {
14525                // Explicit assignment
14526                self.command_hash.insert(cmd.to_string(), path.to_string());
14527                if verbose {
14528                    println!("{}={}", cmd, path);
14529                }
14530            } else if let Some(path) = self.find_in_path(name) {
14531                // Look up in PATH and hash it
14532                self.command_hash.insert(name.clone(), path.clone());
14533                if verbose {
14534                    println!("{}={}", name, path);
14535                }
14536            } else {
14537                eprintln!("zshrs: hash: {}: not found", name);
14538                return 1;
14539            }
14540        }
14541        0
14542    }
14543
14544    /// add-zsh-hook builtin - add function to hook
14545    fn builtin_add_zsh_hook(&mut self, args: &[String]) -> i32 {
14546        // add-zsh-hook [-d] hook function
14547        if args.len() < 2 {
14548            eprintln!("usage: add-zsh-hook [-d] hook function");
14549            return 1;
14550        }
14551
14552        let (delete, hook, func) = if args[0] == "-d" {
14553            if args.len() < 3 {
14554                eprintln!("usage: add-zsh-hook -d hook function");
14555                return 1;
14556            }
14557            (true, &args[1], &args[2])
14558        } else {
14559            (false, &args[0], &args[1])
14560        };
14561
14562        if delete {
14563            // Remove function from hook
14564            if let Some(funcs) = self.hook_functions.get_mut(hook.as_str()) {
14565                funcs.retain(|f| f != func);
14566            }
14567        } else {
14568            // Add function to hook
14569            self.add_hook(hook, func);
14570        }
14571        0
14572    }
14573
14574    fn builtin_command(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
14575        // command [ -pvV ] simple command
14576        // -p: use default PATH
14577        // -v: print path (like which)
14578        // -V: verbose description (like type)
14579        let mut use_default_path = false;
14580        let mut print_path = false;
14581        let mut verbose = false;
14582        let mut positional_args: Vec<&str> = Vec::new();
14583
14584        let mut i = 0;
14585        while i < args.len() {
14586            let arg = &args[i];
14587            if arg.starts_with('-') && arg.len() > 1 && positional_args.is_empty() {
14588                for ch in arg[1..].chars() {
14589                    match ch {
14590                        'p' => use_default_path = true,
14591                        'v' => print_path = true,
14592                        'V' => verbose = true,
14593                        '-' => {
14594                            // -- ends options
14595                            i += 1;
14596                            break;
14597                        }
14598                        _ => {
14599                            eprintln!("command: bad option: -{}", ch);
14600                            return 1;
14601                        }
14602                    }
14603                }
14604            } else {
14605                positional_args.push(arg);
14606            }
14607            i += 1;
14608        }
14609
14610        // Add remaining args after --
14611        while i < args.len() {
14612            positional_args.push(&args[i]);
14613            i += 1;
14614        }
14615
14616        if positional_args.is_empty() {
14617            return 0;
14618        }
14619
14620        let cmd = positional_args[0];
14621
14622        // -v or -V: just print info about command
14623        if print_path || verbose {
14624            // Search PATH for command
14625            let path_var = if use_default_path {
14626                "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string()
14627            } else {
14628                env::var("PATH").unwrap_or_default()
14629            };
14630
14631            for dir in path_var.split(':') {
14632                let full_path = PathBuf::from(dir).join(cmd);
14633                if full_path.exists() && full_path.is_file() {
14634                    if verbose {
14635                        println!("{} is {}", cmd, full_path.display());
14636                    } else {
14637                        println!("{}", full_path.display());
14638                    }
14639                    return 0;
14640                }
14641            }
14642
14643            if verbose {
14644                eprintln!("{} not found", cmd);
14645            }
14646            return 1;
14647        }
14648
14649        // Execute as external command (bypassing functions and aliases)
14650        let cmd_args: Vec<String> = positional_args[1..].iter().map(|s| s.to_string()).collect();
14651
14652        if use_default_path {
14653            // Temporarily set PATH
14654            let old_path = env::var("PATH").ok();
14655            env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin");
14656            let result = self
14657                .execute_external(
14658                    cmd,
14659                    &cmd_args
14660                        .iter()
14661                        .map(|s| s.as_str())
14662                        .collect::<Vec<_>>()
14663                        .join(" ")
14664                        .split_whitespace()
14665                        .map(String::from)
14666                        .collect::<Vec<_>>(),
14667                    redirects,
14668                )
14669                .unwrap_or(127);
14670            if let Some(p) = old_path {
14671                env::set_var("PATH", p);
14672            }
14673            result
14674        } else {
14675            self.execute_external(cmd, &cmd_args, redirects)
14676                .unwrap_or(127)
14677        }
14678    }
14679
14680    fn builtin_builtin(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
14681        // Run builtin, bypassing functions and aliases
14682        if args.is_empty() {
14683            return 0;
14684        }
14685
14686        let cmd = &args[0];
14687        let cmd_args = &args[1..];
14688
14689        match cmd.as_str() {
14690            "cd" => self.builtin_cd(cmd_args),
14691            "pwd" => self.builtin_pwd(redirects),
14692            "echo" => self.builtin_echo(cmd_args, redirects),
14693            "export" => self.builtin_export(cmd_args),
14694            "unset" => self.builtin_unset(cmd_args),
14695            "exit" => self.builtin_exit(cmd_args),
14696            "return" => self.builtin_return(cmd_args),
14697            "true" => 0,
14698            "false" => 1,
14699            ":" => 0,
14700            "test" | "[" => self.builtin_test(cmd_args),
14701            "local" => self.builtin_local(cmd_args),
14702            "declare" | "typeset" => self.builtin_declare(cmd_args),
14703            "read" => self.builtin_read(cmd_args),
14704            "shift" => self.builtin_shift(cmd_args),
14705            "eval" => self.builtin_eval(cmd_args),
14706            "alias" => self.builtin_alias(cmd_args),
14707            "unalias" => self.builtin_unalias(cmd_args),
14708            "set" => self.builtin_set(cmd_args),
14709            "shopt" => self.builtin_shopt(cmd_args),
14710            "getopts" => self.builtin_getopts(cmd_args),
14711            "type" => self.builtin_type(cmd_args),
14712            "hash" => self.builtin_hash(cmd_args),
14713            "add-zsh-hook" => self.builtin_add_zsh_hook(cmd_args),
14714            "autoload" => self.builtin_autoload(cmd_args),
14715            "source" | "." => self.builtin_source(cmd_args),
14716            "functions" => self.builtin_functions(cmd_args),
14717            "zle" => self.builtin_zle(cmd_args),
14718            "bindkey" => self.builtin_bindkey(cmd_args),
14719            "setopt" => self.builtin_setopt(cmd_args),
14720            "unsetopt" => self.builtin_unsetopt(cmd_args),
14721            "emulate" => self.builtin_emulate(cmd_args),
14722            "zstyle" => self.builtin_zstyle(cmd_args),
14723            "compadd" => self.builtin_compadd(cmd_args),
14724            "compset" => self.builtin_compset(cmd_args),
14725            "compdef" => self.builtin_compdef(cmd_args),
14726            "compinit" => self.builtin_compinit(cmd_args),
14727            "cdreplay" => self.builtin_cdreplay(cmd_args),
14728            "zmodload" => self.builtin_zmodload(cmd_args),
14729            "zcompile" => self.builtin_zcompile(cmd_args),
14730            "zformat" => self.builtin_zformat(cmd_args),
14731            "zprof" => self.builtin_zprof(cmd_args),
14732            "print" => self.builtin_print(cmd_args),
14733            "printf" => self.builtin_printf(cmd_args),
14734            "command" => self.builtin_command(cmd_args, redirects),
14735            "whence" => self.builtin_whence(cmd_args),
14736            "which" => self.builtin_which(cmd_args),
14737            "where" => self.builtin_where(cmd_args),
14738            "fc" => self.builtin_fc(cmd_args),
14739            "history" => self.builtin_history(cmd_args),
14740            "dirs" => self.builtin_dirs(cmd_args),
14741            "pushd" => self.builtin_pushd(cmd_args),
14742            "popd" => self.builtin_popd(cmd_args),
14743            "bg" => self.builtin_bg(cmd_args),
14744            "fg" => self.builtin_fg(cmd_args),
14745            "jobs" => self.builtin_jobs(cmd_args),
14746            "kill" => self.builtin_kill(cmd_args),
14747            "wait" => self.builtin_wait(cmd_args),
14748            "trap" => self.builtin_trap(cmd_args),
14749            "umask" => self.builtin_umask(cmd_args),
14750            "ulimit" => self.builtin_ulimit(cmd_args),
14751            "times" => self.builtin_times(cmd_args),
14752            "let" => self.builtin_let(cmd_args),
14753            "integer" => self.builtin_integer(cmd_args),
14754            "float" => self.builtin_float(cmd_args),
14755            "readonly" => self.builtin_readonly(cmd_args),
14756            _ => {
14757                eprintln!("zshrs: builtin: {}: not a shell builtin", cmd);
14758                1
14759            }
14760        }
14761    }
14762
14763    fn builtin_let(&mut self, args: &[String]) -> i32 {
14764        if args.is_empty() {
14765            return 1;
14766        }
14767
14768        let mut result = 0i64;
14769        for expr in args {
14770            result = self.evaluate_arithmetic_expr(expr);
14771        }
14772
14773        // let returns 1 if last expression evaluates to 0, 0 otherwise
14774        if result == 0 {
14775            1
14776        } else {
14777            0
14778        }
14779    }
14780
14781    /// Generate completion candidates
14782    fn builtin_compgen(&self, args: &[String]) -> i32 {
14783        let mut i = 0;
14784        let mut prefix = String::new();
14785        let mut actions = Vec::new();
14786        let mut wordlist = None;
14787        let mut globpat = None;
14788
14789        while i < args.len() {
14790            match args[i].as_str() {
14791                "-W" => {
14792                    i += 1;
14793                    if i < args.len() {
14794                        wordlist = Some(args[i].clone());
14795                    }
14796                }
14797                "-G" => {
14798                    i += 1;
14799                    if i < args.len() {
14800                        globpat = Some(args[i].clone());
14801                    }
14802                }
14803                "-a" => actions.push("alias"),
14804                "-b" => actions.push("builtin"),
14805                "-c" => actions.push("command"),
14806                "-d" => actions.push("directory"),
14807                "-e" => actions.push("export"),
14808                "-f" => actions.push("file"),
14809                "-j" => actions.push("job"),
14810                "-k" => actions.push("keyword"),
14811                "-u" => actions.push("user"),
14812                "-v" => actions.push("variable"),
14813                s if !s.starts_with('-') => prefix = s.to_string(),
14814                _ => {}
14815            }
14816            i += 1;
14817        }
14818
14819        let mut results = Vec::new();
14820
14821        // Generate based on actions
14822        for action in actions {
14823            match action {
14824                "alias" => {
14825                    for name in self.aliases.keys() {
14826                        if name.starts_with(&prefix) {
14827                            results.push(name.clone());
14828                        }
14829                    }
14830                }
14831                "builtin" => {
14832                    for name in [
14833                        "cd", "pwd", "echo", "export", "unset", "source", "exit", "return", "true",
14834                        "false", ":", "test", "[", "local", "declare", "jobs", "fg", "bg", "kill",
14835                        "disown", "wait", "alias", "unalias", "set", "shopt",
14836                    ] {
14837                        if name.starts_with(&prefix) {
14838                            results.push(name.to_string());
14839                        }
14840                    }
14841                }
14842                "directory" => {
14843                    if let Ok(entries) = std::fs::read_dir(".") {
14844                        for entry in entries.flatten() {
14845                            if let Ok(ft) = entry.file_type() {
14846                                if ft.is_dir() {
14847                                    let name = entry.file_name().to_string_lossy().to_string();
14848                                    if name.starts_with(&prefix) {
14849                                        results.push(name);
14850                                    }
14851                                }
14852                            }
14853                        }
14854                    }
14855                }
14856                "file" => {
14857                    if let Ok(entries) = std::fs::read_dir(".") {
14858                        for entry in entries.flatten() {
14859                            let name = entry.file_name().to_string_lossy().to_string();
14860                            if name.starts_with(&prefix) {
14861                                results.push(name);
14862                            }
14863                        }
14864                    }
14865                }
14866                "variable" => {
14867                    for name in self.variables.keys() {
14868                        if name.starts_with(&prefix) {
14869                            results.push(name.clone());
14870                        }
14871                    }
14872                    for name in std::env::vars().map(|(k, _)| k) {
14873                        if name.starts_with(&prefix) && !results.contains(&name) {
14874                            results.push(name);
14875                        }
14876                    }
14877                }
14878                _ => {}
14879            }
14880        }
14881
14882        // Handle wordlist
14883        if let Some(words) = wordlist {
14884            for word in words.split_whitespace() {
14885                if word.starts_with(&prefix) {
14886                    results.push(word.to_string());
14887                }
14888            }
14889        }
14890
14891        // Handle glob pattern
14892        if let Some(_pattern) = globpat {
14893            let full_pattern = format!("{}*", prefix);
14894            if let Ok(paths) = glob::glob(&full_pattern) {
14895                for path in paths.flatten() {
14896                    results.push(path.to_string_lossy().to_string());
14897                }
14898            }
14899        }
14900
14901        results.sort();
14902        results.dedup();
14903        for r in results {
14904            println!("{}", r);
14905        }
14906        0
14907    }
14908
14909    /// Define completion spec for a command
14910    fn builtin_complete(&mut self, args: &[String]) -> i32 {
14911        if args.is_empty() {
14912            // List all completion specs
14913            for (cmd, spec) in &self.completions {
14914                let mut parts = vec!["complete".to_string()];
14915                for action in &spec.actions {
14916                    parts.push(format!("-{}", action));
14917                }
14918                if let Some(ref w) = spec.wordlist {
14919                    parts.push("-W".to_string());
14920                    parts.push(format!("'{}'", w));
14921                }
14922                if let Some(ref f) = spec.function {
14923                    parts.push("-F".to_string());
14924                    parts.push(f.clone());
14925                }
14926                if let Some(ref c) = spec.command {
14927                    parts.push("-C".to_string());
14928                    parts.push(c.clone());
14929                }
14930                parts.push(cmd.clone());
14931                println!("{}", parts.join(" "));
14932            }
14933            return 0;
14934        }
14935
14936        let mut spec = CompSpec::default();
14937        let mut commands = Vec::new();
14938        let mut i = 0;
14939
14940        while i < args.len() {
14941            match args[i].as_str() {
14942                "-W" => {
14943                    i += 1;
14944                    if i < args.len() {
14945                        spec.wordlist = Some(args[i].clone());
14946                    }
14947                }
14948                "-F" => {
14949                    i += 1;
14950                    if i < args.len() {
14951                        spec.function = Some(args[i].clone());
14952                    }
14953                }
14954                "-C" => {
14955                    i += 1;
14956                    if i < args.len() {
14957                        spec.command = Some(args[i].clone());
14958                    }
14959                }
14960                "-G" => {
14961                    i += 1;
14962                    if i < args.len() {
14963                        spec.globpat = Some(args[i].clone());
14964                    }
14965                }
14966                "-P" => {
14967                    i += 1;
14968                    if i < args.len() {
14969                        spec.prefix = Some(args[i].clone());
14970                    }
14971                }
14972                "-S" => {
14973                    i += 1;
14974                    if i < args.len() {
14975                        spec.suffix = Some(args[i].clone());
14976                    }
14977                }
14978                "-a" => spec.actions.push("a".to_string()),
14979                "-b" => spec.actions.push("b".to_string()),
14980                "-c" => spec.actions.push("c".to_string()),
14981                "-d" => spec.actions.push("d".to_string()),
14982                "-e" => spec.actions.push("e".to_string()),
14983                "-f" => spec.actions.push("f".to_string()),
14984                "-j" => spec.actions.push("j".to_string()),
14985                "-r" => {
14986                    // Remove completion spec
14987                    i += 1;
14988                    while i < args.len() {
14989                        self.completions.remove(&args[i]);
14990                        i += 1;
14991                    }
14992                    return 0;
14993                }
14994                s if !s.starts_with('-') => commands.push(s.to_string()),
14995                _ => {}
14996            }
14997            i += 1;
14998        }
14999
15000        for cmd in commands {
15001            self.completions.insert(cmd, spec.clone());
15002        }
15003        0
15004    }
15005
15006    /// Modify completion options
15007    fn builtin_compopt(&mut self, args: &[String]) -> i32 {
15008        // Basic stub - just accept the options
15009        let _ = args;
15010        0
15011    }
15012
15013    /// zsh compadd - add completion matches
15014    fn builtin_compadd(&mut self, args: &[String]) -> i32 {
15015        // Basic stub for zsh completion system
15016        // In a full implementation, this would add completion candidates
15017        let _ = args;
15018        0
15019    }
15020
15021    /// zsh compset - modify completion prefix/suffix
15022    fn builtin_compset(&mut self, args: &[String]) -> i32 {
15023        // Basic stub for zsh completion system
15024        let _ = args;
15025        0
15026    }
15027
15028    /// compdef - register completion functions for commands
15029    /// Usage: compdef _git git
15030    ///        compdef _docker docker docker-compose
15031    ///        compdef -d git  # delete
15032    fn builtin_compdef(&mut self, args: &[String]) -> i32 {
15033        if let Some(cache) = &mut self.compsys_cache {
15034            compsys::compdef::compdef_execute(cache, args)
15035        } else {
15036            // No cache - defer for cdreplay (zinit turbo mode)
15037            self.deferred_compdefs.push(args.to_vec());
15038            0
15039        }
15040    }
15041
15042    /// compinit - initialize the completion system
15043    /// Scans fpath for completion functions and registers them
15044    #[tracing::instrument(level = "info", skip(self))]
15045    fn builtin_compinit(&mut self, args: &[String]) -> i32 {
15046        // Parse options
15047        // -C: use cache if valid (skip fpath scan)
15048        // -D: don't dump (don't write .zcompdump)
15049        // -d file: specify dump file
15050        // -u: use insecure dirs anyway  -i: silently ignore insecure dirs
15051        // -q: quiet
15052        let mut quiet = false;
15053        let mut no_dump = false;
15054        let mut dump_file: Option<String> = None;
15055        let mut use_cache = false;
15056        let mut ignore_insecure = false;
15057        let mut use_insecure = false;
15058
15059        let mut i = 0;
15060        while i < args.len() {
15061            match args[i].as_str() {
15062                "-q" => quiet = true,
15063                "-C" => use_cache = true,
15064                "-D" => no_dump = true,
15065                "-d" => {
15066                    i += 1;
15067                    if i < args.len() {
15068                        dump_file = Some(args[i].clone());
15069                    }
15070                }
15071                "-u" => use_insecure = true,
15072                "-i" => ignore_insecure = true,
15073                _ => {}
15074            }
15075            i += 1;
15076        }
15077
15078        // Run compaudit with SQLite cache (unless -u skips it entirely)
15079        if !use_insecure && !self.posix_mode {
15080            if let Some(ref cache) = self.plugin_cache {
15081                let insecure = cache.compaudit_cached(&self.fpath);
15082                if !insecure.is_empty() && !ignore_insecure {
15083                    if !quiet {
15084                        eprintln!("compinit: insecure directories:");
15085                        for d in &insecure {
15086                            eprintln!("  {}", d);
15087                        }
15088                        eprintln!("compinit: run with -i to ignore or -u to use anyway");
15089                    }
15090                    return 1;
15091                }
15092            }
15093        }
15094
15095        // ZSH COMPAT MODE: Use traditional zsh algorithm (fpath scan, .zcompdump, no SQLite)
15096        if self.zsh_compat {
15097            return self.compinit_compat(quiet, no_dump, dump_file, use_cache);
15098        }
15099
15100        // ZSHRS MODE: Use SQLite cache with function bodies
15101
15102        // Try to use existing cache if -C and cache is valid
15103        if use_cache {
15104            if let Some(cache) = &self.compsys_cache {
15105                if compsys::cache_is_valid(cache) {
15106                    // Load from cache instead of rescanning
15107                    if let Ok(result) = compsys::load_from_cache(cache) {
15108                        if !quiet {
15109                            tracing::info!(
15110                                comps = result.comps.len(),
15111                                "compinit: using cached completions"
15112                            );
15113                        }
15114                        self.assoc_arrays.insert("_comps".to_string(), result.comps);
15115                        self.assoc_arrays
15116                            .insert("_services".to_string(), result.services);
15117                        self.assoc_arrays
15118                            .insert("_patcomps".to_string(), result.patcomps);
15119
15120                        // Background: fill bytecode blobs for any autoloads that have body but no ast.
15121                        // This populates the cache so subsequent autoload calls skip parsing.
15122                        if let Some(ref cache) = self.compsys_cache {
15123                            if let Ok(missing) = cache.count_autoloads_missing_bytecode() {
15124                                if missing > 0 {
15125                                    tracing::info!(
15126                                        count = missing,
15127                                        "compinit: backfilling bytecode blobs on worker pool"
15128                                    );
15129                                    let cache_path = compsys::cache::default_cache_path();
15130                                    let total_missing = missing;
15131                                    self.worker_pool.submit(move || {
15132                                        let mut cache = match compsys::cache::CompsysCache::open(&cache_path) {
15133                                            Ok(c) => c,
15134                                            Err(_) => return,
15135                                        };
15136                                        // Loop in batches of 100: fetch 100 bodies from SQLite,
15137                                        // parse them, write bytecode blobs back, repeat until none left.
15138                                        // Peak memory: ~100 function bodies + ASTs at a time.
15139                                        let mut total_cached = 0usize;
15140                                        loop {
15141                                            let stubs = match cache.get_autoloads_missing_bytecode_batch(100) {
15142                                                Ok(s) if !s.is_empty() => s,
15143                                                _ => break,
15144                                            };
15145                                            let mut batch: Vec<(String, Vec<u8>)> = Vec::with_capacity(stubs.len());
15146                                            for (name, body) in &stubs {
15147                                                let mut parser = crate::parser::ShellParser::new(body);
15148                                                if let Ok(commands) = parser.parse_script() {
15149                                                    if !commands.is_empty() {
15150                                                        let compiler = crate::shell_compiler::ShellCompiler::new();
15151                                                        let chunk = compiler.compile(&commands);
15152                                                        if let Ok(blob) = bincode::serialize(&chunk) {
15153                                                            batch.push((name.clone(), blob));
15154                                                        }
15155                                                    }
15156                                                }
15157                                            }
15158                                            total_cached += batch.len();
15159                                            if let Err(e) = cache.set_autoload_bytecodes_bulk(&batch) {
15160                                                tracing::warn!(error = %e, "compinit: bytecode backfill batch failed");
15161                                                break;
15162                                            }
15163                                            // If we got fewer than 100 results, we're done
15164                                            if stubs.len() < 100 {
15165                                                break;
15166                                            }
15167                                        }
15168                                        tracing::info!(
15169                                            cached = total_cached,
15170                                            total = total_missing,
15171                                            "compinit: bytecode backfill complete"
15172                                        );
15173                                    });
15174                                }
15175                            }
15176                        }
15177
15178                        return 0;
15179                    }
15180                }
15181            }
15182        }
15183
15184        // Ship compinit to worker pool — no ad-hoc thread spawn.
15185        // The heavy work (scan + SQLite write) runs on a pool thread.
15186        // Results are merged into shell state lazily via drain_compinit_bg().
15187        let fpath = self.fpath.clone();
15188        let fpath_count = fpath.len();
15189        let pool_size = self.worker_pool.size();
15190        let (tx, rx) = std::sync::mpsc::channel();
15191        let bg_start = std::time::Instant::now();
15192        tracing::info!(
15193            fpath_dirs = fpath_count,
15194            worker_pool = pool_size,
15195            "compinit: shipping to worker pool"
15196        );
15197        self.worker_pool.submit(move || {
15198            tracing::debug!("compinit-bg: thread started");
15199            let cache_path = compsys::cache::default_cache_path();
15200            if let Some(parent) = cache_path.parent() {
15201                let _ = std::fs::create_dir_all(parent);
15202            }
15203            // Remove old DB to start fresh
15204            let _ = std::fs::remove_file(&cache_path);
15205            let _ = std::fs::remove_file(format!("{}-shm", cache_path.display()));
15206            let _ = std::fs::remove_file(format!("{}-wal", cache_path.display()));
15207
15208            let mut cache = match compsys::cache::CompsysCache::open(&cache_path) {
15209                Ok(c) => c,
15210                Err(e) => {
15211                    tracing::error!("compinit: failed to create cache: {}", e);
15212                    return;
15213                }
15214            };
15215
15216            let result = match compsys::build_cache_from_fpath(&fpath, &mut cache) {
15217                Ok(r) => r,
15218                Err(e) => {
15219                    tracing::error!("compinit: scan failed: {}", e);
15220                    return;
15221                }
15222            };
15223
15224            tracing::info!(
15225                functions = result.files_scanned,
15226                comps = result.comps.len(),
15227                dirs = result.dirs_scanned,
15228                ms = result.scan_time_ms,
15229                "compinit: background scan complete"
15230            );
15231
15232            // Pre-parse function bodies and cache bytecode blobs.
15233            // Stream: parse one → serialize → write → drop. Never accumulate.
15234            // 16k functions × ~10KB AST = OOM if held in memory.
15235            let parse_start = std::time::Instant::now();
15236            let mut parse_ok = 0usize;
15237            let mut parse_fail = 0usize;
15238            let mut no_body = 0usize;
15239            let batch_size = 100;
15240            let mut batch: Vec<(String, Vec<u8>)> = Vec::with_capacity(batch_size);
15241
15242            for file in &result.files {
15243                if let Some(ref body) = file.body {
15244                    let mut parser = crate::parser::ShellParser::new(body);
15245                    match parser.parse_script() {
15246                        Ok(commands) if !commands.is_empty() => {
15247                            // Compile AST → fusevm bytecodes, then serialize the Chunk
15248                            let compiler = crate::shell_compiler::ShellCompiler::new();
15249                            let chunk = compiler.compile(&commands);
15250                            if let Ok(blob) = bincode::serialize(&chunk) {
15251                                batch.push((file.name.clone(), blob));
15252                                parse_ok += 1;
15253                                if batch.len() >= batch_size {
15254                                    let _ = cache.set_autoload_bytecodes_bulk(&batch);
15255                                    batch.clear();
15256                                }
15257                            }
15258                        }
15259                        Ok(_) => {
15260                            parse_fail += 1;
15261                        }
15262                        Err(_) => {
15263                            parse_fail += 1;
15264                        }
15265                    }
15266                } else {
15267                    no_body += 1;
15268                }
15269            }
15270            // Flush remaining
15271            if !batch.is_empty() {
15272                let _ = cache.set_autoload_bytecodes_bulk(&batch);
15273                batch.clear();
15274            }
15275
15276            tracing::info!(
15277                cached = parse_ok,
15278                failed = parse_fail,
15279                no_body = no_body,
15280                total = result.files.len(),
15281                ms = parse_start.elapsed().as_millis() as u64,
15282                "compinit: bytecode blobs cached"
15283            );
15284
15285            let _ = tx.send(CompInitBgResult { result, cache });
15286        });
15287
15288        self.compinit_pending = Some((rx, bg_start));
15289        0
15290    }
15291
15292    /// Non-blocking drain of background compinit results.
15293    /// Call this before any completion lookup (prompt, tab-complete, etc.).
15294    /// If the background thread hasn't finished yet, this is a no-op.
15295    pub fn drain_compinit_bg(&mut self) {
15296        if let Some((rx, start)) = self.compinit_pending.take() {
15297            match rx.try_recv() {
15298                Ok(bg) => {
15299                    let comps = bg.result.comps.len();
15300                    self.assoc_arrays
15301                        .insert("_comps".to_string(), bg.result.comps);
15302                    self.assoc_arrays
15303                        .insert("_services".to_string(), bg.result.services);
15304                    self.assoc_arrays
15305                        .insert("_patcomps".to_string(), bg.result.patcomps);
15306                    self.compsys_cache = Some(bg.cache);
15307                    tracing::info!(
15308                        wall_ms = start.elapsed().as_millis() as u64,
15309                        comps,
15310                        "compinit: background results merged"
15311                    );
15312                }
15313                Err(std::sync::mpsc::TryRecvError::Empty) => {
15314                    // Not ready yet — put the receiver back for next poll
15315                    self.compinit_pending = Some((rx, start));
15316                }
15317                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
15318                    tracing::warn!("compinit: background thread died without sending results");
15319                }
15320            }
15321        }
15322    }
15323
15324    /// Traditional zsh compinit (--zsh-compat mode)
15325    /// Uses fpath scanning, .zcompdump files, no SQLite
15326    fn compinit_compat(
15327        &mut self,
15328        quiet: bool,
15329        no_dump: bool,
15330        dump_file: Option<String>,
15331        use_cache: bool,
15332    ) -> i32 {
15333        let zdotdir = self
15334            .variables
15335            .get("ZDOTDIR")
15336            .cloned()
15337            .or_else(|| std::env::var("ZDOTDIR").ok())
15338            .unwrap_or_else(|| std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()));
15339
15340        let dump_path = dump_file
15341            .map(PathBuf::from)
15342            .unwrap_or_else(|| PathBuf::from(&zdotdir).join(".zcompdump"));
15343
15344        // -C: Try to use existing .zcompdump if valid
15345        if use_cache && dump_path.exists() {
15346            if compsys::check_dump(&dump_path, &self.fpath, "zshrs-0.1.0") {
15347                // Valid dump - source it to load _comps
15348                // For now, just rescan (proper impl would source the dump file)
15349                if !quiet {
15350                    tracing::info!("compinit: .zcompdump valid, rescanning for compat");
15351                }
15352            }
15353        }
15354
15355        // Full fpath scan (traditional zsh algorithm)
15356        let result = compsys::compinit(&self.fpath);
15357
15358        if !quiet {
15359            tracing::info!(
15360                functions = result.files_scanned,
15361                comps = result.comps.len(),
15362                dirs = result.dirs_scanned,
15363                ms = result.scan_time_ms,
15364                "compinit: fpath scan complete"
15365            );
15366        }
15367
15368        // Write .zcompdump unless -D
15369        if !no_dump {
15370            let _ = compsys::compdump(&result, &dump_path, "zshrs-0.1.0");
15371        }
15372
15373        // Set up _comps associative array
15374        self.assoc_arrays
15375            .insert("_comps".to_string(), result.comps.clone());
15376        self.assoc_arrays
15377            .insert("_services".to_string(), result.services.clone());
15378        self.assoc_arrays
15379            .insert("_patcomps".to_string(), result.patcomps.clone());
15380
15381        // No SQLite cache in compat mode
15382        self.compsys_cache = None;
15383
15384        0
15385    }
15386
15387    /// cdreplay - replay deferred compdef calls (zinit turbo mode)
15388    /// Usage: cdreplay [-q]
15389    fn builtin_cdreplay(&mut self, args: &[String]) -> i32 {
15390        let quiet = args.contains(&"-q".to_string());
15391
15392        if self.deferred_compdefs.is_empty() {
15393            return 0;
15394        }
15395
15396        let deferred = std::mem::take(&mut self.deferred_compdefs);
15397        let count = deferred.len();
15398
15399        if let Some(cache) = &mut self.compsys_cache {
15400            for compdef_args in deferred {
15401                compsys::compdef::compdef_execute(cache, &compdef_args);
15402            }
15403        }
15404
15405        if !quiet {
15406            eprintln!("cdreplay: replayed {} compdef calls", count);
15407        }
15408
15409        0
15410    }
15411
15412    /// zsh zstyle - configure styles for completion
15413    fn builtin_zstyle(&mut self, args: &[String]) -> i32 {
15414        if args.is_empty() {
15415            // List all styles
15416            for (pattern, style, values) in self.style_table.list(None) {
15417                println!("zstyle '{}' {} {}", pattern, style, values.join(" "));
15418            }
15419            return 0;
15420        }
15421
15422        // Handle options
15423        if args[0].starts_with('-') {
15424            match args[0].as_str() {
15425                "-d" => {
15426                    // Delete style
15427                    let pattern = args.get(1).map(|s| s.as_str());
15428                    let style = args.get(2).map(|s| s.as_str());
15429                    self.style_table.delete(pattern, style);
15430                    return 0;
15431                }
15432                "-g" => {
15433                    // Get style into array
15434                    if args.len() >= 4 {
15435                        let array_name = &args[1];
15436                        let context = &args[2];
15437                        let style = &args[3];
15438                        if let Some(values) = self.style_table.get(context, style) {
15439                            self.arrays.insert(array_name.clone(), values.to_vec());
15440                            return 0;
15441                        }
15442                    }
15443                    return 1;
15444                }
15445                "-s" => {
15446                    // Get style as scalar
15447                    if args.len() >= 4 {
15448                        let var_name = &args[1];
15449                        let context = &args[2];
15450                        let style = &args[3];
15451                        let sep = args.get(4).map(|s| s.as_str()).unwrap_or(" ");
15452                        if let Some(values) = self.style_table.get(context, style) {
15453                            self.variables.insert(var_name.clone(), values.join(sep));
15454                            return 0;
15455                        }
15456                    }
15457                    return 1;
15458                }
15459                "-t" => {
15460                    // Test style (check if true/yes)
15461                    if args.len() >= 3 {
15462                        let context = &args[1];
15463                        let style = &args[2];
15464                        return if self.style_table.test_bool(context, style).unwrap_or(false) {
15465                            0
15466                        } else {
15467                            1
15468                        };
15469                    }
15470                    return 1;
15471                }
15472                "-L" => {
15473                    // List in re-usable format
15474                    for (pattern, style, values) in self.style_table.list(None) {
15475                        let values_str = values
15476                            .iter()
15477                            .map(|v| format!("'{}'", v.replace('\'', "'\\''")))
15478                            .collect::<Vec<_>>()
15479                            .join(" ");
15480                        println!("zstyle '{}' {} {}", pattern, style, values_str);
15481                    }
15482                    return 0;
15483                }
15484                _ => {}
15485            }
15486        }
15487
15488        // Set style: zstyle pattern style values...
15489        if args.len() >= 2 {
15490            let pattern = &args[0];
15491            let style = &args[1];
15492            let values: Vec<String> = args[2..].to_vec();
15493            self.style_table.set(pattern, style, values.clone(), false);
15494
15495            // Write to SQLite cache for completion lookups
15496            if let Some(cache) = &self.compsys_cache {
15497                let _ = cache.set_zstyle(pattern, style, &values, false);
15498            }
15499
15500            // Also update legacy zstyles for backward compat
15501            let existing = self
15502                .zstyles
15503                .iter_mut()
15504                .find(|s| s.pattern == *pattern && s.style == *style);
15505            if let Some(s) = existing {
15506                s.values = args[2..].to_vec();
15507            } else {
15508                self.zstyles.push(ZStyle {
15509                    pattern: pattern.clone(),
15510                    style: style.clone(),
15511                    values: args[2..].to_vec(),
15512                });
15513            }
15514        }
15515        0
15516    }
15517
15518    /// Tie a parameter to a GDBM database
15519    /// Usage: ztie -d db/gdbm -f /path/to/db.gdbm [-r] PARAM_NAME
15520    fn builtin_ztie(&mut self, args: &[String]) -> i32 {
15521        use crate::db_gdbm;
15522
15523        let mut db_type: Option<String> = None;
15524        let mut file_path: Option<String> = None;
15525        let mut readonly = false;
15526        let mut param_args: Vec<String> = Vec::new();
15527
15528        let mut i = 0;
15529        while i < args.len() {
15530            match args[i].as_str() {
15531                "-d" => {
15532                    if i + 1 < args.len() {
15533                        db_type = Some(args[i + 1].clone());
15534                        i += 2;
15535                    } else {
15536                        eprintln!("ztie: -d requires an argument");
15537                        return 1;
15538                    }
15539                }
15540                "-f" => {
15541                    if i + 1 < args.len() {
15542                        file_path = Some(args[i + 1].clone());
15543                        i += 2;
15544                    } else {
15545                        eprintln!("ztie: -f requires an argument");
15546                        return 1;
15547                    }
15548                }
15549                "-r" => {
15550                    readonly = true;
15551                    i += 1;
15552                }
15553                arg if arg.starts_with('-') => {
15554                    eprintln!("ztie: bad option: {}", arg);
15555                    return 1;
15556                }
15557                _ => {
15558                    param_args.push(args[i].clone());
15559                    i += 1;
15560                }
15561            }
15562        }
15563
15564        match db_gdbm::ztie(
15565            &param_args,
15566            readonly,
15567            db_type.as_deref(),
15568            file_path.as_deref(),
15569        ) {
15570            Ok(()) => 0,
15571            Err(e) => {
15572                eprintln!("ztie: {}", e);
15573                1
15574            }
15575        }
15576    }
15577
15578    /// Untie a parameter from its GDBM database
15579    /// Usage: zuntie [-u] PARAM_NAME...
15580    fn builtin_zuntie(&mut self, args: &[String]) -> i32 {
15581        use crate::db_gdbm;
15582
15583        let mut force_unset = false;
15584        let mut param_args: Vec<String> = Vec::new();
15585
15586        for arg in args {
15587            match arg.as_str() {
15588                "-u" => force_unset = true,
15589                a if a.starts_with('-') => {
15590                    eprintln!("zuntie: bad option: {}", a);
15591                    return 1;
15592                }
15593                _ => param_args.push(arg.clone()),
15594            }
15595        }
15596
15597        if param_args.is_empty() {
15598            eprintln!("zuntie: not enough arguments");
15599            return 1;
15600        }
15601
15602        match db_gdbm::zuntie(&param_args, force_unset) {
15603            Ok(()) => 0,
15604            Err(e) => {
15605                eprintln!("zuntie: {}", e);
15606                1
15607            }
15608        }
15609    }
15610
15611    /// Get the path of a tied GDBM database
15612    /// Usage: zgdbmpath PARAM_NAME
15613    /// Sets $REPLY to the path
15614    fn builtin_zgdbmpath(&mut self, args: &[String]) -> i32 {
15615        use crate::db_gdbm;
15616
15617        if args.is_empty() {
15618            eprintln!(
15619                "zgdbmpath: parameter name (whose path is to be written to $REPLY) is required"
15620            );
15621            return 1;
15622        }
15623
15624        match db_gdbm::zgdbmpath(&args[0]) {
15625            Ok(path) => {
15626                self.variables.insert("REPLY".to_string(), path.clone());
15627                std::env::set_var("REPLY", &path);
15628                0
15629            }
15630            Err(e) => {
15631                eprintln!("zgdbmpath: {}", e);
15632                1
15633            }
15634        }
15635    }
15636
15637    /// Push directory onto stack and cd to it
15638    fn builtin_pushd(&mut self, args: &[String]) -> i32 {
15639        // pushd [ -qsLP ] [ arg ]
15640        // pushd [ -qsLP ] old new
15641        // pushd [ -qsLP ] {+|-}n
15642        // -q: quiet (don't print stack)
15643        // -s: no symlink resolution (use -L cd behavior)
15644        // -L: logical directory (resolve .. before symlinks)
15645        // -P: physical directory (resolve symlinks)
15646
15647        let mut quiet = false;
15648        let mut physical = false;
15649        let mut positional_args: Vec<String> = Vec::new();
15650
15651        for arg in args {
15652            if arg.starts_with('-') && arg.len() > 1 {
15653                // Check if it's a stack index
15654                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
15655                    positional_args.push(arg.clone());
15656                    continue;
15657                }
15658                for ch in arg[1..].chars() {
15659                    match ch {
15660                        'q' => quiet = true,
15661                        's' => physical = false,
15662                        'L' => physical = false,
15663                        'P' => physical = true,
15664                        _ => {}
15665                    }
15666                }
15667            } else if arg.starts_with('+') {
15668                positional_args.push(arg.clone());
15669            } else {
15670                positional_args.push(arg.clone());
15671            }
15672        }
15673
15674        let current = match std::env::current_dir() {
15675            Ok(p) => p,
15676            Err(e) => {
15677                eprintln!("pushd: {}", e);
15678                return 1;
15679            }
15680        };
15681
15682        if positional_args.is_empty() {
15683            // Swap top two directories
15684            if self.dir_stack.is_empty() {
15685                eprintln!("pushd: no other directory");
15686                return 1;
15687            }
15688            let target = self.dir_stack.pop().unwrap();
15689            self.dir_stack.push(current.clone());
15690
15691            let resolved = if physical {
15692                target.canonicalize().unwrap_or(target.clone())
15693            } else {
15694                target.clone()
15695            };
15696
15697            if let Err(e) = std::env::set_current_dir(&resolved) {
15698                eprintln!("pushd: {}: {}", target.display(), e);
15699                self.dir_stack.pop();
15700                self.dir_stack.push(target);
15701                return 1;
15702            }
15703            if !quiet {
15704                self.print_dir_stack();
15705            }
15706            return 0;
15707        }
15708
15709        let arg = &positional_args[0];
15710
15711        // Handle +N and -N for rotating the stack
15712        if arg.starts_with('+') || arg.starts_with('-') {
15713            if let Ok(n) = arg[1..].parse::<usize>() {
15714                let total = self.dir_stack.len() + 1;
15715                if n >= total {
15716                    eprintln!("pushd: {}: directory stack index out of range", arg);
15717                    return 1;
15718                }
15719                // Rotate stack
15720                let rotate_pos = if arg.starts_with('+') { n } else { total - n };
15721                let mut full_stack = vec![current.clone()];
15722                full_stack.extend(self.dir_stack.iter().cloned());
15723                full_stack.rotate_left(rotate_pos);
15724
15725                let target = full_stack.remove(0);
15726                self.dir_stack = full_stack;
15727
15728                let resolved = if physical {
15729                    target.canonicalize().unwrap_or(target.clone())
15730                } else {
15731                    target.clone()
15732                };
15733
15734                if let Err(e) = std::env::set_current_dir(&resolved) {
15735                    eprintln!("pushd: {}: {}", target.display(), e);
15736                    return 1;
15737                }
15738                if !quiet {
15739                    self.print_dir_stack();
15740                }
15741                return 0;
15742            }
15743        }
15744
15745        // Regular directory push
15746        let target = PathBuf::from(arg);
15747        let resolved = if physical {
15748            target.canonicalize().unwrap_or(target.clone())
15749        } else {
15750            target.clone()
15751        };
15752
15753        self.dir_stack.push(current);
15754        if let Err(e) = std::env::set_current_dir(&resolved) {
15755            eprintln!("pushd: {}: {}", arg, e);
15756            self.dir_stack.pop();
15757            return 1;
15758        }
15759        if !quiet {
15760            self.print_dir_stack();
15761        }
15762        0
15763    }
15764
15765    /// Pop directory from stack and cd to it
15766    fn builtin_popd(&mut self, args: &[String]) -> i32 {
15767        // popd [ -qsLP ] [ {+|-}n ]
15768        // -q: quiet (don't print stack)
15769        // -s: no symlink resolution
15770        // -L: logical directory
15771        // -P: physical directory
15772
15773        let mut quiet = false;
15774        let mut physical = false;
15775        let mut stack_index: Option<String> = None;
15776
15777        for arg in args {
15778            if arg.starts_with('-') && arg.len() > 1 {
15779                // Check if it's a stack index
15780                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
15781                    stack_index = Some(arg.clone());
15782                    continue;
15783                }
15784                for ch in arg[1..].chars() {
15785                    match ch {
15786                        'q' => quiet = true,
15787                        's' => physical = false,
15788                        'L' => physical = false,
15789                        'P' => physical = true,
15790                        _ => {}
15791                    }
15792                }
15793            } else if arg.starts_with('+') {
15794                stack_index = Some(arg.clone());
15795            }
15796        }
15797
15798        if self.dir_stack.is_empty() {
15799            eprintln!("popd: directory stack empty");
15800            return 1;
15801        }
15802
15803        // Handle +N and -N
15804        if let Some(arg) = stack_index {
15805            if arg.starts_with('+') || arg.starts_with('-') {
15806                if let Ok(n) = arg[1..].parse::<usize>() {
15807                    let total = self.dir_stack.len() + 1;
15808                    if n >= total {
15809                        eprintln!("popd: {}: directory stack index out of range", arg);
15810                        return 1;
15811                    }
15812                    let remove_pos = if arg.starts_with('+') {
15813                        n
15814                    } else {
15815                        total - 1 - n
15816                    };
15817                    if remove_pos == 0 {
15818                        // Remove current and cd to next
15819                        let target = self.dir_stack.remove(0);
15820                        let resolved = if physical {
15821                            target.canonicalize().unwrap_or(target.clone())
15822                        } else {
15823                            target.clone()
15824                        };
15825                        if let Err(e) = std::env::set_current_dir(&resolved) {
15826                            eprintln!("popd: {}: {}", target.display(), e);
15827                            return 1;
15828                        }
15829                    } else {
15830                        self.dir_stack.remove(remove_pos - 1);
15831                    }
15832                    if !quiet {
15833                        self.print_dir_stack();
15834                    }
15835                    return 0;
15836                }
15837            }
15838        }
15839
15840        let target = self.dir_stack.pop().unwrap();
15841        let resolved = if physical {
15842            target.canonicalize().unwrap_or(target.clone())
15843        } else {
15844            target.clone()
15845        };
15846        if let Err(e) = std::env::set_current_dir(&resolved) {
15847            eprintln!("popd: {}: {}", target.display(), e);
15848            self.dir_stack.push(target);
15849            return 1;
15850        }
15851        if !quiet {
15852            self.print_dir_stack();
15853        }
15854        0
15855    }
15856
15857    /// Display directory stack
15858    fn builtin_dirs(&mut self, args: &[String]) -> i32 {
15859        // dirs [ -c ] [ -l ] [ -p ] [ -v ] [ arg ... ]
15860        // -c: clear the directory stack
15861        // -l: full pathnames (don't use ~)
15862        // -p: print one entry per line
15863        // -v: verbose (numbered list)
15864
15865        let mut clear = false;
15866        let mut full_paths = false;
15867        let mut per_line = false;
15868        let mut verbose = false;
15869        let mut indices: Vec<i32> = Vec::new();
15870
15871        for arg in args {
15872            if arg.starts_with('-') && arg.len() > 1 {
15873                // Check if it's a negative index like -2
15874                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
15875                    if let Ok(n) = arg.parse::<i32>() {
15876                        indices.push(n);
15877                        continue;
15878                    }
15879                }
15880                for ch in arg[1..].chars() {
15881                    match ch {
15882                        'c' => clear = true,
15883                        'l' => full_paths = true,
15884                        'p' => per_line = true,
15885                        'v' => verbose = true,
15886                        _ => {}
15887                    }
15888                }
15889            } else if arg.starts_with('+') && arg.len() > 1 {
15890                if let Ok(n) = arg[1..].parse::<i32>() {
15891                    indices.push(n);
15892                }
15893            } else {
15894                // Could be a number
15895                if let Ok(n) = arg.parse::<i32>() {
15896                    indices.push(n);
15897                }
15898            }
15899        }
15900
15901        if clear {
15902            self.dir_stack.clear();
15903            return 0;
15904        }
15905
15906        let current = std::env::current_dir().unwrap_or_default();
15907        let home = dirs::home_dir().unwrap_or_default();
15908
15909        let format_path = |p: &std::path::Path| -> String {
15910            let path_str = p.to_string_lossy().to_string();
15911            if !full_paths {
15912                let home_str = home.to_string_lossy();
15913                if path_str.starts_with(home_str.as_ref()) {
15914                    return format!("~{}", &path_str[home_str.len()..]);
15915                }
15916            }
15917            path_str
15918        };
15919
15920        // If specific indices requested
15921        if !indices.is_empty() {
15922            let stack_len = self.dir_stack.len() + 1; // +1 for current dir
15923            for idx in indices {
15924                let actual_idx = if idx >= 0 {
15925                    idx as usize
15926                } else {
15927                    stack_len.saturating_sub((-idx) as usize)
15928                };
15929
15930                if actual_idx == 0 {
15931                    println!("{}", format_path(&current));
15932                } else if actual_idx <= self.dir_stack.len() {
15933                    // Stack is reversed, so index from end
15934                    let stack_idx = self.dir_stack.len() - actual_idx;
15935                    if let Some(dir) = self.dir_stack.get(stack_idx) {
15936                        println!("{}", format_path(dir));
15937                    }
15938                }
15939            }
15940            return 0;
15941        }
15942
15943        if verbose {
15944            println!(" 0  {}", format_path(&current));
15945            for (i, dir) in self.dir_stack.iter().rev().enumerate() {
15946                println!("{:2}  {}", i + 1, format_path(dir));
15947            }
15948        } else if per_line {
15949            println!("{}", format_path(&current));
15950            for dir in self.dir_stack.iter().rev() {
15951                println!("{}", format_path(dir));
15952            }
15953        } else {
15954            let mut parts = vec![format_path(&current)];
15955            for dir in self.dir_stack.iter().rev() {
15956                parts.push(format_path(dir));
15957            }
15958            println!("{}", parts.join(" "));
15959        }
15960        0
15961    }
15962
15963    fn print_dir_stack(&self) {
15964        let current = std::env::current_dir().unwrap_or_default();
15965        let mut parts = vec![current.to_string_lossy().to_string()];
15966        for dir in self.dir_stack.iter().rev() {
15967            parts.push(dir.to_string_lossy().to_string());
15968        }
15969        println!("{}", parts.join(" "));
15970    }
15971
15972    /// printf builtin - format and print data (zsh/bash compatible)
15973    fn builtin_printf(&self, args: &[String]) -> i32 {
15974        if args.is_empty() {
15975            eprintln!("printf: usage: printf format [arguments]");
15976            return 1;
15977        }
15978
15979        let format = &args[0];
15980        let format_args = &args[1..];
15981        let mut arg_idx = 0;
15982        let mut output = String::new();
15983        let mut chars = format.chars().peekable();
15984
15985        while let Some(c) = chars.next() {
15986            if c == '\\' {
15987                match chars.next() {
15988                    Some('n') => output.push('\n'),
15989                    Some('t') => output.push('\t'),
15990                    Some('r') => output.push('\r'),
15991                    Some('\\') => output.push('\\'),
15992                    Some('a') => output.push('\x07'),
15993                    Some('b') => output.push('\x08'),
15994                    Some('e') | Some('E') => output.push('\x1b'),
15995                    Some('f') => output.push('\x0c'),
15996                    Some('v') => output.push('\x0b'),
15997                    Some('"') => output.push('"'),
15998                    Some('\'') => output.push('\''),
15999                    Some('0') => {
16000                        let mut octal = String::new();
16001                        while octal.len() < 3 {
16002                            if let Some(&d) = chars.peek() {
16003                                if d >= '0' && d <= '7' {
16004                                    octal.push(d);
16005                                    chars.next();
16006                                } else {
16007                                    break;
16008                                }
16009                            } else {
16010                                break;
16011                            }
16012                        }
16013                        if octal.is_empty() {
16014                            output.push('\0');
16015                        } else if let Ok(val) = u8::from_str_radix(&octal, 8) {
16016                            output.push(val as char);
16017                        }
16018                    }
16019                    Some('x') => {
16020                        let mut hex = String::new();
16021                        while hex.len() < 2 {
16022                            if let Some(&d) = chars.peek() {
16023                                if d.is_ascii_hexdigit() {
16024                                    hex.push(d);
16025                                    chars.next();
16026                                } else {
16027                                    break;
16028                                }
16029                            } else {
16030                                break;
16031                            }
16032                        }
16033                        if !hex.is_empty() {
16034                            if let Ok(val) = u8::from_str_radix(&hex, 16) {
16035                                output.push(val as char);
16036                            }
16037                        }
16038                    }
16039                    Some('u') => {
16040                        let mut hex = String::new();
16041                        while hex.len() < 4 {
16042                            if let Some(&d) = chars.peek() {
16043                                if d.is_ascii_hexdigit() {
16044                                    hex.push(d);
16045                                    chars.next();
16046                                } else {
16047                                    break;
16048                                }
16049                            } else {
16050                                break;
16051                            }
16052                        }
16053                        if !hex.is_empty() {
16054                            if let Ok(val) = u32::from_str_radix(&hex, 16) {
16055                                if let Some(c) = char::from_u32(val) {
16056                                    output.push(c);
16057                                }
16058                            }
16059                        }
16060                    }
16061                    Some('U') => {
16062                        let mut hex = String::new();
16063                        while hex.len() < 8 {
16064                            if let Some(&d) = chars.peek() {
16065                                if d.is_ascii_hexdigit() {
16066                                    hex.push(d);
16067                                    chars.next();
16068                                } else {
16069                                    break;
16070                                }
16071                            } else {
16072                                break;
16073                            }
16074                        }
16075                        if !hex.is_empty() {
16076                            if let Ok(val) = u32::from_str_radix(&hex, 16) {
16077                                if let Some(c) = char::from_u32(val) {
16078                                    output.push(c);
16079                                }
16080                            }
16081                        }
16082                    }
16083                    Some('c') => {
16084                        print!("{}", output);
16085                        return 0;
16086                    }
16087                    Some(other) => {
16088                        output.push('\\');
16089                        output.push(other);
16090                    }
16091                    None => output.push('\\'),
16092                }
16093            } else if c == '%' {
16094                if chars.peek() == Some(&'%') {
16095                    chars.next();
16096                    output.push('%');
16097                    continue;
16098                }
16099
16100                let mut flags = String::new();
16101                while let Some(&f) = chars.peek() {
16102                    if f == '-' || f == '+' || f == ' ' || f == '#' || f == '0' {
16103                        flags.push(f);
16104                        chars.next();
16105                    } else {
16106                        break;
16107                    }
16108                }
16109
16110                let mut width = String::new();
16111                if chars.peek() == Some(&'*') {
16112                    chars.next();
16113                    if arg_idx < format_args.len() {
16114                        width = format_args[arg_idx].clone();
16115                        arg_idx += 1;
16116                    }
16117                } else {
16118                    while let Some(&d) = chars.peek() {
16119                        if d.is_ascii_digit() {
16120                            width.push(d);
16121                            chars.next();
16122                        } else {
16123                            break;
16124                        }
16125                    }
16126                }
16127
16128                let mut precision = String::new();
16129                if chars.peek() == Some(&'.') {
16130                    chars.next();
16131                    if chars.peek() == Some(&'*') {
16132                        chars.next();
16133                        if arg_idx < format_args.len() {
16134                            precision = format_args[arg_idx].clone();
16135                            arg_idx += 1;
16136                        }
16137                    } else {
16138                        while let Some(&d) = chars.peek() {
16139                            if d.is_ascii_digit() {
16140                                precision.push(d);
16141                                chars.next();
16142                            } else {
16143                                break;
16144                            }
16145                        }
16146                    }
16147                }
16148
16149                let specifier = chars.next().unwrap_or('s');
16150                let arg = if arg_idx < format_args.len() {
16151                    let a = &format_args[arg_idx];
16152                    arg_idx += 1;
16153                    a.clone()
16154                } else {
16155                    String::new()
16156                };
16157
16158                let width_val: usize = width.parse().unwrap_or(0);
16159                let prec_val: Option<usize> = if precision.is_empty() {
16160                    None
16161                } else {
16162                    precision.parse().ok()
16163                };
16164                let left_align = flags.contains('-');
16165                let zero_pad = flags.contains('0') && !left_align;
16166                let plus_sign = flags.contains('+');
16167                let space_sign = flags.contains(' ') && !plus_sign;
16168                let alt_form = flags.contains('#');
16169
16170                match specifier {
16171                    's' => {
16172                        let mut s = arg;
16173                        if let Some(p) = prec_val {
16174                            s = s.chars().take(p).collect();
16175                        }
16176                        if width_val > s.len() {
16177                            if left_align {
16178                                output.push_str(&s);
16179                                output.push_str(&" ".repeat(width_val - s.len()));
16180                            } else {
16181                                output.push_str(&" ".repeat(width_val - s.len()));
16182                                output.push_str(&s);
16183                            }
16184                        } else {
16185                            output.push_str(&s);
16186                        }
16187                    }
16188                    'b' => {
16189                        let expanded = self.expand_printf_escapes(&arg);
16190                        if let Some(p) = prec_val {
16191                            let s: String = expanded.chars().take(p).collect();
16192                            output.push_str(&s);
16193                        } else {
16194                            output.push_str(&expanded);
16195                        }
16196                    }
16197                    'c' => {
16198                        if let Some(ch) = arg.chars().next() {
16199                            output.push(ch);
16200                        }
16201                    }
16202                    'q' => {
16203                        output.push('\'');
16204                        for ch in arg.chars() {
16205                            if ch == '\'' {
16206                                output.push_str("'\\''");
16207                            } else {
16208                                output.push(ch);
16209                            }
16210                        }
16211                        output.push('\'');
16212                    }
16213                    'd' | 'i' => {
16214                        let val: i64 = if arg.starts_with("0x") || arg.starts_with("0X") {
16215                            i64::from_str_radix(&arg[2..], 16).unwrap_or(0)
16216                        } else if arg.starts_with("0") && arg.len() > 1 && !arg.contains('.') {
16217                            i64::from_str_radix(&arg[1..], 8).unwrap_or(0)
16218                        } else if arg.starts_with('\'') || arg.starts_with('"') {
16219                            arg.chars().nth(1).map(|c| c as i64).unwrap_or(0)
16220                        } else {
16221                            arg.parse().unwrap_or(0)
16222                        };
16223
16224                        let sign = if val < 0 {
16225                            "-"
16226                        } else if plus_sign {
16227                            "+"
16228                        } else if space_sign {
16229                            " "
16230                        } else {
16231                            ""
16232                        };
16233                        let abs_val = val.abs();
16234                        let num_str = abs_val.to_string();
16235                        let total_len = sign.len() + num_str.len();
16236
16237                        if width_val > total_len {
16238                            if left_align {
16239                                output.push_str(sign);
16240                                output.push_str(&num_str);
16241                                output.push_str(&" ".repeat(width_val - total_len));
16242                            } else if zero_pad {
16243                                output.push_str(sign);
16244                                output.push_str(&"0".repeat(width_val - total_len));
16245                                output.push_str(&num_str);
16246                            } else {
16247                                output.push_str(&" ".repeat(width_val - total_len));
16248                                output.push_str(sign);
16249                                output.push_str(&num_str);
16250                            }
16251                        } else {
16252                            output.push_str(sign);
16253                            output.push_str(&num_str);
16254                        }
16255                    }
16256                    'u' => {
16257                        let val: u64 = if arg.starts_with("0x") || arg.starts_with("0X") {
16258                            u64::from_str_radix(&arg[2..], 16).unwrap_or(0)
16259                        } else if arg.starts_with("0") && arg.len() > 1 {
16260                            u64::from_str_radix(&arg[1..], 8).unwrap_or(0)
16261                        } else {
16262                            arg.parse().unwrap_or(0)
16263                        };
16264                        let num_str = val.to_string();
16265                        if width_val > num_str.len() {
16266                            if left_align {
16267                                output.push_str(&num_str);
16268                                output.push_str(&" ".repeat(width_val - num_str.len()));
16269                            } else if zero_pad {
16270                                output.push_str(&"0".repeat(width_val - num_str.len()));
16271                                output.push_str(&num_str);
16272                            } else {
16273                                output.push_str(&" ".repeat(width_val - num_str.len()));
16274                                output.push_str(&num_str);
16275                            }
16276                        } else {
16277                            output.push_str(&num_str);
16278                        }
16279                    }
16280                    'o' => {
16281                        let val: u64 = arg.parse().unwrap_or(0);
16282                        let num_str = format!("{:o}", val);
16283                        let prefix = if alt_form && val != 0 { "0" } else { "" };
16284                        let total_len = prefix.len() + num_str.len();
16285                        if width_val > total_len {
16286                            if left_align {
16287                                output.push_str(prefix);
16288                                output.push_str(&num_str);
16289                                output.push_str(&" ".repeat(width_val - total_len));
16290                            } else {
16291                                output.push_str(&" ".repeat(width_val - total_len));
16292                                output.push_str(prefix);
16293                                output.push_str(&num_str);
16294                            }
16295                        } else {
16296                            output.push_str(prefix);
16297                            output.push_str(&num_str);
16298                        }
16299                    }
16300                    'x' => {
16301                        let val: u64 = arg.parse().unwrap_or(0);
16302                        let num_str = format!("{:x}", val);
16303                        let prefix = if alt_form && val != 0 { "0x" } else { "" };
16304                        let total_len = prefix.len() + num_str.len();
16305                        if width_val > total_len {
16306                            if left_align {
16307                                output.push_str(prefix);
16308                                output.push_str(&num_str);
16309                                output.push_str(&" ".repeat(width_val - total_len));
16310                            } else {
16311                                output.push_str(&" ".repeat(width_val - total_len));
16312                                output.push_str(prefix);
16313                                output.push_str(&num_str);
16314                            }
16315                        } else {
16316                            output.push_str(prefix);
16317                            output.push_str(&num_str);
16318                        }
16319                    }
16320                    'X' => {
16321                        let val: u64 = arg.parse().unwrap_or(0);
16322                        let num_str = format!("{:X}", val);
16323                        let prefix = if alt_form && val != 0 { "0X" } else { "" };
16324                        let total_len = prefix.len() + num_str.len();
16325                        if width_val > total_len {
16326                            if left_align {
16327                                output.push_str(prefix);
16328                                output.push_str(&num_str);
16329                                output.push_str(&" ".repeat(width_val - total_len));
16330                            } else {
16331                                output.push_str(&" ".repeat(width_val - total_len));
16332                                output.push_str(prefix);
16333                                output.push_str(&num_str);
16334                            }
16335                        } else {
16336                            output.push_str(prefix);
16337                            output.push_str(&num_str);
16338                        }
16339                    }
16340                    'e' | 'E' => {
16341                        let val: f64 = arg.parse().unwrap_or(0.0);
16342                        let prec = prec_val.unwrap_or(6);
16343                        let formatted = if specifier == 'e' {
16344                            format!("{:.prec$e}", val, prec = prec)
16345                        } else {
16346                            format!("{:.prec$E}", val, prec = prec)
16347                        };
16348                        if width_val > formatted.len() {
16349                            if left_align {
16350                                output.push_str(&formatted);
16351                                output.push_str(&" ".repeat(width_val - formatted.len()));
16352                            } else {
16353                                output.push_str(&" ".repeat(width_val - formatted.len()));
16354                                output.push_str(&formatted);
16355                            }
16356                        } else {
16357                            output.push_str(&formatted);
16358                        }
16359                    }
16360                    'f' | 'F' => {
16361                        let val: f64 = arg.parse().unwrap_or(0.0);
16362                        let prec = prec_val.unwrap_or(6);
16363                        let sign = if val < 0.0 {
16364                            "-"
16365                        } else if plus_sign {
16366                            "+"
16367                        } else if space_sign {
16368                            " "
16369                        } else {
16370                            ""
16371                        };
16372                        let formatted = format!("{:.prec$}", val.abs(), prec = prec);
16373                        let total = sign.len() + formatted.len();
16374                        if width_val > total {
16375                            if left_align {
16376                                output.push_str(sign);
16377                                output.push_str(&formatted);
16378                                output.push_str(&" ".repeat(width_val - total));
16379                            } else if zero_pad {
16380                                output.push_str(sign);
16381                                output.push_str(&"0".repeat(width_val - total));
16382                                output.push_str(&formatted);
16383                            } else {
16384                                output.push_str(&" ".repeat(width_val - total));
16385                                output.push_str(sign);
16386                                output.push_str(&formatted);
16387                            }
16388                        } else {
16389                            output.push_str(sign);
16390                            output.push_str(&formatted);
16391                        }
16392                    }
16393                    'g' | 'G' => {
16394                        let val: f64 = arg.parse().unwrap_or(0.0);
16395                        let prec = prec_val.unwrap_or(6).max(1);
16396                        let formatted = if specifier == 'g' {
16397                            format!("{:.prec$}", val, prec = prec)
16398                        } else {
16399                            format!("{:.prec$}", val, prec = prec).to_uppercase()
16400                        };
16401                        output.push_str(&formatted);
16402                    }
16403                    'a' | 'A' => {
16404                        let val: f64 = arg.parse().unwrap_or(0.0);
16405                        let formatted = float_to_hex(val, specifier == 'A');
16406                        output.push_str(&formatted);
16407                    }
16408                    _ => {
16409                        output.push('%');
16410                        output.push(specifier);
16411                    }
16412                }
16413            } else {
16414                output.push(c);
16415            }
16416        }
16417
16418        print!("{}", output);
16419        0
16420    }
16421
16422    fn expand_printf_escapes(&self, s: &str) -> String {
16423        let mut result = String::new();
16424        let mut chars = s.chars().peekable();
16425        while let Some(c) = chars.next() {
16426            if c == '\\' {
16427                match chars.next() {
16428                    Some('n') => result.push('\n'),
16429                    Some('t') => result.push('\t'),
16430                    Some('r') => result.push('\r'),
16431                    Some('\\') => result.push('\\'),
16432                    Some('a') => result.push('\x07'),
16433                    Some('b') => result.push('\x08'),
16434                    Some('e') | Some('E') => result.push('\x1b'),
16435                    Some('f') => result.push('\x0c'),
16436                    Some('v') => result.push('\x0b'),
16437                    Some('0') => {
16438                        let mut octal = String::new();
16439                        while octal.len() < 3 {
16440                            if let Some(&d) = chars.peek() {
16441                                if d >= '0' && d <= '7' {
16442                                    octal.push(d);
16443                                    chars.next();
16444                                } else {
16445                                    break;
16446                                }
16447                            } else {
16448                                break;
16449                            }
16450                        }
16451                        if octal.is_empty() {
16452                            result.push('\0');
16453                        } else if let Ok(val) = u8::from_str_radix(&octal, 8) {
16454                            result.push(val as char);
16455                        }
16456                    }
16457                    Some('c') => break,
16458                    Some(other) => {
16459                        result.push('\\');
16460                        result.push(other);
16461                    }
16462                    None => result.push('\\'),
16463                }
16464            } else {
16465                result.push(c);
16466            }
16467        }
16468        result
16469    }
16470
16471    fn evaluate_arithmetic_expr(&mut self, expr: &str) -> i64 {
16472        self.eval_arith_expr(expr)
16473    }
16474
16475    // ═══════════════════════════════════════════════════════════════════════════
16476    // Additional zsh builtins
16477    // ═══════════════════════════════════════════════════════════════════════════
16478
16479    /// break - exit from for/while/until loop
16480    fn builtin_break(&mut self, args: &[String]) -> i32 {
16481        let levels: i32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
16482        self.breaking = levels.max(1);
16483        0
16484    }
16485
16486    /// continue - skip to next iteration of loop
16487    fn builtin_continue(&mut self, args: &[String]) -> i32 {
16488        let levels: i32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
16489        self.continuing = levels.max(1);
16490        0
16491    }
16492
16493    /// disable - disable shell builtins, aliases, functions
16494    fn builtin_disable(&mut self, args: &[String]) -> i32 {
16495        let mut disable_aliases = false;
16496        let mut disable_builtins = false;
16497        let mut disable_functions = false;
16498        let mut names = Vec::new();
16499
16500        let mut iter = args.iter();
16501        while let Some(arg) = iter.next() {
16502            match arg.as_str() {
16503                "-a" => disable_aliases = true,
16504                "-f" => disable_functions = true,
16505                "-r" => disable_builtins = true,
16506                _ if arg.starts_with('-') => {}
16507                _ => names.push(arg.clone()),
16508            }
16509        }
16510
16511        // Default to builtins if no flags
16512        if !disable_aliases && !disable_functions {
16513            disable_builtins = true;
16514        }
16515
16516        for name in names {
16517            if disable_aliases {
16518                self.aliases.remove(&name);
16519            }
16520            if disable_functions {
16521                self.functions.remove(&name);
16522            }
16523            if disable_builtins {
16524                // Store disabled builtins
16525                self.options.insert(format!("_disabled_{}", name), true);
16526            }
16527        }
16528        0
16529    }
16530
16531    /// enable - enable disabled shell builtins
16532    fn builtin_enable(&mut self, args: &[String]) -> i32 {
16533        for arg in args {
16534            if !arg.starts_with('-') {
16535                self.options.remove(&format!("_disabled_{}", arg));
16536            }
16537        }
16538        0
16539    }
16540
16541    /// emulate - set up zsh emulation mode
16542    fn builtin_emulate(&mut self, args: &[String]) -> i32 {
16543        // emulate [ -lLR ] [ {zsh|sh|ksh|csh} [ flags ... ] ]
16544        // flags can include: -c arg, -o opt, +o opt
16545        let mut local_mode = false;
16546        let mut reset_mode = false;
16547        let mut list_mode = false;
16548        let mut mode: Option<String> = None;
16549        let mut command_arg: Option<String> = None;
16550        let mut extra_set_opts: Vec<String> = Vec::new();
16551        let mut extra_unset_opts: Vec<String> = Vec::new();
16552
16553        let mut i = 0;
16554        while i < args.len() {
16555            let arg = &args[i];
16556
16557            if arg == "-c" {
16558                // -c arg: evaluate arg in emulation mode
16559                i += 1;
16560                if i < args.len() {
16561                    command_arg = Some(args[i].clone());
16562                } else {
16563                    eprintln!("emulate: -c requires an argument");
16564                    return 1;
16565                }
16566            } else if arg == "-o" {
16567                // -o opt: set option
16568                i += 1;
16569                if i < args.len() {
16570                    extra_set_opts.push(args[i].clone());
16571                } else {
16572                    eprintln!("emulate: -o requires an argument");
16573                    return 1;
16574                }
16575            } else if arg == "+o" {
16576                // +o opt: unset option
16577                i += 1;
16578                if i < args.len() {
16579                    extra_unset_opts.push(args[i].clone());
16580                } else {
16581                    eprintln!("emulate: +o requires an argument");
16582                    return 1;
16583                }
16584            } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
16585                // Parse combined flags like -LR
16586                for ch in arg[1..].chars() {
16587                    match ch {
16588                        'L' => local_mode = true,
16589                        'R' => reset_mode = true,
16590                        'l' => list_mode = true,
16591                        _ => {
16592                            eprintln!("emulate: bad option: -{}", ch);
16593                            return 1;
16594                        }
16595                    }
16596                }
16597            } else if arg.starts_with('+') && arg.len() > 1 {
16598                // +X flags (unset single-letter options)
16599                for ch in arg[1..].chars() {
16600                    // Map single-letter to option name if needed
16601                    extra_unset_opts.push(ch.to_string());
16602                }
16603            } else if mode.is_none() {
16604                mode = Some(arg.clone());
16605            }
16606            i += 1;
16607        }
16608
16609        // -L and -c are mutually exclusive
16610        if local_mode && command_arg.is_some() {
16611            eprintln!("emulate: -L and -c are mutually exclusive");
16612            return 1;
16613        }
16614
16615        // No argument: print current emulation mode
16616        if mode.is_none() && !list_mode {
16617            let current = self
16618                .variables
16619                .get("EMULATE")
16620                .cloned()
16621                .unwrap_or_else(|| "zsh".to_string());
16622            println!("{}", current);
16623            return 0;
16624        }
16625
16626        let mode = mode.unwrap_or_else(|| "zsh".to_string());
16627
16628        // Get the options that would be set for this mode
16629        let (set_opts, unset_opts) = Self::emulate_mode_options(&mode, reset_mode);
16630
16631        // -l: just list the options, don't apply
16632        if list_mode {
16633            for opt in &set_opts {
16634                println!("{}", opt);
16635            }
16636            for opt in &unset_opts {
16637                println!("no{}", opt);
16638            }
16639            if local_mode {
16640                println!("localoptions");
16641                println!("localpatterns");
16642                println!("localtraps");
16643            }
16644            return 0;
16645        }
16646
16647        // Save current state if -c is used
16648        let saved_options = if command_arg.is_some() {
16649            Some(self.options.clone())
16650        } else {
16651            None
16652        };
16653        let saved_emulate = if command_arg.is_some() {
16654            self.variables.get("EMULATE").cloned()
16655        } else {
16656            None
16657        };
16658
16659        // Apply the emulation
16660        self.variables.insert("EMULATE".to_string(), mode.clone());
16661
16662        // Set options for this mode
16663        for opt in &set_opts {
16664            let opt_name = opt.to_lowercase().replace('_', "");
16665            self.options.insert(opt_name, true);
16666        }
16667        for opt in &unset_opts {
16668            let opt_name = opt.to_lowercase().replace('_', "");
16669            self.options.insert(opt_name, false);
16670        }
16671
16672        // Apply extra -o / +o options
16673        for opt in &extra_set_opts {
16674            let opt_name = opt.to_lowercase().replace('_', "");
16675            self.options.insert(opt_name, true);
16676        }
16677        for opt in &extra_unset_opts {
16678            let opt_name = opt.to_lowercase().replace('_', "");
16679            self.options.insert(opt_name, false);
16680        }
16681
16682        // -L: set local options/traps
16683        if local_mode {
16684            self.options.insert("localoptions".to_string(), true);
16685            self.options.insert("localpatterns".to_string(), true);
16686            self.options.insert("localtraps".to_string(), true);
16687        }
16688
16689        // -c arg: execute command then restore
16690        let result = if let Some(cmd) = command_arg {
16691            let status = self.execute_script(&cmd).unwrap_or(1);
16692
16693            // Restore saved state
16694            if let Some(opts) = saved_options {
16695                self.options = opts;
16696            }
16697            if let Some(emu) = saved_emulate {
16698                self.variables.insert("EMULATE".to_string(), emu);
16699            } else {
16700                self.variables.remove("EMULATE");
16701            }
16702
16703            status
16704        } else {
16705            0
16706        };
16707
16708        result
16709    }
16710
16711    /// Get options to set/unset for an emulation mode
16712    fn emulate_mode_options(mode: &str, reset: bool) -> (Vec<&'static str>, Vec<&'static str>) {
16713        match mode {
16714            "zsh" => {
16715                if reset {
16716                    // Full reset: return to zsh defaults
16717                    (
16718                        vec![
16719                            "aliases",
16720                            "alwayslastprompt",
16721                            "autolist",
16722                            "automenu",
16723                            "autoparamslash",
16724                            "autoremoveslash",
16725                            "banghist",
16726                            "bareglobqual",
16727                            "completeinword",
16728                            "extendedhistory",
16729                            "functionargzero",
16730                            "glob",
16731                            "hashcmds",
16732                            "hashdirs",
16733                            "histexpand",
16734                            "histignoredups",
16735                            "interactivecomments",
16736                            "listambiguous",
16737                            "listtypes",
16738                            "multios",
16739                            "nomatch",
16740                            "notify",
16741                            "promptpercent",
16742                            "promptsubst",
16743                        ],
16744                        vec![
16745                            "ksharrays",
16746                            "kshglob",
16747                            "shwordsplit",
16748                            "shglob",
16749                            "posixbuiltins",
16750                            "posixidentifiers",
16751                            "posixstrings",
16752                            "bsdecho",
16753                            "ignorebraces",
16754                        ],
16755                    )
16756                } else {
16757                    // Minimal changes for portability
16758                    (vec!["functionargzero"], vec!["ksharrays", "shwordsplit"])
16759                }
16760            }
16761            "sh" => {
16762                let set = vec![
16763                    "ksharrays",
16764                    "shwordsplit",
16765                    "posixbuiltins",
16766                    "shglob",
16767                    "shfileexpansion",
16768                    "globsubst",
16769                    "interactivecomments",
16770                    "rmstarsilent",
16771                    "bsdecho",
16772                    "ignorebraces",
16773                ];
16774                let unset = vec![
16775                    "badpattern",
16776                    "banghist",
16777                    "bgnice",
16778                    "equals",
16779                    "functionargzero",
16780                    "globalexport",
16781                    "multios",
16782                    "nomatch",
16783                    "notify",
16784                    "promptpercent",
16785                ];
16786                (set, unset)
16787            }
16788            "ksh" => {
16789                let set = vec![
16790                    "ksharrays",
16791                    "kshglob",
16792                    "shwordsplit",
16793                    "posixbuiltins",
16794                    "kshoptionprint",
16795                    "localoptions",
16796                    "promptbang",
16797                    "promptsubst",
16798                    "singlelinezle",
16799                    "interactivecomments",
16800                ];
16801                let unset = vec![
16802                    "badpattern",
16803                    "banghist",
16804                    "bgnice",
16805                    "equals",
16806                    "functionargzero",
16807                    "globalexport",
16808                    "multios",
16809                    "nomatch",
16810                    "notify",
16811                    "promptpercent",
16812                ];
16813                (set, unset)
16814            }
16815            "csh" => {
16816                // C shell emulation (limited)
16817                (vec!["cshnullglob", "cshjunkiequotes"], vec!["nomatch"])
16818            }
16819            "bash" => {
16820                let set = vec![
16821                    "ksharrays",
16822                    "shwordsplit",
16823                    "interactivecomments",
16824                    "shfileexpansion",
16825                    "globsubst",
16826                ];
16827                let unset = vec![
16828                    "badpattern",
16829                    "banghist",
16830                    "functionargzero",
16831                    "multios",
16832                    "nomatch",
16833                    "notify",
16834                    "promptpercent",
16835                ];
16836                (set, unset)
16837            }
16838            _ => (vec![], vec![]),
16839        }
16840    }
16841
16842    /// exec - replace the shell with a command
16843    fn builtin_exec(&mut self, args: &[String]) -> i32 {
16844        // exec [ -c ] [ -l ] [ -a argv0 ] [ command [ arg ... ] ]
16845        // -c: clear environment
16846        // -l: place - at front of argv[0] (login shell)
16847        // -a argv0: set argv[0] to specified name
16848
16849        let mut clear_env = false;
16850        let mut login_shell = false;
16851        let mut argv0: Option<String> = None;
16852        let mut cmd_args: Vec<String> = Vec::new();
16853
16854        let mut i = 0;
16855        while i < args.len() {
16856            let arg = &args[i];
16857
16858            if arg == "-c" && cmd_args.is_empty() {
16859                clear_env = true;
16860            } else if arg == "-l" && cmd_args.is_empty() {
16861                login_shell = true;
16862            } else if arg == "-a" && cmd_args.is_empty() {
16863                i += 1;
16864                if i < args.len() {
16865                    argv0 = Some(args[i].clone());
16866                }
16867            } else if arg.starts_with('-') && cmd_args.is_empty() {
16868                // Combined flags like -cl
16869                for ch in arg[1..].chars() {
16870                    match ch {
16871                        'c' => clear_env = true,
16872                        'l' => login_shell = true,
16873                        'a' => {
16874                            i += 1;
16875                            if i < args.len() {
16876                                argv0 = Some(args[i].clone());
16877                            }
16878                        }
16879                        _ => {}
16880                    }
16881                }
16882            } else {
16883                cmd_args.push(arg.clone());
16884            }
16885            i += 1;
16886        }
16887
16888        if cmd_args.is_empty() {
16889            // No command: just modify shell's environment
16890            if clear_env {
16891                for (key, _) in env::vars() {
16892                    env::remove_var(&key);
16893                }
16894            }
16895            return 0;
16896        }
16897
16898        let cmd = &cmd_args[0];
16899        let rest_args: Vec<&str> = cmd_args[1..].iter().map(|s| s.as_str()).collect();
16900
16901        // Determine argv[0]
16902        let effective_argv0 = if let Some(a0) = argv0 {
16903            a0
16904        } else if login_shell {
16905            format!("-{}", cmd)
16906        } else {
16907            cmd.clone()
16908        };
16909
16910        use std::os::unix::process::CommandExt;
16911        let mut command = std::process::Command::new(cmd);
16912        command.arg0(&effective_argv0);
16913        command.args(&rest_args);
16914
16915        if clear_env {
16916            command.env_clear();
16917        }
16918
16919        let err = command.exec();
16920        eprintln!("exec: {}: {}", cmd, err);
16921        1
16922    }
16923
16924    /// float - declare floating point variables
16925    fn builtin_float(&mut self, args: &[String]) -> i32 {
16926        for arg in args {
16927            if arg.starts_with('-') {
16928                continue;
16929            }
16930            if let Some(eq_pos) = arg.find('=') {
16931                let name = &arg[..eq_pos];
16932                let value = &arg[eq_pos + 1..];
16933                let float_val: f64 = value.parse().unwrap_or(0.0);
16934                self.variables
16935                    .insert(name.to_string(), float_val.to_string());
16936                self.options.insert(format!("_float_{}", name), true);
16937            } else {
16938                self.variables.insert(arg.clone(), "0.0".to_string());
16939                self.options.insert(format!("_float_{}", arg), true);
16940            }
16941        }
16942        0
16943    }
16944
16945    /// integer - declare integer variables
16946    fn builtin_integer(&mut self, args: &[String]) -> i32 {
16947        for arg in args {
16948            if arg.starts_with('-') {
16949                continue;
16950            }
16951            if let Some(eq_pos) = arg.find('=') {
16952                let name = &arg[..eq_pos];
16953                let value = &arg[eq_pos + 1..];
16954                let int_val: i64 = value.parse().unwrap_or(0);
16955                self.variables.insert(name.to_string(), int_val.to_string());
16956                self.options.insert(format!("_integer_{}", name), true);
16957            } else {
16958                self.variables.insert(arg.clone(), "0".to_string());
16959                self.options.insert(format!("_integer_{}", arg), true);
16960            }
16961        }
16962        0
16963    }
16964
16965    /// functions - list or manipulate function definitions
16966    fn builtin_functions(&self, args: &[String]) -> i32 {
16967        let mut list_only = false;
16968        let mut show_trace = false;
16969        let mut names: Vec<&str> = Vec::new();
16970
16971        for arg in args {
16972            match arg.as_str() {
16973                "-l" => list_only = true,
16974                "-t" => show_trace = true,
16975                _ if arg.starts_with('-') => {}
16976                _ => names.push(arg),
16977            }
16978        }
16979
16980        if names.is_empty() {
16981            // List all functions
16982            let mut func_names: Vec<_> = self.functions.keys().collect();
16983            func_names.sort();
16984            for name in func_names {
16985                if list_only {
16986                    println!("{}", name);
16987                } else if let Some(func) = self.functions.get(name) {
16988                    let body = crate::text::getpermtext(func);
16989                    println!("{} () {{\n\t{}\n}}", name, body.trim());
16990                }
16991            }
16992        } else {
16993            // Show specific functions
16994            for name in names {
16995                if let Some(func) = self.functions.get(name) {
16996                    if show_trace {
16997                        println!("functions -t {}", name);
16998                    } else {
16999                        let body = crate::text::getpermtext(func);
17000                        println!("{} () {{\n\t{}\n}}", name, body.trim());
17001                    }
17002                } else {
17003                    eprintln!("functions: no such function: {}", name);
17004                    return 1;
17005                }
17006            }
17007        }
17008        0
17009    }
17010
17011    /// print - zsh print builtin with many options
17012    fn builtin_print(&mut self, args: &[String]) -> i32 {
17013        // print [ -abcDilmnNoOpPrsSz ] [ -u n ] [ -f format ] [ -C cols ]
17014        //       [ -v name ] [ -xX tabstop ] [ -R [ -en ]] [ arg ... ]
17015        let mut no_newline = false;
17016        let mut one_per_line = false;
17017        let mut interpret_escapes = true; // zsh default is to interpret
17018        let mut raw_mode = false;
17019        let mut prompt_expand = false;
17020        let mut fd: i32 = 1; // stdout
17021        let mut columns = 0usize;
17022        let mut null_terminate = false;
17023        let mut push_to_stack = false;
17024        let mut add_to_history = false;
17025        let mut sort_asc = false;
17026        let mut sort_desc = false;
17027        let mut named_dir_subst = false;
17028        let mut store_var: Option<String> = None;
17029        let mut format_string: Option<String> = None;
17030        let mut output_args: Vec<String> = Vec::new();
17031
17032        let mut i = 0;
17033        while i < args.len() {
17034            let arg = &args[i];
17035
17036            if arg == "--" {
17037                i += 1;
17038                while i < args.len() {
17039                    output_args.push(args[i].clone());
17040                    i += 1;
17041                }
17042                break;
17043            }
17044
17045            if arg.starts_with('-')
17046                && arg.len() > 1
17047                && !arg
17048                    .chars()
17049                    .nth(1)
17050                    .map(|c| c.is_ascii_digit())
17051                    .unwrap_or(false)
17052            {
17053                let mut chars = arg[1..].chars().peekable();
17054                while let Some(ch) = chars.next() {
17055                    match ch {
17056                        'n' => no_newline = true,
17057                        'l' => one_per_line = true,
17058                        'r' => {
17059                            raw_mode = true;
17060                            interpret_escapes = false;
17061                        }
17062                        'R' => {
17063                            raw_mode = true;
17064                            interpret_escapes = false;
17065                        }
17066                        'e' => interpret_escapes = true,
17067                        'E' => interpret_escapes = false,
17068                        'P' => prompt_expand = true,
17069                        'N' => null_terminate = true,
17070                        'z' => push_to_stack = true,
17071                        's' => add_to_history = true,
17072                        'o' => sort_asc = true,
17073                        'O' => sort_desc = true,
17074                        'D' => named_dir_subst = true,
17075                        'c' => columns = 1,
17076                        'a' | 'b' | 'i' | 'm' | 'p' | 'S' | 'x' | 'X' => {} // TODO
17077                        'u' => {
17078                            // -u n: output to fd n
17079                            let rest: String = chars.collect();
17080                            if !rest.is_empty() {
17081                                fd = rest.parse().unwrap_or(1);
17082                            } else {
17083                                i += 1;
17084                                if i < args.len() {
17085                                    fd = args[i].parse().unwrap_or(1);
17086                                }
17087                            }
17088                            break;
17089                        }
17090                        'C' => {
17091                            // -C n: n columns
17092                            let rest: String = chars.collect();
17093                            if !rest.is_empty() {
17094                                columns = rest.parse().unwrap_or(0);
17095                            } else {
17096                                i += 1;
17097                                if i < args.len() {
17098                                    columns = args[i].parse().unwrap_or(0);
17099                                }
17100                            }
17101                            break;
17102                        }
17103                        'v' => {
17104                            // -v name: store in variable
17105                            let rest: String = chars.collect();
17106                            if !rest.is_empty() {
17107                                store_var = Some(rest);
17108                            } else {
17109                                i += 1;
17110                                if i < args.len() {
17111                                    store_var = Some(args[i].clone());
17112                                }
17113                            }
17114                            break;
17115                        }
17116                        'f' => {
17117                            // -f format: printf-style format
17118                            let rest: String = chars.collect();
17119                            if !rest.is_empty() {
17120                                format_string = Some(rest);
17121                            } else {
17122                                i += 1;
17123                                if i < args.len() {
17124                                    format_string = Some(args[i].clone());
17125                                }
17126                            }
17127                            break;
17128                        }
17129                        _ => {}
17130                    }
17131                }
17132            } else {
17133                output_args.push(arg.clone());
17134            }
17135            i += 1;
17136        }
17137
17138        let _ = push_to_stack; // TODO: implement push to buffer stack
17139        let _ = fd; // TODO: implement fd selection
17140
17141        // Sort if requested
17142        if sort_asc {
17143            output_args.sort();
17144        } else if sort_desc {
17145            output_args.sort_by(|a, b| b.cmp(a));
17146        }
17147
17148        // Handle -f format
17149        if let Some(fmt) = format_string {
17150            let output = self.printf_format(&fmt, &output_args);
17151            if let Some(var) = store_var {
17152                self.variables.insert(var, output);
17153            } else {
17154                print!("{}", output);
17155            }
17156            return 0;
17157        }
17158
17159        // Process output
17160        let processed: Vec<String> = output_args
17161            .iter()
17162            .map(|s| {
17163                let mut result = s.clone();
17164                if prompt_expand {
17165                    result = self.expand_prompt_string(&result);
17166                }
17167                if interpret_escapes && !raw_mode {
17168                    result = self.expand_printf_escapes(&result);
17169                }
17170                if named_dir_subst {
17171                    // Replace home dir with ~
17172                    if let Ok(home) = env::var("HOME") {
17173                        if result.starts_with(&home) {
17174                            result = format!("~{}", &result[home.len()..]);
17175                        }
17176                    }
17177                    // Replace named dirs
17178                    for (name, path) in &self.named_dirs {
17179                        let path_str = path.to_string_lossy();
17180                        if result.starts_with(path_str.as_ref()) {
17181                            result = format!("~{}{}", name, &result[path_str.len()..]);
17182                            break;
17183                        }
17184                    }
17185                }
17186                result
17187            })
17188            .collect();
17189
17190        // Determine separator and terminator
17191        let separator = if one_per_line { "\n" } else { " " };
17192        let terminator = if null_terminate {
17193            "\0"
17194        } else if no_newline {
17195            ""
17196        } else {
17197            "\n"
17198        };
17199
17200        // Build output
17201        let output = if one_per_line {
17202            processed.join("\n")
17203        } else if columns > 0 {
17204            // Column output - calculate column widths
17205            let mut result = String::new();
17206            let num_items = processed.len();
17207            let rows = (num_items + columns - 1) / columns;
17208            for row in 0..rows {
17209                let mut row_items = Vec::new();
17210                for col in 0..columns {
17211                    let idx = row + col * rows;
17212                    if idx < num_items {
17213                        row_items.push(processed[idx].as_str());
17214                    }
17215                }
17216                result.push_str(&row_items.join("\t"));
17217                if row < rows - 1 {
17218                    result.push('\n');
17219                }
17220            }
17221            result
17222        } else {
17223            processed.join(separator)
17224        };
17225
17226        // Add to history if -s
17227        if add_to_history {
17228            if let Some(ref mut engine) = self.history {
17229                let _ = engine.add(&output, None);
17230            }
17231        }
17232
17233        // Store in variable or print
17234        if let Some(var) = store_var {
17235            self.variables.insert(var, output);
17236        } else {
17237            print!("{}{}", output, terminator);
17238        }
17239
17240        0
17241    }
17242
17243    fn printf_format(&self, format: &str, args: &[String]) -> String {
17244        let mut result = String::new();
17245        let mut arg_idx = 0;
17246        let mut chars = format.chars().peekable();
17247
17248        while let Some(ch) = chars.next() {
17249            if ch == '%' {
17250                if chars.peek() == Some(&'%') {
17251                    chars.next();
17252                    result.push('%');
17253                    continue;
17254                }
17255
17256                // Parse format specifier
17257                let mut spec = String::from("%");
17258
17259                // Flags
17260                while let Some(&c) = chars.peek() {
17261                    if c == '-' || c == '+' || c == ' ' || c == '#' || c == '0' {
17262                        spec.push(c);
17263                        chars.next();
17264                    } else {
17265                        break;
17266                    }
17267                }
17268
17269                // Width
17270                while let Some(&c) = chars.peek() {
17271                    if c.is_ascii_digit() {
17272                        spec.push(c);
17273                        chars.next();
17274                    } else {
17275                        break;
17276                    }
17277                }
17278
17279                // Precision
17280                if chars.peek() == Some(&'.') {
17281                    spec.push('.');
17282                    chars.next();
17283                    while let Some(&c) = chars.peek() {
17284                        if c.is_ascii_digit() {
17285                            spec.push(c);
17286                            chars.next();
17287                        } else {
17288                            break;
17289                        }
17290                    }
17291                }
17292
17293                // Conversion specifier
17294                if let Some(conv) = chars.next() {
17295                    let arg = args.get(arg_idx).map(|s| s.as_str()).unwrap_or("");
17296                    arg_idx += 1;
17297
17298                    match conv {
17299                        's' => result.push_str(arg),
17300                        'd' | 'i' => {
17301                            let n: i64 = arg.parse().unwrap_or(0);
17302                            result.push_str(&n.to_string());
17303                        }
17304                        'u' => {
17305                            let n: u64 = arg.parse().unwrap_or(0);
17306                            result.push_str(&n.to_string());
17307                        }
17308                        'x' => {
17309                            let n: i64 = arg.parse().unwrap_or(0);
17310                            result.push_str(&format!("{:x}", n));
17311                        }
17312                        'X' => {
17313                            let n: i64 = arg.parse().unwrap_or(0);
17314                            result.push_str(&format!("{:X}", n));
17315                        }
17316                        'o' => {
17317                            let n: i64 = arg.parse().unwrap_or(0);
17318                            result.push_str(&format!("{:o}", n));
17319                        }
17320                        'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
17321                            let n: f64 = arg.parse().unwrap_or(0.0);
17322                            result.push_str(&format!("{}", n));
17323                        }
17324                        'c' => {
17325                            if let Some(c) = arg.chars().next() {
17326                                result.push(c);
17327                            }
17328                        }
17329                        'b' => {
17330                            result.push_str(&self.expand_printf_escapes(arg));
17331                        }
17332                        'n' => result.push('\n'),
17333                        _ => {
17334                            result.push('%');
17335                            result.push(conv);
17336                        }
17337                    }
17338                }
17339            } else {
17340                result.push(ch);
17341            }
17342        }
17343
17344        result
17345    }
17346
17347    /// whence - show how a command would be interpreted
17348    fn builtin_whence(&self, args: &[String]) -> i32 {
17349        // whence [ -vcwfpamsS ] [ -x num ] name ...
17350        // -v: verbose (like type)
17351        // -c: csh-style output
17352        // -w: print word type (alias, builtin, command, function, hashed, reserved, none)
17353        // -f: skip functions
17354        // -p: search path only
17355        // -a: show all matches
17356        // -m: pattern match with glob
17357        // -s: show symlink resolution
17358        // -S: show steps of symlink resolution
17359        // -x num: expand tabs to num spaces
17360
17361        let mut verbose = false;
17362        let mut csh_style = false;
17363        let mut word_type = false;
17364        let mut skip_functions = false;
17365        let mut path_only = false;
17366        let mut show_all = false;
17367        let mut pattern_mode = false;
17368        let mut show_symlink = false;
17369        let mut show_symlink_steps = false;
17370        let mut tab_expand: Option<usize> = None;
17371        let mut names: Vec<&str> = Vec::new();
17372
17373        let mut i = 0;
17374        while i < args.len() {
17375            let arg = &args[i];
17376
17377            if arg == "--" {
17378                i += 1;
17379                while i < args.len() {
17380                    names.push(&args[i]);
17381                    i += 1;
17382                }
17383                break;
17384            }
17385
17386            if arg.starts_with('-') && arg.len() > 1 {
17387                let mut chars = arg[1..].chars().peekable();
17388                while let Some(ch) = chars.next() {
17389                    match ch {
17390                        'v' => verbose = true,
17391                        'c' => csh_style = true,
17392                        'w' => word_type = true,
17393                        'f' => skip_functions = true,
17394                        'p' => path_only = true,
17395                        'a' => show_all = true,
17396                        'm' => pattern_mode = true,
17397                        's' => show_symlink = true,
17398                        'S' => show_symlink_steps = true,
17399                        'x' => {
17400                            // -x num: tab expansion
17401                            let rest: String = chars.collect();
17402                            if !rest.is_empty() {
17403                                tab_expand = rest.parse().ok();
17404                            } else {
17405                                i += 1;
17406                                if i < args.len() {
17407                                    tab_expand = args[i].parse().ok();
17408                                }
17409                            }
17410                            break;
17411                        }
17412                        _ => {}
17413                    }
17414                }
17415            } else {
17416                names.push(arg);
17417            }
17418            i += 1;
17419        }
17420
17421        let _ = csh_style; // TODO: implement csh-style output
17422        let _ = pattern_mode; // TODO: implement glob pattern matching
17423        let _ = tab_expand;
17424
17425        let mut status = 0;
17426        for name in names {
17427            let mut found = false;
17428            let mut word = "none";
17429
17430            if !path_only {
17431                // Check reserved words
17432                if self.is_reserved_word(name) {
17433                    found = true;
17434                    word = "reserved";
17435                    if word_type {
17436                        println!("{}: {}", name, word);
17437                    } else if verbose {
17438                        println!("{} is a reserved word", name);
17439                    } else {
17440                        println!("{}", name);
17441                    }
17442                    if !show_all {
17443                        continue;
17444                    }
17445                }
17446
17447                // Check aliases
17448                if let Some(alias_val) = self.aliases.get(name) {
17449                    found = true;
17450                    word = "alias";
17451                    if word_type {
17452                        println!("{}: {}", name, word);
17453                    } else if verbose {
17454                        println!("{} is an alias for {}", name, alias_val);
17455                    } else {
17456                        println!("{}", alias_val);
17457                    }
17458                    if !show_all {
17459                        continue;
17460                    }
17461                }
17462
17463                // Check functions (unless -f)
17464                if !skip_functions && self.functions.contains_key(name) {
17465                    found = true;
17466                    word = "function";
17467                    if word_type {
17468                        println!("{}: {}", name, word);
17469                    } else if verbose {
17470                        println!("{} is a shell function", name);
17471                    } else {
17472                        println!("{}", name);
17473                    }
17474                    if !show_all {
17475                        continue;
17476                    }
17477                }
17478
17479                // Check builtins
17480                if self.is_builtin(name) {
17481                    found = true;
17482                    word = "builtin";
17483                    if word_type {
17484                        println!("{}: {}", name, word);
17485                    } else if verbose {
17486                        println!("{} is a shell builtin", name);
17487                    } else {
17488                        println!("{}", name);
17489                    }
17490                    if !show_all {
17491                        continue;
17492                    }
17493                }
17494
17495                // Check hashed commands (named_dirs can serve as a command hash)
17496                // The hash builtin adds to named_dirs for now
17497                if let Some(path) = self.named_dirs.get(name) {
17498                    found = true;
17499                    word = "hashed";
17500                    if word_type {
17501                        println!("{}: {}", name, word);
17502                    } else if verbose {
17503                        println!("{} is hashed ({})", name, path.display());
17504                    } else {
17505                        println!("{}", path.display());
17506                    }
17507                    if !show_all {
17508                        continue;
17509                    }
17510                }
17511            }
17512
17513            // Check PATH
17514            if let Some(path) = self.find_in_path(name) {
17515                found = true;
17516                word = "command";
17517
17518                // Handle symlink resolution
17519                let display_path = if show_symlink || show_symlink_steps {
17520                    let p = std::path::Path::new(&path);
17521                    if show_symlink_steps {
17522                        let mut current = p.to_path_buf();
17523                        let mut steps = vec![path.clone()];
17524                        while let Ok(target) = std::fs::read_link(&current) {
17525                            let resolved = if target.is_absolute() {
17526                                target.clone()
17527                            } else {
17528                                current
17529                                    .parent()
17530                                    .unwrap_or(std::path::Path::new("/"))
17531                                    .join(&target)
17532                            };
17533                            steps.push(resolved.to_string_lossy().to_string());
17534                            current = resolved;
17535                        }
17536                        steps.join(" -> ")
17537                    } else {
17538                        match p.canonicalize() {
17539                            Ok(resolved) => format!("{} -> {}", path, resolved.display()),
17540                            Err(_) => path.clone(),
17541                        }
17542                    }
17543                } else {
17544                    path.clone()
17545                };
17546
17547                if word_type {
17548                    println!("{}: {}", name, word);
17549                } else if verbose {
17550                    println!("{} is {}", name, display_path);
17551                } else {
17552                    println!("{}", display_path);
17553                }
17554            }
17555
17556            if !found {
17557                if word_type {
17558                    println!("{}: none", name);
17559                } else if verbose {
17560                    println!("{} not found", name);
17561                }
17562                status = 1;
17563            }
17564        }
17565        status
17566    }
17567
17568    fn is_reserved_word(&self, name: &str) -> bool {
17569        matches!(
17570            name,
17571            "if" | "then"
17572                | "else"
17573                | "elif"
17574                | "fi"
17575                | "case"
17576                | "esac"
17577                | "for"
17578                | "select"
17579                | "while"
17580                | "until"
17581                | "do"
17582                | "done"
17583                | "in"
17584                | "function"
17585                | "time"
17586                | "coproc"
17587                | "{"
17588                | "}"
17589                | "!"
17590                | "[["
17591                | "]]"
17592                | "(("
17593                | "))"
17594        )
17595    }
17596
17597    /// where - show all locations of a command
17598    fn builtin_where(&self, args: &[String]) -> i32 {
17599        // where is like whence -ca
17600        let mut new_args = vec!["-a".to_string(), "-v".to_string()];
17601        new_args.extend(args.iter().cloned());
17602        self.builtin_whence(&new_args)
17603    }
17604
17605    /// which - show path of command
17606    fn builtin_which(&self, args: &[String]) -> i32 {
17607        // which is like whence -c
17608        let mut new_args = vec!["-c".to_string()];
17609        new_args.extend(args.iter().cloned());
17610        self.builtin_whence(&new_args)
17611    }
17612
17613    /// Helper to check if name is a builtin
17614    /// O(1) builtin check via static HashSet — replaces 130+ arm linear match
17615    fn is_builtin(&self, name: &str) -> bool {
17616        BUILTIN_SET.contains(name) || name.starts_with('_')
17617    }
17618
17619    /// Helper to find command in PATH — checks command_hash first for O(1) hit
17620    fn find_in_path(&self, name: &str) -> Option<String> {
17621        // O(1) hash table lookup from rehash
17622        if let Some(path) = self.command_hash.get(name) {
17623            return Some(path.clone());
17624        }
17625        // Fallback: linear PATH walk
17626        let path_var = env::var("PATH").unwrap_or_default();
17627        for dir in path_var.split(':') {
17628            let full_path = format!("{}/{}", dir, name);
17629            if std::path::Path::new(&full_path).exists() {
17630                return Some(full_path);
17631            }
17632        }
17633        None
17634    }
17635
17636    /// ulimit - get/set resource limits
17637    fn builtin_ulimit(&self, args: &[String]) -> i32 {
17638        use libc::{getrlimit, rlimit, setrlimit};
17639        use libc::{RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE};
17640        use libc::{RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK};
17641
17642        let mut resource = RLIMIT_FSIZE; // default: file size
17643        let mut hard = false;
17644        let mut soft = true;
17645        let mut value: Option<u64> = None;
17646
17647        let mut iter = args.iter();
17648        while let Some(arg) = iter.next() {
17649            match arg.as_str() {
17650                "-H" => {
17651                    hard = true;
17652                    soft = false;
17653                }
17654                "-S" => {
17655                    soft = true;
17656                    hard = false;
17657                }
17658                "-a" => {
17659                    // Print all limits
17660                    self.print_all_limits(soft);
17661                    return 0;
17662                }
17663                "-c" => resource = RLIMIT_CORE,
17664                "-d" => resource = RLIMIT_DATA,
17665                "-f" => resource = RLIMIT_FSIZE,
17666                "-n" => resource = RLIMIT_NOFILE,
17667                "-s" => resource = RLIMIT_STACK,
17668                "-t" => resource = RLIMIT_CPU,
17669                "-u" => resource = RLIMIT_NPROC,
17670                "-v" => resource = RLIMIT_AS,
17671                "-m" => resource = RLIMIT_RSS,
17672                "unlimited" => value = Some(libc::RLIM_INFINITY as u64),
17673                _ if !arg.starts_with('-') => {
17674                    value = arg.parse().ok();
17675                }
17676                _ => {}
17677            }
17678        }
17679
17680        let mut rlim = rlimit {
17681            rlim_cur: 0,
17682            rlim_max: 0,
17683        };
17684        unsafe {
17685            if getrlimit(resource, &mut rlim) != 0 {
17686                eprintln!("ulimit: cannot get limit");
17687                return 1;
17688            }
17689        }
17690
17691        if let Some(v) = value {
17692            // Set limit
17693            if soft {
17694                rlim.rlim_cur = v as libc::rlim_t;
17695            }
17696            if hard {
17697                rlim.rlim_max = v as libc::rlim_t;
17698            }
17699            unsafe {
17700                if setrlimit(resource, &rlim) != 0 {
17701                    eprintln!("ulimit: cannot set limit");
17702                    return 1;
17703                }
17704            }
17705        } else {
17706            // Print limit
17707            let limit = if hard { rlim.rlim_max } else { rlim.rlim_cur };
17708            if limit == libc::RLIM_INFINITY as libc::rlim_t {
17709                println!("unlimited");
17710            } else {
17711                println!("{}", limit);
17712            }
17713        }
17714        0
17715    }
17716
17717    fn print_all_limits(&self, soft: bool) {
17718        use libc::{getrlimit, rlimit};
17719        use libc::{RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE};
17720        use libc::{RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK};
17721
17722        let limits = [
17723            (RLIMIT_CORE, "core file size", "blocks", 512),
17724            (RLIMIT_DATA, "data seg size", "kbytes", 1024),
17725            (RLIMIT_FSIZE, "file size", "blocks", 512),
17726            (RLIMIT_NOFILE, "open files", "", 1),
17727            (RLIMIT_STACK, "stack size", "kbytes", 1024),
17728            (RLIMIT_CPU, "cpu time", "seconds", 1),
17729            (RLIMIT_NPROC, "max user processes", "", 1),
17730            (RLIMIT_AS, "virtual memory", "kbytes", 1024),
17731            (RLIMIT_RSS, "max memory size", "kbytes", 1024),
17732        ];
17733
17734        for (resource, name, unit, divisor) in limits {
17735            let mut rlim = rlimit {
17736                rlim_cur: 0,
17737                rlim_max: 0,
17738            };
17739            unsafe {
17740                if getrlimit(resource, &mut rlim) == 0 {
17741                    let limit = if soft { rlim.rlim_cur } else { rlim.rlim_max };
17742                    let unit_str = if unit.is_empty() {
17743                        ""
17744                    } else {
17745                        &format!("({})", unit)
17746                    };
17747                    if limit == libc::RLIM_INFINITY as libc::rlim_t {
17748                        println!("{:25} {} unlimited", name, unit_str);
17749                    } else {
17750                        println!("{:25} {} {}", name, unit_str, limit / divisor);
17751                    }
17752                }
17753            }
17754        }
17755    }
17756
17757    /// limit - csh-style resource limits
17758    fn builtin_limit(&self, args: &[String]) -> i32 {
17759        // Delegate to ulimit with csh-style names
17760        if args.is_empty() {
17761            // Print all resource limits in csh format
17762            use libc::{getrlimit, rlimit, RLIM_INFINITY};
17763            let resources = [
17764                (libc::RLIMIT_CPU, "cputime", 1, "seconds"),
17765                (libc::RLIMIT_FSIZE, "filesize", 1024, "kB"),
17766                (libc::RLIMIT_DATA, "datasize", 1024, "kB"),
17767                (libc::RLIMIT_STACK, "stacksize", 1024, "kB"),
17768                (libc::RLIMIT_CORE, "coredumpsize", 1024, "kB"),
17769                (libc::RLIMIT_RSS, "memoryuse", 1024, "kB"),
17770                #[cfg(target_os = "linux")]
17771                (libc::RLIMIT_NPROC, "maxproc", 1, ""),
17772                (libc::RLIMIT_NOFILE, "descriptors", 1, ""),
17773            ];
17774            for (res, name, divisor, unit) in resources {
17775                let mut rl: rlimit = unsafe { std::mem::zeroed() };
17776                unsafe {
17777                    getrlimit(res, &mut rl);
17778                }
17779                let val = if rl.rlim_cur == RLIM_INFINITY as u64 {
17780                    "unlimited".to_string()
17781                } else {
17782                    let v = rl.rlim_cur as u64 / divisor;
17783                    if unit.is_empty() {
17784                        format!("{}", v)
17785                    } else {
17786                        format!("{}{}", v, unit)
17787                    }
17788                };
17789                println!("{:<16}{}", name, val);
17790            }
17791            return 0;
17792        }
17793        self.builtin_ulimit(args)
17794    }
17795
17796    /// unlimit - remove resource limits
17797    fn builtin_unlimit(&self, args: &[String]) -> i32 {
17798        let mut new_args = args.to_vec();
17799        new_args.push("unlimited".to_string());
17800        self.builtin_ulimit(&new_args)
17801    }
17802
17803    /// umask - get/set file creation mask
17804    fn builtin_umask(&self, args: &[String]) -> i32 {
17805        use libc::umask;
17806
17807        let mut symbolic = false;
17808        let mut value: Option<&str> = None;
17809
17810        for arg in args {
17811            match arg.as_str() {
17812                "-S" => symbolic = true,
17813                _ if !arg.starts_with('-') => value = Some(arg),
17814                _ => {}
17815            }
17816        }
17817
17818        if let Some(v) = value {
17819            // Set umask
17820            if let Ok(mask) = u32::from_str_radix(v, 8) {
17821                unsafe {
17822                    umask(mask as libc::mode_t);
17823                }
17824            } else {
17825                eprintln!("umask: invalid mask: {}", v);
17826                return 1;
17827            }
17828        } else {
17829            // Get umask
17830            let mask = unsafe {
17831                let m = umask(0);
17832                umask(m);
17833                m
17834            };
17835            if symbolic {
17836                let u = 7 - ((mask >> 6) & 7);
17837                let g = 7 - ((mask >> 3) & 7);
17838                let o = 7 - (mask & 7);
17839                println!(
17840                    "u={}{}{}g={}{}{}o={}{}{}",
17841                    if u & 4 != 0 { "r" } else { "" },
17842                    if u & 2 != 0 { "w" } else { "" },
17843                    if u & 1 != 0 { "x" } else { "" },
17844                    if g & 4 != 0 { "r" } else { "" },
17845                    if g & 2 != 0 { "w" } else { "" },
17846                    if g & 1 != 0 { "x" } else { "" },
17847                    if o & 4 != 0 { "r" } else { "" },
17848                    if o & 2 != 0 { "w" } else { "" },
17849                    if o & 1 != 0 { "x" } else { "" },
17850                );
17851            } else {
17852                println!("{:04o}", mask);
17853            }
17854        }
17855        0
17856    }
17857
17858    /// rehash - rebuild command hash table
17859    fn builtin_rehash(&mut self, args: &[String]) -> i32 {
17860        // rehash [ -d ] [ -f ] [ -v ]
17861        // -d: rehash named directories
17862        // -f: force rehash of all commands in PATH
17863        // -v: verbose (print each command being hashed)
17864
17865        let mut rehash_dirs = false;
17866        let mut force = false;
17867        let mut verbose = false;
17868
17869        for arg in args {
17870            if arg.starts_with('-') {
17871                for ch in arg[1..].chars() {
17872                    match ch {
17873                        'd' => rehash_dirs = true,
17874                        'f' => force = true,
17875                        'v' => verbose = true,
17876                        _ => {}
17877                    }
17878                }
17879            }
17880        }
17881
17882        if rehash_dirs {
17883            // Rebuild named directories from special params like ~user
17884            // For now just clear and rebuild from HOME
17885            self.named_dirs.clear();
17886            if let Ok(home) = env::var("HOME") {
17887                self.named_dirs.insert(String::new(), PathBuf::from(&home)); // ~ without name
17888            }
17889            return 0;
17890        }
17891
17892        // Clear command hash table
17893        self.command_hash.clear();
17894
17895        if force {
17896            // Parallel PATH scan — each PATH dir on a pool thread.
17897            // zsh does this single-threaded; we fan out across workers.
17898            if let Ok(path_var) = env::var("PATH") {
17899                let dirs: Vec<String> = path_var
17900                    .split(':')
17901                    .filter(|s| !s.is_empty())
17902                    .map(|s| s.to_string())
17903                    .collect();
17904
17905                let (tx, rx) = std::sync::mpsc::channel::<Vec<(String, String)>>();
17906
17907                for dir in dirs {
17908                    let tx = tx.clone();
17909                    self.worker_pool.submit(move || {
17910                        let mut batch = Vec::new();
17911                        if let Ok(entries) = std::fs::read_dir(&dir) {
17912                            for entry in entries.flatten() {
17913                                if let Ok(ft) = entry.file_type() {
17914                                    if ft.is_file() || ft.is_symlink() {
17915                                        if let Some(name) = entry.file_name().to_str() {
17916                                            let path = entry.path().to_string_lossy().to_string();
17917                                            batch.push((name.to_string(), path));
17918                                        }
17919                                    }
17920                                }
17921                            }
17922                        }
17923                        let _ = tx.send(batch);
17924                    });
17925                }
17926                drop(tx);
17927
17928                for batch in rx {
17929                    for (name, path) in batch {
17930                        if verbose {
17931                            println!("{}={}", name, path);
17932                        }
17933                        self.command_hash.insert(name, path);
17934                    }
17935                }
17936            }
17937        }
17938
17939        0
17940    }
17941
17942    /// unhash - remove entries from hash table
17943    fn builtin_unhash(&mut self, args: &[String]) -> i32 {
17944        let mut remove_aliases = false;
17945        let mut remove_functions = false;
17946        let mut remove_dirs = false;
17947        let mut names: Vec<&str> = Vec::new();
17948
17949        for arg in args {
17950            match arg.as_str() {
17951                "-a" => remove_aliases = true,
17952                "-f" => remove_functions = true,
17953                "-d" => remove_dirs = true,
17954                "-m" => {} // pattern matching (TODO)
17955                _ if arg.starts_with('-') => {}
17956                _ => names.push(arg),
17957            }
17958        }
17959
17960        for name in names {
17961            if remove_aliases {
17962                self.aliases.remove(name);
17963            }
17964            if remove_functions {
17965                self.functions.remove(name);
17966            }
17967            if remove_dirs {
17968                // Remove from named directories (TODO)
17969            }
17970        }
17971        0
17972    }
17973
17974    /// times - print accumulated user and system times
17975    fn builtin_times(&self, _args: &[String]) -> i32 {
17976        use libc::{getrusage, rusage, RUSAGE_CHILDREN, RUSAGE_SELF};
17977
17978        let mut self_usage: rusage = unsafe { std::mem::zeroed() };
17979        let mut child_usage: rusage = unsafe { std::mem::zeroed() };
17980
17981        unsafe {
17982            getrusage(RUSAGE_SELF, &mut self_usage);
17983            getrusage(RUSAGE_CHILDREN, &mut child_usage);
17984        }
17985
17986        let self_user =
17987            self_usage.ru_utime.tv_sec as f64 + self_usage.ru_utime.tv_usec as f64 / 1_000_000.0;
17988        let self_sys =
17989            self_usage.ru_stime.tv_sec as f64 + self_usage.ru_stime.tv_usec as f64 / 1_000_000.0;
17990        let child_user =
17991            child_usage.ru_utime.tv_sec as f64 + child_usage.ru_utime.tv_usec as f64 / 1_000_000.0;
17992        let child_sys =
17993            child_usage.ru_stime.tv_sec as f64 + child_usage.ru_stime.tv_usec as f64 / 1_000_000.0;
17994
17995        println!("{:.3}s {:.3}s", self_user, self_sys);
17996        println!("{:.3}s {:.3}s", child_user, child_sys);
17997        0
17998    }
17999
18000    /// zmodload - load/unload zsh modules (stub)
18001    fn builtin_zmodload(&mut self, args: &[String]) -> i32 {
18002        let mut list_loaded = false;
18003        let mut unload = false;
18004        let mut modules: Vec<&str> = Vec::new();
18005
18006        for arg in args {
18007            match arg.as_str() {
18008                "-l" | "-L" => list_loaded = true,
18009                "-u" => unload = true,
18010                "-a" | "-b" | "-c" | "-d" | "-e" | "-f" | "-i" | "-p" | "-s" => {}
18011                _ if arg.starts_with('-') => {}
18012                _ => modules.push(arg),
18013            }
18014        }
18015
18016        if list_loaded || modules.is_empty() {
18017            // List loaded modules (stub - we don't really have modules)
18018            println!("zsh/complete");
18019            println!("zsh/complist");
18020            println!("zsh/parameter");
18021            println!("zsh/zutil");
18022            return 0;
18023        }
18024
18025        for module in modules {
18026            if unload {
18027                // Unload module (stub)
18028                self.options.remove(&format!("_module_{}", module));
18029            } else {
18030                // Load module (stub)
18031                self.options.insert(format!("_module_{}", module), true);
18032            }
18033        }
18034        0
18035    }
18036
18037    /// r - redo last command (alias for fc -e -)
18038    fn builtin_r(&mut self, args: &[String]) -> i32 {
18039        let mut fc_args = vec!["-e".to_string(), "-".to_string()];
18040        fc_args.extend(args.iter().cloned());
18041        self.builtin_fc(&fc_args)
18042    }
18043
18044    /// ttyctl - control terminal settings
18045    fn builtin_ttyctl(&self, args: &[String]) -> i32 {
18046        for arg in args {
18047            match arg.as_str() {
18048                "-f" => {
18049                    // Freeze terminal settings
18050                    // In a full implementation, this would save terminal state
18051                }
18052                "-u" => {
18053                    // Unfreeze terminal settings
18054                }
18055                _ => {}
18056            }
18057        }
18058        0
18059    }
18060
18061    /// noglob - run command without globbing
18062    fn builtin_noglob(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
18063        if args.is_empty() {
18064            return 0;
18065        }
18066
18067        // Temporarily disable globbing
18068        let saved = self.options.get("noglob").cloned();
18069        self.options.insert("noglob".to_string(), true);
18070
18071        // Execute the command
18072        let status = self.builtin_command(args, redirects);
18073
18074        // Restore globbing state
18075        if let Some(v) = saved {
18076            self.options.insert("noglob".to_string(), v);
18077        } else {
18078            self.options.remove("noglob");
18079        }
18080
18081        status
18082    }
18083
18084    // ═══════════════════════════════════════════════════════════════════════════
18085    // zsh module builtins
18086    // ═══════════════════════════════════════════════════════════════════════════
18087
18088    /// zstat - file status (zsh/stat module)
18089    fn builtin_zstat(&self, args: &[String]) -> i32 {
18090        use std::os::unix::fs::MetadataExt;
18091        use std::os::unix::fs::PermissionsExt;
18092
18093        let mut show_all = true;
18094        let mut symbolic_mode = false;
18095        let mut show_link = false;
18096        let mut _as_array = false;
18097        let mut _array_name = String::new();
18098        let mut format_time = String::new();
18099        let mut elements: Vec<String> = Vec::new();
18100        let mut files: Vec<&str> = Vec::new();
18101
18102        let mut iter = args.iter().peekable();
18103        while let Some(arg) = iter.next() {
18104            match arg.as_str() {
18105                "-s" => symbolic_mode = true,
18106                "-L" => show_link = true,
18107                "-N" => {} // Don't resolve symlinks
18108                "-n" => {} // Numeric user/group
18109                "-o" => show_all = false,
18110                "-A" => {
18111                    _as_array = true;
18112                    if let Some(name) = iter.next() {
18113                        _array_name = name.clone();
18114                    }
18115                }
18116                "-F" => {
18117                    if let Some(fmt) = iter.next() {
18118                        format_time = fmt.clone();
18119                    }
18120                }
18121                s if s.starts_with('+') => {
18122                    elements.push(s[1..].to_string());
18123                    show_all = false;
18124                }
18125                s if !s.starts_with('-') => files.push(s),
18126                _ => {}
18127            }
18128        }
18129
18130        if files.is_empty() {
18131            eprintln!("zstat: no files specified");
18132            return 1;
18133        }
18134
18135        for file in files {
18136            let meta = if show_link {
18137                std::fs::symlink_metadata(file)
18138            } else {
18139                std::fs::metadata(file)
18140            };
18141
18142            let meta = match meta {
18143                Ok(m) => m,
18144                Err(e) => {
18145                    eprintln!("zstat: {}: {}", file, e);
18146                    return 1;
18147                }
18148            };
18149
18150            let output_element = |name: &str, value: &str| {
18151                if _as_array {
18152                    // Would need mutable self to store in array
18153                    println!("{}={}", name, value);
18154                } else if show_all || elements.contains(&name.to_string()) {
18155                    println!("{}: {}", name, value);
18156                }
18157            };
18158
18159            output_element("device", &meta.dev().to_string());
18160            output_element("inode", &meta.ino().to_string());
18161
18162            if symbolic_mode {
18163                let mode = meta.permissions().mode();
18164                let mode_str = format!(
18165                    "{}{}{}{}{}{}{}{}{}{}",
18166                    match mode & 0o170000 {
18167                        0o040000 => 'd',
18168                        0o120000 => 'l',
18169                        0o100000 => '-',
18170                        0o060000 => 'b',
18171                        0o020000 => 'c',
18172                        0o010000 => 'p',
18173                        0o140000 => 's',
18174                        _ => '?',
18175                    },
18176                    if mode & 0o400 != 0 { 'r' } else { '-' },
18177                    if mode & 0o200 != 0 { 'w' } else { '-' },
18178                    if mode & 0o4000 != 0 {
18179                        's'
18180                    } else if mode & 0o100 != 0 {
18181                        'x'
18182                    } else {
18183                        '-'
18184                    },
18185                    if mode & 0o040 != 0 { 'r' } else { '-' },
18186                    if mode & 0o020 != 0 { 'w' } else { '-' },
18187                    if mode & 0o2000 != 0 {
18188                        's'
18189                    } else if mode & 0o010 != 0 {
18190                        'x'
18191                    } else {
18192                        '-'
18193                    },
18194                    if mode & 0o004 != 0 { 'r' } else { '-' },
18195                    if mode & 0o002 != 0 { 'w' } else { '-' },
18196                    if mode & 0o1000 != 0 {
18197                        't'
18198                    } else if mode & 0o001 != 0 {
18199                        'x'
18200                    } else {
18201                        '-'
18202                    },
18203                );
18204                output_element("mode", &mode_str);
18205            } else {
18206                output_element("mode", &format!("{:o}", meta.permissions().mode()));
18207            }
18208
18209            output_element("nlink", &meta.nlink().to_string());
18210            output_element("uid", &meta.uid().to_string());
18211            output_element("gid", &meta.gid().to_string());
18212            output_element("rdev", &meta.rdev().to_string());
18213            output_element("size", &meta.len().to_string());
18214
18215            let format_timestamp = |secs: i64| -> String {
18216                if format_time.is_empty() {
18217                    secs.to_string()
18218                } else {
18219                    chrono::DateTime::from_timestamp(secs, 0)
18220                        .map(|dt| dt.format(&format_time).to_string())
18221                        .unwrap_or_else(|| secs.to_string())
18222                }
18223            };
18224
18225            output_element("atime", &format_timestamp(meta.atime()));
18226            output_element("mtime", &format_timestamp(meta.mtime()));
18227            output_element("ctime", &format_timestamp(meta.ctime()));
18228            output_element("blksize", &meta.blksize().to_string());
18229            output_element("blocks", &meta.blocks().to_string());
18230
18231            if show_link && meta.file_type().is_symlink() {
18232                if let Ok(target) = std::fs::read_link(file) {
18233                    output_element("link", &target.to_string_lossy());
18234                }
18235            }
18236        }
18237
18238        0
18239    }
18240
18241    /// strftime - format date/time (zsh/datetime module)
18242    fn builtin_strftime(&self, args: &[String]) -> i32 {
18243        let mut format = "%c".to_string();
18244        let mut timestamp: Option<i64> = None;
18245        let mut to_var = false;
18246        let mut var_name = String::new();
18247
18248        let mut iter = args.iter();
18249        while let Some(arg) = iter.next() {
18250            match arg.as_str() {
18251                "-s" => {
18252                    to_var = true;
18253                    if let Some(name) = iter.next() {
18254                        var_name = name.clone();
18255                    }
18256                }
18257                "-r" => {
18258                    // Reference time from a variable
18259                    if let Some(ts_str) = iter.next() {
18260                        timestamp = ts_str.parse().ok();
18261                    }
18262                }
18263                s if !s.starts_with('-') => {
18264                    if format == "%c" {
18265                        format = s.to_string();
18266                    } else if timestamp.is_none() {
18267                        timestamp = s.parse().ok();
18268                    }
18269                }
18270                _ => {}
18271            }
18272        }
18273
18274        let ts = timestamp.unwrap_or_else(|| chrono::Local::now().timestamp());
18275
18276        let result = chrono::DateTime::from_timestamp(ts, 0)
18277            .map(|dt: chrono::DateTime<chrono::Utc>| {
18278                dt.with_timezone(&chrono::Local).format(&format).to_string()
18279            })
18280            .unwrap_or_else(|| "invalid timestamp".to_string());
18281
18282        if to_var && !var_name.is_empty() {
18283            // Would need mutable self
18284            println!("{}={}", var_name, result);
18285        } else {
18286            println!("{}", result);
18287        }
18288
18289        0
18290    }
18291
18292    /// zsleep - sleep with fractional seconds
18293    fn builtin_zsleep(&self, args: &[String]) -> i32 {
18294        if args.is_empty() {
18295            eprintln!("zsleep: missing argument");
18296            return 1;
18297        }
18298
18299        let secs: f64 = match args[0].parse() {
18300            Ok(s) => s,
18301            Err(_) => {
18302                eprintln!("zsleep: invalid number: {}", args[0]);
18303                return 1;
18304            }
18305        };
18306
18307        std::thread::sleep(std::time::Duration::from_secs_f64(secs));
18308        0
18309    }
18310
18311    /// zsystem - system interface (zsh/system module)
18312    /// Ported from zsh/Src/Modules/system.c bin_zsystem() lines 805-816
18313    fn builtin_zsystem(&mut self, args: &[String]) -> i32 {
18314        if args.is_empty() {
18315            eprintln!("zsystem: subcommand expected");
18316            return 1;
18317        }
18318        match args[0].as_str() {
18319            "flock" => self.builtin_zsystem_flock(&args[1..]),
18320            "supports" => self.builtin_zsystem_supports(&args[1..]),
18321            _ => {
18322                eprintln!("zsystem: unknown subcommand: {}", args[0]);
18323                1
18324            }
18325        }
18326    }
18327
18328    /// zsystem supports - ported from system.c bin_zsystem_supports() lines 780-801
18329    fn builtin_zsystem_supports(&self, args: &[String]) -> i32 {
18330        if args.is_empty() {
18331            eprintln!("zsystem: supports: not enough arguments");
18332            return 255;
18333        }
18334        if args.len() > 1 {
18335            eprintln!("zsystem: supports: too many arguments");
18336            return 255;
18337        }
18338        match args[0].as_str() {
18339            "supports" | "flock" => 0,
18340            _ => 1,
18341        }
18342    }
18343
18344    /// zsystem flock - ported from system.c bin_zsystem_flock() lines 546-774
18345    fn builtin_zsystem_flock(&mut self, args: &[String]) -> i32 {
18346        #[cfg(unix)]
18347        {
18348            use std::os::unix::io::AsRawFd;
18349
18350            let mut cloexec = true;
18351            let mut readlock = false;
18352            let mut timeout: Option<f64> = None;
18353            let mut fdvar: Option<String> = None;
18354            let mut file: Option<&str> = None;
18355
18356            let mut i = 0;
18357            while i < args.len() {
18358                let arg = &args[i];
18359                if arg == "--" {
18360                    i += 1;
18361                    if i < args.len() {
18362                        file = Some(&args[i]);
18363                    }
18364                    break;
18365                }
18366                if !arg.starts_with('-') {
18367                    file = Some(arg);
18368                    break;
18369                }
18370                let mut chars = arg[1..].chars().peekable();
18371                while let Some(c) = chars.next() {
18372                    match c {
18373                        'e' => cloexec = false,
18374                        'r' => readlock = true,
18375                        'u' => return 0,
18376                        'f' => {
18377                            let rest: String = chars.collect();
18378                            if !rest.is_empty() {
18379                                fdvar = Some(rest);
18380                            } else {
18381                                i += 1;
18382                                if i < args.len() {
18383                                    fdvar = Some(args[i].clone());
18384                                } else {
18385                                    eprintln!("zsystem: flock: option f requires a variable name");
18386                                    return 1;
18387                                }
18388                            }
18389                            break;
18390                        }
18391                        't' => {
18392                            let rest: String = chars.collect();
18393                            let val = if !rest.is_empty() {
18394                                rest
18395                            } else {
18396                                i += 1;
18397                                if i < args.len() {
18398                                    args[i].clone()
18399                                } else {
18400                                    eprintln!(
18401                                        "zsystem: flock: option t requires a numeric timeout"
18402                                    );
18403                                    return 1;
18404                                }
18405                            };
18406                            match val.parse::<f64>() {
18407                                Ok(t) => timeout = Some(t),
18408                                Err(_) => {
18409                                    eprintln!("zsystem: flock: invalid timeout value: '{}'", val);
18410                                    return 1;
18411                                }
18412                            }
18413                            break;
18414                        }
18415                        'i' => {
18416                            let rest: String = chars.collect();
18417                            if rest.is_empty() {
18418                                i += 1;
18419                                if i >= args.len() {
18420                                    eprintln!("zsystem: flock: option i requires a numeric retry interval");
18421                                    return 1;
18422                                }
18423                            }
18424                            break;
18425                        }
18426                        _ => {
18427                            eprintln!("zsystem: flock: unknown option: -{}", c);
18428                            return 1;
18429                        }
18430                    }
18431                }
18432                i += 1;
18433            }
18434
18435            let filepath = match file {
18436                Some(f) => f,
18437                None => {
18438                    eprintln!("zsystem: flock: not enough arguments");
18439                    return 1;
18440                }
18441            };
18442
18443            use std::fs::OpenOptions;
18444            let file_handle = match OpenOptions::new()
18445                .read(true)
18446                .write(!readlock)
18447                .create(true)
18448                .truncate(false)
18449                .open(filepath)
18450            {
18451                Ok(f) => f,
18452                Err(e) => {
18453                    eprintln!("zsystem: flock: {}: {}", filepath, e);
18454                    return 1;
18455                }
18456            };
18457
18458            let lock_type = if readlock {
18459                libc::F_RDLCK as i16
18460            } else {
18461                libc::F_WRLCK as i16
18462            };
18463
18464            let mut flock = libc::flock {
18465                l_type: lock_type,
18466                l_whence: libc::SEEK_SET as i16,
18467                l_start: 0,
18468                l_len: 0,
18469                l_pid: 0,
18470            };
18471
18472            let cmd = if timeout.is_some() {
18473                libc::F_SETLK
18474            } else {
18475                libc::F_SETLKW
18476            };
18477            let start = std::time::Instant::now();
18478            let timeout_duration = timeout.map(|t| std::time::Duration::from_secs_f64(t));
18479
18480            loop {
18481                let ret = unsafe { libc::fcntl(file_handle.as_raw_fd(), cmd, &mut flock) };
18482                if ret == 0 {
18483                    if let Some(ref var) = fdvar {
18484                        let fd = file_handle.as_raw_fd();
18485                        std::mem::forget(file_handle);
18486                        self.variables.insert(var.clone(), fd.to_string());
18487                    } else {
18488                        std::mem::forget(file_handle);
18489                    }
18490                    let _ = cloexec;
18491                    return 0;
18492                }
18493                let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
18494                if errno != libc::EACCES && errno != libc::EAGAIN {
18495                    eprintln!(
18496                        "zsystem: flock: {}: {}",
18497                        filepath,
18498                        std::io::Error::last_os_error()
18499                    );
18500                    return 1;
18501                }
18502                if let Some(td) = timeout_duration {
18503                    if start.elapsed() >= td {
18504                        return 2;
18505                    }
18506                    std::thread::sleep(std::time::Duration::from_millis(100));
18507                } else {
18508                    eprintln!(
18509                        "zsystem: flock: {}: {}",
18510                        filepath,
18511                        std::io::Error::last_os_error()
18512                    );
18513                    return 1;
18514                }
18515            }
18516        }
18517        #[cfg(not(unix))]
18518        {
18519            eprintln!("zsystem: flock: not supported on this platform");
18520            1
18521        }
18522    }
18523
18524    /// sync - flush filesystem buffers
18525    /// Port from zsh/Src/Modules/files.c bin_sync() lines 52-57
18526    fn builtin_sync(&self, _args: &[String]) -> i32 {
18527        #[cfg(unix)]
18528        unsafe {
18529            libc::sync();
18530        }
18531        0
18532    }
18533
18534    /// mkdir - create directories
18535    /// Port from zsh/Src/Modules/files.c bin_mkdir() lines 62-111
18536    fn builtin_mkdir(&self, args: &[String]) -> i32 {
18537        let mut mode: u32 = 0o777;
18538        let mut parents = false;
18539        let mut dirs: Vec<&str> = Vec::new();
18540
18541        let mut i = 0;
18542        while i < args.len() {
18543            let arg = &args[i];
18544            if arg == "-p" {
18545                parents = true;
18546            } else if arg == "-m" && i + 1 < args.len() {
18547                i += 1;
18548                mode = u32::from_str_radix(&args[i], 8).unwrap_or(0o777);
18549            } else if arg.starts_with("-m") {
18550                mode = u32::from_str_radix(&arg[2..], 8).unwrap_or(0o777);
18551            } else if !arg.starts_with('-') || arg == "-" || arg == "--" {
18552                if arg == "--" {
18553                    dirs.extend(args[i + 1..].iter().map(|s| s.as_str()));
18554                    break;
18555                }
18556                dirs.push(arg);
18557            }
18558            i += 1;
18559        }
18560
18561        let mut err = 0;
18562        for dir in dirs {
18563            let path = std::path::Path::new(dir);
18564            let result = if parents {
18565                std::fs::create_dir_all(path)
18566            } else {
18567                std::fs::create_dir(path)
18568            };
18569            if let Err(e) = result {
18570                eprintln!("mkdir: cannot create directory '{}': {}", dir, e);
18571                err = 1;
18572            } else {
18573                #[cfg(unix)]
18574                {
18575                    use std::os::unix::fs::PermissionsExt;
18576                    let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode));
18577                }
18578            }
18579        }
18580        err
18581    }
18582
18583    /// rmdir - remove directories
18584    /// Port from zsh/Src/Modules/files.c bin_rmdir() lines 149-166
18585    fn builtin_rmdir(&self, args: &[String]) -> i32 {
18586        let mut err = 0;
18587        for arg in args {
18588            if arg.starts_with('-') {
18589                continue;
18590            }
18591            if let Err(e) = std::fs::remove_dir(arg) {
18592                eprintln!("rmdir: cannot remove '{}': {}", arg, e);
18593                err = 1;
18594            }
18595        }
18596        err
18597    }
18598
18599    /// ln - create links
18600    /// Port from zsh/Src/Modules/files.c bin_ln() lines 200-294
18601    fn builtin_ln(&self, args: &[String]) -> i32 {
18602        let mut symbolic = false;
18603        let mut force = false;
18604        let mut no_deref = false;
18605        let mut files: Vec<&str> = Vec::new();
18606
18607        for arg in args {
18608            match arg.as_str() {
18609                "-s" => symbolic = true,
18610                "-f" => force = true,
18611                "-n" | "-h" => no_deref = true,
18612                s if !s.starts_with('-') => files.push(s),
18613                _ => {}
18614            }
18615        }
18616
18617        if files.len() < 2 {
18618            if files.len() == 1 {
18619                let src = files[0];
18620                let target = std::path::Path::new(src)
18621                    .file_name()
18622                    .map(|n| n.to_string_lossy().to_string())
18623                    .unwrap_or_else(|| src.to_string());
18624                files.push(Box::leak(target.into_boxed_str()));
18625            } else {
18626                eprintln!("ln: missing file operand");
18627                return 1;
18628            }
18629        }
18630
18631        let target = files.pop().unwrap();
18632        let target_path = std::path::Path::new(target);
18633        let is_dir = !no_deref && target_path.is_dir();
18634
18635        for src in files {
18636            let dest = if is_dir {
18637                format!(
18638                    "{}/{}",
18639                    target,
18640                    std::path::Path::new(src)
18641                        .file_name()
18642                        .map(|n| n.to_string_lossy().to_string())
18643                        .unwrap_or_else(|| src.to_string())
18644                )
18645            } else {
18646                target.to_string()
18647            };
18648
18649            let dest_path = std::path::Path::new(&dest);
18650            if force && dest_path.exists() {
18651                let _ = std::fs::remove_file(&dest);
18652            }
18653
18654            let result = if symbolic {
18655                #[cfg(unix)]
18656                {
18657                    std::os::unix::fs::symlink(src, &dest)
18658                }
18659                #[cfg(not(unix))]
18660                {
18661                    Err(std::io::Error::new(
18662                        std::io::ErrorKind::Unsupported,
18663                        "symlinks not supported",
18664                    ))
18665                }
18666            } else {
18667                std::fs::hard_link(src, &dest)
18668            };
18669
18670            if let Err(e) = result {
18671                eprintln!("ln: cannot create link '{}' -> '{}': {}", dest, src, e);
18672                return 1;
18673            }
18674        }
18675        0
18676    }
18677
18678    /// mv - move/rename files
18679    /// Port from zsh/Src/Modules/files.c bin_ln()/domove() for mv mode
18680    fn builtin_mv(&self, args: &[String]) -> i32 {
18681        let mut force = false;
18682        let mut interactive = false;
18683        let mut verbose = false;
18684        let mut files: Vec<&str> = Vec::new();
18685
18686        for arg in args {
18687            match arg.as_str() {
18688                "-f" => force = true,
18689                "-i" => interactive = true,
18690                "-v" => verbose = true,
18691                s if !s.starts_with('-') => files.push(s),
18692                _ => {}
18693            }
18694        }
18695
18696        if files.len() < 2 {
18697            eprintln!("mv: missing file operand");
18698            return 1;
18699        }
18700
18701        let target = files.pop().unwrap();
18702        let target_path = std::path::Path::new(target);
18703        let is_dir = target_path.is_dir();
18704
18705        for src in files {
18706            let dest = if is_dir {
18707                format!(
18708                    "{}/{}",
18709                    target,
18710                    std::path::Path::new(src)
18711                        .file_name()
18712                        .map(|n| n.to_string_lossy().to_string())
18713                        .unwrap_or_else(|| src.to_string())
18714                )
18715            } else {
18716                target.to_string()
18717            };
18718
18719            let dest_path = std::path::Path::new(&dest);
18720            if dest_path.exists() && !force {
18721                if interactive {
18722                    eprint!("mv: overwrite '{}'? ", dest);
18723                    let mut response = String::new();
18724                    if std::io::stdin().read_line(&mut response).is_err()
18725                        || !response.trim().eq_ignore_ascii_case("y")
18726                    {
18727                        continue;
18728                    }
18729                } else {
18730                    eprintln!("mv: cannot overwrite '{}': File exists", dest);
18731                    return 1;
18732                }
18733            }
18734
18735            if let Err(e) = std::fs::rename(src, &dest) {
18736                eprintln!("mv: cannot move '{}' to '{}': {}", src, dest, e);
18737                return 1;
18738            }
18739
18740            if verbose {
18741                println!("'{}' -> '{}'", src, dest);
18742            }
18743        }
18744        0
18745    }
18746
18747    /// cp - copy files
18748    /// Port from zsh/Src/Modules/files.c recursive copy functionality
18749    fn builtin_cp(&self, args: &[String]) -> i32 {
18750        let mut recursive = false;
18751        let mut force = false;
18752        let mut interactive = false;
18753        let mut preserve = false;
18754        let mut verbose = false;
18755        let mut files: Vec<&str> = Vec::new();
18756
18757        for arg in args {
18758            match arg.as_str() {
18759                "-r" | "-R" => recursive = true,
18760                "-f" => force = true,
18761                "-i" => interactive = true,
18762                "-p" => preserve = true,
18763                "-v" => verbose = true,
18764                s if !s.starts_with('-') => files.push(s),
18765                _ => {}
18766            }
18767        }
18768
18769        let _ = preserve; // unused for now
18770
18771        if files.len() < 2 {
18772            eprintln!("cp: missing file operand");
18773            return 1;
18774        }
18775
18776        let target = files.pop().unwrap();
18777        let target_path = std::path::Path::new(target);
18778        let is_dir = target_path.is_dir();
18779
18780        for src in files {
18781            let src_path = std::path::Path::new(src);
18782            let dest = if is_dir {
18783                format!(
18784                    "{}/{}",
18785                    target,
18786                    src_path
18787                        .file_name()
18788                        .map(|n| n.to_string_lossy().to_string())
18789                        .unwrap_or_else(|| src.to_string())
18790                )
18791            } else {
18792                target.to_string()
18793            };
18794
18795            let dest_path = std::path::Path::new(&dest);
18796            if dest_path.exists() && !force {
18797                if interactive {
18798                    eprint!("cp: overwrite '{}'? ", dest);
18799                    let mut response = String::new();
18800                    if std::io::stdin().read_line(&mut response).is_err()
18801                        || !response.trim().eq_ignore_ascii_case("y")
18802                    {
18803                        continue;
18804                    }
18805                }
18806            }
18807
18808            let result = if src_path.is_dir() {
18809                if recursive {
18810                    Self::copy_dir_recursive(src_path, dest_path)
18811                } else {
18812                    eprintln!("cp: -r not specified; omitting directory '{}'", src);
18813                    continue;
18814                }
18815            } else {
18816                std::fs::copy(src, &dest).map(|_| ())
18817            };
18818
18819            if let Err(e) = result {
18820                eprintln!("cp: cannot copy '{}' to '{}': {}", src, dest, e);
18821                return 1;
18822            }
18823
18824            if verbose {
18825                println!("'{}' -> '{}'", src, dest);
18826            }
18827        }
18828        0
18829    }
18830
18831    fn copy_dir_recursive(src: &std::path::Path, dest: &std::path::Path) -> std::io::Result<()> {
18832        if !dest.exists() {
18833            std::fs::create_dir_all(dest)?;
18834        }
18835        for entry in std::fs::read_dir(src)? {
18836            let entry = entry?;
18837            let file_type = entry.file_type()?;
18838            let src_path = entry.path();
18839            let dest_path = dest.join(entry.file_name());
18840
18841            if file_type.is_dir() {
18842                Self::copy_dir_recursive(&src_path, &dest_path)?;
18843            } else {
18844                std::fs::copy(&src_path, &dest_path)?;
18845            }
18846        }
18847        Ok(())
18848    }
18849
18850    /// rm - remove files
18851    fn builtin_rm(&self, args: &[String]) -> i32 {
18852        let mut recursive = false;
18853        let mut force = false;
18854        let mut interactive = false;
18855        let mut verbose = false;
18856        let mut files: Vec<&str> = Vec::new();
18857
18858        for arg in args {
18859            match arg.as_str() {
18860                "-r" | "-R" => recursive = true,
18861                "-f" => force = true,
18862                "-i" => interactive = true,
18863                "-v" => verbose = true,
18864                "-rf" | "-fr" => {
18865                    recursive = true;
18866                    force = true;
18867                }
18868                s if !s.starts_with('-') => files.push(s),
18869                _ => {}
18870            }
18871        }
18872
18873        for file in files {
18874            let path = std::path::Path::new(file);
18875
18876            if !path.exists() {
18877                if !force {
18878                    eprintln!("rm: cannot remove '{}': No such file or directory", file);
18879                    return 1;
18880                }
18881                continue;
18882            }
18883
18884            if interactive {
18885                let file_type = if path.is_dir() { "directory" } else { "file" };
18886                eprint!("rm: remove {} '{}'? ", file_type, file);
18887                let mut response = String::new();
18888                if std::io::stdin().read_line(&mut response).is_err()
18889                    || !response.trim().eq_ignore_ascii_case("y")
18890                {
18891                    continue;
18892                }
18893            }
18894
18895            let result = if path.is_dir() {
18896                if recursive {
18897                    std::fs::remove_dir_all(path)
18898                } else {
18899                    eprintln!("rm: cannot remove '{}': Is a directory", file);
18900                    return 1;
18901                }
18902            } else {
18903                std::fs::remove_file(path)
18904            };
18905
18906            if let Err(e) = result {
18907                if !force {
18908                    eprintln!("rm: cannot remove '{}': {}", file, e);
18909                    return 1;
18910                }
18911            } else if verbose {
18912                println!("removed '{}'", file);
18913            }
18914        }
18915        0
18916    }
18917
18918    /// chown - change file owner (Unix only)
18919    #[cfg(unix)]
18920    fn builtin_chown(&self, args: &[String]) -> i32 {
18921        use std::os::unix::fs::MetadataExt;
18922
18923        let mut recursive = false;
18924        let mut positional: Vec<&str> = Vec::new();
18925
18926        for arg in args {
18927            match arg.as_str() {
18928                "-R" => recursive = true,
18929                "-h" => {} // don't deference symlinks (default on most systems)
18930                s if !s.starts_with('-') => positional.push(s),
18931                _ => {}
18932            }
18933        }
18934
18935        if positional.len() < 2 {
18936            eprintln!("chown: missing operand");
18937            return 1;
18938        }
18939
18940        let owner_spec = positional[0];
18941        let files = &positional[1..];
18942
18943        // Parse owner[:group]
18944        let (user, group) = if let Some(colon_pos) = owner_spec.find(':') {
18945            (&owner_spec[..colon_pos], Some(&owner_spec[colon_pos + 1..]))
18946        } else {
18947            (owner_spec, None)
18948        };
18949
18950        let uid: u32 = if user.is_empty() {
18951            u32::MAX
18952        } else if let Ok(id) = user.parse() {
18953            id
18954        } else {
18955            // Look up user name
18956            unsafe {
18957                let c_user = std::ffi::CString::new(user).unwrap();
18958                let pw = libc::getpwnam(c_user.as_ptr());
18959                if pw.is_null() {
18960                    eprintln!("chown: invalid user: '{}'", user);
18961                    return 1;
18962                }
18963                (*pw).pw_uid
18964            }
18965        };
18966
18967        let gid: u32 = match group {
18968            Some(g) if !g.is_empty() => {
18969                if let Ok(id) = g.parse() {
18970                    id
18971                } else {
18972                    unsafe {
18973                        let c_group = std::ffi::CString::new(g).unwrap();
18974                        let gr = libc::getgrnam(c_group.as_ptr());
18975                        if gr.is_null() {
18976                            eprintln!("chown: invalid group: '{}'", g);
18977                            return 1;
18978                        }
18979                        (*gr).gr_gid
18980                    }
18981                }
18982            }
18983            _ => u32::MAX,
18984        };
18985
18986        fn do_chown(path: &std::path::Path, uid: u32, gid: u32, recursive: bool) -> i32 {
18987            let c_path = match std::ffi::CString::new(path.to_string_lossy().as_bytes()) {
18988                Ok(p) => p,
18989                Err(_) => return 1,
18990            };
18991
18992            let ret = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
18993            if ret != 0 {
18994                eprintln!(
18995                    "chown: changing ownership of '{}': {}",
18996                    path.display(),
18997                    std::io::Error::last_os_error()
18998                );
18999                return 1;
19000            }
19001
19002            if recursive && path.is_dir() {
19003                if let Ok(entries) = std::fs::read_dir(path) {
19004                    for entry in entries.flatten() {
19005                        if do_chown(&entry.path(), uid, gid, true) != 0 {
19006                            return 1;
19007                        }
19008                    }
19009                }
19010            }
19011            0
19012        }
19013
19014        for file in files {
19015            if do_chown(std::path::Path::new(file), uid, gid, recursive) != 0 {
19016                return 1;
19017            }
19018        }
19019        0
19020    }
19021
19022    #[cfg(not(unix))]
19023    fn builtin_chown(&self, _args: &[String]) -> i32 {
19024        eprintln!("chown: not supported on this platform");
19025        1
19026    }
19027
19028    /// chmod - change file permissions
19029    fn builtin_chmod(&self, args: &[String]) -> i32 {
19030        let mut recursive = false;
19031        let mut positional: Vec<&str> = Vec::new();
19032
19033        for arg in args {
19034            match arg.as_str() {
19035                "-R" => recursive = true,
19036                s if !s.starts_with('-') => positional.push(s),
19037                _ => {}
19038            }
19039        }
19040
19041        if positional.len() < 2 {
19042            eprintln!("chmod: missing operand");
19043            return 1;
19044        }
19045
19046        let mode_spec = positional[0];
19047        let files = &positional[1..];
19048
19049        // Parse mode (octal or symbolic)
19050        let mode: Option<u32> = u32::from_str_radix(mode_spec, 8).ok();
19051
19052        if mode.is_none() {
19053            // Symbolic mode not fully implemented
19054            eprintln!("chmod: symbolic mode not implemented, use octal");
19055            return 1;
19056        }
19057
19058        let mode = mode.unwrap();
19059
19060        fn do_chmod(path: &std::path::Path, mode: u32, recursive: bool) -> i32 {
19061            #[cfg(unix)]
19062            {
19063                use std::os::unix::fs::PermissionsExt;
19064                if let Err(e) =
19065                    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
19066                {
19067                    eprintln!("chmod: changing permissions of '{}': {}", path.display(), e);
19068                    return 1;
19069                }
19070
19071                if recursive && path.is_dir() {
19072                    if let Ok(entries) = std::fs::read_dir(path) {
19073                        for entry in entries.flatten() {
19074                            if do_chmod(&entry.path(), mode, true) != 0 {
19075                                return 1;
19076                            }
19077                        }
19078                    }
19079                }
19080            }
19081            #[cfg(not(unix))]
19082            {
19083                let _ = (path, mode, recursive);
19084            }
19085            0
19086        }
19087
19088        for file in files {
19089            if do_chmod(std::path::Path::new(file), mode, recursive) != 0 {
19090                return 1;
19091            }
19092        }
19093        0
19094    }
19095
19096    /// zln/zmv/zcp - file operations (zsh/files module)
19097    fn builtin_zfiles(&self, cmd: &str, args: &[String]) -> i32 {
19098        let mut force = false;
19099        let mut verbose = false;
19100        let mut files: Vec<&str> = Vec::new();
19101
19102        for arg in args {
19103            match arg.as_str() {
19104                "-f" => force = true,
19105                "-v" => verbose = true,
19106                "-i" => {} // interactive - ignored
19107                s if !s.starts_with('-') => files.push(s),
19108                _ => {}
19109            }
19110        }
19111
19112        if files.len() < 2 {
19113            eprintln!("{}: missing operand", cmd);
19114            return 1;
19115        }
19116
19117        let target = files.pop().unwrap();
19118        let target_is_dir = std::path::Path::new(target).is_dir();
19119
19120        for src in files {
19121            let dest = if target_is_dir {
19122                format!(
19123                    "{}/{}",
19124                    target,
19125                    std::path::Path::new(src)
19126                        .file_name()
19127                        .map(|n| n.to_string_lossy().to_string())
19128                        .unwrap_or_else(|| src.to_string())
19129                )
19130            } else {
19131                target.to_string()
19132            };
19133
19134            if !force && std::path::Path::new(&dest).exists() {
19135                eprintln!("{}: '{}' already exists", cmd, dest);
19136                continue;
19137            }
19138
19139            let result = match cmd {
19140                "zln" => {
19141                    #[cfg(unix)]
19142                    {
19143                        std::os::unix::fs::symlink(src, &dest)
19144                    }
19145                    #[cfg(not(unix))]
19146                    {
19147                        Err(std::io::Error::new(
19148                            std::io::ErrorKind::Unsupported,
19149                            "symlinks not supported",
19150                        ))
19151                    }
19152                }
19153                "zcp" => std::fs::copy(src, &dest).map(|_| ()),
19154                "zmv" => std::fs::rename(src, &dest),
19155                _ => Ok(()),
19156            };
19157
19158            match result {
19159                Ok(()) => {
19160                    if verbose {
19161                        println!("{} -> {}", src, dest);
19162                    }
19163                }
19164                Err(e) => {
19165                    eprintln!("{}: {}: {}", cmd, src, e);
19166                    return 1;
19167                }
19168            }
19169        }
19170
19171        0
19172    }
19173
19174    /// coproc - manage coprocesses
19175    fn builtin_coproc(&mut self, args: &[String]) -> i32 {
19176        // Basic coproc implementation
19177        if args.is_empty() {
19178            // List coprocesses
19179            println!("(no coprocesses)");
19180            return 0;
19181        }
19182
19183        // Start a coprocess
19184        let cmd = args.join(" ");
19185        match std::process::Command::new("sh")
19186            .arg("-c")
19187            .arg(&cmd)
19188            .stdin(std::process::Stdio::piped())
19189            .stdout(std::process::Stdio::piped())
19190            .spawn()
19191        {
19192            Ok(child) => {
19193                println!("[coproc] {}", child.id());
19194                0
19195            }
19196            Err(e) => {
19197                eprintln!("coproc: {}", e);
19198                1
19199            }
19200        }
19201    }
19202
19203    /// zparseopts - parse options from positional parameters
19204    fn builtin_zparseopts(&mut self, args: &[String]) -> i32 {
19205        let mut remove_parsed = false; // -D
19206        let mut keep_going = false; // -E
19207        let mut fail_on_error = false; // -F
19208        let mut keep_values = false; // -K
19209        let mut _map_names = false; // -M (TODO: implement)
19210        let mut array_name: Option<String> = None; // -a
19211        let mut assoc_name: Option<String> = None; // -A
19212        let mut specs: Vec<String> = Vec::new();
19213
19214        let mut iter = args.iter().peekable();
19215
19216        // Parse zparseopts options
19217        while let Some(arg) = iter.next() {
19218            match arg.as_str() {
19219                "-D" => remove_parsed = true,
19220                "-E" => keep_going = true,
19221                "-F" => fail_on_error = true,
19222                "-K" => keep_values = true,
19223                "-M" => _map_names = true,
19224                "-a" => {
19225                    if let Some(name) = iter.next() {
19226                        array_name = Some(name.clone());
19227                    }
19228                }
19229                "-A" => {
19230                    if let Some(name) = iter.next() {
19231                        assoc_name = Some(name.clone());
19232                    }
19233                }
19234                "-" | "--" => break,
19235                s if !s.starts_with('-') || s.contains('=') || s.contains(':') => {
19236                    specs.push(s.to_string());
19237                }
19238                _ => specs.push(arg.clone()),
19239            }
19240        }
19241
19242        // Collect remaining specs
19243        for arg in iter {
19244            specs.push(arg.clone());
19245        }
19246
19247        // Parse the specs to understand what options we're looking for
19248        #[derive(Clone)]
19249        struct OptSpec {
19250            name: String,
19251            takes_arg: bool,
19252            optional_arg: bool,
19253            #[allow(dead_code)]
19254            append: bool,
19255            target_array: Option<String>,
19256        }
19257
19258        let mut opt_specs: Vec<OptSpec> = Vec::new();
19259        for spec in &specs {
19260            let mut s = spec.as_str();
19261            let mut target = None;
19262
19263            // Check for =array at end
19264            if let Some(eq_pos) = s.rfind('=') {
19265                if !s[eq_pos + 1..].contains(':') {
19266                    target = Some(s[eq_pos + 1..].to_string());
19267                    s = &s[..eq_pos];
19268                }
19269            }
19270
19271            let append = s.ends_with('+') || s.contains("+:");
19272            let s = s.trim_end_matches('+');
19273
19274            let (name, takes_arg, optional_arg) = if s.ends_with("::") {
19275                (s.trim_end_matches(':').trim_end_matches(':'), true, true)
19276            } else if s.ends_with(':') {
19277                (s.trim_end_matches(':'), true, false)
19278            } else {
19279                (s, false, false)
19280            };
19281
19282            opt_specs.push(OptSpec {
19283                name: name.to_string(),
19284                takes_arg,
19285                optional_arg,
19286                append,
19287                target_array: target,
19288            });
19289        }
19290
19291        // Get positional parameters to parse
19292        let positionals: Vec<String> = (1..=99)
19293            .map(|i| self.get_variable(&i.to_string()))
19294            .take_while(|v| !v.is_empty())
19295            .collect();
19296
19297        // Results
19298        let mut results: Vec<(String, Option<String>)> = Vec::new();
19299        let mut i = 0;
19300        let mut parsed_count = 0;
19301
19302        while i < positionals.len() {
19303            let arg = &positionals[i];
19304
19305            if arg == "-" || arg == "--" {
19306                parsed_count = i + 1;
19307                break;
19308            }
19309
19310            if !arg.starts_with('-') {
19311                if !keep_going {
19312                    break;
19313                }
19314                i += 1;
19315                continue;
19316            }
19317
19318            // Try to match against specs
19319            let opt_name = arg.trim_start_matches('-');
19320            let mut matched = false;
19321
19322            for spec in &opt_specs {
19323                if opt_name == spec.name || opt_name.starts_with(&format!("{}=", spec.name)) {
19324                    matched = true;
19325
19326                    if spec.takes_arg {
19327                        let arg_value = if opt_name.contains('=') {
19328                            Some(opt_name.splitn(2, '=').nth(1).unwrap_or("").to_string())
19329                        } else if i + 1 < positionals.len()
19330                            && (!positionals[i + 1].starts_with('-') || spec.optional_arg)
19331                        {
19332                            i += 1;
19333                            Some(positionals[i].clone())
19334                        } else if spec.optional_arg {
19335                            None
19336                        } else if fail_on_error {
19337                            eprintln!("zparseopts: missing argument for option: {}", spec.name);
19338                            return 1;
19339                        } else {
19340                            None
19341                        };
19342                        results.push((format!("-{}", spec.name), arg_value));
19343                    } else {
19344                        results.push((format!("-{}", spec.name), None));
19345                    }
19346                    break;
19347                }
19348            }
19349
19350            if !matched && !keep_going {
19351                break;
19352            }
19353
19354            i += 1;
19355            parsed_count = i;
19356        }
19357
19358        // Store results in array
19359        if let Some(arr_name) = &array_name {
19360            let mut arr_values: Vec<String> = Vec::new();
19361            for (opt, val) in &results {
19362                arr_values.push(opt.clone());
19363                if let Some(v) = val {
19364                    arr_values.push(v.clone());
19365                }
19366            }
19367            self.arrays.insert(arr_name.clone(), arr_values);
19368        }
19369
19370        // Store in associative array
19371        if let Some(assoc) = &assoc_name {
19372            let mut map: HashMap<String, String> = HashMap::new();
19373            for (opt, val) in &results {
19374                map.insert(opt.clone(), val.clone().unwrap_or_default());
19375            }
19376            self.assoc_arrays.insert(assoc.clone(), map);
19377        }
19378
19379        // Store in per-option arrays
19380        for spec in &opt_specs {
19381            if let Some(target) = &spec.target_array {
19382                let values: Vec<String> = results
19383                    .iter()
19384                    .filter(|(opt, _)| opt.trim_start_matches('-') == spec.name)
19385                    .flat_map(|(opt, val)| {
19386                        let mut v = vec![opt.clone()];
19387                        if let Some(arg) = val {
19388                            v.push(arg.clone());
19389                        }
19390                        v
19391                    })
19392                    .collect();
19393                if !values.is_empty() || !keep_values {
19394                    self.arrays.insert(target.clone(), values);
19395                }
19396            }
19397        }
19398
19399        // Remove parsed arguments if -D
19400        if remove_parsed && parsed_count > 0 {
19401            for i in 1..=parsed_count {
19402                self.variables.remove(&i.to_string());
19403                std::env::remove_var(i.to_string());
19404            }
19405            // Shift remaining
19406            let remaining: Vec<String> = ((parsed_count + 1)..=99)
19407                .map(|i| self.get_variable(&i.to_string()))
19408                .take_while(|v| !v.is_empty())
19409                .collect();
19410            for (i, val) in remaining.iter().enumerate() {
19411                self.variables.insert((i + 1).to_string(), val.clone());
19412            }
19413        }
19414
19415        0
19416    }
19417
19418    /// readonly - mark variables as read-only
19419    fn builtin_readonly(&mut self, args: &[String]) -> i32 {
19420        if args.is_empty() {
19421            // List readonly variables
19422            for name in &self.readonly_vars {
19423                if let Some(val) = self.variables.get(name) {
19424                    println!("readonly {}={}", name, val);
19425                }
19426            }
19427            return 0;
19428        }
19429
19430        for arg in args {
19431            if arg == "-p" {
19432                for name in &self.readonly_vars {
19433                    if let Some(val) = self.variables.get(name) {
19434                        println!("declare -r {}=\"{}\"", name, val);
19435                    }
19436                }
19437            } else if let Some(eq_pos) = arg.find('=') {
19438                let name = &arg[..eq_pos];
19439                let value = &arg[eq_pos + 1..];
19440                self.variables.insert(name.to_string(), value.to_string());
19441                self.readonly_vars.insert(name.to_string());
19442            } else {
19443                self.readonly_vars.insert(arg.clone());
19444            }
19445        }
19446        0
19447    }
19448
19449    /// unfunction - remove function definitions
19450    fn builtin_unfunction(&mut self, args: &[String]) -> i32 {
19451        for name in args {
19452            if self.functions.remove(name).is_none() {
19453                eprintln!("unfunction: no such function: {}", name);
19454            }
19455        }
19456        0
19457    }
19458
19459    /// getln - read line from buffer
19460    fn builtin_getln(&mut self, args: &[String]) -> i32 {
19461        if args.is_empty() {
19462            eprintln!("getln: missing variable name");
19463            return 1;
19464        }
19465        // Read from line buffer (simplified - just reads from stdin)
19466        let mut line = String::new();
19467        if std::io::stdin().read_line(&mut line).is_ok() {
19468            let line = line.trim_end_matches('\n');
19469            self.variables.insert(args[0].clone(), line.to_string());
19470            0
19471        } else {
19472            1
19473        }
19474    }
19475
19476    /// pushln - push line to buffer
19477    fn builtin_pushln(&mut self, args: &[String]) -> i32 {
19478        for arg in args {
19479            println!("{}", arg);
19480        }
19481        0
19482    }
19483
19484    /// bindkey - key binding management
19485    fn builtin_bindkey(&mut self, args: &[String]) -> i32 {
19486        use crate::zle::{zle, KeymapName};
19487
19488        if args.is_empty() {
19489            // List all bindings in main keymap
19490            let zle = zle();
19491            for (keys, widget) in zle
19492                .keymaps
19493                .get(&KeymapName::Main)
19494                .map(|km| km.list_bindings().collect::<Vec<_>>())
19495                .unwrap_or_default()
19496            {
19497                println!("\"{}\" {}", keys, widget);
19498            }
19499            return 0;
19500        }
19501
19502        let mut iter = args.iter().peekable();
19503        let mut keymap = KeymapName::Main;
19504        let mut list_mode = false;
19505        let mut list_all = false;
19506        let mut remove = false;
19507
19508        while let Some(arg) = iter.next() {
19509            match arg.as_str() {
19510                "-l" => {
19511                    list_mode = true;
19512                }
19513                "-L" => {
19514                    list_mode = true;
19515                    list_all = true;
19516                }
19517                "-la" | "-lL" => {
19518                    list_mode = true;
19519                    list_all = true;
19520                }
19521                "-M" => {
19522                    if let Some(name) = iter.next() {
19523                        if let Some(km) = KeymapName::from_str(name) {
19524                            keymap = km;
19525                        }
19526                    }
19527                }
19528                "-r" => {
19529                    remove = true;
19530                }
19531                "-A" => {
19532                    // Link keymaps - stub
19533                    return 0;
19534                }
19535                "-N" => {
19536                    // Create new keymap - stub
19537                    return 0;
19538                }
19539                "-e" => {
19540                    keymap = KeymapName::Emacs;
19541                }
19542                "-v" => {
19543                    keymap = KeymapName::ViInsert;
19544                }
19545                "-a" => {
19546                    keymap = KeymapName::ViCommand;
19547                }
19548                key if !key.starts_with('-') => {
19549                    // Key sequence - next arg is widget
19550                    if let Some(widget) = iter.next() {
19551                        let mut zle = zle();
19552                        if remove {
19553                            zle.unbind_key(keymap, key);
19554                        } else {
19555                            zle.bind_key(keymap, key, widget);
19556                        }
19557                    }
19558                    return 0;
19559                }
19560                _ => {}
19561            }
19562        }
19563
19564        if list_mode {
19565            let zle = zle();
19566            if list_all {
19567                for km_name in &[
19568                    KeymapName::Emacs,
19569                    KeymapName::ViInsert,
19570                    KeymapName::ViCommand,
19571                ] {
19572                    println!("{}", km_name.as_str());
19573                }
19574            } else {
19575                if let Some(km) = zle.keymaps.get(&keymap) {
19576                    for (keys, widget) in km.list_bindings() {
19577                        println!("bindkey \"{}\" {}", keys, widget);
19578                    }
19579                }
19580            }
19581        }
19582
19583        0
19584    }
19585
19586    /// zle - line editor control
19587    fn builtin_zle(&mut self, args: &[String]) -> i32 {
19588        use crate::zle::zle;
19589
19590        if args.is_empty() {
19591            return 0;
19592        }
19593
19594        let mut iter = args.iter().peekable();
19595
19596        while let Some(arg) = iter.next() {
19597            match arg.as_str() {
19598                "-l" => {
19599                    // List widgets
19600                    let zle = zle();
19601                    let mut widgets: Vec<&str> = zle.list_widgets();
19602                    widgets.sort();
19603                    for w in widgets {
19604                        println!("{}", w);
19605                    }
19606                    return 0;
19607                }
19608                "-la" | "-lL" => {
19609                    // List all widgets with details
19610                    let zle = zle();
19611                    let mut widgets: Vec<&str> = zle.list_widgets();
19612                    widgets.sort();
19613                    for w in widgets {
19614                        println!("{}", w);
19615                    }
19616                    return 0;
19617                }
19618                "-N" => {
19619                    // Define new widget: zle -N widget-name [function]
19620                    if let Some(widget_name) = iter.next() {
19621                        let func_name = iter
19622                            .next()
19623                            .map(|s| s.as_str())
19624                            .unwrap_or(widget_name.as_str());
19625                        let mut zle = zle();
19626                        zle.define_widget(widget_name, func_name);
19627                    }
19628                    return 0;
19629                }
19630                "-D" => {
19631                    // Delete widget - stub
19632                    return 0;
19633                }
19634                "-A" => {
19635                    // Define widget alias - stub
19636                    return 0;
19637                }
19638                "-R" => {
19639                    // Redisplay
19640                    return 0;
19641                }
19642                "-U" => {
19643                    // Unget characters - stub
19644                    return 0;
19645                }
19646                "-K" => {
19647                    // Select keymap - stub
19648                    return 0;
19649                }
19650                "-F" => {
19651                    // Install file descriptor handler - stub
19652                    return 0;
19653                }
19654                "-M" => {
19655                    // Display message - stub
19656                    return 0;
19657                }
19658                "-I" => {
19659                    // Invalidate completion - stub
19660                    return 0;
19661                }
19662                "-f" => {
19663                    // Check widget exists
19664                    if let Some(name) = iter.next() {
19665                        let zle = zle();
19666                        return if zle.get_widget(name).is_some() { 0 } else { 1 };
19667                    }
19668                    return 1;
19669                }
19670                widget_name if !widget_name.starts_with('-') => {
19671                    // Call widget
19672                    let mut zle = zle();
19673                    match zle.execute_widget(widget_name, None) {
19674                        crate::zle::WidgetResult::Ok => return 0,
19675                        crate::zle::WidgetResult::Error(e) => {
19676                            eprintln!("zle: {}", e);
19677                            return 1;
19678                        }
19679                        crate::zle::WidgetResult::CallFunction(func) => {
19680                            // Would need to call shell function
19681                            drop(zle);
19682                            if let Some(f) = self.functions.get(&func).cloned() {
19683                                return self.call_function(&f, &[]).unwrap_or(1);
19684                            }
19685                            return 1;
19686                        }
19687                        _ => return 0,
19688                    }
19689                }
19690                _ => {}
19691            }
19692        }
19693
19694        0
19695    }
19696
19697    /// sched - scheduled command execution (stub)
19698    fn builtin_sched(&mut self, args: &[String]) -> i32 {
19699        use std::time::{Duration, SystemTime};
19700
19701        if args.is_empty() {
19702            // List scheduled commands
19703            if self.scheduled_commands.is_empty() {
19704                return 0;
19705            }
19706            let now = SystemTime::now();
19707            for cmd in &self.scheduled_commands {
19708                let remaining = cmd.run_at.duration_since(now).unwrap_or(Duration::ZERO);
19709                println!("{:3}  +{:5}  {}", cmd.id, remaining.as_secs(), cmd.command);
19710            }
19711            return 0;
19712        }
19713
19714        let mut i = 0;
19715        while i < args.len() {
19716            match args[i].as_str() {
19717                "-" => {
19718                    // Remove scheduled item
19719                    i += 1;
19720                    if i >= args.len() {
19721                        eprintln!("sched: -: need item number");
19722                        return 1;
19723                    }
19724                    if let Ok(id) = args[i].parse::<u32>() {
19725                        self.scheduled_commands.retain(|c| c.id != id);
19726                        return 0;
19727                    } else {
19728                        eprintln!("sched: invalid item number");
19729                        return 1;
19730                    }
19731                }
19732                "+" => {
19733                    // Schedule relative time
19734                    i += 1;
19735                    if i >= args.len() {
19736                        eprintln!("sched: +: need time");
19737                        return 1;
19738                    }
19739                    let secs: u64 = args[i].parse().unwrap_or(0);
19740                    i += 1;
19741                    let command = args[i..].join(" ");
19742
19743                    let id = self.scheduled_commands.len() as u32 + 1;
19744                    self.scheduled_commands.push(ScheduledCommand {
19745                        id,
19746                        run_at: SystemTime::now() + Duration::from_secs(secs),
19747                        command,
19748                    });
19749                    return 0;
19750                }
19751                time_str => {
19752                    // Parse HH:MM or HH:MM:SS
19753                    let parts: Vec<&str> = time_str.split(':').collect();
19754                    if parts.len() >= 2 {
19755                        let hour: u32 = parts[0].parse().unwrap_or(0);
19756                        let min: u32 = parts[1].parse().unwrap_or(0);
19757                        let sec: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
19758
19759                        // Calculate duration until that time today/tomorrow
19760                        let now = SystemTime::now();
19761                        let target_secs = (hour * 3600 + min * 60 + sec) as u64;
19762                        let _day_secs = 86400u64;
19763
19764                        // Simplified: just add as seconds from now
19765                        let run_at = now + Duration::from_secs(target_secs);
19766
19767                        i += 1;
19768                        let command = args[i..].join(" ");
19769
19770                        let id = self.scheduled_commands.len() as u32 + 1;
19771                        self.scheduled_commands.push(ScheduledCommand {
19772                            id,
19773                            run_at,
19774                            command,
19775                        });
19776                        return 0;
19777                    } else {
19778                        eprintln!("sched: invalid time format");
19779                        return 1;
19780                    }
19781                }
19782            }
19783        }
19784        0
19785    }
19786
19787    /// zcompile - compile shell scripts to ZWC format
19788    fn builtin_zcompile(&mut self, args: &[String]) -> i32 {
19789        use crate::zwc::{ZwcBuilder, ZwcFile};
19790
19791        let mut list_mode = false; // -t: list functions in zwc
19792        let mut compile_current = false; // -c: compile current functions
19793        let mut compile_auto = false; // -a: compile autoload functions
19794        let mut files: Vec<String> = Vec::new();
19795
19796        let mut i = 0;
19797        while i < args.len() {
19798            let arg = &args[i];
19799            if arg.starts_with('-') && arg.len() > 1 {
19800                for c in arg[1..].chars() {
19801                    match c {
19802                        't' => list_mode = true,
19803                        'c' => compile_current = true,
19804                        'a' => compile_auto = true,
19805                        'U' | 'M' | 'R' | 'm' | 'z' | 'k' => {} // ignored for now
19806                        _ => {
19807                            eprintln!("zcompile: unknown option: -{}", c);
19808                            return 1;
19809                        }
19810                    }
19811                }
19812            } else {
19813                files.push(arg.clone());
19814            }
19815            i += 1;
19816        }
19817
19818        if files.is_empty() {
19819            eprintln!("zcompile: not enough arguments");
19820            return 1;
19821        }
19822
19823        // -t mode: list functions in ZWC file
19824        if list_mode {
19825            let zwc_path = if files[0].ends_with(".zwc") {
19826                files[0].clone()
19827            } else {
19828                format!("{}.zwc", files[0])
19829            };
19830
19831            match ZwcFile::load(&zwc_path) {
19832                Ok(zwc) => {
19833                    println!("zwc file for zshrs-{}", env!("CARGO_PKG_VERSION"));
19834                    if files.len() > 1 {
19835                        // Check specific functions
19836                        for name in &files[1..] {
19837                            if zwc.get_function(name).is_some() {
19838                                println!("{}", name);
19839                            } else {
19840                                eprintln!("zcompile: function not found: {}", name);
19841                                return 1;
19842                            }
19843                        }
19844                    } else {
19845                        // List all functions
19846                        for name in zwc.list_functions() {
19847                            println!("{}", name);
19848                        }
19849                    }
19850                    return 0;
19851                }
19852                Err(e) => {
19853                    eprintln!("zcompile: can't read zwc file: {}: {}", zwc_path, e);
19854                    return 1;
19855                }
19856            }
19857        }
19858
19859        // -c or -a mode: compile current/autoload functions
19860        if compile_current || compile_auto {
19861            let zwc_path = if files[0].ends_with(".zwc") {
19862                files[0].clone()
19863            } else {
19864                format!("{}.zwc", files[0])
19865            };
19866
19867            let mut builder = ZwcBuilder::new();
19868
19869            if files.len() > 1 {
19870                // Compile specific functions
19871                for name in &files[1..] {
19872                    if let Some(func) = self.functions.get(name) {
19873                        // Serialize the function (simplified - just store as comment for now)
19874                        let source = format!("# Compiled function: {}\n# Body: {:?}", name, func);
19875                        builder.add_source(name, &source);
19876                    } else if compile_auto && self.autoload_pending.contains_key(name) {
19877                        // Try to load autoload function source
19878                        if let Some(path) = self.find_function_file(name) {
19879                            if let Err(e) = builder.add_file(&path) {
19880                                eprintln!("zcompile: can't read {}: {}", name, e);
19881                                return 1;
19882                            }
19883                        }
19884                    } else {
19885                        eprintln!("zcompile: no such function: {}", name);
19886                        return 1;
19887                    }
19888                }
19889            } else {
19890                // Compile all functions
19891                for (name, func) in &self.functions {
19892                    let source = format!("# Compiled function: {}\n# Body: {:?}", name, func);
19893                    builder.add_source(name, &source);
19894                }
19895            }
19896
19897            if let Err(e) = builder.write(&zwc_path) {
19898                eprintln!("zcompile: can't write {}: {}", zwc_path, e);
19899                return 1;
19900            }
19901            return 0;
19902        }
19903
19904        // Default: compile files to ZWC
19905        let zwc_path = if files[0].ends_with(".zwc") {
19906            files[0].clone()
19907        } else {
19908            format!("{}.zwc", files[0])
19909        };
19910
19911        let mut builder = ZwcBuilder::new();
19912
19913        // If only one file given, it's both the source and output base
19914        let source_files = if files.len() == 1 {
19915            // Check if it's a directory
19916            let path = std::path::Path::new(&files[0]);
19917            if path.is_dir() {
19918                // Compile all files in directory
19919                match std::fs::read_dir(path) {
19920                    Ok(entries) => {
19921                        for entry in entries.flatten() {
19922                            let p = entry.path();
19923                            if p.is_file() && !p.extension().map_or(false, |e| e == "zwc") {
19924                                if let Err(e) = builder.add_file(&p) {
19925                                    eprintln!("zcompile: can't read {:?}: {}", p, e);
19926                                }
19927                            }
19928                        }
19929                    }
19930                    Err(e) => {
19931                        eprintln!("zcompile: can't read directory: {}", e);
19932                        return 1;
19933                    }
19934                }
19935                vec![]
19936            } else {
19937                vec![files[0].clone()]
19938            }
19939        } else {
19940            files[1..].to_vec()
19941        };
19942
19943        for file in &source_files {
19944            let path = std::path::Path::new(file);
19945            if let Err(e) = builder.add_file(path) {
19946                eprintln!("zcompile: can't read {}: {}", file, e);
19947                return 1;
19948            }
19949        }
19950
19951        if let Err(e) = builder.write(&zwc_path) {
19952            eprintln!("zcompile: can't write {}: {}", zwc_path, e);
19953            return 1;
19954        }
19955
19956        0
19957    }
19958
19959    /// zformat - format strings
19960    fn builtin_zformat(&self, args: &[String]) -> i32 {
19961        if args.len() < 2 {
19962            eprintln!("zformat: not enough arguments");
19963            return 1;
19964        }
19965
19966        match args[0].as_str() {
19967            "-f" => {
19968                // Format string: zformat -f var format specs...
19969                if args.len() < 3 {
19970                    return 1;
19971                }
19972                let _var_name = &args[1];
19973                let format = &args[2];
19974                let specs: HashMap<char, &str> = args[3..]
19975                    .iter()
19976                    .filter_map(|s| {
19977                        let mut chars = s.chars();
19978                        let key = chars.next()?;
19979                        if chars.next() == Some(':') {
19980                            Some((key, &s[2..]))
19981                        } else {
19982                            None
19983                        }
19984                    })
19985                    .collect();
19986
19987                let mut result = String::new();
19988                let mut chars = format.chars().peekable();
19989                while let Some(c) = chars.next() {
19990                    if c == '%' {
19991                        if let Some(&spec_char) = chars.peek() {
19992                            if let Some(replacement) = specs.get(&spec_char) {
19993                                result.push_str(replacement);
19994                                chars.next();
19995                                continue;
19996                            }
19997                        }
19998                    }
19999                    result.push(c);
20000                }
20001                println!("{}", result);
20002            }
20003            "-a" => {
20004                // Format into array elements: zformat -a array sep specs...
20005                // Each spec is "text:value" or "text:value:cond"
20006                if args.len() < 4 {
20007                    eprintln!("zformat -a: need array, separator, and specs");
20008                    return 1;
20009                }
20010                let _array_name = &args[1];
20011                let sep = &args[2];
20012
20013                let mut results = Vec::new();
20014                for spec in &args[3..] {
20015                    let parts: Vec<&str> = spec.splitn(3, ':').collect();
20016                    if parts.len() >= 2 {
20017                        let text = parts[0];
20018                        let value = parts[1];
20019                        let cond = parts.get(2).copied();
20020
20021                        // If condition exists and is empty/false, skip
20022                        if let Some(c) = cond {
20023                            if c.is_empty() || c == "0" {
20024                                continue;
20025                            }
20026                        }
20027
20028                        if !value.is_empty() {
20029                            results.push(format!("{}{}{}", text, sep, value));
20030                        }
20031                    }
20032                }
20033
20034                for r in results {
20035                    println!("{}", r);
20036                }
20037            }
20038            _ => {
20039                eprintln!("zformat: unknown option: {}", args[0]);
20040                return 1;
20041            }
20042        }
20043        0
20044    }
20045
20046    /// vared - visually edit a variable
20047    fn builtin_vared(&mut self, args: &[String]) -> i32 {
20048        if args.is_empty() {
20049            eprintln!("vared: not enough arguments");
20050            return 1;
20051        }
20052
20053        let mut var_name = String::new();
20054        let mut prompt = String::new();
20055        let mut rprompt = String::new();
20056        let mut _history = false; // TODO: implement history completion
20057        let mut i = 0;
20058
20059        while i < args.len() {
20060            match args[i].as_str() {
20061                "-p" if i + 1 < args.len() => {
20062                    i += 1;
20063                    prompt = args[i].clone();
20064                }
20065                "-r" if i + 1 < args.len() => {
20066                    i += 1;
20067                    rprompt = args[i].clone();
20068                }
20069                "-h" => _history = true,
20070                "-c" => {} // Use completion - ignored
20071                "-e" => {} // Use emacs mode - ignored
20072                "-M" | "-m" => {
20073                    i += 1;
20074                } // Main/alt keymap - skip arg
20075                "-a" | "-A" => {
20076                    i += 1;
20077                } // Array assignment - skip arg
20078                s if !s.starts_with('-') => {
20079                    var_name = s.to_string();
20080                }
20081                _ => {}
20082            }
20083            i += 1;
20084        }
20085
20086        if var_name.is_empty() {
20087            eprintln!("vared: not enough arguments");
20088            return 1;
20089        }
20090
20091        // Get current value
20092        let current = self.get_variable(&var_name);
20093
20094        // Simple line editing using stdin
20095        if !prompt.is_empty() {
20096            eprint!("{}", prompt);
20097        }
20098        print!("{}", current);
20099        if !rprompt.is_empty() {
20100            eprint!("{}", rprompt);
20101        }
20102
20103        let mut input = String::new();
20104        if std::io::stdin().read_line(&mut input).is_ok() {
20105            let value = input.trim_end_matches('\n').to_string();
20106            self.variables.insert(var_name, value);
20107            return 0;
20108        }
20109        1
20110    }
20111
20112    /// echotc - output termcap value
20113    fn builtin_echotc(&self, args: &[String]) -> i32 {
20114        if args.is_empty() {
20115            eprintln!("echotc: not enough arguments");
20116            return 1;
20117        }
20118
20119        // Common termcap capabilities
20120        match args[0].as_str() {
20121            "cl" => print!("\x1b[H\x1b[2J"), // clear screen
20122            "cd" => print!("\x1b[J"),        // clear to end of display
20123            "ce" => print!("\x1b[K"),        // clear to end of line
20124            "cm" => {
20125                // cursor motion - needs row, col args
20126                if args.len() >= 3 {
20127                    if let (Ok(row), Ok(col)) = (args[1].parse::<u32>(), args[2].parse::<u32>()) {
20128                        print!("\x1b[{};{}H", row + 1, col + 1);
20129                    }
20130                }
20131            }
20132            "up" => print!("\x1b[A"),    // cursor up
20133            "do" => print!("\x1b[B"),    // cursor down
20134            "le" => print!("\x1b[D"),    // cursor left
20135            "nd" => print!("\x1b[C"),    // cursor right
20136            "ho" => print!("\x1b[H"),    // home cursor
20137            "vi" => print!("\x1b[?25l"), // invisible cursor
20138            "ve" => print!("\x1b[?25h"), // visible cursor
20139            "so" => print!("\x1b[7m"),   // standout mode
20140            "se" => print!("\x1b[27m"),  // end standout
20141            "us" => print!("\x1b[4m"),   // underline
20142            "ue" => print!("\x1b[24m"),  // end underline
20143            "md" => print!("\x1b[1m"),   // bold
20144            "me" => print!("\x1b[0m"),   // end all attributes
20145            "mr" => print!("\x1b[7m"),   // reverse video
20146            "AF" | "setaf" => {
20147                // Set foreground color
20148                if args.len() >= 2 {
20149                    if let Ok(color) = args[1].parse::<u32>() {
20150                        print!("\x1b[38;5;{}m", color);
20151                    }
20152                }
20153            }
20154            "AB" | "setab" => {
20155                // Set background color
20156                if args.len() >= 2 {
20157                    if let Ok(color) = args[1].parse::<u32>() {
20158                        print!("\x1b[48;5;{}m", color);
20159                    }
20160                }
20161            }
20162            "Co" | "colors" => {
20163                // Number of colors - assume 256
20164                println!("256");
20165            }
20166            "co" | "cols" => {
20167                // Number of columns
20168                println!(
20169                    "{}",
20170                    std::env::var("COLUMNS")
20171                        .ok()
20172                        .and_then(|s| s.parse().ok())
20173                        .unwrap_or(80u16)
20174                );
20175            }
20176            "li" | "lines" => {
20177                // Number of lines
20178                println!(
20179                    "{}",
20180                    std::env::var("LINES")
20181                        .ok()
20182                        .and_then(|s| s.parse().ok())
20183                        .unwrap_or(24u16)
20184                );
20185            }
20186            cap => {
20187                eprintln!("echotc: unknown capability: {}", cap);
20188                return 1;
20189            }
20190        }
20191        use std::io::Write;
20192        let _ = std::io::stdout().flush();
20193        0
20194    }
20195
20196    /// echoti - output terminfo value
20197    fn builtin_echoti(&self, args: &[String]) -> i32 {
20198        // echoti is similar to echotc but uses terminfo names
20199        // For simplicity, we'll use the same implementation
20200        self.builtin_echotc(args)
20201    }
20202
20203    /// zpty - manage pseudo-terminals
20204    fn builtin_zpty(&mut self, args: &[String]) -> i32 {
20205        use std::io::{Read, Write};
20206        use std::process::{Command, Stdio};
20207
20208        if args.is_empty() {
20209            // List all ptys
20210            if self.zptys.is_empty() {
20211                return 0;
20212            }
20213            for (name, state) in &self.zptys {
20214                println!("{}: {} (pid {})", name, state.cmd, state.pid);
20215            }
20216            return 0;
20217        }
20218
20219        let mut i = 0;
20220        while i < args.len() {
20221            match args[i].as_str() {
20222                "-d" => {
20223                    // Delete pty
20224                    i += 1;
20225                    if i >= args.len() {
20226                        eprintln!("zpty: -d requires pty name");
20227                        return 1;
20228                    }
20229                    let name = &args[i];
20230                    if let Some(mut state) = self.zptys.remove(name) {
20231                        if let Some(ref mut child) = state.child {
20232                            let _ = child.kill();
20233                        }
20234                        return 0;
20235                    } else {
20236                        eprintln!("zpty: no such pty: {}", name);
20237                        return 1;
20238                    }
20239                }
20240                "-w" => {
20241                    // Write to pty: zpty -w name string...
20242                    i += 1;
20243                    if i >= args.len() {
20244                        eprintln!("zpty: -w requires pty name");
20245                        return 1;
20246                    }
20247                    let name = args[i].clone();
20248                    i += 1;
20249                    let data = args[i..].join(" ") + "\n";
20250
20251                    if let Some(state) = self.zptys.get_mut(&name) {
20252                        if let Some(ref mut stdin) = state.stdin {
20253                            if stdin.write_all(data.as_bytes()).is_ok() {
20254                                let _ = stdin.flush();
20255                                return 0;
20256                            }
20257                        }
20258                        eprintln!("zpty: write failed");
20259                        return 1;
20260                    } else {
20261                        eprintln!("zpty: no such pty: {}", name);
20262                        return 1;
20263                    }
20264                }
20265                "-r" => {
20266                    // Read from pty: zpty -r name [param]
20267                    i += 1;
20268                    if i >= args.len() {
20269                        eprintln!("zpty: -r requires pty name");
20270                        return 1;
20271                    }
20272                    let name = args[i].clone();
20273                    i += 1;
20274                    let var_name = if i < args.len() {
20275                        args[i].clone()
20276                    } else {
20277                        "REPLY".to_string()
20278                    };
20279
20280                    if let Some(state) = self.zptys.get_mut(&name) {
20281                        if let Some(ref mut stdout) = state.stdout {
20282                            let mut buf = vec![0u8; 4096];
20283                            match stdout.read(&mut buf) {
20284                                Ok(n) => {
20285                                    let data = String::from_utf8_lossy(&buf[..n]).to_string();
20286                                    self.variables.insert(var_name, data);
20287                                    return 0;
20288                                }
20289                                Err(_) => return 1,
20290                            }
20291                        }
20292                        return 1;
20293                    } else {
20294                        eprintln!("zpty: no such pty: {}", name);
20295                        return 1;
20296                    }
20297                }
20298                "-t" => {
20299                    // Test if data available
20300                    i += 1;
20301                    if i >= args.len() {
20302                        return 1;
20303                    }
20304                    let name = &args[i];
20305                    if self.zptys.contains_key(name) {
20306                        return 0; // Assume data available if pty exists
20307                    }
20308                    return 1;
20309                }
20310                "-L" => {
20311                    // List in script-friendly format
20312                    for (name, state) in &self.zptys {
20313                        println!("zpty {} {}", name, state.cmd);
20314                    }
20315                    return 0;
20316                }
20317                "-b" | "-e" => {
20318                    // Options: -b (blocking), -e (echo)
20319                    i += 1;
20320                    continue;
20321                }
20322                name if !name.starts_with('-') => {
20323                    // Create new pty: zpty name command [args...]
20324                    i += 1;
20325                    if i >= args.len() {
20326                        eprintln!("zpty: command required");
20327                        return 1;
20328                    }
20329                    let cmd_str = args[i..].join(" ");
20330
20331                    match Command::new("sh")
20332                        .arg("-c")
20333                        .arg(&cmd_str)
20334                        .stdin(Stdio::piped())
20335                        .stdout(Stdio::piped())
20336                        .stderr(Stdio::piped())
20337                        .spawn()
20338                    {
20339                        Ok(mut child) => {
20340                            let pid = child.id();
20341                            let stdin = child.stdin.take();
20342                            let stdout = child.stdout.take();
20343
20344                            self.zptys.insert(
20345                                name.to_string(),
20346                                ZptyState {
20347                                    pid,
20348                                    cmd: cmd_str,
20349                                    stdin,
20350                                    stdout,
20351                                    child: Some(child),
20352                                },
20353                            );
20354                            return 0;
20355                        }
20356                        Err(e) => {
20357                            eprintln!("zpty: failed to start: {}", e);
20358                            return 1;
20359                        }
20360                    }
20361                }
20362                _ => {
20363                    i += 1;
20364                }
20365            }
20366            i += 1;
20367        }
20368        0
20369    }
20370
20371    /// zprof - profiling support
20372    fn builtin_zprof(&mut self, args: &[String]) -> i32 {
20373        use crate::zprof::ZprofOptions;
20374
20375        let options = ZprofOptions {
20376            clear: args.iter().any(|a| a == "-c"),
20377        };
20378
20379        let (status, output) = crate::zprof::builtin_zprof(&mut self.profiler, &options);
20380        if !output.is_empty() {
20381            print!("{}", output);
20382        }
20383        status
20384    }
20385
20386    /// zsocket - create/manage sockets
20387    fn builtin_zsocket(&mut self, args: &[String]) -> i32 {
20388        use std::os::unix::net::{UnixListener, UnixStream};
20389
20390        if args.is_empty() {
20391            // List open sockets
20392            if self.unix_sockets.is_empty() {
20393                return 0;
20394            }
20395            for (fd, state) in &self.unix_sockets {
20396                let path = state
20397                    .path
20398                    .as_ref()
20399                    .map(|p| p.display().to_string())
20400                    .unwrap_or_default();
20401                let status = if state.listening {
20402                    "listening"
20403                } else {
20404                    "connected"
20405                };
20406                println!("{}: {} ({})", fd, path, status);
20407            }
20408            return 0;
20409        }
20410
20411        let mut i = 0;
20412        let mut verbose = false;
20413        let mut var_name = "REPLY".to_string();
20414
20415        while i < args.len() {
20416            match args[i].as_str() {
20417                "-v" => {
20418                    verbose = true;
20419                    i += 1;
20420                    if i < args.len() && !args[i].starts_with('-') {
20421                        var_name = args[i].clone();
20422                    }
20423                }
20424                "-l" => {
20425                    // Listen on Unix socket: zsocket -l path
20426                    i += 1;
20427                    if i >= args.len() {
20428                        eprintln!("zsocket: -l requires path");
20429                        return 1;
20430                    }
20431                    let path = PathBuf::from(&args[i]);
20432
20433                    // Remove existing socket file
20434                    let _ = std::fs::remove_file(&path);
20435
20436                    match UnixListener::bind(&path) {
20437                        Ok(listener) => {
20438                            let fd = self.next_fd;
20439                            self.next_fd += 1;
20440
20441                            self.unix_sockets.insert(
20442                                fd,
20443                                UnixSocketState {
20444                                    path: Some(path),
20445                                    listening: true,
20446                                    stream: None,
20447                                    listener: Some(listener),
20448                                },
20449                            );
20450
20451                            if verbose {
20452                                self.variables.insert(var_name.clone(), fd.to_string());
20453                            }
20454                            println!("{}", fd);
20455                            return 0;
20456                        }
20457                        Err(e) => {
20458                            eprintln!("zsocket: bind failed: {}", e);
20459                            return 1;
20460                        }
20461                    }
20462                }
20463                "-a" => {
20464                    // Accept connection: zsocket -a fd
20465                    i += 1;
20466                    if i >= args.len() {
20467                        eprintln!("zsocket: -a requires fd");
20468                        return 1;
20469                    }
20470                    let listen_fd: i32 = args[i].parse().unwrap_or(-1);
20471
20472                    if let Some(state) = self.unix_sockets.get(&listen_fd) {
20473                        if let Some(ref listener) = state.listener {
20474                            match listener.accept() {
20475                                Ok((stream, _addr)) => {
20476                                    let new_fd = self.next_fd;
20477                                    self.next_fd += 1;
20478
20479                                    self.unix_sockets.insert(
20480                                        new_fd,
20481                                        UnixSocketState {
20482                                            path: None,
20483                                            listening: false,
20484                                            stream: Some(stream),
20485                                            listener: None,
20486                                        },
20487                                    );
20488
20489                                    if verbose {
20490                                        self.variables.insert(var_name.clone(), new_fd.to_string());
20491                                    }
20492                                    println!("{}", new_fd);
20493                                    return 0;
20494                                }
20495                                Err(e) => {
20496                                    eprintln!("zsocket: accept failed: {}", e);
20497                                    return 1;
20498                                }
20499                            }
20500                        }
20501                    }
20502                    eprintln!("zsocket: invalid fd");
20503                    return 1;
20504                }
20505                "-d" => {
20506                    // Close socket: zsocket -d fd
20507                    i += 1;
20508                    if i >= args.len() {
20509                        eprintln!("zsocket: -d requires fd");
20510                        return 1;
20511                    }
20512                    let fd: i32 = args[i].parse().unwrap_or(-1);
20513
20514                    if let Some(state) = self.unix_sockets.remove(&fd) {
20515                        if let Some(path) = state.path {
20516                            let _ = std::fs::remove_file(path);
20517                        }
20518                        return 0;
20519                    }
20520                    eprintln!("zsocket: no such fd");
20521                    return 1;
20522                }
20523                path if !path.starts_with('-') => {
20524                    // Connect to Unix socket: zsocket path
20525                    match UnixStream::connect(path) {
20526                        Ok(stream) => {
20527                            let fd = self.next_fd;
20528                            self.next_fd += 1;
20529
20530                            self.unix_sockets.insert(
20531                                fd,
20532                                UnixSocketState {
20533                                    path: Some(PathBuf::from(path)),
20534                                    listening: false,
20535                                    stream: Some(stream),
20536                                    listener: None,
20537                                },
20538                            );
20539
20540                            if verbose {
20541                                self.variables.insert(var_name.clone(), fd.to_string());
20542                            }
20543                            println!("{}", fd);
20544                            return 0;
20545                        }
20546                        Err(e) => {
20547                            eprintln!("zsocket: connect failed: {}", e);
20548                            return 1;
20549                        }
20550                    }
20551                }
20552                _ => {}
20553            }
20554            i += 1;
20555        }
20556        0
20557    }
20558
20559    /// ztcp - TCP socket operations
20560    fn builtin_ztcp(&mut self, args: &[String]) -> i32 {
20561        // Similar to zsocket but TCP specific
20562        self.builtin_zsocket(args)
20563    }
20564
20565    /// zregexparse - parse with regex
20566    fn builtin_zregexparse(&mut self, args: &[String]) -> i32 {
20567        if args.len() < 2 {
20568            eprintln!("zregexparse: usage: zregexparse var pattern [string]");
20569            return 1;
20570        }
20571
20572        let var_name = &args[0];
20573        let pattern = &args[1];
20574        let string = if args.len() > 2 {
20575            args[2].clone()
20576        } else {
20577            self.variables.get("REPLY").cloned().unwrap_or_default()
20578        };
20579
20580        match regex::Regex::new(pattern) {
20581            Ok(re) => {
20582                if let Some(captures) = re.captures(&string) {
20583                    // Store full match in var
20584                    if let Some(m) = captures.get(0) {
20585                        self.variables
20586                            .insert(var_name.clone(), m.as_str().to_string());
20587                    }
20588
20589                    // Store capture groups in MATCH array
20590                    let mut match_array = Vec::new();
20591                    let mut mbegin_array = Vec::new();
20592                    let mut mend_array = Vec::new();
20593
20594                    for (i, cap) in captures.iter().enumerate() {
20595                        if let Some(c) = cap {
20596                            match_array.push(c.as_str().to_string());
20597                            mbegin_array.push((c.start() + 1).to_string());
20598                            mend_array.push(c.end().to_string());
20599                            self.variables
20600                                .insert(format!("match[{}]", i), c.as_str().to_string());
20601                        }
20602                    }
20603                    self.arrays.insert("match".to_string(), match_array);
20604                    self.arrays.insert("mbegin".to_string(), mbegin_array);
20605                    self.arrays.insert("mend".to_string(), mend_array);
20606
20607                    // Store match positions
20608                    if let Some(m) = captures.get(0) {
20609                        self.variables
20610                            .insert("MBEGIN".to_string(), (m.start() + 1).to_string());
20611                        self.variables
20612                            .insert("MEND".to_string(), m.end().to_string());
20613                    }
20614
20615                    0
20616                } else {
20617                    1
20618                }
20619            }
20620            Err(e) => {
20621                eprintln!("zregexparse: invalid regex: {}", e);
20622                2
20623            }
20624        }
20625    }
20626
20627    /// clone - create a subshell with forked state
20628    fn builtin_clone(&mut self, args: &[String]) -> i32 {
20629        use std::process::Command;
20630
20631        // clone creates a subshell that shares the parent's state
20632        // We simulate this by spawning a new zshrs process
20633        let mut cmd =
20634            Command::new(std::env::current_exe().unwrap_or_else(|_| PathBuf::from("zshrs")));
20635
20636        if !args.is_empty() {
20637            // If args provided, run them in the subshell
20638            cmd.arg("-c").arg(args.join(" "));
20639        }
20640
20641        // Export current variables to child
20642        for (k, v) in &self.variables {
20643            cmd.env(k, v);
20644        }
20645
20646        match cmd.spawn() {
20647            Ok(mut child) => match child.wait() {
20648                Ok(status) => status.code().unwrap_or(0),
20649                Err(_) => 1,
20650            },
20651            Err(e) => {
20652                eprintln!("clone: failed to spawn subshell: {}", e);
20653                1
20654            }
20655        }
20656    }
20657
20658    /// log - same as logout for login shells
20659    fn builtin_log(&mut self, args: &[String]) -> i32 {
20660        self.builtin_exit(args)
20661    }
20662
20663    // Completion system builtins (stubs for compsys)
20664
20665    /// comparguments - parse completion arguments
20666    fn builtin_comparguments(&mut self, _args: &[String]) -> i32 {
20667        // Used internally by _arguments
20668        0
20669    }
20670
20671    /// compcall - call completion function
20672    fn builtin_compcall(&mut self, _args: &[String]) -> i32 {
20673        // Calls the completion function
20674        0
20675    }
20676
20677    /// compctl - old-style completion (deprecated)
20678    fn builtin_compctl(&mut self, args: &[String]) -> i32 {
20679        if args.is_empty() {
20680            println!("compctl: old-style completion system");
20681            println!("Use the new completion system (compsys) instead");
20682            return 0;
20683        }
20684        // Parse compctl options for backwards compatibility
20685        0
20686    }
20687
20688    /// compdescribe - describe completions
20689    fn builtin_compdescribe(&mut self, _args: &[String]) -> i32 {
20690        0
20691    }
20692
20693    /// compfiles - complete files
20694    fn builtin_compfiles(&mut self, _args: &[String]) -> i32 {
20695        0
20696    }
20697
20698    /// compgroups - group completions
20699    fn builtin_compgroups(&mut self, _args: &[String]) -> i32 {
20700        0
20701    }
20702
20703    /// compquote - quote completion strings
20704    fn builtin_compquote(&mut self, _args: &[String]) -> i32 {
20705        0
20706    }
20707
20708    /// comptags - manage completion tags
20709    fn builtin_comptags(&mut self, args: &[String]) -> i32 {
20710        if args.is_empty() {
20711            return 1;
20712        }
20713        match args[0].as_str() {
20714            "-i" => {
20715                // Initialize tags
20716                0
20717            }
20718            "-S" => {
20719                // Set tags
20720                0
20721            }
20722            _ => 1,
20723        }
20724    }
20725
20726    /// comptry - try completion
20727    fn builtin_comptry(&mut self, _args: &[String]) -> i32 {
20728        1 // No match
20729    }
20730
20731    /// compvalues - complete values
20732    fn builtin_compvalues(&mut self, _args: &[String]) -> i32 {
20733        0
20734    }
20735
20736    /// cap/getcap/setcap - Linux capabilities (stub on macOS)
20737    fn builtin_cap(&self, args: &[String]) -> i32 {
20738        // Linux capabilities are not available on macOS
20739        // On Linux, these would interact with libcap
20740        if args.is_empty() {
20741            println!("cap: display/set capabilities");
20742            println!("  getcap file...  - display capabilities");
20743            println!("  setcap caps file - set capabilities");
20744            return 0;
20745        }
20746
20747        #[cfg(target_os = "linux")]
20748        {
20749            // On Linux, we could use libcap bindings
20750            // For now, just run the external commands
20751            let status = std::process::Command::new(&args[0])
20752                .args(&args[1..])
20753                .status();
20754            return status.map(|s| s.code().unwrap_or(1)).unwrap_or(1);
20755        }
20756
20757        #[cfg(not(target_os = "linux"))]
20758        {
20759            eprintln!("cap: capabilities not supported on this platform");
20760            1
20761        }
20762    }
20763
20764    /// zcurses - curses interface (stub)
20765    fn builtin_zcurses(&mut self, args: &[String]) -> i32 {
20766        if args.is_empty() {
20767            eprintln!("zcurses: requires subcommand");
20768            return 1;
20769        }
20770
20771        match args[0].as_str() {
20772            "init" => {
20773                // Initialize curses
20774                println!("zcurses: would initialize curses");
20775                0
20776            }
20777            "end" => {
20778                // End curses mode
20779                println!("zcurses: would end curses");
20780                0
20781            }
20782            "addwin" => {
20783                // Add a window
20784                0
20785            }
20786            "delwin" => {
20787                // Delete a window
20788                0
20789            }
20790            "refresh" => {
20791                // Refresh display
20792                0
20793            }
20794            "move" => {
20795                // Move cursor
20796                0
20797            }
20798            "clear" => {
20799                // Clear window
20800                0
20801            }
20802            "char" | "string" => {
20803                // Output character/string
20804                0
20805            }
20806            "border" => {
20807                // Draw border
20808                0
20809            }
20810            "attr" => {
20811                // Set attributes
20812                0
20813            }
20814            "color" => {
20815                // Set colors
20816                0
20817            }
20818            "scroll" => {
20819                // Scroll window
20820                0
20821            }
20822            "input" => {
20823                // Get input
20824                0
20825            }
20826            "mouse" => {
20827                // Mouse support
20828                0
20829            }
20830            "querychar" => {
20831                // Query character at position
20832                0
20833            }
20834            "resize" => {
20835                // Resize window
20836                0
20837            }
20838            cmd => {
20839                eprintln!("zcurses: unknown subcommand: {}", cmd);
20840                1
20841            }
20842        }
20843    }
20844
20845    /// sysread - low-level read (zsh/system module)
20846    fn builtin_sysread(&mut self, args: &[String]) -> i32 {
20847        use std::io::Read;
20848
20849        let mut fd = 0i32; // stdin
20850        let mut count: Option<usize> = None;
20851        let mut var_name = "REPLY".to_string();
20852        let mut i = 0;
20853
20854        while i < args.len() {
20855            match args[i].as_str() {
20856                "-c" if i + 1 < args.len() => {
20857                    i += 1;
20858                    count = args[i].parse().ok();
20859                }
20860                "-i" if i + 1 < args.len() => {
20861                    i += 1;
20862                    fd = args[i].parse().unwrap_or(0);
20863                }
20864                "-o" if i + 1 < args.len() => {
20865                    i += 1;
20866                    var_name = args[i].clone();
20867                }
20868                "-t" if i + 1 < args.len() => {
20869                    i += 1;
20870                    // Timeout - ignored for now
20871                }
20872                _ => {
20873                    var_name = args[i].clone();
20874                }
20875            }
20876            i += 1;
20877        }
20878
20879        let mut buffer = vec![0u8; count.unwrap_or(8192)];
20880
20881        // Only support stdin for now
20882        if fd == 0 {
20883            match std::io::stdin().read(&mut buffer) {
20884                Ok(n) => {
20885                    buffer.truncate(n);
20886                    let s = String::from_utf8_lossy(&buffer).to_string();
20887                    self.variables.insert(var_name, s);
20888                    0
20889                }
20890                Err(_) => 1,
20891            }
20892        } else {
20893            eprintln!("sysread: only fd 0 (stdin) supported");
20894            1
20895        }
20896    }
20897
20898    /// syswrite - low-level write (zsh/system module)
20899    fn builtin_syswrite(&mut self, args: &[String]) -> i32 {
20900        use std::io::Write;
20901
20902        let mut fd = 1i32; // stdout
20903        let mut data = String::new();
20904        let mut i = 0;
20905
20906        while i < args.len() {
20907            match args[i].as_str() {
20908                "-o" if i + 1 < args.len() => {
20909                    i += 1;
20910                    fd = args[i].parse().unwrap_or(1);
20911                }
20912                "-c" if i + 1 < args.len() => {
20913                    i += 1;
20914                    // Count - ignored
20915                }
20916                _ => {
20917                    data = args[i].clone();
20918                }
20919            }
20920            i += 1;
20921        }
20922
20923        match fd {
20924            1 => {
20925                let _ = std::io::stdout().write_all(data.as_bytes());
20926                let _ = std::io::stdout().flush();
20927                0
20928            }
20929            2 => {
20930                let _ = std::io::stderr().write_all(data.as_bytes());
20931                let _ = std::io::stderr().flush();
20932                0
20933            }
20934            _ => {
20935                eprintln!("syswrite: only fd 1 (stdout) and 2 (stderr) supported");
20936                1
20937            }
20938        }
20939    }
20940
20941    /// syserror - get error message (zsh/system module)
20942    fn builtin_syserror(&self, args: &[String]) -> i32 {
20943        let errno = if args.is_empty() {
20944            // Use last errno
20945            std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
20946        } else {
20947            args[0].parse().unwrap_or(0)
20948        };
20949
20950        let err = std::io::Error::from_raw_os_error(errno);
20951        println!("{}", err);
20952        0
20953    }
20954
20955    /// sysopen - open file descriptor (zsh/system module)
20956    fn builtin_sysopen(&mut self, args: &[String]) -> i32 {
20957        use std::fs::OpenOptions;
20958
20959        let mut filename = String::new();
20960        let mut var_name = "REPLY".to_string();
20961        let mut read = false;
20962        let mut write = false;
20963        let mut append = false;
20964        let mut create = false;
20965        let mut truncate = false;
20966
20967        let mut i = 0;
20968        while i < args.len() {
20969            match args[i].as_str() {
20970                "-r" => read = true,
20971                "-w" => write = true,
20972                "-a" => append = true,
20973                "-c" => create = true,
20974                "-t" => truncate = true,
20975                "-u" => {
20976                    i += 1;
20977                    if i < args.len() {
20978                        var_name = args[i].clone();
20979                    }
20980                }
20981                "-o" => {
20982                    i += 1;
20983                    // Mode flags like O_RDONLY etc - parse as needed
20984                }
20985                s if !s.starts_with('-') => {
20986                    filename = s.to_string();
20987                }
20988                _ => {}
20989            }
20990            i += 1;
20991        }
20992
20993        if filename.is_empty() {
20994            eprintln!("sysopen: need filename");
20995            return 1;
20996        }
20997
20998        // Default to read if nothing specified
20999        if !read && !write && !append {
21000            read = true;
21001        }
21002
21003        let file = OpenOptions::new()
21004            .read(read)
21005            .write(write || append || truncate)
21006            .append(append)
21007            .create(create || write)
21008            .truncate(truncate)
21009            .open(&filename);
21010
21011        match file {
21012            Ok(f) => {
21013                let fd = self.next_fd;
21014                self.next_fd += 1;
21015                self.open_fds.insert(fd, f);
21016                self.variables.insert(var_name, fd.to_string());
21017                0
21018            }
21019            Err(e) => {
21020                eprintln!("sysopen: {}: {}", filename, e);
21021                1
21022            }
21023        }
21024    }
21025
21026    /// sysseek - seek on file descriptor (zsh/system module)
21027    fn builtin_sysseek(&mut self, args: &[String]) -> i32 {
21028        use std::io::{Seek, SeekFrom};
21029
21030        let mut fd = -1i32;
21031        let mut offset = 0i64;
21032        let mut whence = SeekFrom::Start(0);
21033
21034        let mut i = 0;
21035        while i < args.len() {
21036            match args[i].as_str() {
21037                "-u" => {
21038                    i += 1;
21039                    if i < args.len() {
21040                        fd = args[i].parse().unwrap_or(-1);
21041                    }
21042                }
21043                "-w" => {
21044                    i += 1;
21045                    if i < args.len() {
21046                        whence = match args[i].as_str() {
21047                            "start" | "set" | "0" => SeekFrom::Start(offset as u64),
21048                            "current" | "cur" | "1" => SeekFrom::Current(offset),
21049                            "end" | "2" => SeekFrom::End(offset),
21050                            _ => SeekFrom::Start(offset as u64),
21051                        };
21052                    }
21053                }
21054                s if !s.starts_with('-') => {
21055                    offset = s.parse().unwrap_or(0);
21056                }
21057                _ => {}
21058            }
21059            i += 1;
21060        }
21061
21062        if fd < 0 {
21063            eprintln!("sysseek: need fd (-u)");
21064            return 1;
21065        }
21066
21067        // Update whence with actual offset
21068        whence = match whence {
21069            SeekFrom::Start(_) => SeekFrom::Start(offset as u64),
21070            SeekFrom::Current(_) => SeekFrom::Current(offset),
21071            SeekFrom::End(_) => SeekFrom::End(offset),
21072        };
21073
21074        if let Some(file) = self.open_fds.get_mut(&fd) {
21075            match file.seek(whence) {
21076                Ok(pos) => {
21077                    self.variables.insert("REPLY".to_string(), pos.to_string());
21078                    0
21079                }
21080                Err(e) => {
21081                    eprintln!("sysseek: {}", e);
21082                    1
21083                }
21084            }
21085        } else {
21086            eprintln!("sysseek: bad fd: {}", fd);
21087            1
21088        }
21089    }
21090
21091    /// private - declare private variables (zsh/param/private module)
21092    fn builtin_private(&mut self, args: &[String]) -> i32 {
21093        // Similar to local but with stricter scoping
21094        self.builtin_local(args)
21095    }
21096
21097    /// zgetattr/zsetattr/zdelattr/zlistattr - extended attributes (zsh/attr module)
21098    /// Direct syscalls — no fork to xattr/getfattr
21099    fn builtin_zattr(&self, cmd: &str, args: &[String]) -> i32 {
21100        match cmd {
21101            "zgetattr" => {
21102                if args.len() < 2 {
21103                    eprintln!("zgetattr: need file and attribute name");
21104                    return 1;
21105                }
21106                let path = std::ffi::CString::new(args[0].as_str()).unwrap_or_default();
21107                let name = std::ffi::CString::new(args[1].as_str()).unwrap_or_default();
21108                let mut buf = vec![0u8; 4096];
21109
21110                #[cfg(target_os = "macos")]
21111                let len = unsafe {
21112                    libc::getxattr(
21113                        path.as_ptr(),
21114                        name.as_ptr(),
21115                        buf.as_mut_ptr() as *mut libc::c_void,
21116                        buf.len(),
21117                        0,
21118                        0,
21119                    )
21120                };
21121
21122                #[cfg(target_os = "linux")]
21123                let len = unsafe {
21124                    libc::getxattr(
21125                        path.as_ptr(),
21126                        name.as_ptr(),
21127                        buf.as_mut_ptr() as *mut libc::c_void,
21128                        buf.len(),
21129                    )
21130                };
21131
21132                #[cfg(not(any(target_os = "macos", target_os = "linux")))]
21133                let len: isize = -1;
21134
21135                if len >= 0 {
21136                    buf.truncate(len as usize);
21137                    println!("{}", String::from_utf8_lossy(&buf));
21138                    0
21139                } else {
21140                    eprintln!("zgetattr: {}: {}", args[0], std::io::Error::last_os_error());
21141                    1
21142                }
21143            }
21144            "zsetattr" => {
21145                if args.len() < 3 {
21146                    eprintln!("zsetattr: need file, attribute name, and value");
21147                    return 1;
21148                }
21149                let path = std::ffi::CString::new(args[0].as_str()).unwrap_or_default();
21150                let name = std::ffi::CString::new(args[1].as_str()).unwrap_or_default();
21151                let value = args[2].as_bytes();
21152
21153                #[cfg(target_os = "macos")]
21154                let ret = unsafe {
21155                    libc::setxattr(
21156                        path.as_ptr(),
21157                        name.as_ptr(),
21158                        value.as_ptr() as *const libc::c_void,
21159                        value.len(),
21160                        0,
21161                        0,
21162                    )
21163                };
21164
21165                #[cfg(target_os = "linux")]
21166                let ret = unsafe {
21167                    libc::setxattr(
21168                        path.as_ptr(),
21169                        name.as_ptr(),
21170                        value.as_ptr() as *const libc::c_void,
21171                        value.len(),
21172                        0,
21173                    )
21174                };
21175
21176                #[cfg(not(any(target_os = "macos", target_os = "linux")))]
21177                let ret: i32 = -1;
21178
21179                if ret == 0 { 0 } else {
21180                    eprintln!("zsetattr: {}: {}", args[0], std::io::Error::last_os_error());
21181                    1
21182                }
21183            }
21184            "zdelattr" => {
21185                if args.len() < 2 {
21186                    eprintln!("zdelattr: need file and attribute name");
21187                    return 1;
21188                }
21189                let path = std::ffi::CString::new(args[0].as_str()).unwrap_or_default();
21190                let name = std::ffi::CString::new(args[1].as_str()).unwrap_or_default();
21191
21192                #[cfg(target_os = "macos")]
21193                let ret = unsafe { libc::removexattr(path.as_ptr(), name.as_ptr(), 0) };
21194
21195                #[cfg(target_os = "linux")]
21196                let ret = unsafe { libc::removexattr(path.as_ptr(), name.as_ptr()) };
21197
21198                #[cfg(not(any(target_os = "macos", target_os = "linux")))]
21199                let ret: i32 = -1;
21200
21201                if ret == 0 { 0 } else {
21202                    eprintln!("zdelattr: {}: {}", args[0], std::io::Error::last_os_error());
21203                    1
21204                }
21205            }
21206            "zlistattr" => {
21207                if args.is_empty() {
21208                    eprintln!("zlistattr: need file");
21209                    return 1;
21210                }
21211                let path = std::ffi::CString::new(args[0].as_str()).unwrap_or_default();
21212                let mut buf = vec![0u8; 4096];
21213
21214                #[cfg(target_os = "macos")]
21215                let len = unsafe {
21216                    libc::listxattr(path.as_ptr(), buf.as_mut_ptr() as *mut i8, buf.len(), 0)
21217                };
21218
21219                #[cfg(target_os = "linux")]
21220                let len = unsafe {
21221                    libc::listxattr(path.as_ptr(), buf.as_mut_ptr() as *mut i8, buf.len())
21222                };
21223
21224                #[cfg(not(any(target_os = "macos", target_os = "linux")))]
21225                let len: isize = -1;
21226
21227                if len >= 0 {
21228                    buf.truncate(len as usize);
21229                    for name in buf.split(|&b| b == 0).filter(|s| !s.is_empty()) {
21230                        println!("{}", String::from_utf8_lossy(name));
21231                    }
21232                    0
21233                } else {
21234                    eprintln!("zlistattr: {}: {}", args[0], std::io::Error::last_os_error());
21235                    1
21236                }
21237            }
21238            _ => 1,
21239        }
21240    }
21241
21242    /// zftp - FTP client builtin
21243    fn builtin_zftp(&mut self, args: &[String]) -> i32 {
21244        if args.is_empty() {
21245            println!("zftp: FTP client");
21246            println!("  zftp open host [port]");
21247            println!("  zftp login [user [password]]");
21248            println!("  zftp cd dir");
21249            println!("  zftp get file [localfile]");
21250            println!("  zftp put file [remotefile]");
21251            println!("  zftp ls [dir]");
21252            println!("  zftp close");
21253            return 0;
21254        }
21255
21256        match args[0].as_str() {
21257            "open" => {
21258                if args.len() < 2 {
21259                    eprintln!("zftp open: need hostname");
21260                    return 1;
21261                }
21262                // Would connect to FTP server
21263                println!("zftp: would connect to {}", args[1]);
21264                0
21265            }
21266            "login" => {
21267                // Would authenticate
21268                println!("zftp: would login");
21269                0
21270            }
21271            "cd" => {
21272                if args.len() < 2 {
21273                    eprintln!("zftp cd: need directory");
21274                    return 1;
21275                }
21276                println!("zftp: would cd to {}", args[1]);
21277                0
21278            }
21279            "get" => {
21280                if args.len() < 2 {
21281                    eprintln!("zftp get: need filename");
21282                    return 1;
21283                }
21284                println!("zftp: would download {}", args[1]);
21285                0
21286            }
21287            "put" => {
21288                if args.len() < 2 {
21289                    eprintln!("zftp put: need filename");
21290                    return 1;
21291                }
21292                println!("zftp: would upload {}", args[1]);
21293                0
21294            }
21295            "ls" => {
21296                println!("zftp: would list directory");
21297                0
21298            }
21299            "close" | "quit" => {
21300                println!("zftp: would close connection");
21301                0
21302            }
21303            "params" => {
21304                // Display/set FTP parameters
21305                println!("ZFTP_HOST=");
21306                println!("ZFTP_PORT=21");
21307                println!("ZFTP_USER=");
21308                println!("ZFTP_PWD=");
21309                println!("ZFTP_TYPE=A");
21310                0
21311            }
21312            cmd => {
21313                eprintln!("zftp: unknown command: {}", cmd);
21314                1
21315            }
21316        }
21317    }
21318
21319    /// promptinit - initialize prompt theme system
21320    fn builtin_promptinit(&mut self, _args: &[String]) -> i32 {
21321        self.arrays.insert(
21322            "prompt_themes".to_string(),
21323            vec![
21324                "adam1".to_string(),
21325                "adam2".to_string(),
21326                "bart".to_string(),
21327                "bigfade".to_string(),
21328                "clint".to_string(),
21329                "default".to_string(),
21330                "elite".to_string(),
21331                "elite2".to_string(),
21332                "fade".to_string(),
21333                "fire".to_string(),
21334                "minimal".to_string(),
21335                "off".to_string(),
21336                "oliver".to_string(),
21337                "pws".to_string(),
21338                "redhat".to_string(),
21339                "restore".to_string(),
21340                "suse".to_string(),
21341                "walters".to_string(),
21342                "zefram".to_string(),
21343            ],
21344        );
21345        self.variables
21346            .insert("prompt_theme".to_string(), "default".to_string());
21347        0
21348    }
21349
21350    /// prompt - set or list prompt themes
21351    fn builtin_prompt(&mut self, args: &[String]) -> i32 {
21352        if args.is_empty() {
21353            let theme = self
21354                .variables
21355                .get("prompt_theme")
21356                .cloned()
21357                .unwrap_or_else(|| "default".to_string());
21358            println!("Current prompt theme: {}", theme);
21359            return 0;
21360        }
21361        match args[0].as_str() {
21362            "-l" | "--list" => {
21363                println!("Available prompt themes:");
21364                if let Some(themes) = self.arrays.get("prompt_themes") {
21365                    for theme in themes {
21366                        println!("  {}", theme);
21367                    }
21368                }
21369            }
21370            "-p" | "--preview" => {
21371                let theme = args.get(1).map(|s| s.as_str()).unwrap_or("default");
21372                self.apply_prompt_theme(theme, true);
21373            }
21374            "-h" | "--help" => {
21375                println!("prompt [options] [theme]");
21376                println!("  -l, --list     List available themes");
21377                println!("  -p, --preview  Preview a theme");
21378                println!("  -s, --setup    Set up a theme");
21379            }
21380            _ => {
21381                let theme = if args[0].starts_with('-') {
21382                    args.get(1).map(|s| s.as_str()).unwrap_or("default")
21383                } else {
21384                    args[0].as_str()
21385                };
21386                self.apply_prompt_theme(theme, false);
21387            }
21388        }
21389        0
21390    }
21391
21392    fn apply_prompt_theme(&mut self, theme: &str, preview: bool) {
21393        let (ps1, rps1) = match theme {
21394            "minimal" => ("%# ", ""),
21395            "off" => ("$ ", ""),
21396            "adam1" => (
21397                "%B%F{cyan}%n@%m %F{blue}%~%f%b %# ",
21398                "%F{yellow}%D{%H:%M}%f",
21399            ),
21400            "redhat" => ("[%n@%m %~]$ ", ""),
21401            _ => ("%n@%m %~ %# ", ""),
21402        };
21403        if preview {
21404            println!("PS1={:?}", ps1);
21405            println!("RPS1={:?}", rps1);
21406        } else {
21407            self.variables.insert("PS1".to_string(), ps1.to_string());
21408            self.variables.insert("RPS1".to_string(), rps1.to_string());
21409            self.variables
21410                .insert("prompt_theme".to_string(), theme.to_string());
21411        }
21412    }
21413
21414    /// pcre_compile - compile a PCRE pattern
21415    fn builtin_pcre_compile(&mut self, args: &[String]) -> i32 {
21416        use crate::pcre::{pcre_compile, PcreCompileOptions};
21417
21418        let mut pattern = String::new();
21419        let mut options = PcreCompileOptions::default();
21420
21421        for arg in args {
21422            match arg.as_str() {
21423                "-a" => options.anchored = true,
21424                "-i" => options.caseless = true,
21425                "-m" => options.multiline = true,
21426                "-s" => options.dotall = true,
21427                "-x" => options.extended = true,
21428                s if !s.starts_with('-') => pattern = s.to_string(),
21429                _ => {}
21430            }
21431        }
21432
21433        if pattern.is_empty() {
21434            eprintln!("pcre_compile: no pattern specified");
21435            return 1;
21436        }
21437
21438        match pcre_compile(&pattern, &options, &mut self.pcre_state) {
21439            Ok(()) => 0,
21440            Err(e) => {
21441                eprintln!("pcre_compile: {}", e);
21442                1
21443            }
21444        }
21445    }
21446
21447    /// pcre_match - match string against compiled PCRE
21448    fn builtin_pcre_match(&mut self, args: &[String]) -> i32 {
21449        use crate::pcre::{pcre_match, PcreMatchOptions};
21450
21451        let mut var_name = "MATCH".to_string();
21452        let mut array_name = "match".to_string();
21453        let mut string = String::new();
21454        let mut i = 0;
21455
21456        while i < args.len() {
21457            match args[i].as_str() {
21458                "-v" => {
21459                    i += 1;
21460                    if i < args.len() {
21461                        var_name = args[i].clone();
21462                    }
21463                }
21464                "-a" => {
21465                    i += 1;
21466                    if i < args.len() {
21467                        array_name = args[i].clone();
21468                    }
21469                }
21470                s if !s.starts_with('-') => string = s.to_string(),
21471                _ => {}
21472            }
21473            i += 1;
21474        }
21475
21476        let options = PcreMatchOptions {
21477            match_var: Some(var_name.clone()),
21478            array_var: Some(array_name.clone()),
21479            ..Default::default()
21480        };
21481
21482        match pcre_match(&string, &options, &self.pcre_state) {
21483            Ok(result) => {
21484                if result.matched {
21485                    if let Some(m) = result.full_match {
21486                        self.variables.insert(var_name, m);
21487                    }
21488                    let matches: Vec<String> =
21489                        result.captures.into_iter().filter_map(|c| c).collect();
21490                    self.arrays.insert(array_name, matches);
21491                    0
21492                } else {
21493                    1
21494                }
21495            }
21496            Err(e) => {
21497                eprintln!("pcre_match: {}", e);
21498                1
21499            }
21500        }
21501    }
21502
21503    /// pcre_study - optimize compiled PCRE (no-op in Rust regex)
21504    fn builtin_pcre_study(&mut self, _args: &[String]) -> i32 {
21505        use crate::pcre::pcre_study;
21506
21507        match pcre_study(&self.pcre_state) {
21508            Ok(()) => 0,
21509            Err(e) => {
21510                eprintln!("pcre_study: {}", e);
21511                1
21512            }
21513        }
21514    }
21515
21516    // =========================================================================
21517    // Process control functions - Port from exec.c
21518    // =========================================================================
21519
21520    /// Fork a new process
21521    /// Port of zfork() from exec.c
21522    pub fn zfork(&mut self, flags: ForkFlags) -> std::io::Result<ForkResult> {
21523        // Check for job control
21524        let can_background = self.options.get("monitor").copied().unwrap_or(false);
21525
21526        unsafe {
21527            match libc::fork() {
21528                -1 => Err(std::io::Error::last_os_error()),
21529                0 => {
21530                    // Child process
21531                    if !flags.contains(ForkFlags::NOJOB) && can_background {
21532                        // Set up job control
21533                        let pid = libc::getpid();
21534                        if flags.contains(ForkFlags::NEWGRP) {
21535                            libc::setpgid(0, 0);
21536                        }
21537                        if flags.contains(ForkFlags::FGTTY) {
21538                            libc::tcsetpgrp(0, pid);
21539                        }
21540                    }
21541
21542                    // Reset signal handlers
21543                    if !flags.contains(ForkFlags::KEEPSIGS) {
21544                        self.reset_signals();
21545                    }
21546
21547                    Ok(ForkResult::Child)
21548                }
21549                pid => {
21550                    // Parent process
21551                    if !flags.contains(ForkFlags::NOJOB) {
21552                        // Add to job table
21553                        self.add_child_process(pid);
21554                    }
21555                    Ok(ForkResult::Parent(pid))
21556                }
21557            }
21558        }
21559    }
21560
21561    /// Add a child process to tracking
21562    fn add_child_process(&mut self, pid: i32) {
21563        // Would track in job table
21564        self.variables.insert("!".to_string(), pid.to_string());
21565    }
21566
21567    /// Reset signal handlers to defaults
21568    fn reset_signals(&self) {
21569        unsafe {
21570            libc::signal(libc::SIGINT, libc::SIG_DFL);
21571            libc::signal(libc::SIGQUIT, libc::SIG_DFL);
21572            libc::signal(libc::SIGTERM, libc::SIG_DFL);
21573            libc::signal(libc::SIGTSTP, libc::SIG_DFL);
21574            libc::signal(libc::SIGTTIN, libc::SIG_DFL);
21575            libc::signal(libc::SIGTTOU, libc::SIG_DFL);
21576            libc::signal(libc::SIGCHLD, libc::SIG_DFL);
21577        }
21578    }
21579
21580    /// Execute a command in the current process (exec family)
21581    /// Port of zexecve() from exec.c
21582    pub fn zexecve(&self, cmd: &str, args: &[String]) -> ! {
21583        use std::ffi::CString;
21584        use std::os::unix::ffi::OsStrExt;
21585
21586        let c_cmd = CString::new(cmd).expect("CString::new failed");
21587
21588        // Build argv
21589        let c_args: Vec<CString> = std::iter::once(c_cmd.clone())
21590            .chain(args.iter().map(|s| CString::new(s.as_str()).unwrap()))
21591            .collect();
21592
21593        let c_argv: Vec<*const libc::c_char> = c_args
21594            .iter()
21595            .map(|s| s.as_ptr())
21596            .chain(std::iter::once(std::ptr::null()))
21597            .collect();
21598
21599        // Build envp from current environment
21600        let env_vars: Vec<CString> = std::env::vars()
21601            .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
21602            .collect();
21603
21604        let c_envp: Vec<*const libc::c_char> = env_vars
21605            .iter()
21606            .map(|s| s.as_ptr())
21607            .chain(std::iter::once(std::ptr::null()))
21608            .collect();
21609
21610        unsafe {
21611            libc::execve(c_cmd.as_ptr(), c_argv.as_ptr(), c_envp.as_ptr());
21612            // If we get here, exec failed
21613            eprintln!(
21614                "zshrs: exec failed: {}: {}",
21615                cmd,
21616                std::io::Error::last_os_error()
21617            );
21618            std::process::exit(127);
21619        }
21620    }
21621
21622    /// Enter a subshell
21623    /// Port of entersubsh() from exec.c
21624    pub fn entersubsh(&mut self, flags: SubshellFlags) {
21625        // Increment subshell level
21626        let level = self
21627            .get_variable("ZSH_SUBSHELL")
21628            .parse::<i32>()
21629            .unwrap_or(0);
21630        self.variables
21631            .insert("ZSH_SUBSHELL".to_string(), (level + 1).to_string());
21632
21633        // Handle job control
21634        if flags.contains(SubshellFlags::NOMONITOR) {
21635            self.options.insert("monitor".to_string(), false);
21636        }
21637
21638        // Close unneeded fds
21639        if !flags.contains(SubshellFlags::KEEPFDS) {
21640            self.close_extra_fds();
21641        }
21642
21643        // Reset traps
21644        if !flags.contains(SubshellFlags::KEEPTRAPS) {
21645            self.reset_traps();
21646        }
21647    }
21648
21649    /// Close extra file descriptors
21650    fn close_extra_fds(&self) {
21651        // Close fds > 10 (common shell convention)
21652        for fd in 10..256 {
21653            unsafe {
21654                libc::close(fd);
21655            }
21656        }
21657    }
21658
21659    /// Reset all traps
21660    fn reset_traps(&mut self) {
21661        self.traps.clear();
21662    }
21663
21664    /// Execute a shell function
21665    /// Port of doshfunc() from exec.c
21666    pub fn doshfunc(
21667        &mut self,
21668        name: &str,
21669        func: &ShellCommand,
21670        args: &[String],
21671    ) -> Result<i32, String> {
21672        // Save current state
21673        let old_argv = self.positional_params.clone();
21674        let old_funcstack = self.arrays.get("funcstack").cloned();
21675        let old_funcsourcetrace = self.arrays.get("funcsourcetrace").cloned();
21676
21677        // Set positional parameters to function arguments
21678        self.positional_params = args.to_vec();
21679
21680        // Update funcstack
21681        let mut funcstack = old_funcstack.clone().unwrap_or_default();
21682        funcstack.insert(0, name.to_string());
21683        self.arrays.insert("funcstack".to_string(), funcstack);
21684
21685        // Execute function body
21686        let result = self.execute_command(func);
21687
21688        // Restore state
21689        self.positional_params = old_argv;
21690        if let Some(fs) = old_funcstack {
21691            self.arrays.insert("funcstack".to_string(), fs);
21692        } else {
21693            self.arrays.remove("funcstack");
21694        }
21695        if let Some(fst) = old_funcsourcetrace {
21696            self.arrays.insert("funcsourcetrace".to_string(), fst);
21697        }
21698
21699        result
21700    }
21701
21702    /// Execute arithmetic expression
21703    /// Port of execarith() from exec.c
21704    pub fn execarith(&mut self, expr: &str) -> i32 {
21705        let result = self.eval_arith_expr(expr);
21706        if result == 0 {
21707            1
21708        } else {
21709            0
21710        }
21711    }
21712
21713    /// Execute conditional expression
21714    /// Port of execcond() from exec.c
21715    pub fn execcond(&mut self, cond: &CondExpr) -> i32 {
21716        if self.eval_cond_expr(cond) {
21717            0
21718        } else {
21719            1
21720        }
21721    }
21722
21723    /// Execute command and capture time
21724    /// Port of exectime() from exec.c
21725    pub fn exectime(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
21726        use std::time::Instant;
21727
21728        let start = Instant::now();
21729        let result = self.execute_command(cmd);
21730        let elapsed = start.elapsed();
21731
21732        // Print time in zsh format
21733        let user_time = elapsed.as_secs_f64() * 0.7; // Approximation
21734        let sys_time = elapsed.as_secs_f64() * 0.1;
21735        let real_time = elapsed.as_secs_f64();
21736
21737        eprintln!(
21738            "{:.2}s user {:.2}s system {:.0}% cpu {:.3} total",
21739            user_time,
21740            sys_time,
21741            ((user_time + sys_time) / real_time * 100.0).min(100.0),
21742            real_time
21743        );
21744
21745        result
21746    }
21747
21748    /// Find command in PATH
21749    /// Port of findcmd() from exec.c
21750    pub fn findcmd(&self, name: &str, do_hash: bool) -> Option<String> {
21751        // Check command hash table first
21752        if do_hash {
21753            if let Some(path) = self.command_hash.get(name) {
21754                if std::path::Path::new(path).exists() {
21755                    return Some(path.clone());
21756                }
21757            }
21758        }
21759
21760        // Search PATH
21761        if let Ok(path_var) = std::env::var("PATH") {
21762            for dir in path_var.split(':') {
21763                let full_path = format!("{}/{}", dir, name);
21764                if std::path::Path::new(&full_path).is_file() {
21765                    return Some(full_path);
21766                }
21767            }
21768        }
21769
21770        None
21771    }
21772
21773    /// Hash a command (add to command hash table)
21774    /// Port of hashcmd() from exec.c
21775    pub fn hashcmd(&mut self, name: &str, path: &str) {
21776        self.command_hash.insert(name.to_string(), path.to_string());
21777    }
21778
21779    /// Check if command exists and is executable
21780    /// Port of iscom() from exec.c
21781    pub fn iscom(&self, name: &str) -> bool {
21782        // Check if it's a builtin
21783        if self.is_builtin_cmd(name) {
21784            return true;
21785        }
21786
21787        // Check if it's a function
21788        if self.functions.contains_key(name) {
21789            return true;
21790        }
21791
21792        // Check if it's an alias
21793        if self.aliases.contains_key(name) {
21794            return true;
21795        }
21796
21797        // Check in PATH
21798        self.findcmd(name, true).is_some()
21799    }
21800
21801    /// Check if name is a builtin (process control version)
21802    fn is_builtin_cmd(&self, name: &str) -> bool {
21803        BUILTIN_SET.contains(name)
21804    }
21805
21806    /// Close all file descriptors except stdin/stdout/stderr
21807    /// Port of closem() from exec.c
21808    pub fn closem(&self, exceptions: &[i32]) {
21809        for fd in 3..256 {
21810            if !exceptions.contains(&fd) {
21811                unsafe {
21812                    libc::close(fd);
21813                }
21814            }
21815        }
21816    }
21817
21818    /// Create a pipe
21819    /// Port of mpipe() from exec.c
21820    pub fn mpipe(&self) -> std::io::Result<(i32, i32)> {
21821        let mut fds = [0i32; 2];
21822        let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
21823        if result == -1 {
21824            Err(std::io::Error::last_os_error())
21825        } else {
21826            Ok((fds[0], fds[1]))
21827        }
21828    }
21829
21830    /// Add a file descriptor for redirection
21831    /// Port of addfd() from exec.c
21832    pub fn addfd(&self, fd: i32, target_fd: i32, mode: RedirMode) -> std::io::Result<()> {
21833        match mode {
21834            RedirMode::Dup => {
21835                if fd != target_fd {
21836                    unsafe {
21837                        if libc::dup2(fd, target_fd) == -1 {
21838                            return Err(std::io::Error::last_os_error());
21839                        }
21840                    }
21841                }
21842            }
21843            RedirMode::Close => unsafe {
21844                libc::close(target_fd);
21845            },
21846        }
21847        Ok(())
21848    }
21849
21850    /// Get heredoc content
21851    /// Port of gethere() from exec.c
21852    pub fn gethere(&mut self, terminator: &str, strip_tabs: bool) -> String {
21853        let mut content = String::new();
21854
21855        // Would read until terminator is found
21856        // This is simplified - real impl reads from input
21857
21858        if strip_tabs {
21859            content = content
21860                .lines()
21861                .map(|line| line.trim_start_matches('\t'))
21862                .collect::<Vec<_>>()
21863                .join("\n");
21864        }
21865
21866        content
21867    }
21868
21869    /// Get herestring content
21870    /// Port of getherestr() from exec.c
21871    pub fn getherestr(&mut self, word: &str) -> String {
21872        let expanded = self.expand_string(word);
21873        format!("{}\n", expanded)
21874    }
21875
21876    /// Resolve a builtin command
21877    /// Port of resolvebuiltin() from exec.c
21878    pub fn resolvebuiltin(&self, name: &str) -> Option<BuiltinType> {
21879        if self.is_builtin_cmd(name) {
21880            Some(BuiltinType::Normal)
21881        } else {
21882            // Check disabled_builtins if we had that field
21883            None
21884        }
21885    }
21886
21887    /// Check if cd is possible
21888    /// Port of cancd() from exec.c
21889    pub fn cancd(&self, path_str: &str) -> bool {
21890        use std::os::unix::fs::PermissionsExt;
21891
21892        let path = std::path::Path::new(path_str);
21893        if !path.is_dir() {
21894            return false;
21895        }
21896
21897        if let Ok(meta) = path.metadata() {
21898            let mode = meta.permissions().mode();
21899            // Check execute permission (needed for cd)
21900            let uid = unsafe { libc::getuid() };
21901            let gid = unsafe { libc::getgid() };
21902            let file_uid = meta.uid();
21903            let file_gid = meta.gid();
21904
21905            if uid == file_uid {
21906                return (mode & 0o100) != 0;
21907            } else if gid == file_gid {
21908                return (mode & 0o010) != 0;
21909            } else {
21910                return (mode & 0o001) != 0;
21911            }
21912        }
21913
21914        false
21915    }
21916
21917    /// Command not found handler
21918    /// Port of commandnotfound() from exec.c
21919    pub fn commandnotfound(&mut self, name: &str, args: &[String]) -> i32 {
21920        // Check for command_not_found_handler function
21921        if self.functions.contains_key("command_not_found_handler") {
21922            let mut handler_args = vec![name.to_string()];
21923            handler_args.extend(args.iter().cloned());
21924
21925            if let Some(func) = self.functions.get("command_not_found_handler").cloned() {
21926                if let Ok(code) = self.doshfunc("command_not_found_handler", &func, &handler_args) {
21927                    return code;
21928                }
21929            }
21930        }
21931
21932        eprintln!("zshrs: command not found: {}", name);
21933        127
21934    }
21935
21936    // ═══════════════════════════════════════════════════════════════════════
21937    // Coreutils builtins (anti-fork) — only active when !posix_mode
21938    // ═══════════════════════════════════════════════════════════════════════
21939
21940    fn builtin_cat(&self, args: &[String]) -> i32 {
21941        use std::io::{self, Read, Write};
21942
21943        let mut show_line_numbers = false;
21944        let mut files: Vec<&str> = Vec::new();
21945
21946        for arg in args {
21947            match arg.as_str() {
21948                "-n" => show_line_numbers = true,
21949                "-" => files.push("-"),
21950                a if a.starts_with('-') => {} // ignore other flags for now
21951                _ => files.push(arg),
21952            }
21953        }
21954
21955        if files.is_empty() {
21956            files.push("-");
21957        }
21958
21959        let mut stdout = io::stdout().lock();
21960        let mut line_num = 1usize;
21961
21962        for file in files {
21963            let result: io::Result<()> = (|| {
21964                if file == "-" {
21965                    let stdin = io::stdin();
21966                    let mut handle = stdin.lock();
21967                    if show_line_numbers {
21968                        let mut buf = String::new();
21969                        handle.read_to_string(&mut buf)?;
21970                        for line in buf.lines() {
21971                            writeln!(stdout, "{:6}\t{}", line_num, line)?;
21972                            line_num += 1;
21973                        }
21974                    } else {
21975                        io::copy(&mut handle, &mut stdout)?;
21976                    }
21977                } else {
21978                    let mut f = match std::fs::File::open(file) {
21979                        Ok(f) => f,
21980                        Err(e) => {
21981                            eprintln!("cat: {}: {}", file, e);
21982                            return Err(e);
21983                        }
21984                    };
21985                    if show_line_numbers {
21986                        let mut buf = String::new();
21987                        f.read_to_string(&mut buf)?;
21988                        for line in buf.lines() {
21989                            writeln!(stdout, "{:6}\t{}", line_num, line)?;
21990                            line_num += 1;
21991                        }
21992                    } else {
21993                        io::copy(&mut f, &mut stdout)?;
21994                    }
21995                }
21996                Ok(())
21997            })();
21998
21999            if result.is_err() {
22000                return 1;
22001            }
22002        }
22003        0
22004    }
22005
22006    fn builtin_head(&self, args: &[String]) -> i32 {
22007        use std::io::{BufRead, BufReader};
22008
22009        let mut lines = 10usize;
22010        let mut files: Vec<&str> = Vec::new();
22011        let mut i = 0;
22012
22013        while i < args.len() {
22014            let arg = &args[i];
22015            if arg == "-n" && i + 1 < args.len() {
22016                i += 1;
22017                lines = args[i].parse().unwrap_or(10);
22018            } else if arg.starts_with("-n") {
22019                lines = arg[2..].parse().unwrap_or(10);
22020            } else if arg.starts_with('-') && arg.len() > 1 && arg[1..].chars().all(|c| c.is_ascii_digit()) {
22021                lines = arg[1..].parse().unwrap_or(10);
22022            } else if !arg.starts_with('-') {
22023                files.push(arg);
22024            }
22025            i += 1;
22026        }
22027
22028        if files.is_empty() {
22029            files.push("-");
22030        }
22031
22032        let show_headers = files.len() > 1;
22033
22034        for (idx, file) in files.iter().enumerate() {
22035            if show_headers {
22036                if idx > 0 {
22037                    println!();
22038                }
22039                println!("==> {} <==", file);
22040            }
22041
22042            let reader: Box<dyn BufRead> = if *file == "-" {
22043                Box::new(BufReader::new(std::io::stdin()))
22044            } else {
22045                match std::fs::File::open(file) {
22046                    Ok(f) => Box::new(BufReader::new(f)),
22047                    Err(e) => {
22048                        eprintln!("head: {}: {}", file, e);
22049                        return 1;
22050                    }
22051                }
22052            };
22053
22054            for line in reader.lines().take(lines) {
22055                match line {
22056                    Ok(l) => println!("{}", l),
22057                    Err(_) => break,
22058                }
22059            }
22060        }
22061        0
22062    }
22063
22064    fn builtin_tail(&self, args: &[String]) -> i32 {
22065        use std::collections::VecDeque;
22066        use std::io::{BufRead, BufReader};
22067
22068        let mut lines = 10usize;
22069        let mut files: Vec<&str> = Vec::new();
22070        let mut i = 0;
22071
22072        while i < args.len() {
22073            let arg = &args[i];
22074            if arg == "-n" && i + 1 < args.len() {
22075                i += 1;
22076                lines = args[i].parse().unwrap_or(10);
22077            } else if arg.starts_with("-n") {
22078                lines = arg[2..].parse().unwrap_or(10);
22079            } else if arg.starts_with('-') && arg.len() > 1 && arg[1..].chars().all(|c| c.is_ascii_digit()) {
22080                lines = arg[1..].parse().unwrap_or(10);
22081            } else if !arg.starts_with('-') || arg == "-" {
22082                files.push(arg);
22083            }
22084            i += 1;
22085        }
22086
22087        if files.is_empty() {
22088            files.push("-");
22089        }
22090
22091        let show_headers = files.len() > 1;
22092
22093        for (idx, file) in files.iter().enumerate() {
22094            if show_headers {
22095                if idx > 0 {
22096                    println!();
22097                }
22098                println!("==> {} <==", file);
22099            }
22100
22101            let reader: Box<dyn BufRead> = if *file == "-" {
22102                Box::new(BufReader::new(std::io::stdin()))
22103            } else {
22104                match std::fs::File::open(file) {
22105                    Ok(f) => Box::new(BufReader::new(f)),
22106                    Err(e) => {
22107                        eprintln!("tail: {}: {}", file, e);
22108                        return 1;
22109                    }
22110                }
22111            };
22112
22113            let mut ring: VecDeque<String> = VecDeque::with_capacity(lines);
22114            for line in reader.lines().flatten() {
22115                if ring.len() == lines {
22116                    ring.pop_front();
22117                }
22118                ring.push_back(line);
22119            }
22120            for line in ring {
22121                println!("{}", line);
22122            }
22123        }
22124        0
22125    }
22126
22127    fn builtin_wc(&self, args: &[String]) -> i32 {
22128        use std::io::{BufRead, BufReader};
22129
22130        let mut count_lines = false;
22131        let mut count_words = false;
22132        let mut count_chars = false;
22133        let mut files: Vec<&str> = Vec::new();
22134
22135        for arg in args {
22136            match arg.as_str() {
22137                "-l" => count_lines = true,
22138                "-w" => count_words = true,
22139                "-c" | "-m" => count_chars = true,
22140                a if a.starts_with('-') => {
22141                    for c in a[1..].chars() {
22142                        match c {
22143                            'l' => count_lines = true,
22144                            'w' => count_words = true,
22145                            'c' | 'm' => count_chars = true,
22146                            _ => {}
22147                        }
22148                    }
22149                }
22150                _ => files.push(arg),
22151            }
22152        }
22153
22154        if !count_lines && !count_words && !count_chars {
22155            count_lines = true;
22156            count_words = true;
22157            count_chars = true;
22158        }
22159
22160        if files.is_empty() {
22161            files.push("-");
22162        }
22163
22164        let mut total_lines = 0usize;
22165        let mut total_words = 0usize;
22166        let mut total_chars = 0usize;
22167
22168        for file in &files {
22169            let reader: Box<dyn BufRead> = if *file == "-" {
22170                Box::new(BufReader::new(std::io::stdin()))
22171            } else {
22172                match std::fs::File::open(file) {
22173                    Ok(f) => Box::new(BufReader::new(f)),
22174                    Err(e) => {
22175                        eprintln!("wc: {}: {}", file, e);
22176                        return 1;
22177                    }
22178                }
22179            };
22180
22181            let mut lines = 0usize;
22182            let mut words = 0usize;
22183            let mut chars = 0usize;
22184
22185            for line in reader.lines().flatten() {
22186                lines += 1;
22187                words += line.split_whitespace().count();
22188                chars += line.len() + 1; // +1 for newline
22189            }
22190
22191            total_lines += lines;
22192            total_words += words;
22193            total_chars += chars;
22194
22195            let mut out = String::new();
22196            if count_lines {
22197                out.push_str(&format!("{:8}", lines));
22198            }
22199            if count_words {
22200                out.push_str(&format!("{:8}", words));
22201            }
22202            if count_chars {
22203                out.push_str(&format!("{:8}", chars));
22204            }
22205            if *file != "-" {
22206                out.push_str(&format!(" {}", file));
22207            }
22208            println!("{}", out.trim_start());
22209        }
22210
22211        if files.len() > 1 {
22212            let mut out = String::new();
22213            if count_lines {
22214                out.push_str(&format!("{:8}", total_lines));
22215            }
22216            if count_words {
22217                out.push_str(&format!("{:8}", total_words));
22218            }
22219            if count_chars {
22220                out.push_str(&format!("{:8}", total_chars));
22221            }
22222            out.push_str(" total");
22223            println!("{}", out.trim_start());
22224        }
22225        0
22226    }
22227
22228    fn builtin_basename(&self, args: &[String]) -> i32 {
22229        if args.is_empty() {
22230            eprintln!("basename: missing operand");
22231            return 1;
22232        }
22233
22234        let path = &args[0];
22235        let suffix = args.get(1).map(|s| s.as_str());
22236
22237        let mut name = std::path::Path::new(path)
22238            .file_name()
22239            .map(|s| s.to_string_lossy().to_string())
22240            .unwrap_or_else(|| path.clone());
22241
22242        if let Some(suf) = suffix {
22243            if name.ends_with(suf) && name.len() > suf.len() {
22244                name.truncate(name.len() - suf.len());
22245            }
22246        }
22247
22248        println!("{}", name);
22249        0
22250    }
22251
22252    fn builtin_dirname(&self, args: &[String]) -> i32 {
22253        if args.is_empty() {
22254            eprintln!("dirname: missing operand");
22255            return 1;
22256        }
22257
22258        for path in args {
22259            let dir = std::path::Path::new(path)
22260                .parent()
22261                .map(|p| p.to_string_lossy().to_string())
22262                .unwrap_or_else(|| ".".to_string());
22263            println!("{}", if dir.is_empty() { "." } else { &dir });
22264        }
22265        0
22266    }
22267
22268    fn builtin_touch(&self, args: &[String]) -> i32 {
22269        use std::fs::OpenOptions;
22270
22271        if args.is_empty() {
22272            eprintln!("touch: missing file operand");
22273            return 1;
22274        }
22275
22276        let mut status = 0;
22277        for file in args {
22278            if file.starts_with('-') {
22279                continue; // ignore flags for now
22280            }
22281            let path = std::path::Path::new(file);
22282            if path.exists() {
22283                // Update mtime
22284                let now = std::time::SystemTime::now();
22285                if let Err(e) = filetime::set_file_mtime(path, filetime::FileTime::from_system_time(now)) {
22286                    eprintln!("touch: {}: {}", file, e);
22287                    status = 1;
22288                }
22289            } else {
22290                // Create empty file
22291                if let Err(e) = OpenOptions::new().create(true).write(true).open(path) {
22292                    eprintln!("touch: {}: {}", file, e);
22293                    status = 1;
22294                }
22295            }
22296        }
22297        status
22298    }
22299
22300    fn builtin_realpath(&self, args: &[String]) -> i32 {
22301        if args.is_empty() {
22302            eprintln!("realpath: missing operand");
22303            return 1;
22304        }
22305
22306        let mut status = 0;
22307        for path in args {
22308            if path.starts_with('-') {
22309                continue;
22310            }
22311            match std::fs::canonicalize(path) {
22312                Ok(abs) => println!("{}", abs.display()),
22313                Err(e) => {
22314                    eprintln!("realpath: {}: {}", path, e);
22315                    status = 1;
22316                }
22317            }
22318        }
22319        status
22320    }
22321
22322    fn builtin_sort(&self, args: &[String]) -> i32 {
22323        use std::io::{BufRead, BufReader};
22324
22325        let mut reverse = false;
22326        let mut numeric = false;
22327        let mut unique = false;
22328        let mut files: Vec<&str> = Vec::new();
22329
22330        for arg in args {
22331            match arg.as_str() {
22332                "-r" => reverse = true,
22333                "-n" => numeric = true,
22334                "-u" => unique = true,
22335                "-rn" | "-nr" => { reverse = true; numeric = true; }
22336                a if a.starts_with('-') => {
22337                    for c in a[1..].chars() {
22338                        match c {
22339                            'r' => reverse = true,
22340                            'n' => numeric = true,
22341                            'u' => unique = true,
22342                            _ => {}
22343                        }
22344                    }
22345                }
22346                _ => files.push(arg),
22347            }
22348        }
22349
22350        let mut lines: Vec<String> = Vec::new();
22351
22352        if files.is_empty() {
22353            let stdin = std::io::stdin();
22354            for line in stdin.lock().lines().flatten() {
22355                lines.push(line);
22356            }
22357        } else {
22358            for file in files {
22359                match std::fs::File::open(file) {
22360                    Ok(f) => {
22361                        for line in BufReader::new(f).lines().flatten() {
22362                            lines.push(line);
22363                        }
22364                    }
22365                    Err(e) => {
22366                        eprintln!("sort: {}: {}", file, e);
22367                        return 1;
22368                    }
22369                }
22370            }
22371        }
22372
22373        if numeric {
22374            lines.sort_by(|a, b| {
22375                let na: f64 = a.split_whitespace().next().and_then(|s| s.parse().ok()).unwrap_or(0.0);
22376                let nb: f64 = b.split_whitespace().next().and_then(|s| s.parse().ok()).unwrap_or(0.0);
22377                na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
22378            });
22379        } else {
22380            lines.sort();
22381        }
22382
22383        if reverse {
22384            lines.reverse();
22385        }
22386
22387        if unique {
22388            lines.dedup();
22389        }
22390
22391        for line in lines {
22392            println!("{}", line);
22393        }
22394        0
22395    }
22396
22397    fn builtin_find(&self, args: &[String]) -> i32 {
22398        use std::path::Path;
22399
22400        let mut paths: Vec<&str> = Vec::new();
22401        let mut name_pattern: Option<&str> = None;
22402        let mut type_filter: Option<char> = None;
22403        let mut i = 0;
22404
22405        while i < args.len() {
22406            let arg = &args[i];
22407            match arg.as_str() {
22408                "-name" if i + 1 < args.len() => {
22409                    i += 1;
22410                    name_pattern = Some(&args[i]);
22411                }
22412                "-type" if i + 1 < args.len() => {
22413                    i += 1;
22414                    type_filter = args[i].chars().next();
22415                }
22416                a if !a.starts_with('-') => paths.push(a),
22417                _ => {}
22418            }
22419            i += 1;
22420        }
22421
22422        if paths.is_empty() {
22423            paths.push(".");
22424        }
22425
22426        fn walk(dir: &Path, name_pat: Option<&str>, type_f: Option<char>) {
22427            if let Ok(entries) = std::fs::read_dir(dir) {
22428                for entry in entries.flatten() {
22429                    let path = entry.path();
22430                    let meta = entry.metadata().ok();
22431                    let is_dir = meta.as_ref().map(|m| m.is_dir()).unwrap_or(false);
22432                    let is_file = meta.as_ref().map(|m| m.is_file()).unwrap_or(false);
22433
22434                    let type_match = match type_f {
22435                        Some('d') => is_dir,
22436                        Some('f') => is_file,
22437                        _ => true,
22438                    };
22439
22440                    let name_match = match name_pat {
22441                        Some(pat) => {
22442                            let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
22443                            glob_match(pat, name)
22444                        }
22445                        None => true,
22446                    };
22447
22448                    if type_match && name_match {
22449                        println!("{}", path.display());
22450                    }
22451
22452                    if is_dir {
22453                        walk(&path, name_pat, type_f);
22454                    }
22455                }
22456            }
22457        }
22458
22459        fn glob_match(pattern: &str, name: &str) -> bool {
22460            if pattern == "*" { return true; }
22461            if let Some(suffix) = pattern.strip_prefix('*') {
22462                return name.ends_with(suffix);
22463            }
22464            if let Some(prefix) = pattern.strip_suffix('*') {
22465                return name.starts_with(prefix);
22466            }
22467            pattern == name
22468        }
22469
22470        for p in paths {
22471            let path = Path::new(p);
22472            if path.is_dir() {
22473                println!("{}", path.display());
22474                walk(path, name_pattern, type_filter);
22475            } else if path.exists() {
22476                println!("{}", path.display());
22477            } else {
22478                eprintln!("find: '{}': No such file or directory", p);
22479            }
22480        }
22481        0
22482    }
22483
22484    fn builtin_uniq(&self, args: &[String]) -> i32 {
22485        use std::io::{BufRead, BufReader};
22486
22487        let mut count = false;
22488        let mut repeated = false;
22489        let mut files: Vec<&str> = Vec::new();
22490
22491        for arg in args {
22492            match arg.as_str() {
22493                "-c" => count = true,
22494                "-d" => repeated = true,
22495                a if !a.starts_with('-') => files.push(a),
22496                _ => {}
22497            }
22498        }
22499
22500        let reader: Box<dyn BufRead> = if files.is_empty() || files[0] == "-" {
22501            Box::new(BufReader::new(std::io::stdin()))
22502        } else {
22503            match std::fs::File::open(files[0]) {
22504                Ok(f) => Box::new(BufReader::new(f)),
22505                Err(e) => {
22506                    eprintln!("uniq: {}: {}", files[0], e);
22507                    return 1;
22508                }
22509            }
22510        };
22511
22512        let mut prev: Option<String> = None;
22513        let mut cnt = 0usize;
22514
22515        for line in reader.lines().flatten() {
22516            if prev.as_ref() == Some(&line) {
22517                cnt += 1;
22518            } else {
22519                if let Some(p) = prev.take() {
22520                    if !repeated || cnt > 1 {
22521                        if count {
22522                            println!("{:7} {}", cnt, p);
22523                        } else {
22524                            println!("{}", p);
22525                        }
22526                    }
22527                }
22528                prev = Some(line);
22529                cnt = 1;
22530            }
22531        }
22532
22533        if let Some(p) = prev {
22534            if !repeated || cnt > 1 {
22535                if count {
22536                    println!("{:7} {}", cnt, p);
22537                } else {
22538                    println!("{}", p);
22539                }
22540            }
22541        }
22542        0
22543    }
22544
22545    fn builtin_cut(&self, args: &[String]) -> i32 {
22546        use std::io::{BufRead, BufReader};
22547
22548        let mut delimiter = '\t';
22549        let mut fields: Vec<usize> = Vec::new();
22550        let mut files: Vec<&str> = Vec::new();
22551        let mut i = 0;
22552
22553        while i < args.len() {
22554            let arg = &args[i];
22555            if arg == "-d" && i + 1 < args.len() {
22556                i += 1;
22557                delimiter = args[i].chars().next().unwrap_or('\t');
22558            } else if arg.starts_with("-d") {
22559                delimiter = arg[2..].chars().next().unwrap_or('\t');
22560            } else if arg == "-f" && i + 1 < args.len() {
22561                i += 1;
22562                for part in args[i].split(',') {
22563                    if let Ok(n) = part.parse::<usize>() {
22564                        if n > 0 { fields.push(n - 1); }
22565                    }
22566                }
22567            } else if arg.starts_with("-f") {
22568                for part in arg[2..].split(',') {
22569                    if let Ok(n) = part.parse::<usize>() {
22570                        if n > 0 { fields.push(n - 1); }
22571                    }
22572                }
22573            } else if !arg.starts_with('-') {
22574                files.push(arg);
22575            }
22576            i += 1;
22577        }
22578
22579        let reader: Box<dyn BufRead> = if files.is_empty() || files[0] == "-" {
22580            Box::new(BufReader::new(std::io::stdin()))
22581        } else {
22582            match std::fs::File::open(files[0]) {
22583                Ok(f) => Box::new(BufReader::new(f)),
22584                Err(e) => {
22585                    eprintln!("cut: {}: {}", files[0], e);
22586                    return 1;
22587                }
22588            }
22589        };
22590
22591        for line in reader.lines().flatten() {
22592            let parts: Vec<&str> = line.split(delimiter).collect();
22593            let selected: Vec<&str> = fields.iter()
22594                .filter_map(|&idx| parts.get(idx).copied())
22595                .collect();
22596            println!("{}", selected.join(&delimiter.to_string()));
22597        }
22598        0
22599    }
22600
22601    fn builtin_tr(&self, args: &[String]) -> i32 {
22602        use std::io::Read;
22603
22604        if args.len() < 2 {
22605            eprintln!("tr: missing operand");
22606            return 1;
22607        }
22608
22609        let delete = args.iter().any(|a| a == "-d");
22610        let set1: &str;
22611        let set2: &str;
22612
22613        if delete {
22614            set1 = args.iter().find(|a| !a.starts_with('-')).map(|s| s.as_str()).unwrap_or("");
22615            set2 = "";
22616        } else {
22617            let non_flag: Vec<&str> = args.iter().filter(|a| !a.starts_with('-')).map(|s| s.as_str()).collect();
22618            set1 = non_flag.first().copied().unwrap_or("");
22619            set2 = non_flag.get(1).copied().unwrap_or("");
22620        }
22621
22622        let mut input = String::new();
22623        std::io::stdin().read_to_string(&mut input).ok();
22624
22625        let output: String = if delete {
22626            input.chars().filter(|c| !set1.contains(*c)).collect()
22627        } else {
22628            let s1: Vec<char> = set1.chars().collect();
22629            let s2: Vec<char> = set2.chars().collect();
22630            input.chars().map(|c| {
22631                if let Some(pos) = s1.iter().position(|&x| x == c) {
22632                    s2.get(pos).or(s2.last()).copied().unwrap_or(c)
22633                } else {
22634                    c
22635                }
22636            }).collect()
22637        };
22638
22639        print!("{}", output);
22640        0
22641    }
22642
22643    fn builtin_seq(&self, args: &[String]) -> i32 {
22644        let nums: Vec<i64> = args.iter()
22645            .filter(|a| !a.starts_with('-') || a.parse::<i64>().is_ok())
22646            .filter_map(|a| a.parse().ok())
22647            .collect();
22648
22649        let (first, inc, last) = match nums.len() {
22650            1 => (1, 1, nums[0]),
22651            2 => (nums[0], 1, nums[1]),
22652            3 => (nums[0], nums[1], nums[2]),
22653            _ => {
22654                eprintln!("seq: missing operand");
22655                return 1;
22656            }
22657        };
22658
22659        if inc == 0 {
22660            eprintln!("seq: zero increment");
22661            return 1;
22662        }
22663
22664        let mut i = first;
22665        if inc > 0 {
22666            while i <= last {
22667                println!("{}", i);
22668                i += inc;
22669            }
22670        } else {
22671            while i >= last {
22672                println!("{}", i);
22673                i += inc;
22674            }
22675        }
22676        0
22677    }
22678
22679    fn builtin_rev(&self, args: &[String]) -> i32 {
22680        use std::io::{BufRead, BufReader};
22681
22682        let reader: Box<dyn BufRead> = if args.is_empty() || args[0] == "-" {
22683            Box::new(BufReader::new(std::io::stdin()))
22684        } else {
22685            match std::fs::File::open(&args[0]) {
22686                Ok(f) => Box::new(BufReader::new(f)),
22687                Err(e) => {
22688                    eprintln!("rev: {}: {}", args[0], e);
22689                    return 1;
22690                }
22691            }
22692        };
22693
22694        for line in reader.lines().flatten() {
22695            println!("{}", line.chars().rev().collect::<String>());
22696        }
22697        0
22698    }
22699
22700    fn builtin_tee(&self, args: &[String]) -> i32 {
22701        use std::io::{Read, Write};
22702
22703        let append = args.iter().any(|a| a == "-a");
22704        let files: Vec<&str> = args.iter()
22705            .filter(|a| !a.starts_with('-'))
22706            .map(|s| s.as_str())
22707            .collect();
22708
22709        let mut input = Vec::new();
22710        std::io::stdin().read_to_end(&mut input).ok();
22711
22712        // Write to stdout
22713        std::io::stdout().write_all(&input).ok();
22714
22715        // Write to files
22716        for file in files {
22717            let result = if append {
22718                std::fs::OpenOptions::new().create(true).append(true).open(file)
22719            } else {
22720                std::fs::File::create(file)
22721            };
22722
22723            match result {
22724                Ok(mut f) => { f.write_all(&input).ok(); }
22725                Err(e) => eprintln!("tee: {}: {}", file, e),
22726            }
22727        }
22728        0
22729    }
22730
22731    fn builtin_sleep(&self, args: &[String]) -> i32 {
22732        if args.is_empty() {
22733            eprintln!("sleep: missing operand");
22734            return 1;
22735        }
22736
22737        let mut total_secs = 0.0f64;
22738        for arg in args {
22739            if arg.starts_with('-') { continue; }
22740            let (num, suffix) = if arg.ends_with('s') {
22741                (&arg[..arg.len()-1], 1.0)
22742            } else if arg.ends_with('m') {
22743                (&arg[..arg.len()-1], 60.0)
22744            } else if arg.ends_with('h') {
22745                (&arg[..arg.len()-1], 3600.0)
22746            } else if arg.ends_with('d') {
22747                (&arg[..arg.len()-1], 86400.0)
22748            } else {
22749                (arg.as_str(), 1.0)
22750            };
22751            if let Ok(n) = num.parse::<f64>() {
22752                total_secs += n * suffix;
22753            }
22754        }
22755
22756        std::thread::sleep(std::time::Duration::from_secs_f64(total_secs));
22757        0
22758    }
22759
22760    fn builtin_whoami(&self, _args: &[String]) -> i32 {
22761        if let Ok(user) = std::env::var("USER") {
22762            println!("{}", user);
22763            0
22764        } else {
22765            let uid = unsafe { libc::getuid() };
22766            println!("{}", uid);
22767            0
22768        }
22769    }
22770
22771    fn builtin_id(&self, args: &[String]) -> i32 {
22772        let uid = unsafe { libc::getuid() };
22773        let gid = unsafe { libc::getgid() };
22774        let euid = unsafe { libc::geteuid() };
22775        let egid = unsafe { libc::getegid() };
22776
22777        if args.iter().any(|a| a == "-u") {
22778            println!("{}", uid);
22779        } else if args.iter().any(|a| a == "-g") {
22780            println!("{}", gid);
22781        } else if args.iter().any(|a| a == "-un") {
22782            if let Ok(user) = std::env::var("USER") {
22783                println!("{}", user);
22784            } else {
22785                println!("{}", uid);
22786            }
22787        } else {
22788            let user = std::env::var("USER").unwrap_or_else(|_| uid.to_string());
22789            print!("uid={}({}) gid={}", uid, user, gid);
22790            if euid != uid {
22791                print!(" euid={}", euid);
22792            }
22793            if egid != gid {
22794                print!(" egid={}", egid);
22795            }
22796            println!();
22797        }
22798        0
22799    }
22800
22801    fn builtin_hostname(&self, _args: &[String]) -> i32 {
22802        let mut buf = [0u8; 256];
22803        let result = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut i8, buf.len()) };
22804        if result == 0 {
22805            let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
22806            println!("{}", String::from_utf8_lossy(&buf[..len]));
22807            0
22808        } else {
22809            eprintln!("hostname: cannot get hostname");
22810            1
22811        }
22812    }
22813
22814    fn builtin_uname(&self, args: &[String]) -> i32 {
22815        let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
22816        if unsafe { libc::uname(&mut uts) } != 0 {
22817            eprintln!("uname: cannot get system info");
22818            return 1;
22819        }
22820
22821        let sysname = unsafe { std::ffi::CStr::from_ptr(uts.sysname.as_ptr()) }.to_string_lossy();
22822        let nodename = unsafe { std::ffi::CStr::from_ptr(uts.nodename.as_ptr()) }.to_string_lossy();
22823        let release = unsafe { std::ffi::CStr::from_ptr(uts.release.as_ptr()) }.to_string_lossy();
22824        let version = unsafe { std::ffi::CStr::from_ptr(uts.version.as_ptr()) }.to_string_lossy();
22825        let machine = unsafe { std::ffi::CStr::from_ptr(uts.machine.as_ptr()) }.to_string_lossy();
22826
22827        if args.is_empty() || args.iter().any(|a| a == "-s") {
22828            println!("{}", sysname);
22829        } else if args.iter().any(|a| a == "-a") {
22830            println!("{} {} {} {} {}", sysname, nodename, release, version, machine);
22831        } else if args.iter().any(|a| a == "-n") {
22832            println!("{}", nodename);
22833        } else if args.iter().any(|a| a == "-r") {
22834            println!("{}", release);
22835        } else if args.iter().any(|a| a == "-v") {
22836            println!("{}", version);
22837        } else if args.iter().any(|a| a == "-m") {
22838            println!("{}", machine);
22839        } else {
22840            println!("{}", sysname);
22841        }
22842        0
22843    }
22844
22845    fn builtin_date(&self, args: &[String]) -> i32 {
22846        use std::time::{SystemTime, UNIX_EPOCH};
22847
22848        let now = SystemTime::now()
22849            .duration_since(UNIX_EPOCH)
22850            .unwrap_or_default()
22851            .as_secs() as i64;
22852
22853        let mut format: Option<&str> = None;
22854        for arg in args {
22855            if arg.starts_with('+') {
22856                format = Some(&arg[1..]);
22857            }
22858        }
22859
22860        if let Some(fmt) = format {
22861            let tm = unsafe {
22862                let t = now as libc::time_t;
22863                *libc::localtime(&t)
22864            };
22865            let mut buf = [0i8; 256];
22866            let fmt_cstr = std::ffi::CString::new(fmt).unwrap_or_default();
22867            let len = unsafe {
22868                libc::strftime(buf.as_mut_ptr(), buf.len(), fmt_cstr.as_ptr(), &tm)
22869            };
22870            if len > 0 {
22871                let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
22872                println!("{}", s.to_string_lossy());
22873            }
22874        } else {
22875            let tm = unsafe {
22876                let t = now as libc::time_t;
22877                *libc::localtime(&t)
22878            };
22879            let mut buf = [0i8; 256];
22880            let fmt = std::ffi::CString::new("%a %b %e %H:%M:%S %Z %Y").unwrap();
22881            let len = unsafe {
22882                libc::strftime(buf.as_mut_ptr(), buf.len(), fmt.as_ptr(), &tm)
22883            };
22884            if len > 0 {
22885                let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
22886                println!("{}", s.to_string_lossy());
22887            }
22888        }
22889        0
22890    }
22891
22892    fn builtin_mktemp(&self, args: &[String]) -> i32 {
22893        let mut dir = false;
22894        let mut template: Option<&str> = None;
22895
22896        for arg in args {
22897            match arg.as_str() {
22898                "-d" => dir = true,
22899                a if !a.starts_with('-') => template = Some(a),
22900                _ => {}
22901            }
22902        }
22903
22904        let tmpdir = std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".to_string());
22905        let base = template.unwrap_or("tmp.XXXXXXXXXX");
22906        
22907        let rand_suffix: String = (0..10)
22908            .map(|_| {
22909                let idx = (std::time::SystemTime::now()
22910                    .duration_since(std::time::UNIX_EPOCH)
22911                    .unwrap_or_default()
22912                    .subsec_nanos() as usize) % 36;
22913                "abcdefghijklmnopqrstuvwxyz0123456789".chars().nth(idx).unwrap()
22914            })
22915            .collect();
22916
22917        let name = if base.contains("XXXXXX") {
22918            base.replace("XXXXXXXXXX", &rand_suffix)
22919                .replace("XXXXXX", &rand_suffix[..6])
22920        } else {
22921            format!("{}.{}", base, rand_suffix)
22922        };
22923
22924        let path = std::path::Path::new(&tmpdir).join(&name);
22925
22926        if dir {
22927            match std::fs::create_dir(&path) {
22928                Ok(_) => {
22929                    println!("{}", path.display());
22930                    0
22931                }
22932                Err(e) => {
22933                    eprintln!("mktemp: {}: {}", path.display(), e);
22934                    1
22935                }
22936            }
22937        } else {
22938            match std::fs::File::create(&path) {
22939                Ok(_) => {
22940                    println!("{}", path.display());
22941                    0
22942                }
22943                Err(e) => {
22944                    eprintln!("mktemp: {}: {}", path.display(), e);
22945                    1
22946                }
22947            }
22948        }
22949    }
22950}
22951
22952use std::os::unix::fs::MetadataExt;
22953
22954bitflags::bitflags! {
22955    /// Flags for zfork()
22956    #[derive(Debug, Clone, Copy, Default)]
22957    pub struct ForkFlags: u32 {
22958        const NOJOB = 1 << 0;    // Don't add to job table
22959        const NEWGRP = 1 << 1;   // Create new process group
22960        const FGTTY = 1 << 2;    // Take foreground terminal
22961        const KEEPSIGS = 1 << 3; // Keep signal handlers
22962    }
22963}
22964
22965bitflags::bitflags! {
22966    /// Flags for entersubsh()
22967    #[derive(Debug, Clone, Copy, Default)]
22968    pub struct SubshellFlags: u32 {
22969        const NOMONITOR = 1 << 0; // Disable job control
22970        const KEEPFDS = 1 << 1;   // Keep file descriptors
22971        const KEEPTRAPS = 1 << 2; // Keep trap handlers
22972    }
22973}
22974
22975/// Result of fork operation
22976#[derive(Debug)]
22977pub enum ForkResult {
22978    Parent(i32), // Contains child PID
22979    Child,
22980}
22981
22982/// Redirection mode
22983#[derive(Debug, Clone, Copy)]
22984pub enum RedirMode {
22985    Dup,
22986    Close,
22987}
22988
22989/// Builtin command type
22990#[derive(Debug, Clone, Copy)]
22991pub enum BuiltinType {
22992    Normal,
22993    Disabled,
22994}