Skip to main content

zsh/
exec.rs

1//! Shell command executor for zshrs
2//!
3//! Executes the parsed shell AST.
4
5use crate::history::HistoryEngine;
6use crate::math::MathEval;
7use crate::pcre::PcreState;
8use crate::prompt::{expand_prompt, PromptContext};
9use crate::tcp::TcpSessions;
10use crate::zftp::Zftp;
11use crate::zprof::Profiler;
12use crate::zutil::StyleTable;
13use compsys::cache::CompsysCache;
14use compsys::CompInitResult;
15use parking_lot::Mutex;
16use std::collections::HashSet;
17
18/// AOP advice type — before, after, or around.
19#[derive(Debug, Clone)]
20pub enum AdviceKind {
21    /// Run code before the command executes.
22    Before,
23    /// Run code after the command executes. $? and INTERCEPT_MS available.
24    After,
25    /// Wrap the command. Code must call `intercept_proceed` to run original.
26    Around,
27}
28
29/// An intercept registration.
30#[derive(Debug, Clone)]
31pub struct Intercept {
32    /// Pattern to match command names. Supports glob: "git *", "_*", "*".
33    pub pattern: String,
34    /// What kind of advice.
35    pub kind: AdviceKind,
36    /// Shell code to execute as advice.
37    pub code: String,
38    /// Unique ID for removal.
39    pub id: u32,
40}
41
42/// Result from background compinit thread
43pub struct CompInitBgResult {
44    pub result: CompInitResult,
45    pub cache: CompsysCache,
46}
47use std::io::Write;
48use std::sync::LazyLock;
49
50/// State snapshot for plugin delta computation.
51struct PluginSnapshot {
52    functions: std::collections::HashSet<String>,
53    aliases: std::collections::HashSet<String>,
54    global_aliases: std::collections::HashSet<String>,
55    suffix_aliases: std::collections::HashSet<String>,
56    variables: HashMap<String, String>,
57    arrays: std::collections::HashSet<String>,
58    assoc_arrays: std::collections::HashSet<String>,
59    fpath: Vec<PathBuf>,
60    options: HashMap<String, bool>,
61    hooks: HashMap<String, Vec<String>>,
62    autoloads: std::collections::HashSet<String>,
63}
64
65/// Cached compiled regexes for hot paths
66static REGEX_CACHE: LazyLock<Mutex<std::collections::HashMap<String, regex::Regex>>> =
67    LazyLock::new(|| Mutex::new(std::collections::HashMap::with_capacity(64)));
68
69/// Match an intercept pattern against a command name or full command string.
70/// Supports: exact match, glob ("git *", "_*", "*"), or "all".
71fn intercept_matches(pattern: &str, cmd_name: &str, full_cmd: &str) -> bool {
72    if pattern == "*" || pattern == "all" {
73        return true;
74    }
75    if pattern == cmd_name {
76        return true;
77    }
78    // Glob match against full command (e.g. "git *" matches "git push")
79    if pattern.contains('*') || pattern.contains('?') {
80        if let Ok(pat) = glob::Pattern::new(pattern) {
81            return pat.matches(cmd_name) || pat.matches(full_cmd);
82        }
83    }
84    false
85}
86
87/// Get or compile a regex, caching the result
88fn cached_regex(pattern: &str) -> Option<regex::Regex> {
89    let mut cache = REGEX_CACHE.lock();
90    if let Some(re) = cache.get(pattern) {
91        return Some(re.clone());
92    }
93    match regex::Regex::new(pattern) {
94        Ok(re) => {
95            cache.insert(pattern.to_string(), re.clone());
96            Some(re)
97        }
98        Err(_) => None,
99    }
100}
101
102/// HashSet of all zsh options for O(1) lookup
103static ZSH_OPTIONS_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
104    [
105        "aliases",
106        "allexport",
107        "alwayslastprompt",
108        "alwaystoend",
109        "appendcreate",
110        "appendhistory",
111        "autocd",
112        "autocontinue",
113        "autolist",
114        "automenu",
115        "autonamedirs",
116        "autoparamkeys",
117        "autoparamslash",
118        "autopushd",
119        "autoremoveslash",
120        "autoresume",
121        "badpattern",
122        "banghist",
123        "bareglobqual",
124        "bashautolist",
125        "bashrematch",
126        "beep",
127        "bgnice",
128        "braceccl",
129        "bsdecho",
130        "caseglob",
131        "casematch",
132        "cbases",
133        "cdablevars",
134        "cdsilent",
135        "chasedots",
136        "chaselinks",
137        "checkjobs",
138        "checkrunningjobs",
139        "clobber",
140        "combiningchars",
141        "completealiases",
142        "completeinword",
143        "continueonerror",
144        "correct",
145        "correctall",
146        "cprecedences",
147        "cshjunkiehistory",
148        "cshjunkieloops",
149        "cshjunkiequotes",
150        "cshnullcmd",
151        "cshnullglob",
152        "debugbeforecmd",
153        "dotglob",
154        "dvorak",
155        "emacs",
156        "equals",
157        "errexit",
158        "errreturn",
159        "evallineno",
160        "exec",
161        "extendedglob",
162        "extendedhistory",
163        "flowcontrol",
164        "forcefloat",
165        "functionargzero",
166        "glob",
167        "globassign",
168        "globcomplete",
169        "globdots",
170        "globstarshort",
171        "globsubst",
172        "globalexport",
173        "globalrcs",
174        "hashall",
175        "hashcmds",
176        "hashdirs",
177        "hashexecutablesonly",
178        "hashlistall",
179        "histallowclobber",
180        "histappend",
181        "histbeep",
182        "histexpand",
183        "histexpiredupsfirst",
184        "histfcntllock",
185        "histfindnodups",
186        "histignorealldups",
187        "histignoredups",
188        "histignorespace",
189        "histlexwords",
190        "histnofunctions",
191        "histnostore",
192        "histreduceblanks",
193        "histsavebycopy",
194        "histsavenodups",
195        "histsubstpattern",
196        "histverify",
197        "hup",
198        "ignorebraces",
199        "ignoreclosebraces",
200        "ignoreeof",
201        "incappendhistory",
202        "incappendhistorytime",
203        "interactive",
204        "interactivecomments",
205        "ksharrays",
206        "kshautoload",
207        "kshglob",
208        "kshoptionprint",
209        "kshtypeset",
210        "kshzerosubscript",
211        "listambiguous",
212        "listbeep",
213        "listpacked",
214        "listrowsfirst",
215        "listtypes",
216        "localloops",
217        "localoptions",
218        "localpatterns",
219        "localtraps",
220        "log",
221        "login",
222        "longlistjobs",
223        "magicequalsubst",
224        "mailwarn",
225        "mailwarning",
226        "markdirs",
227        "menucomplete",
228        "monitor",
229        "multibyte",
230        "multifuncdef",
231        "multios",
232        "nomatch",
233        "notify",
234        "nullglob",
235        "numericglobsort",
236        "octalzeroes",
237        "onecmd",
238        "overstrike",
239        "pathdirs",
240        "pathscript",
241        "physical",
242        "pipefail",
243        "posixaliases",
244        "posixargzero",
245        "posixbuiltins",
246        "posixcd",
247        "posixidentifiers",
248        "posixjobs",
249        "posixstrings",
250        "posixtraps",
251        "printeightbit",
252        "printexitvalue",
253        "privileged",
254        "promptbang",
255        "promptcr",
256        "promptpercent",
257        "promptsp",
258        "promptsubst",
259        "promptvars",
260        "pushdignoredups",
261        "pushdminus",
262        "pushdsilent",
263        "pushdtohome",
264        "rcexpandparam",
265        "rcquotes",
266        "rcs",
267        "recexact",
268        "rematchpcre",
269        "restricted",
270        "rmstarsilent",
271        "rmstarwait",
272        "sharehistory",
273        "shfileexpansion",
274        "shglob",
275        "shinstdin",
276        "shnullcmd",
277        "shoptionletters",
278        "shortloops",
279        "shortrepeat",
280        "shwordsplit",
281        "singlecommand",
282        "singlelinezle",
283        "sourcetrace",
284        "stdin",
285        "sunkeyboardhack",
286        "trackall",
287        "transientrprompt",
288        "trapsasync",
289        "typesetsilent",
290        "typesettounset",
291        "unset",
292        "verbose",
293        "vi",
294        "warncreateglobal",
295        "warnnestedvar",
296        "xtrace",
297        "zle",
298    ]
299    .into_iter()
300    .collect()
301});
302
303/// O(1) builtin lookup — replaces the 130+ arm matches! macro in is_builtin()
304static BUILTIN_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
305    [
306        "cd", "chdir", "pwd", "echo", "export", "unset", "source", "exit",
307        "return", "bye", "logout", "log", "true", "false", "test", "local",
308        "declare", "typeset", "read", "shift", "eval", "jobs", "fg", "bg",
309        "kill", "disown", "wait", "autoload", "history", "fc", "trap",
310        "suspend", "alias", "unalias", "set", "shopt", "setopt", "unsetopt",
311        "getopts", "type", "hash", "command", "builtin", "let", "pushd",
312        "popd", "dirs", "printf", "break", "continue", "disable", "enable",
313        "emulate", "exec", "float", "integer", "functions", "print", "whence",
314        "where", "which", "ulimit", "limit", "unlimit", "umask", "rehash",
315        "unhash", "times", "zmodload", "r", "ttyctl", "noglob", "zstat",
316        "stat", "strftime", "zsleep", "zln", "zmv", "zcp", "coproc",
317        "zparseopts", "readonly", "unfunction", "getln", "pushln", "bindkey",
318        "zle", "sched", "zformat", "zcompile", "vared", "echotc", "echoti",
319        "zpty", "zprof", "zsocket", "ztcp", "zregexparse", "clone",
320        "comparguments", "compcall", "compctl", "compdef", "compdescribe",
321        "compfiles", "compgroups", "compinit", "compquote", "comptags",
322        "comptry", "compvalues", "cdreplay", "cap", "getcap", "setcap",
323        "zftp", "zcurses", "sysread", "syswrite", "syserror", "sysopen",
324        "sysseek", "private", "zgetattr", "zsetattr", "zdelattr", "zlistattr",
325        "[", ".", ":", "compgen", "complete",
326    ]
327    .into_iter()
328    .collect()
329});
330
331/// Convert float to hex representation (%a/%A format)
332fn float_to_hex(val: f64, uppercase: bool) -> String {
333    if val.is_nan() {
334        return if uppercase { "NAN" } else { "nan" }.to_string();
335    }
336    if val.is_infinite() {
337        return if val > 0.0 {
338            if uppercase {
339                "INF"
340            } else {
341                "inf"
342            }
343        } else {
344            if uppercase {
345                "-INF"
346            } else {
347                "-inf"
348            }
349        }
350        .to_string();
351    }
352    if val == 0.0 {
353        let sign = if val.is_sign_negative() { "-" } else { "" };
354        return if uppercase {
355            format!("{}0X0P+0", sign)
356        } else {
357            format!("{}0x0p+0", sign)
358        };
359    }
360
361    let sign = if val < 0.0 { "-" } else { "" };
362    let abs_val = val.abs();
363    let bits = abs_val.to_bits();
364    let exponent = ((bits >> 52) & 0x7ff) as i32 - 1023;
365    let mantissa = bits & 0xfffffffffffff;
366
367    let hex_mantissa = format!("{:013x}", mantissa);
368    let hex_mantissa = hex_mantissa.trim_end_matches('0');
369    let hex_mantissa = if hex_mantissa.is_empty() {
370        "0"
371    } else {
372        hex_mantissa
373    };
374
375    if uppercase {
376        format!("{}0X1.{}P{:+}", sign, hex_mantissa.to_uppercase(), exponent)
377    } else {
378        format!("{}0x1.{}p{:+}", sign, hex_mantissa, exponent)
379    }
380}
381
382/// Quote a string for shell output (like zsh's set output)
383fn shell_quote(s: &str) -> String {
384    if s.is_empty() {
385        return "''".to_string();
386    }
387    // Check if quoting is needed
388    let needs_quotes = s.chars().any(|c| {
389        matches!(
390            c,
391            ' ' | '\t'
392                | '\n'
393                | '\''
394                | '"'
395                | '\\'
396                | '$'
397                | '`'
398                | '!'
399                | '*'
400                | '?'
401                | '['
402                | ']'
403                | '{'
404                | '}'
405                | '('
406                | ')'
407                | '<'
408                | '>'
409                | '|'
410                | '&'
411                | ';'
412                | '#'
413                | '~'
414        )
415    });
416    if !needs_quotes {
417        return s.to_string();
418    }
419    // Use single quotes, escaping single quotes as '\''
420    format!("'{}'", s.replace('\'', "'\\''"))
421}
422
423/// Quote a value for typeset -p output (re-executable code)
424/// Uses single quoting only when the value contains special characters
425fn shell_quote_value(s: &str) -> String {
426    if s.is_empty() {
427        return "''".to_string();
428    }
429    let needs_quotes = s.chars().any(|c| {
430        matches!(
431            c,
432            ' ' | '\t'
433                | '\n'
434                | '\''
435                | '"'
436                | '\\'
437                | '$'
438                | '`'
439                | '!'
440                | '*'
441                | '?'
442                | '['
443                | ']'
444                | '{'
445                | '}'
446                | '('
447                | ')'
448                | '<'
449                | '>'
450                | '|'
451                | '&'
452                | ';'
453                | '#'
454                | '~'
455                | '^'
456        )
457    });
458    if !needs_quotes {
459        return s.to_string();
460    }
461    format!("'{}'", s.replace('\'', "'\\''"))
462}
463
464use crate::jobs::{continue_job, wait_for_child, wait_for_job, JobState, JobTable};
465use crate::parser::{
466    CaseTerminator, CompoundCommand, CondExpr, ListOp, Redirect, RedirectOp, ShellCommand,
467    ShellParser, ShellWord, SimpleCommand, VarModifier, ZshParamFlag,
468};
469use crate::zwc::ZwcFile;
470use std::collections::HashMap;
471use std::env;
472use std::fs::{self, File, OpenOptions};
473use std::io;
474use std::path::{Path, PathBuf};
475use std::process::{Child, Command, Stdio};
476
477/// A completion specification for the `complete` builtin
478#[derive(Debug, Clone, Default)]
479pub struct CompSpec {
480    pub actions: Vec<String>,     // -a, -b, -c, etc.
481    pub wordlist: Option<String>, // -W wordlist
482    pub function: Option<String>, // -F function
483    pub command: Option<String>,  // -C command
484    pub globpat: Option<String>,  // -G glob
485    pub prefix: Option<String>,   // -P prefix
486    pub suffix: Option<String>,   // -S suffix
487}
488
489/// A single completion match for zsh-style completion
490#[derive(Debug, Clone)]
491pub struct CompMatch {
492    pub word: String,                   // The actual completion word
493    pub display: Option<String>,        // Display string (-d)
494    pub prefix: Option<String>,         // -P prefix (inserted but not part of match)
495    pub suffix: Option<String>,         // -S suffix (inserted but not part of match)
496    pub hidden_prefix: Option<String>,  // -p hidden prefix
497    pub hidden_suffix: Option<String>,  // -s hidden suffix
498    pub ignored_prefix: Option<String>, // -i ignored prefix
499    pub ignored_suffix: Option<String>, // -I ignored suffix
500    pub group: Option<String>,          // -J/-V group name
501    pub description: Option<String>,    // -X explanation
502    pub remove_suffix: Option<String>,  // -r remove chars
503    pub file_match: bool,               // -f flag
504    pub quote_match: bool,              // -q flag
505}
506
507impl Default for CompMatch {
508    fn default() -> Self {
509        Self {
510            word: String::new(),
511            display: None,
512            prefix: None,
513            suffix: None,
514            hidden_prefix: None,
515            hidden_suffix: None,
516            ignored_prefix: None,
517            ignored_suffix: None,
518            group: None,
519            description: None,
520            remove_suffix: None,
521            file_match: false,
522            quote_match: false,
523        }
524    }
525}
526
527/// Completion group for organizing matches
528#[derive(Debug, Clone, Default)]
529pub struct CompGroup {
530    pub name: String,
531    pub matches: Vec<CompMatch>,
532    pub explanation: Option<String>,
533    pub sorted: bool,
534}
535
536/// zsh completion state (compstate associative array)
537#[derive(Debug, Clone, Default)]
538pub struct CompState {
539    pub context: String,               // completion context
540    pub exact: String,                 // exact match handling
541    pub exact_string: String,          // the exact string if matched
542    pub ignored: i32,                  // number of ignored matches
543    pub insert: String,                // what to insert
544    pub insert_positions: String,      // cursor positions after insert
545    pub last_prompt: String,           // whether to return to last prompt
546    pub list: String,                  // listing style
547    pub list_lines: i32,               // number of lines for listing
548    pub list_max: i32,                 // max matches to list
549    pub nmatches: i32,                 // number of matches
550    pub old_insert: String,            // previous insert value
551    pub old_list: String,              // previous list value
552    pub parameter: String,             // parameter being completed
553    pub pattern_insert: String,        // pattern insert mode
554    pub pattern_match: String,         // pattern matching mode
555    pub quote: String,                 // quoting type
556    pub quoting: String,               // current quoting
557    pub redirect: String,              // redirection type
558    pub restore: String,               // restore mode
559    pub to_end: String,                // move to end mode
560    pub unambiguous: String,           // unambiguous prefix
561    pub unambiguous_cursor: i32,       // cursor pos in unambiguous
562    pub unambiguous_positions: String, // positions in unambiguous
563    pub vared: String,                 // vared context
564}
565
566/// zstyle entry for completion configuration
567#[derive(Debug, Clone)]
568pub struct ZStyle {
569    pub pattern: String,
570    pub style: String,
571    pub values: Vec<String>,
572}
573
574bitflags::bitflags! {
575    /// Flags for autoloaded functions
576    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
577    pub struct AutoloadFlags: u32 {
578        const NO_ALIAS = 0b00000001;      // -U: don't expand aliases
579        const ZSH_STYLE = 0b00000010;     // -z: zsh-style autoload
580        const KSH_STYLE = 0b00000100;     // -k: ksh-style autoload
581        const TRACE = 0b00001000;         // -t: trace execution
582        const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
583        const LOADED = 0b00100000;        // function has been loaded
584    }
585}
586
587/// State for a zpty pseudo-terminal
588pub struct ZptyState {
589    pub pid: u32,
590    pub cmd: String,
591    pub stdin: Option<std::process::ChildStdin>,
592    pub stdout: Option<std::process::ChildStdout>,
593    pub child: Option<std::process::Child>,
594}
595
596/// Scheduled command for sched builtin
597pub struct ScheduledCommand {
598    pub id: u32,
599    pub run_at: std::time::SystemTime,
600    pub command: String,
601}
602
603/// Profiling entry for zprof
604#[derive(Clone, Default)]
605pub struct ProfileEntry {
606    pub calls: u64,
607    pub total_time_us: u64,
608    pub self_time_us: u64,
609}
610
611/// Unix domain socket state
612pub struct UnixSocketState {
613    pub path: Option<PathBuf>,
614    pub listening: bool,
615    pub stream: Option<std::os::unix::net::UnixStream>,
616    pub listener: Option<std::os::unix::net::UnixListener>,
617}
618
619pub struct ShellExecutor {
620    pub functions: HashMap<String, ShellCommand>,
621    pub aliases: HashMap<String, String>,
622    pub global_aliases: HashMap<String, String>, // alias -g: expand anywhere
623    pub suffix_aliases: HashMap<String, String>, // alias -s: expand by file extension
624    pub last_status: i32,
625    pub variables: HashMap<String, String>,
626    pub arrays: HashMap<String, Vec<String>>,
627    pub assoc_arrays: HashMap<String, HashMap<String, String>>, // zsh associative arrays
628    pub jobs: JobTable,
629    pub fpath: Vec<PathBuf>,
630    pub zwc_cache: HashMap<PathBuf, ZwcFile>,
631    pub positional_params: Vec<String>,
632    pub history: Option<HistoryEngine>,
633    process_sub_counter: u32,
634    pub traps: HashMap<String, String>,
635    pub options: HashMap<String, bool>,
636    pub completions: HashMap<String, CompSpec>, // command -> completion spec
637    pub dir_stack: Vec<PathBuf>,
638    // zsh completion system state
639    pub comp_matches: Vec<CompMatch>, // Current completion matches
640    pub comp_groups: Vec<CompGroup>,  // Completion groups
641    pub comp_state: CompState,        // compstate associative array
642    pub zstyles: Vec<ZStyle>,         // zstyle configurations
643    pub comp_words: Vec<String>,      // words on command line
644    pub comp_current: i32,            // current word index (1-based)
645    pub comp_prefix: String,          // PREFIX parameter
646    pub comp_suffix: String,          // SUFFIX parameter
647    pub comp_iprefix: String,         // IPREFIX parameter
648    pub comp_isuffix: String,         // ISUFFIX parameter
649    pub readonly_vars: std::collections::HashSet<String>, // Read-only variables
650    /// Stack for `local` variable save/restore (name, old_value).
651    pub local_save_stack: Vec<(String, Option<String>)>,
652    /// Current function scope depth for `local` tracking.
653    pub local_scope_depth: usize,
654    pub autoload_pending: HashMap<String, AutoloadFlags>, // Functions marked for autoload
655    // zsh hooks (precmd, preexec, chpwd, etc.)
656    pub hook_functions: HashMap<String, Vec<String>>, // hook_name -> [function_names]
657    // Named directories (hash -d)
658    pub named_dirs: HashMap<String, PathBuf>, // name -> path
659    // zpty - pseudo-terminal management
660    pub zptys: HashMap<String, ZptyState>,
661    // sysopen - file descriptor management
662    pub open_fds: HashMap<i32, std::fs::File>,
663    pub next_fd: i32,
664    // sched - scheduled commands
665    pub scheduled_commands: Vec<ScheduledCommand>,
666    // zprof - profiling data
667    pub profile_data: HashMap<String, ProfileEntry>,
668    pub profiling_enabled: bool,
669    // zsocket - Unix domain sockets
670    pub unix_sockets: HashMap<i32, UnixSocketState>,
671    // compsys - completion system cache
672    pub compsys_cache: Option<CompsysCache>,
673    // Background compinit — receiver for async fpath scan result
674    pub compinit_pending: Option<(std::sync::mpsc::Receiver<CompInitBgResult>, std::time::Instant)>,
675    // Plugin source cache — stores side effects of source/. in SQLite
676    pub plugin_cache: Option<crate::plugin_cache::PluginCache>,
677    // cdreplay - deferred compdef calls for zinit turbo mode
678    pub deferred_compdefs: Vec<Vec<String>>,
679    // command hash table (hash builtin)
680    pub command_hash: HashMap<String, String>,
681    // Control flow signals
682    returning: Option<i32>, // Set by return builtin, cleared after function returns
683    breaking: i32,          // break level (0 = not breaking, N = break N levels)
684    continuing: i32,        // continue level
685    // New module state
686    pub pcre_state: PcreState,
687    pub tcp_sessions: TcpSessions,
688    pub zftp: Zftp,
689    pub profiler: Profiler,
690    pub style_table: StyleTable,
691    /// zsh compatibility mode - use .zcompdump, fpath scanning, etc.
692    pub zsh_compat: bool,
693    /// POSIX sh strict mode — no SQLite, no worker pool, no zsh extensions
694    pub posix_mode: bool,
695    /// Worker thread pool for background tasks (compinit, process subs, etc.)
696    pub worker_pool: std::sync::Arc<crate::worker::WorkerPool>,
697    /// AOP intercept table: command/function name → advice chain.
698    /// Glob patterns supported (e.g. "git *", "*").
699    pub intercepts: Vec<Intercept>,
700    /// Async job handles: id → receiver for (status, stdout)
701    pub async_jobs: HashMap<u32, crossbeam_channel::Receiver<(i32, String)>>,
702    /// Next async job ID
703    pub next_async_id: u32,
704    /// Defer stack: commands to run on scope exit (LIFO).
705    pub defer_stack: Vec<Vec<String>>,
706}
707
708impl ShellExecutor {
709    pub fn new() -> Self {
710        tracing::debug!("ShellExecutor::new() initializing");
711        // Initialize fpath from FPATH env var or use defaults
712        let fpath = env::var("FPATH")
713            .unwrap_or_default()
714            .split(':')
715            .filter(|s| !s.is_empty())
716            .map(PathBuf::from)
717            .collect();
718
719        let history = HistoryEngine::new().ok();
720
721        // Initialize standard zsh variables
722        let mut variables = HashMap::new();
723        variables.insert("ZSH_VERSION".to_string(), "5.9".to_string());
724        variables.insert(
725            "ZSH_PATCHLEVEL".to_string(),
726            "zsh-5.9-0-g73d3173".to_string(),
727        );
728        variables.insert("ZSH_NAME".to_string(), "zsh".to_string());
729        variables.insert(
730            "SHLVL".to_string(),
731            env::var("SHLVL")
732                .map(|v| {
733                    v.parse::<i32>()
734                        .map(|n| (n + 1).to_string())
735                        .unwrap_or_else(|_| "1".to_string())
736                })
737                .unwrap_or_else(|_| "1".to_string()),
738        );
739
740        Self {
741            functions: HashMap::new(),
742            aliases: HashMap::new(),
743            global_aliases: HashMap::new(),
744            suffix_aliases: HashMap::new(),
745            last_status: 0,
746            variables,
747            arrays: {
748                let mut a = HashMap::new();
749                // $path mirrors $PATH (tied array)
750                let path_dirs: Vec<String> = env::var("PATH")
751                    .unwrap_or_default()
752                    .split(':')
753                    .map(|s| s.to_string())
754                    .collect();
755                a.insert("path".to_string(), path_dirs);
756                a
757            },
758            assoc_arrays: HashMap::new(),
759            jobs: JobTable::new(),
760            fpath,
761            zwc_cache: HashMap::new(),
762            positional_params: Vec::new(),
763            history,
764            completions: HashMap::new(),
765            dir_stack: Vec::new(),
766            process_sub_counter: 0,
767            traps: HashMap::new(),
768            options: Self::default_options(),
769            // zsh completion system
770            comp_matches: Vec::new(),
771            comp_groups: Vec::new(),
772            comp_state: CompState::default(),
773            zstyles: Vec::new(),
774            comp_words: Vec::new(),
775            comp_current: 0,
776            comp_prefix: String::new(),
777            comp_suffix: String::new(),
778            comp_iprefix: String::new(),
779            comp_isuffix: String::new(),
780            readonly_vars: std::collections::HashSet::new(),
781            local_save_stack: Vec::new(),
782            local_scope_depth: 0,
783            autoload_pending: HashMap::new(),
784            hook_functions: HashMap::new(),
785            named_dirs: HashMap::new(),
786            zptys: HashMap::new(),
787            open_fds: HashMap::new(),
788            next_fd: 10,
789            scheduled_commands: Vec::new(),
790            profile_data: HashMap::new(),
791            profiling_enabled: false,
792            unix_sockets: HashMap::new(),
793            compsys_cache: {
794                let cache_path = compsys::cache::default_cache_path();
795                if cache_path.exists() {
796                    let db_size = std::fs::metadata(&cache_path).map(|m| m.len()).unwrap_or(0);
797                    match CompsysCache::open(&cache_path) {
798                        Ok(c) => {
799                            tracing::info!(
800                                db_bytes = db_size,
801                                path = %cache_path.display(),
802                                "compsys: sqlite cache opened"
803                            );
804                            Some(c)
805                        }
806                        Err(e) => {
807                            tracing::warn!(error = %e, "compsys: failed to open cache");
808                            None
809                        }
810                    }
811                } else {
812                    tracing::debug!("compsys: no cache at {}", cache_path.display());
813                    None
814                }
815            },
816            compinit_pending: None, // (receiver, start_time)
817            plugin_cache: {
818                let pc_path = crate::plugin_cache::default_cache_path();
819                if let Some(parent) = pc_path.parent() {
820                    let _ = std::fs::create_dir_all(parent);
821                }
822                match crate::plugin_cache::PluginCache::open(&pc_path) {
823                    Ok(pc) => {
824                        let (plugins, functions) = pc.stats();
825                        tracing::info!(
826                            plugins,
827                            cached_functions = functions,
828                            path = %pc_path.display(),
829                            "plugin_cache: sqlite opened"
830                        );
831                        Some(pc)
832                    }
833                    Err(e) => {
834                        tracing::warn!(error = %e, "plugin_cache: failed to open");
835                        None
836                    }
837                }
838            },
839            deferred_compdefs: Vec::new(),
840            command_hash: HashMap::new(),
841            returning: None,
842            breaking: 0,
843            continuing: 0,
844            pcre_state: PcreState::new(),
845            tcp_sessions: TcpSessions::new(),
846            zftp: Zftp::new(),
847            profiler: Profiler::new(),
848            style_table: StyleTable::new(),
849            zsh_compat: false,
850            posix_mode: false,
851            worker_pool: {
852                let config = crate::config::load();
853                let pool_size = crate::config::resolve_pool_size(&config.worker_pool);
854                std::sync::Arc::new(crate::worker::WorkerPool::new(pool_size))
855            },
856            intercepts: Vec::new(),
857            async_jobs: HashMap::new(),
858            next_async_id: 1,
859            defer_stack: Vec::new(),
860        }
861    }
862
863    /// Enter POSIX strict mode — drop all SQLite caches, shrink worker pool to minimum.
864    /// No zsh extensions, no caching, no threads beyond the bare minimum. Dinosaur mode.
865    pub fn enter_posix_mode(&mut self) {
866        self.posix_mode = true;
867        self.plugin_cache = None;
868        self.compsys_cache = None;
869        self.compinit_pending = None;
870        // Worker pool stays at size 1 — we can't drop it entirely because
871        // some code paths use it unconditionally, but with 1 thread it's
872        // effectively serial.
873        self.worker_pool = std::sync::Arc::new(crate::worker::WorkerPool::new(1));
874        tracing::info!("POSIX strict mode: SQLite caches dropped, worker pool shrunk to 1");
875    }
876
877    /// Run hook functions (precmd, preexec, chpwd, etc.)
878    pub fn run_hooks(&mut self, hook_name: &str) {
879        if let Some(funcs) = self.hook_functions.get(hook_name).cloned() {
880            for func_name in funcs {
881                if self.functions.contains_key(&func_name) {
882                    let _ = self.execute_script(&format!("{}", func_name));
883                }
884            }
885        }
886        // Also check for hook arrays (e.g., precmd_functions)
887        let array_name = format!("{}_functions", hook_name);
888        if let Some(funcs) = self.arrays.get(&array_name).cloned() {
889            for func_name in funcs {
890                if self.functions.contains_key(&func_name) {
891                    let _ = self.execute_script(&format!("{}", func_name));
892                }
893            }
894        }
895    }
896
897    /// Add a function to a hook
898    pub fn add_hook(&mut self, hook_name: &str, func_name: &str) {
899        self.hook_functions
900            .entry(hook_name.to_string())
901            .or_default()
902            .push(func_name.to_string());
903    }
904
905    /// Add a named directory (hash -d name=path)
906    pub fn add_named_dir(&mut self, name: &str, path: &str) {
907        self.named_dirs
908            .insert(name.to_string(), PathBuf::from(path));
909    }
910
911    /// Expand ~ with named directories
912    pub fn expand_tilde_named(&self, path: &str) -> String {
913        if path.starts_with('~') {
914            let rest = &path[1..];
915            // Check for ~name or ~name/...
916            let (name, suffix) = if let Some(slash_pos) = rest.find('/') {
917                (&rest[..slash_pos], &rest[slash_pos..])
918            } else {
919                (rest, "")
920            };
921
922            if name.is_empty() {
923                // Regular ~ expansion
924                if let Ok(home) = std::env::var("HOME") {
925                    return format!("{}{}", home, suffix);
926                }
927            } else if let Some(dir) = self.named_dirs.get(name) {
928                return format!("{}{}", dir.display(), suffix);
929            }
930        }
931        path.to_string()
932    }
933
934    fn all_zsh_options() -> &'static [&'static str] {
935        &[
936            "aliases",
937            "aliasfuncdef",
938            "allexport",
939            "alwayslastprompt",
940            "alwaystoend",
941            "appendcreate",
942            "appendhistory",
943            "autocd",
944            "autocontinue",
945            "autolist",
946            "automenu",
947            "autonamedirs",
948            "autoparamkeys",
949            "autoparamslash",
950            "autopushd",
951            "autoremoveslash",
952            "autoresume",
953            "badpattern",
954            "banghist",
955            "bareglobqual",
956            "bashautolist",
957            "bashrematch",
958            "beep",
959            "bgnice",
960            "braceccl",
961            "braceexpand",
962            "bsdecho",
963            "caseglob",
964            "casematch",
965            "casepaths",
966            "cbases",
967            "cdablevars",
968            "cdsilent",
969            "chasedots",
970            "chaselinks",
971            "checkjobs",
972            "checkrunningjobs",
973            "clobber",
974            "clobberempty",
975            "combiningchars",
976            "completealiases",
977            "completeinword",
978            "continueonerror",
979            "correct",
980            "correctall",
981            "cprecedences",
982            "cshjunkiehistory",
983            "cshjunkieloops",
984            "cshjunkiequotes",
985            "cshnullcmd",
986            "cshnullglob",
987            "debugbeforecmd",
988            "dotglob",
989            "dvorak",
990            "emacs",
991            "equals",
992            "errexit",
993            "errreturn",
994            "evallineno",
995            "exec",
996            "extendedglob",
997            "extendedhistory",
998            "flowcontrol",
999            "forcefloat",
1000            "functionargzero",
1001            "glob",
1002            "globassign",
1003            "globcomplete",
1004            "globdots",
1005            "globstarshort",
1006            "globsubst",
1007            "globalexport",
1008            "globalrcs",
1009            "hashall",
1010            "hashcmds",
1011            "hashdirs",
1012            "hashexecutablesonly",
1013            "hashlistall",
1014            "histallowclobber",
1015            "histappend",
1016            "histbeep",
1017            "histexpand",
1018            "histexpiredupsfirst",
1019            "histfcntllock",
1020            "histfindnodups",
1021            "histignorealldups",
1022            "histignoredups",
1023            "histignorespace",
1024            "histlexwords",
1025            "histnofunctions",
1026            "histnostore",
1027            "histreduceblanks",
1028            "histsavebycopy",
1029            "histsavenodups",
1030            "histsubstpattern",
1031            "histverify",
1032            "hup",
1033            "ignorebraces",
1034            "ignoreclosebraces",
1035            "ignoreeof",
1036            "incappendhistory",
1037            "incappendhistorytime",
1038            "interactive",
1039            "interactivecomments",
1040            "ksharrays",
1041            "kshautoload",
1042            "kshglob",
1043            "kshoptionprint",
1044            "kshtypeset",
1045            "kshzerosubscript",
1046            "listambiguous",
1047            "listbeep",
1048            "listpacked",
1049            "listrowsfirst",
1050            "listtypes",
1051            "localloops",
1052            "localoptions",
1053            "localpatterns",
1054            "localtraps",
1055            "log",
1056            "login",
1057            "longlistjobs",
1058            "magicequalsubst",
1059            "mailwarn",
1060            "mailwarning",
1061            "markdirs",
1062            "menucomplete",
1063            "monitor",
1064            "multibyte",
1065            "multifuncdef",
1066            "multios",
1067            "nomatch",
1068            "notify",
1069            "nullglob",
1070            "numericglobsort",
1071            "octalzeroes",
1072            "onecmd",
1073            "overstrike",
1074            "pathdirs",
1075            "pathscript",
1076            "physical",
1077            "pipefail",
1078            "posixaliases",
1079            "posixargzero",
1080            "posixbuiltins",
1081            "posixcd",
1082            "posixidentifiers",
1083            "posixjobs",
1084            "posixstrings",
1085            "posixtraps",
1086            "printeightbit",
1087            "printexitvalue",
1088            "privileged",
1089            "promptbang",
1090            "promptcr",
1091            "promptpercent",
1092            "promptsp",
1093            "promptsubst",
1094            "promptvars",
1095            "pushdignoredups",
1096            "pushdminus",
1097            "pushdsilent",
1098            "pushdtohome",
1099            "rcexpandparam",
1100            "rcquotes",
1101            "rcs",
1102            "recexact",
1103            "rematchpcre",
1104            "restricted",
1105            "rmstarsilent",
1106            "rmstarwait",
1107            "sharehistory",
1108            "shfileexpansion",
1109            "shglob",
1110            "shinstdin",
1111            "shnullcmd",
1112            "shoptionletters",
1113            "shortloops",
1114            "shortrepeat",
1115            "shwordsplit",
1116            "singlecommand",
1117            "singlelinezle",
1118            "sourcetrace",
1119            "stdin",
1120            "sunkeyboardhack",
1121            "trackall",
1122            "transientrprompt",
1123            "trapsasync",
1124            "typesetsilent",
1125            "typesettounset",
1126            "unset",
1127            "verbose",
1128            "vi",
1129            "warncreateglobal",
1130            "warnnestedvar",
1131            "xtrace",
1132            "zle",
1133        ]
1134    }
1135
1136    fn default_options() -> HashMap<String, bool> {
1137        let mut opts = HashMap::new();
1138        // Initialize all options to false first
1139        for opt in Self::all_zsh_options() {
1140            opts.insert(opt.to_string(), false);
1141        }
1142        // Set zsh defaults (options marked with <D> or <Z> in zshoptions man page)
1143        let defaults_on = [
1144            "aliases",
1145            "alwayslastprompt",
1146            "appendhistory",
1147            "autolist",
1148            "automenu",
1149            "autoparamkeys",
1150            "autoparamslash",
1151            "autoremoveslash",
1152            "badpattern",
1153            "banghist",
1154            "bareglobqual",
1155            "beep",
1156            "bgnice",
1157            "caseglob",
1158            "casematch",
1159            "checkjobs",
1160            "checkrunningjobs",
1161            "clobber",
1162            "debugbeforecmd",
1163            "equals",
1164            "evallineno",
1165            "exec",
1166            "flowcontrol",
1167            "functionargzero",
1168            "glob",
1169            "globalexport",
1170            "globalrcs",
1171            "hashcmds",
1172            "hashdirs",
1173            "hashlistall",
1174            "histbeep",
1175            "histsavebycopy",
1176            "hup",
1177            "interactive",
1178            "listambiguous",
1179            "listbeep",
1180            "listtypes",
1181            "monitor",
1182            "multibyte",
1183            "multifuncdef",
1184            "multios",
1185            "nomatch",
1186            "notify",
1187            "promptcr",
1188            "promptpercent",
1189            "promptsp",
1190            "rcs",
1191            "shinstdin",
1192            "shortloops",
1193            "unset",
1194            "zle",
1195        ];
1196        for opt in defaults_on {
1197            opts.insert(opt.to_string(), true);
1198        }
1199        opts
1200    }
1201
1202    /// Normalize option name: lowercase, remove underscores/hyphens, handle "no" prefix
1203    fn normalize_option_name(name: &str) -> (String, bool) {
1204        let normalized = name.to_lowercase().replace(['-', '_'], "");
1205        if let Some(stripped) = normalized.strip_prefix("no") {
1206            // O(1) lookup in HashSet instead of linear scan
1207            if ZSH_OPTIONS_SET.contains(stripped) {
1208                return (stripped.to_string(), false);
1209            }
1210        }
1211        (normalized, true)
1212    }
1213
1214    /// Check if option name matches a pattern (for -m flag)
1215    fn option_matches_pattern(opt: &str, pattern: &str) -> bool {
1216        let pat = pattern.to_lowercase().replace(['-', '_'], "");
1217        let opt_lower = opt.to_lowercase();
1218
1219        if pat.contains('*') || pat.contains('?') || pat.contains('[') {
1220            let regex_pat = pat.replace('.', "\\.").replace('*', ".*").replace('?', ".");
1221            let full_pattern = format!("^{}$", regex_pat);
1222            cached_regex(&full_pattern)
1223                .map(|re| re.is_match(&opt_lower))
1224                .unwrap_or(false)
1225        } else {
1226            opt_lower == pat
1227        }
1228    }
1229
1230    /// Try to load a function from ZWC files in fpath
1231    pub fn autoload_function(&mut self, name: &str) -> Option<ShellCommand> {
1232        // First check if already loaded
1233        if let Some(func) = self.functions.get(name) {
1234            return Some(func.clone());
1235        }
1236
1237        // Search fpath for the function - use index to avoid borrow issues
1238        for i in 0..self.fpath.len() {
1239            let dir = self.fpath[i].clone();
1240            // Try directory.zwc first
1241            let zwc_path = dir.with_extension("zwc");
1242            if zwc_path.exists() {
1243                if let Some(func) = self.load_function_from_zwc(&zwc_path, name) {
1244                    return Some(func);
1245                }
1246            }
1247
1248            // Try individual function.zwc
1249            let func_zwc = dir.join(format!("{}.zwc", name));
1250            if func_zwc.exists() {
1251                if let Some(func) = self.load_function_from_zwc(&func_zwc, name) {
1252                    return Some(func);
1253                }
1254            }
1255
1256            // Look for directory/*.zwc files containing this function
1257            if dir.is_dir() {
1258                if let Ok(entries) = fs::read_dir(&dir) {
1259                    for entry in entries.flatten() {
1260                        let path = entry.path();
1261                        if path.extension().map_or(false, |e| e == "zwc") {
1262                            if let Some(func) = self.load_function_from_zwc(&path, name) {
1263                                return Some(func);
1264                            }
1265                        }
1266                    }
1267                }
1268            }
1269        }
1270
1271        None
1272    }
1273
1274    /// Load a specific function from a ZWC file
1275    fn load_function_from_zwc(&mut self, path: &Path, name: &str) -> Option<ShellCommand> {
1276        // Check cache
1277        let zwc = if let Some(cached) = self.zwc_cache.get(path) {
1278            cached
1279        } else {
1280            // Load and cache the ZWC file
1281            let zwc = ZwcFile::load(path).ok()?;
1282            self.zwc_cache.insert(path.to_path_buf(), zwc);
1283            self.zwc_cache.get(path)?
1284        };
1285
1286        // Find the function
1287        let func = zwc.get_function(name)?;
1288        let decoded = zwc.decode_function(func)?;
1289
1290        // Convert to shell command and cache
1291        let shell_func = decoded.to_shell_function()?;
1292
1293        // Register the function
1294        if let ShellCommand::FunctionDef(fname, body) = &shell_func {
1295            self.functions.insert(fname.clone(), (**body).clone());
1296        }
1297
1298        Some(shell_func)
1299    }
1300
1301    /// Add a directory to fpath
1302    pub fn add_fpath(&mut self, path: PathBuf) {
1303        if !self.fpath.contains(&path) {
1304            self.fpath.insert(0, path);
1305        }
1306    }
1307
1308    /// Match a string against a shell glob pattern
1309    fn glob_match(&self, s: &str, pattern: &str) -> bool {
1310        // Convert shell glob to regex
1311        let mut regex_pattern = String::from("^");
1312        let mut chars = pattern.chars().peekable();
1313
1314        while let Some(c) = chars.next() {
1315            match c {
1316                '*' => regex_pattern.push_str(".*"),
1317                '?' => regex_pattern.push('.'),
1318                '[' => {
1319                    regex_pattern.push('[');
1320                    // Handle character class
1321                    while let Some(cc) = chars.next() {
1322                        if cc == ']' {
1323                            regex_pattern.push(']');
1324                            break;
1325                        }
1326                        regex_pattern.push(cc);
1327                    }
1328                }
1329                '(' => {
1330                    // Handle alternation (a|b|c) -> (a|b|c)
1331                    regex_pattern.push('(');
1332                }
1333                ')' => regex_pattern.push(')'),
1334                '|' => regex_pattern.push('|'),
1335                '.' | '+' | '^' | '$' | '\\' | '{' | '}' => {
1336                    regex_pattern.push('\\');
1337                    regex_pattern.push(c);
1338                }
1339                _ => regex_pattern.push(c),
1340            }
1341        }
1342        regex_pattern.push('$');
1343
1344        regex::Regex::new(&regex_pattern)
1345            .map(|re| re.is_match(s))
1346            .unwrap_or(false)
1347    }
1348
1349    /// Static glob match — same logic as glob_match but callable without &self,
1350    /// needed for Rayon parallel iterators that can't capture &self.
1351    pub fn glob_match_static(s: &str, pattern: &str) -> bool {
1352        let mut regex_pattern = String::from("^");
1353        let mut chars = pattern.chars().peekable();
1354        while let Some(c) = chars.next() {
1355            match c {
1356                '*' => regex_pattern.push_str(".*"),
1357                '?' => regex_pattern.push('.'),
1358                '[' => {
1359                    regex_pattern.push('[');
1360                    while let Some(cc) = chars.next() {
1361                        if cc == ']' {
1362                            regex_pattern.push(']');
1363                            break;
1364                        }
1365                        regex_pattern.push(cc);
1366                    }
1367                }
1368                '(' => regex_pattern.push('('),
1369                ')' => regex_pattern.push(')'),
1370                '|' => regex_pattern.push('|'),
1371                '.' | '+' | '^' | '$' | '\\' | '{' | '}' => {
1372                    regex_pattern.push('\\');
1373                    regex_pattern.push(c);
1374                }
1375                _ => regex_pattern.push(c),
1376            }
1377        }
1378        regex_pattern.push('$');
1379        regex::Regex::new(&regex_pattern)
1380            .map(|re| re.is_match(s))
1381            .unwrap_or(false)
1382    }
1383
1384    /// Execute a script file with bytecode caching — skips lex+parse+compile on cache hit.
1385    /// The AST is stored in SQLite keyed by (path, mtime).
1386    pub fn execute_script_file(&mut self, file_path: &str) -> Result<i32, String> {
1387        let path = std::path::Path::new(file_path);
1388        let mtime = crate::plugin_cache::file_mtime(path);
1389
1390        // Try AST cache first
1391        if let (Some(ref cache), Some((mt_s, mt_ns))) = (&self.plugin_cache, mtime) {
1392            if let Some(ast_bytes) = cache.check_ast(file_path, mt_s, mt_ns) {
1393                if let Ok(commands) = bincode::deserialize::<Vec<crate::parser::ShellCommand>>(&ast_bytes) {
1394                    tracing::info!(
1395                        path = file_path,
1396                        cmds = commands.len(),
1397                        bytes = ast_bytes.len(),
1398                        "execute_script_file: bytecode cache hit, skipping lex+parse"
1399                    );
1400                    for cmd in commands {
1401                        self.execute_command(&cmd)?;
1402                    }
1403                    return Ok(self.last_status);
1404                }
1405            }
1406        }
1407
1408        // Cache miss — read file, parse, execute, cache AST on worker
1409        let content = std::fs::read_to_string(file_path)
1410            .map_err(|e| format!("{}: {}", file_path, e))?;
1411        let expanded = self.expand_history(&content);
1412        let mut parser = ShellParser::new(&expanded);
1413        let mut commands = parser.parse_script()?;
1414        tracing::debug!(
1415            path = file_path,
1416            cmds = commands.len(),
1417            "execute_script_file: bytecode cache miss, parsed from source"
1418        );
1419
1420        // Optimize AST before execution and caching — constant folding, literal merging
1421        crate::ast_opt::optimize(&mut commands);
1422
1423        // Execute
1424        for cmd in &commands {
1425            self.execute_command(cmd)?;
1426        }
1427
1428        // Async-store the optimized AST in SQLite
1429        if let Some((mt_s, mt_ns)) = mtime {
1430            if let Ok(ast_bytes) = bincode::serialize(&commands) {
1431                let store_path = file_path.to_string();
1432                let cache_db_path = crate::plugin_cache::default_cache_path();
1433                let ast_size = ast_bytes.len();
1434                self.worker_pool.submit(move || {
1435                    match crate::plugin_cache::PluginCache::open(&cache_db_path) {
1436                        Ok(cache) => {
1437                            if let Err(e) = cache.store_ast(&store_path, mt_s, mt_ns, &ast_bytes) {
1438                                tracing::error!(path = %store_path, error = %e, "AST cache store failed");
1439                            } else {
1440                                tracing::debug!(path = %store_path, bytes = ast_size, "bytecode cached");
1441                            }
1442                        }
1443                        Err(e) => tracing::error!(error = %e, "plugin_cache: open for AST write failed"),
1444                    }
1445                });
1446            }
1447        }
1448
1449        Ok(self.last_status)
1450    }
1451
1452    #[tracing::instrument(skip(self, script), fields(len = script.len()))]
1453    pub fn execute_script(&mut self, script: &str) -> Result<i32, String> {
1454        // Expand history references before parsing
1455        let expanded = self.expand_history(script);
1456
1457        let mut parser = ShellParser::new(&expanded);
1458        let commands = parser.parse_script()?;
1459        tracing::trace!(cmds = commands.len(), "execute_script: parsed");
1460
1461        // Compile to fusevm bytecodes and execute on the VM.
1462        // The VM handles pure computation (arithmetic, control flow, variables).
1463        // Shell ops (Exec, Redirect, Pipeline, Glob, TestFile) callback into
1464        // the executor via execute_command for anything that needs shell state.
1465        // Primary path: compile to fusevm bytecodes and execute on the VM.
1466        // Fallback: tree-walker for commands the VM can't fully handle yet
1467        // (builtins that need executor state, complex redirects, etc.)
1468        let compiler = crate::shell_compiler::ShellCompiler::new();
1469        let chunk = compiler.compile(&commands);
1470
1471        if !chunk.ops.is_empty() {
1472            let mut vm = fusevm::VM::new(chunk);
1473            match vm.run() {
1474                fusevm::VMResult::Ok(_) | fusevm::VMResult::Halted => {
1475                    self.last_status = vm.last_status;
1476                }
1477                fusevm::VMResult::Error(_) => {
1478                    // VM error — fall back to tree-walker for compatibility
1479                    for cmd in &commands {
1480                        self.execute_command(cmd)?;
1481                    }
1482                }
1483            }
1484        } else {
1485            // Empty compilation (no ops emitted) — tree-walk
1486            for cmd in &commands {
1487                self.execute_command(cmd)?;
1488            }
1489        }
1490
1491        // Fire EXIT trap if set (matches zsh's zshexit behavior).
1492        // Remove it first to prevent infinite recursion.
1493        if let Some(action) = self.traps.remove("EXIT") {
1494            tracing::debug!("firing EXIT trap");
1495            let _ = self.execute_script(&action);
1496        }
1497
1498        Ok(self.last_status)
1499    }
1500
1501    /// Expand history references: !!, !n, !-n, !string, !?string?
1502    fn expand_history(&self, input: &str) -> String {
1503        let Some(ref engine) = self.history else {
1504            return input.to_string();
1505        };
1506
1507        // Quick check: nothing to expand
1508        if !input.contains('!') && !input.starts_with('^') {
1509            return input.to_string();
1510        }
1511
1512        let history_count = engine.count().unwrap_or(0) as usize;
1513        if history_count == 0 {
1514            return input.to_string();
1515        }
1516
1517        let chars: Vec<char> = input.chars().collect();
1518
1519        // ^foo^bar quick substitution (only at start of input)
1520        if chars.first() == Some(&'^') {
1521            if let Some(expanded) = self.history_quick_subst(&chars, engine) {
1522                return expanded;
1523            }
1524        }
1525
1526        let mut result = String::new();
1527        let mut i = 0;
1528        let mut in_single_quote = false;
1529        let mut in_brace = 0; // Track ${...} nesting
1530        let mut last_subst: Option<(String, String)> = None; // for :& modifier
1531
1532        while i < chars.len() {
1533            // Track single quotes — no history expansion inside them
1534            if chars[i] == '\'' && in_brace == 0 {
1535                in_single_quote = !in_single_quote;
1536                result.push(chars[i]);
1537                i += 1;
1538                continue;
1539            }
1540            if in_single_quote {
1541                result.push(chars[i]);
1542                i += 1;
1543                continue;
1544            }
1545
1546            // Track ${...} nesting
1547            if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' {
1548                in_brace += 1;
1549                result.push(chars[i]);
1550                i += 1;
1551                result.push(chars[i]);
1552                i += 1;
1553                continue;
1554            }
1555            if chars[i] == '}' && in_brace > 0 {
1556                in_brace -= 1;
1557                result.push(chars[i]);
1558                i += 1;
1559                continue;
1560            }
1561
1562            // Backslash-escaped ! is literal
1563            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '!' {
1564                result.push('!');
1565                i += 2;
1566                continue;
1567            }
1568
1569            if chars[i] == '!' && in_brace == 0 {
1570                if i + 1 >= chars.len() {
1571                    // Trailing ! — literal
1572                    result.push('!');
1573                    i += 1;
1574                    continue;
1575                }
1576
1577                let next = chars[i + 1];
1578                // ! followed by space, =, ( — literal (zsh rule)
1579                if next == ' ' || next == '\t' || next == '=' || next == '(' || next == '\n' {
1580                    result.push('!');
1581                    i += 1;
1582                    continue;
1583                }
1584
1585                // Resolve the event string
1586                let (event_str, new_i) = self.history_resolve_event(&chars, i, engine, &result);
1587                if let Some(ev) = event_str {
1588                    // Check for word designators and modifiers
1589                    let (final_str, final_i) =
1590                        self.history_apply_designators_and_modifiers(&chars, new_i, &ev, &mut last_subst);
1591                    result.push_str(&final_str);
1592                    i = final_i;
1593                } else {
1594                    // Could not resolve — keep the ! literal
1595                    result.push('!');
1596                    i += 1;
1597                }
1598                continue;
1599            }
1600            result.push(chars[i]);
1601            i += 1;
1602        }
1603
1604        result
1605    }
1606
1607    /// ^foo^bar quick substitution — replace first occurrence of foo with bar
1608    /// in the previous command.
1609    fn history_quick_subst(
1610        &self,
1611        chars: &[char],
1612        engine: &crate::history::HistoryEngine,
1613    ) -> Option<String> {
1614        let mut i = 1; // skip leading ^
1615        let mut old = String::new();
1616        while i < chars.len() && chars[i] != '^' {
1617            old.push(chars[i]);
1618            i += 1;
1619        }
1620        if i >= chars.len() {
1621            return None;
1622        }
1623        i += 1; // skip middle ^
1624        let mut new = String::new();
1625        while i < chars.len() && chars[i] != '^' && chars[i] != '\n' {
1626            new.push(chars[i]);
1627            i += 1;
1628        }
1629        let prev = engine.get_by_offset(0).ok()??;
1630        Some(prev.command.replacen(&old, &new, 1))
1631    }
1632
1633    /// Resolve which history event ! refers to.  Returns (Some(full_command), index_after_event)
1634    /// or (None, original_index) if we can't resolve.
1635    fn history_resolve_event(
1636        &self,
1637        chars: &[char],
1638        bang_pos: usize,
1639        engine: &crate::history::HistoryEngine,
1640        current_line: &str,
1641    ) -> (Option<String>, usize) {
1642        let mut i = bang_pos + 1; // past the !
1643
1644        // !{...} brace-wrapped event
1645        let in_brace = i < chars.len() && chars[i] == '{';
1646        if in_brace {
1647            i += 1;
1648        }
1649
1650        let c = if i < chars.len() { chars[i] } else { return (None, bang_pos); };
1651
1652        let (event, new_i) = match c {
1653            '!' => {
1654                // !! — previous command
1655                let entry = engine.get_by_offset(0).ok().flatten();
1656                (entry.map(|e| e.command), i + 1)
1657            }
1658            '#' => {
1659                // !# — current command line so far
1660                (Some(current_line.to_string()), i + 1)
1661            }
1662            '-' => {
1663                // !-n — nth previous command
1664                i += 1;
1665                let start = i;
1666                while i < chars.len() && chars[i].is_ascii_digit() {
1667                    i += 1;
1668                }
1669                if i > start {
1670                    let n: usize = chars[start..i].iter().collect::<String>().parse().unwrap_or(0);
1671                    if n > 0 {
1672                        let entry = engine.get_by_offset(n - 1).ok().flatten();
1673                        (entry.map(|e| e.command), i)
1674                    } else {
1675                        (None, bang_pos)
1676                    }
1677                } else {
1678                    (None, bang_pos)
1679                }
1680            }
1681            '?' => {
1682                // !?string? — contains search
1683                i += 1;
1684                let start = i;
1685                while i < chars.len() && chars[i] != '?' && chars[i] != '\n' {
1686                    i += 1;
1687                }
1688                let search: String = chars[start..i].iter().collect();
1689                if i < chars.len() && chars[i] == '?' {
1690                    i += 1;
1691                }
1692                let entry = engine.search(&search, 1).ok().and_then(|v| v.into_iter().next());
1693                (entry.map(|e| e.command), i)
1694            }
1695            c if c.is_ascii_digit() => {
1696                // !n — command by absolute number
1697                let start = i;
1698                while i < chars.len() && chars[i].is_ascii_digit() {
1699                    i += 1;
1700                }
1701                let n: i64 = chars[start..i].iter().collect::<String>().parse().unwrap_or(0);
1702                if n > 0 {
1703                    let entry = engine.get_by_number(n).ok().flatten();
1704                    (entry.map(|e| e.command), i)
1705                } else {
1706                    (None, bang_pos)
1707                }
1708            }
1709            '$' => {
1710                // !$ — last word of previous command (shorthand for !!:$)
1711                let entry = engine.get_by_offset(0).ok().flatten();
1712                let word = entry.and_then(|e| {
1713                    Self::history_split_words(&e.command).last().cloned()
1714                });
1715                // Return the word directly — skip designator parsing
1716                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
1717                    i + 2
1718                } else {
1719                    i + 1
1720                };
1721                return (word, final_i);
1722            }
1723            '^' => {
1724                // !^ — first arg of previous command (shorthand for !!:1)
1725                let entry = engine.get_by_offset(0).ok().flatten();
1726                let word = entry.and_then(|e| {
1727                    let words = Self::history_split_words(&e.command);
1728                    words.get(1).cloned()
1729                });
1730                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
1731                    i + 2
1732                } else {
1733                    i + 1
1734                };
1735                return (word, final_i);
1736            }
1737            '*' => {
1738                // !* — all args of previous command (shorthand for !!:*)
1739                let entry = engine.get_by_offset(0).ok().flatten();
1740                let word = entry.map(|e| {
1741                    let words = Self::history_split_words(&e.command);
1742                    if words.len() > 1 { words[1..].join(" ") } else { String::new() }
1743                });
1744                let final_i = if in_brace && i + 1 < chars.len() && chars[i + 1] == '}' {
1745                    i + 2
1746                } else {
1747                    i + 1
1748                };
1749                return (word, final_i);
1750            }
1751            c if c.is_alphabetic() || c == '_' || c == '/' || c == '.' => {
1752                // !string — prefix search
1753                let start = i;
1754                while i < chars.len()
1755                    && !chars[i].is_whitespace()
1756                    && chars[i] != ':'
1757                    && chars[i] != '!'
1758                    && chars[i] != '}'
1759                {
1760                    i += 1;
1761                }
1762                let prefix: String = chars[start..i].iter().collect();
1763                let entry = engine
1764                    .search_prefix(&prefix, 1)
1765                    .ok()
1766                    .and_then(|v| v.into_iter().next());
1767                (entry.map(|e| e.command), i)
1768            }
1769            _ => (None, bang_pos),
1770        };
1771
1772        // Skip closing brace
1773        let final_i = if in_brace && new_i < chars.len() && chars[new_i] == '}' {
1774            new_i + 1
1775        } else {
1776            new_i
1777        };
1778
1779        (event, final_i)
1780    }
1781
1782    /// Split a command string into words for word designators, respecting quotes.
1783    fn history_split_words(cmd: &str) -> Vec<String> {
1784        let mut words = Vec::new();
1785        let mut current = String::new();
1786        let mut in_sq = false;
1787        let mut in_dq = false;
1788        let mut escaped = false;
1789
1790        for c in cmd.chars() {
1791            if escaped {
1792                current.push(c);
1793                escaped = false;
1794                continue;
1795            }
1796            if c == '\\' {
1797                current.push(c);
1798                escaped = true;
1799                continue;
1800            }
1801            if c == '\'' && !in_dq {
1802                in_sq = !in_sq;
1803                current.push(c);
1804                continue;
1805            }
1806            if c == '"' && !in_sq {
1807                in_dq = !in_dq;
1808                current.push(c);
1809                continue;
1810            }
1811            if c.is_whitespace() && !in_sq && !in_dq {
1812                if !current.is_empty() {
1813                    words.push(std::mem::take(&mut current));
1814                }
1815                continue;
1816            }
1817            current.push(c);
1818        }
1819        if !current.is_empty() {
1820            words.push(current);
1821        }
1822        words
1823    }
1824
1825    /// Apply word designators (:0, :n, :^, :$, :*, :n-m) and modifiers
1826    /// (:h, :t, :r, :e, :s/old/new/, :gs/old/new/, :p, :l, :u, :q, :Q, :a, :A)
1827    /// to an already-resolved event string.
1828    fn history_apply_designators_and_modifiers(
1829        &self,
1830        chars: &[char],
1831        mut i: usize,
1832        event: &str,
1833        last_subst: &mut Option<(String, String)>,
1834    ) -> (String, usize) {
1835        let words = Self::history_split_words(event);
1836        let argc = words.len().saturating_sub(1); // last word index
1837
1838        // Check for word designator — either :N or bare :^ :$ :*
1839        let mut sline = event.to_string();
1840
1841        if i < chars.len() && chars[i] == ':' {
1842            i += 1;
1843            if i < chars.len() {
1844                // Parse word designator
1845                let (farg, larg, new_i) = self.history_parse_word_range(chars, i, argc);
1846                i = new_i;
1847                if farg.is_some() || larg.is_some() {
1848                    let f = farg.unwrap_or(0);
1849                    let l = larg.unwrap_or(argc);
1850                    let selected: Vec<&String> = words.iter().enumerate()
1851                        .filter(|(idx, _)| *idx >= f && *idx <= l)
1852                        .map(|(_, w)| w)
1853                        .collect();
1854                    sline = selected.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" ");
1855                }
1856            }
1857        } else if i < chars.len() && chars[i] == '*' {
1858            // !!* shorthand for !!:1-$
1859            i += 1;
1860            if words.len() > 1 {
1861                sline = words[1..].join(" ");
1862            } else {
1863                sline = String::new();
1864            }
1865        }
1866
1867        // Apply modifiers (:h :t :r :e :s :gs :p :l :u :q :Q :a :A)
1868        while i < chars.len() && chars[i] == ':' {
1869            i += 1;
1870            if i >= chars.len() {
1871                break;
1872            }
1873            let mut global = false;
1874            if chars[i] == 'g' && i + 1 < chars.len() {
1875                global = true;
1876                i += 1;
1877            }
1878            match chars[i] {
1879                'h' => {
1880                    // Head — remove trailing path component
1881                    i += 1;
1882                    if let Some(pos) = sline.rfind('/') {
1883                        if pos > 0 {
1884                            sline = sline[..pos].to_string();
1885                        } else {
1886                            sline = "/".to_string();
1887                        }
1888                    }
1889                }
1890                't' => {
1891                    // Tail — remove leading path components
1892                    i += 1;
1893                    if let Some(pos) = sline.rfind('/') {
1894                        sline = sline[pos + 1..].to_string();
1895                    }
1896                }
1897                'r' => {
1898                    // Remove extension
1899                    i += 1;
1900                    if let Some(pos) = sline.rfind('.') {
1901                        if pos > 0 && sline[..pos].rfind('/').map_or(true, |sp| sp < pos) {
1902                            sline = sline[..pos].to_string();
1903                        }
1904                    }
1905                }
1906                'e' => {
1907                    // Extension only
1908                    i += 1;
1909                    if let Some(pos) = sline.rfind('.') {
1910                        sline = sline[pos + 1..].to_string();
1911                    } else {
1912                        sline = String::new();
1913                    }
1914                }
1915                'l' => {
1916                    // Lowercase
1917                    i += 1;
1918                    sline = sline.to_lowercase();
1919                }
1920                'u' => {
1921                    // Uppercase
1922                    i += 1;
1923                    sline = sline.to_uppercase();
1924                }
1925                'p' => {
1926                    // Print only, don't execute (we just expand — caller handles this)
1927                    i += 1;
1928                    // For now, just expand — :p suppression would need upstream support
1929                }
1930                'q' => {
1931                    // Quote — single-quote the result
1932                    i += 1;
1933                    sline = format!("'{}'", sline.replace('\'', "'\\''"));
1934                }
1935                'Q' => {
1936                    // Unquote — strip one level of quotes
1937                    i += 1;
1938                    sline = sline.replace('\'', "").replace('"', "");
1939                }
1940                'a' => {
1941                    // Absolute path
1942                    i += 1;
1943                    if !sline.starts_with('/') {
1944                        if let Ok(cwd) = std::env::current_dir() {
1945                            sline = format!("{}/{}", cwd.display(), sline);
1946                        }
1947                    }
1948                }
1949                'A' => {
1950                    // Realpath
1951                    i += 1;
1952                    if let Ok(real) = std::fs::canonicalize(&sline) {
1953                        sline = real.to_string_lossy().to_string();
1954                    }
1955                }
1956                's' | 'S' => {
1957                    // :s/old/new/ or :gs/old/new/
1958                    i += 1;
1959                    if i < chars.len() {
1960                        let delim = chars[i];
1961                        i += 1;
1962                        let mut old_s = String::new();
1963                        while i < chars.len() && chars[i] != delim {
1964                            old_s.push(chars[i]);
1965                            i += 1;
1966                        }
1967                        if i < chars.len() { i += 1; } // skip delimiter
1968                        let mut new_s = String::new();
1969                        while i < chars.len() && chars[i] != delim && chars[i] != ':' && chars[i] != ' ' {
1970                            new_s.push(chars[i]);
1971                            i += 1;
1972                        }
1973                        if i < chars.len() && chars[i] == delim { i += 1; } // skip trailing delimiter
1974                        *last_subst = Some((old_s.clone(), new_s.clone()));
1975                        if global {
1976                            sline = sline.replace(&old_s, &new_s);
1977                        } else {
1978                            sline = sline.replacen(&old_s, &new_s, 1);
1979                        }
1980                    }
1981                }
1982                '&' => {
1983                    // Repeat last substitution
1984                    i += 1;
1985                    if let Some((ref old_s, ref new_s)) = last_subst {
1986                        if global {
1987                            sline = sline.replace(old_s.as_str(), new_s.as_str());
1988                        } else {
1989                            sline = sline.replacen(old_s.as_str(), new_s.as_str(), 1);
1990                        }
1991                    }
1992                }
1993                _ => {
1994                    if global {
1995                        // 'g' was consumed but next char isn't s/S/& — put back
1996                        // by not advancing i further
1997                    }
1998                    break;
1999                }
2000            }
2001        }
2002
2003        (sline, i)
2004    }
2005
2006    /// Parse a word range like 0, 1, ^, $, *, n-m, n-
2007    fn history_parse_word_range(
2008        &self,
2009        chars: &[char],
2010        mut i: usize,
2011        argc: usize,
2012    ) -> (Option<usize>, Option<usize>, usize) {
2013        if i >= chars.len() {
2014            return (None, None, i);
2015        }
2016
2017        // Check for modifiers that aren't word designators
2018        match chars[i] {
2019            'h' | 't' | 'r' | 'e' | 's' | 'S' | 'g' | 'p' | 'q' | 'Q' | 'l' | 'u' | 'a' | 'A' | '&' => {
2020                // This is a modifier, not a word designator — back up
2021                return (None, None, i - 1); // -1 to re-read the ':'
2022            }
2023            _ => {}
2024        }
2025
2026        let farg = if chars[i] == '^' {
2027            i += 1;
2028            Some(1usize)
2029        } else if chars[i] == '$' {
2030            i += 1;
2031            return (Some(argc), Some(argc), i);
2032        } else if chars[i] == '*' {
2033            i += 1;
2034            return (Some(1), Some(argc), i);
2035        } else if chars[i].is_ascii_digit() {
2036            let start = i;
2037            while i < chars.len() && chars[i].is_ascii_digit() {
2038                i += 1;
2039            }
2040            let n: usize = chars[start..i].iter().collect::<String>().parse().unwrap_or(0);
2041            Some(n)
2042        } else {
2043            None
2044        };
2045
2046        // Check for range: n-m or n-
2047        if i < chars.len() && chars[i] == '-' {
2048            i += 1;
2049            if i < chars.len() && chars[i] == '$' {
2050                i += 1;
2051                return (farg, Some(argc), i);
2052            } else if i < chars.len() && chars[i].is_ascii_digit() {
2053                let start = i;
2054                while i < chars.len() && chars[i].is_ascii_digit() {
2055                    i += 1;
2056                }
2057                let m: usize = chars[start..i].iter().collect::<String>().parse().unwrap_or(0);
2058                return (farg, Some(m), i);
2059            } else {
2060                // n- means n to argc-1
2061                return (farg, Some(argc.saturating_sub(1)), i);
2062            }
2063        }
2064
2065        if farg.is_some() {
2066            (farg, farg, i)
2067        } else {
2068            (None, None, i)
2069        }
2070    }
2071
2072    #[tracing::instrument(level = "trace", skip_all)]
2073    pub fn execute_command(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
2074        match cmd {
2075            ShellCommand::Simple(simple) => self.execute_simple(simple),
2076            ShellCommand::Pipeline(cmds, negated) => {
2077                let status = self.execute_pipeline(cmds)?;
2078                if *negated {
2079                    self.last_status = if status == 0 { 1 } else { 0 };
2080                } else {
2081                    self.last_status = status;
2082                }
2083                Ok(self.last_status)
2084            }
2085            ShellCommand::List(items) => self.execute_list(items),
2086            ShellCommand::Compound(compound) => self.execute_compound(compound),
2087            ShellCommand::FunctionDef(name, body) => {
2088                if name.is_empty() {
2089                    // Anonymous function - execute immediately
2090                    let result = self.execute_command(body);
2091                    // Clear returning flag since the anonymous function has completed
2092                    if let Some(ret) = self.returning.take() {
2093                        self.last_status = ret;
2094                        return Ok(ret);
2095                    }
2096                    result
2097                } else {
2098                    // Named function - just define it
2099                    self.functions.insert(name.clone(), (**body).clone());
2100                    self.last_status = 0;
2101                    Ok(0)
2102                }
2103            }
2104        }
2105    }
2106
2107    #[tracing::instrument(level = "trace", skip_all)]
2108    fn execute_simple(&mut self, cmd: &SimpleCommand) -> Result<i32, String> {
2109        // Handle assignments
2110        for (var, val, is_append) in &cmd.assignments {
2111            match val {
2112                ShellWord::ArrayLiteral(elements) => {
2113                    // Array assignment: arr=(a b c) or arr+=(a b c)
2114                    // For associative arrays: assoc=(k1 v1 k2 v2)
2115                    // Use expand_word_split so $(cmd) and $var undergo
2116                    // word splitting into separate array elements (C zsh behavior).
2117                    let new_elements: Vec<String> = elements
2118                        .iter()
2119                        .flat_map(|e| self.expand_word_split(e))
2120                        .collect();
2121
2122                    // Check if this is an associative array
2123                    if self.assoc_arrays.contains_key(var) {
2124                        // Associative array: treat pairs as key-value
2125                        if *is_append {
2126                            let assoc = self.assoc_arrays.get_mut(var).unwrap();
2127                            let mut iter = new_elements.iter();
2128                            while let Some(key) = iter.next() {
2129                                if let Some(val) = iter.next() {
2130                                    assoc.insert(key.clone(), val.clone());
2131                                }
2132                            }
2133                        } else {
2134                            let mut assoc = HashMap::new();
2135                            let mut iter = new_elements.iter();
2136                            while let Some(key) = iter.next() {
2137                                if let Some(val) = iter.next() {
2138                                    assoc.insert(key.clone(), val.clone());
2139                                }
2140                            }
2141                            self.assoc_arrays.insert(var.clone(), assoc);
2142                        }
2143                    } else if *is_append {
2144                        // Append to existing indexed array
2145                        let arr = self.arrays.entry(var.clone()).or_insert_with(Vec::new);
2146                        arr.extend(new_elements);
2147                    } else {
2148                        self.arrays.insert(var.clone(), new_elements);
2149                    }
2150                }
2151                _ => {
2152                    let expanded = self.expand_word(val);
2153
2154                    // Check for array element assignment: arr[idx]=value or assoc[key]=value
2155                    if let Some(bracket_pos) = var.find('[') {
2156                        if var.ends_with(']') {
2157                            let array_name = &var[..bracket_pos];
2158                            let key = &var[bracket_pos + 1..var.len() - 1];
2159                            let key = self.expand_string(key); // Expand the key/index
2160
2161                            // Check if it's an associative array
2162                            if self.assoc_arrays.contains_key(array_name) {
2163                                let assoc = self.assoc_arrays.get_mut(array_name).unwrap();
2164                                if *is_append {
2165                                    let existing = assoc.get(&key).cloned().unwrap_or_default();
2166                                    assoc.insert(key, existing + &expanded);
2167                                } else {
2168                                    assoc.insert(key, expanded);
2169                                }
2170                            } else if let Ok(idx) = key.parse::<i64>() {
2171                                // Regular indexed array
2172                                let idx = if idx < 0 { 0 } else { (idx - 1) as usize }; // zsh is 1-indexed
2173                                let arr = self
2174                                    .arrays
2175                                    .entry(array_name.to_string())
2176                                    .or_insert_with(Vec::new);
2177                                while arr.len() <= idx {
2178                                    arr.push(String::new());
2179                                }
2180                                if *is_append {
2181                                    arr[idx].push_str(&expanded);
2182                                } else {
2183                                    arr[idx] = expanded;
2184                                }
2185                            } else {
2186                                // Non-numeric key on non-assoc array - treat as assoc
2187                                let assoc = self
2188                                    .assoc_arrays
2189                                    .entry(array_name.to_string())
2190                                    .or_insert_with(HashMap::new);
2191                                if *is_append {
2192                                    let existing = assoc.get(&key).cloned().unwrap_or_default();
2193                                    assoc.insert(key, existing + &expanded);
2194                                } else {
2195                                    assoc.insert(key, expanded);
2196                                }
2197                            }
2198                            continue;
2199                        }
2200                    }
2201
2202                    // Regular variable assignment or append
2203                    let final_value = if *is_append {
2204                        let existing = self.variables.get(var).cloned().unwrap_or_default();
2205                        existing + &expanded
2206                    } else {
2207                        expanded
2208                    };
2209
2210                    if self.readonly_vars.contains(var) {
2211                        eprintln!("zshrs: read-only variable: {}", var);
2212                        self.last_status = 1;
2213                        return Ok(1);
2214                    }
2215                    if cmd.words.is_empty() {
2216                        // Just assignment, set in environment
2217                        env::set_var(var, &final_value);
2218                    }
2219                    self.variables.insert(var.clone(), final_value);
2220                }
2221            }
2222        }
2223
2224        if cmd.words.is_empty() {
2225            self.last_status = 0;
2226            return Ok(0);
2227        }
2228
2229        // Check if this is a noglob precommand — suppress glob expansion
2230        let is_noglob = cmd.words.first().map(|w| self.expand_word(w) == "noglob").unwrap_or(false);
2231        let saved_noglob = if is_noglob {
2232            let saved = self.options.get("noglob").copied();
2233            self.options.insert("noglob".to_string(), true);
2234            saved
2235        } else {
2236            None
2237        };
2238
2239        // Pre-launch external command substitutions in parallel before expanding words.
2240        // Each external $(cmd) gets spawned on the worker pool immediately.
2241        // When we reach that word during sequential expansion, we collect the result.
2242        let preflight = self.preflight_command_subs(&cmd.words);
2243
2244        let mut words: Vec<String> = cmd
2245            .words
2246            .iter()
2247            .enumerate()
2248            .flat_map(|(i, w)| {
2249                if let Some(rx) = &preflight[i] {
2250                    // Pre-launched external command sub — collect result
2251                    vec![rx.recv().unwrap_or_default()]
2252                } else {
2253                    self.expand_word_glob(w)
2254                }
2255            })
2256            .collect();
2257
2258        // Restore noglob after expansion
2259        if is_noglob {
2260            match saved_noglob {
2261                Some(v) => { self.options.insert("noglob".to_string(), v); }
2262                None => { self.options.remove("noglob"); }
2263            }
2264        }
2265        if words.is_empty() {
2266            self.last_status = 0;
2267            return Ok(0);
2268        }
2269
2270        // Expand global aliases (alias -g) in all word positions
2271        if !self.global_aliases.is_empty() {
2272            let global_aliases = self.global_aliases.clone();
2273            words = words
2274                .into_iter()
2275                .map(|w| global_aliases.get(&w).cloned().unwrap_or(w))
2276                .collect();
2277        }
2278
2279        // xtrace: print expanded command to stderr (zsh -x / set -x)
2280        if self.options.get("xtrace").copied().unwrap_or(false) {
2281            let ps4 = self.variables.get("PS4").cloned().unwrap_or_else(|| "+".to_string());
2282            eprintln!("{}{}", ps4, words.join(" "));
2283        }
2284
2285        // Check for regular alias expansion (alias > builtin > function > command)
2286        let cmd_name = &words[0];
2287        if let Some(alias_value) = self.aliases.get(cmd_name).cloned() {
2288            // Expand the alias: replace cmd_name with alias value, keep remaining args
2289            let expanded_cmd = if words.len() > 1 {
2290                format!("{} {}", alias_value, words[1..].join(" "))
2291            } else {
2292                alias_value
2293            };
2294            // Re-execute the expanded command
2295            return self.execute_script(&expanded_cmd);
2296        }
2297
2298        // Check for suffix alias expansion (alias -s) when command is a file path
2299        if !self.suffix_aliases.is_empty() {
2300            let cmd_path = std::path::Path::new(cmd_name);
2301            if let Some(ext) = cmd_path.extension().and_then(|e| e.to_str()) {
2302                if let Some(handler) = self.suffix_aliases.get(ext).cloned() {
2303                    // Suffix alias: "alias -s txt=vim" makes "foo.txt" run "vim foo.txt"
2304                    let expanded_cmd = format!("{} {}", handler, words.join(" "));
2305                    return self.execute_script(&expanded_cmd);
2306                }
2307            }
2308        }
2309
2310        let args = &words[1..];
2311
2312        // Check if this is `exec` with only redirects (no command args)
2313        // For exec, redirects with {varname} allocate FDs; redirects are permanent
2314        let is_exec_with_redirects_only =
2315            cmd_name == "exec" && args.is_empty() && !cmd.redirects.is_empty();
2316
2317        // Apply redirects for builtins
2318        let mut saved_fds: Vec<(i32, i32)> = Vec::new();
2319        for redirect in &cmd.redirects {
2320            let target = self.expand_word(&redirect.target);
2321
2322            // Handle {varname}>file syntax - allocate new FD and store in variable
2323            if let Some(ref var_name) = redirect.fd_var {
2324                use std::os::unix::io::IntoRawFd;
2325                let file_result = match redirect.op {
2326                    RedirectOp::Write | RedirectOp::Clobber => std::fs::File::create(&target),
2327                    RedirectOp::Append => std::fs::OpenOptions::new()
2328                        .create(true)
2329                        .append(true)
2330                        .open(&target),
2331                    RedirectOp::Read => std::fs::File::open(&target),
2332                    _ => continue,
2333                };
2334                match file_result {
2335                    Ok(file) => {
2336                        let new_fd = file.into_raw_fd();
2337                        self.variables.insert(var_name.clone(), new_fd.to_string());
2338                        // Store allocated FD for potential cleanup (not for exec)
2339                        if !is_exec_with_redirects_only {
2340                            // For non-exec, we might want to track these
2341                        }
2342                    }
2343                    Err(e) => {
2344                        eprintln!("{}: {}: {}", cmd_name, target, e);
2345                        return Ok(1);
2346                    }
2347                }
2348                continue;
2349            }
2350
2351            let fd = redirect.fd.unwrap_or(match redirect.op {
2352                RedirectOp::Read
2353                | RedirectOp::HereDoc
2354                | RedirectOp::HereString
2355                | RedirectOp::ReadWrite => 0,
2356                _ => 1,
2357            });
2358
2359            match redirect.op {
2360                RedirectOp::Write | RedirectOp::Clobber => {
2361                    use std::os::unix::io::IntoRawFd;
2362                    if !is_exec_with_redirects_only {
2363                        let saved = unsafe { libc::dup(fd) };
2364                        if saved >= 0 {
2365                            saved_fds.push((fd, saved));
2366                        }
2367                    }
2368                    if let Ok(file) = std::fs::File::create(&target) {
2369                        let new_fd = file.into_raw_fd();
2370                        unsafe {
2371                            libc::dup2(new_fd, fd);
2372                        }
2373                        unsafe {
2374                            libc::close(new_fd);
2375                        }
2376                    }
2377                }
2378                RedirectOp::Append => {
2379                    use std::os::unix::io::IntoRawFd;
2380                    if !is_exec_with_redirects_only {
2381                        let saved = unsafe { libc::dup(fd) };
2382                        if saved >= 0 {
2383                            saved_fds.push((fd, saved));
2384                        }
2385                    }
2386                    if let Ok(file) = std::fs::OpenOptions::new()
2387                        .create(true)
2388                        .append(true)
2389                        .open(&target)
2390                    {
2391                        let new_fd = file.into_raw_fd();
2392                        unsafe {
2393                            libc::dup2(new_fd, fd);
2394                        }
2395                        unsafe {
2396                            libc::close(new_fd);
2397                        }
2398                    }
2399                }
2400                RedirectOp::Read => {
2401                    use std::os::unix::io::IntoRawFd;
2402                    if !is_exec_with_redirects_only {
2403                        let saved = unsafe { libc::dup(fd) };
2404                        if saved >= 0 {
2405                            saved_fds.push((fd, saved));
2406                        }
2407                    }
2408                    if let Ok(file) = std::fs::File::open(&target) {
2409                        let new_fd = file.into_raw_fd();
2410                        unsafe {
2411                            libc::dup2(new_fd, fd);
2412                        }
2413                        unsafe {
2414                            libc::close(new_fd);
2415                        }
2416                    }
2417                }
2418                RedirectOp::DupWrite | RedirectOp::DupRead => {
2419                    if let Ok(target_fd) = target.parse::<i32>() {
2420                        if !is_exec_with_redirects_only {
2421                            let saved = unsafe { libc::dup(fd) };
2422                            if saved >= 0 {
2423                                saved_fds.push((fd, saved));
2424                            }
2425                        }
2426                        unsafe {
2427                            libc::dup2(target_fd, fd);
2428                        }
2429                    }
2430                }
2431                _ => {}
2432            }
2433        }
2434
2435        // For exec with only redirects, we're done - redirects are applied permanently
2436        if is_exec_with_redirects_only {
2437            self.last_status = 0;
2438            return Ok(0);
2439        }
2440
2441        // Check for shell builtins
2442        let status = match cmd_name.as_str() {
2443            "cd" => self.builtin_cd(args),
2444            "pwd" => self.builtin_pwd(&cmd.redirects),
2445            "echo" => self.builtin_echo(args, &cmd.redirects),
2446            "export" => self.builtin_export(args),
2447            "unset" => self.builtin_unset(args),
2448            "source" | "." => self.builtin_source(args),
2449            "exit" | "bye" | "logout" => self.builtin_exit(args),
2450            "return" => self.builtin_return(args),
2451            "true" => 0,
2452            "false" => 1,
2453            ":" => 0,
2454            "chdir" => self.builtin_cd(args),
2455            "test" | "[" => self.builtin_test(args),
2456            "local" => self.builtin_local(args),
2457            "declare" | "typeset" => self.builtin_declare(args),
2458            "read" => self.builtin_read(args),
2459            "shift" => self.builtin_shift(args),
2460            "eval" => self.builtin_eval(args),
2461            "jobs" => self.builtin_jobs(args),
2462            "fg" => self.builtin_fg(args),
2463            "bg" => self.builtin_bg(args),
2464            "kill" => self.builtin_kill(args),
2465            "disown" => self.builtin_disown(args),
2466            "wait" => self.builtin_wait(args),
2467            "autoload" => self.builtin_autoload(args),
2468            "history" => self.builtin_history(args),
2469            "fc" => self.builtin_fc(args),
2470            "trap" => self.builtin_trap(args),
2471            "suspend" => self.builtin_suspend(args),
2472            "alias" => self.builtin_alias(args),
2473            "unalias" => self.builtin_unalias(args),
2474            "set" => self.builtin_set(args),
2475            "shopt" => self.builtin_shopt(args),
2476            // Bash compatibility
2477            "bind" => self.builtin_bindkey(args),
2478            "caller" => self.builtin_caller(args),
2479            "help" => self.builtin_help(args),
2480            "doctor" => self.builtin_doctor(args),
2481            "dbview" => self.builtin_dbview(args),
2482            "profile" => self.builtin_profile(args),
2483            "intercept" => self.builtin_intercept(args),
2484            "intercept_proceed" => self.builtin_intercept_proceed(args),
2485            // ── Concurrent primitives ──
2486            "async" => self.builtin_async(args),
2487            "await" => self.builtin_await(args),
2488            "pmap" => self.builtin_pmap(args),
2489            "pgrep" => self.builtin_pgrep(args),
2490            "peach" => self.builtin_peach(args),
2491            "barrier" => self.builtin_barrier(args),
2492            "readarray" | "mapfile" => self.builtin_readarray(args),
2493            "setopt" => self.builtin_setopt(args),
2494            "unsetopt" => self.builtin_unsetopt(args),
2495            "getopts" => self.builtin_getopts(args),
2496            "type" => self.builtin_type(args),
2497            "hash" => self.builtin_hash(args),
2498            "add-zsh-hook" => self.builtin_add_zsh_hook(args),
2499            "command" => self.builtin_command(args, &cmd.redirects),
2500            "builtin" => self.builtin_builtin(args, &cmd.redirects),
2501            "let" => self.builtin_let(args),
2502            "compgen" => self.builtin_compgen(args),
2503            "complete" => self.builtin_complete(args),
2504            "compopt" => self.builtin_compopt(args),
2505            "compadd" => self.builtin_compadd(args),
2506            "compset" => self.builtin_compset(args),
2507            "compdef" => self.builtin_compdef(args),
2508            "compinit" => self.builtin_compinit(args),
2509            "cdreplay" => self.builtin_cdreplay(args),
2510            "zstyle" => self.builtin_zstyle(args),
2511            // GDBM database bindings
2512            "ztie" => self.builtin_ztie(args),
2513            "zuntie" => self.builtin_zuntie(args),
2514            "zgdbmpath" => self.builtin_zgdbmpath(args),
2515            "pushd" => self.builtin_pushd(args),
2516            "popd" => self.builtin_popd(args),
2517            "dirs" => self.builtin_dirs(args),
2518            "printf" => self.builtin_printf(args),
2519            // Control flow
2520            "break" => self.builtin_break(args),
2521            "continue" => self.builtin_continue(args),
2522            // Enable/disable builtins
2523            "disable" => self.builtin_disable(args),
2524            "enable" => self.builtin_enable(args),
2525            // Emulation
2526            "emulate" => self.builtin_emulate(args),
2527            // Prompt themes
2528            "promptinit" => self.builtin_promptinit(args),
2529            "prompt" => self.builtin_prompt(args),
2530            // PCRE
2531            "pcre_compile" => self.builtin_pcre_compile(args),
2532            "pcre_match" => self.builtin_pcre_match(args),
2533            "pcre_study" => self.builtin_pcre_study(args),
2534            // Exec
2535            "exec" => self.builtin_exec(args),
2536            // Typed variables
2537            "float" => self.builtin_float(args),
2538            "integer" => self.builtin_integer(args),
2539            // Functions
2540            "functions" => self.builtin_functions(args),
2541            // Print (zsh style)
2542            "print" => self.builtin_print(args),
2543            // Command lookup
2544            "whence" => self.builtin_whence(args),
2545            "where" => self.builtin_where(args),
2546            "which" => self.builtin_which(args),
2547            // Resource limits
2548            "ulimit" => self.builtin_ulimit(args),
2549            "limit" => self.builtin_limit(args),
2550            "unlimit" => self.builtin_unlimit(args),
2551            // File mask
2552            "umask" => self.builtin_umask(args),
2553            // Hash table
2554            "rehash" => self.builtin_rehash(args),
2555            "unhash" => self.builtin_unhash(args),
2556            // Times
2557            "times" => self.builtin_times(args),
2558            // Module loading (stub)
2559            "zmodload" => self.builtin_zmodload(args),
2560            // Redo
2561            "r" => self.builtin_r(args),
2562            // TTY control
2563            "ttyctl" => self.builtin_ttyctl(args),
2564            // Noglob
2565            "noglob" => self.builtin_noglob(args, &cmd.redirects),
2566            // zsh/stat module
2567            "zstat" | "stat" => self.builtin_zstat(args),
2568            // zsh/datetime module
2569            "strftime" => self.builtin_strftime(args),
2570            // sleep with fractional seconds
2571            "zsleep" => self.builtin_zsleep(args),
2572            // zsh/system module - ported from Src/Modules/system.c
2573            "zsystem" => self.builtin_zsystem(args),
2574            // zsh/files module - ported from Src/Modules/files.c
2575            "sync" => self.builtin_sync(args),
2576            "mkdir" => self.builtin_mkdir(args),
2577            "rmdir" => self.builtin_rmdir(args),
2578            "ln" => self.builtin_ln(args),
2579            "mv" => self.builtin_mv(args),
2580            "cp" => self.builtin_cp(args),
2581            "rm" => self.builtin_rm(args),
2582            "chown" => self.builtin_chown(args),
2583            "chmod" => self.builtin_chmod(args),
2584            "zln" | "zmv" | "zcp" => self.builtin_zfiles(cmd_name, args),
2585            // coproc management
2586            "coproc" => self.builtin_coproc(args),
2587            // zparseopts - option parsing
2588            "zparseopts" => self.builtin_zparseopts(args),
2589            // readonly/unfunction
2590            "readonly" => self.builtin_readonly(args),
2591            "unfunction" => self.builtin_unfunction(args),
2592            // getln/pushln
2593            "getln" => self.builtin_getln(args),
2594            "pushln" => self.builtin_pushln(args),
2595            // bindkey stub
2596            "bindkey" => self.builtin_bindkey(args),
2597            // zle stub
2598            "zle" => self.builtin_zle(args),
2599            // sched
2600            "sched" => self.builtin_sched(args),
2601            // zformat
2602            "zformat" => self.builtin_zformat(args),
2603            // zcompile
2604            "zcompile" => self.builtin_zcompile(args),
2605            // vared - visual edit
2606            "vared" => self.builtin_vared(args),
2607            // terminal capabilities
2608            "echotc" => self.builtin_echotc(args),
2609            "echoti" => self.builtin_echoti(args),
2610            // PTY and socket operations
2611            "zpty" => self.builtin_zpty(args),
2612            "zprof" => self.builtin_zprof(args),
2613            "zsocket" => self.builtin_zsocket(args),
2614            "ztcp" => self.builtin_ztcp(args),
2615            "zregexparse" => self.builtin_zregexparse(args),
2616            "clone" => self.builtin_clone(args),
2617            "log" => self.builtin_log(args),
2618            // Completion system builtins
2619            "comparguments" => self.builtin_comparguments(args),
2620            "compcall" => self.builtin_compcall(args),
2621            "compctl" => self.builtin_compctl(args),
2622            "compdescribe" => self.builtin_compdescribe(args),
2623            "compfiles" => self.builtin_compfiles(args),
2624            "compgroups" => self.builtin_compgroups(args),
2625            "compquote" => self.builtin_compquote(args),
2626            "comptags" => self.builtin_comptags(args),
2627            "comptry" => self.builtin_comptry(args),
2628            "compvalues" => self.builtin_compvalues(args),
2629            // Capabilities (Linux-specific, stubs on macOS)
2630            "cap" | "getcap" | "setcap" => self.builtin_cap(args),
2631            // FTP client
2632            "zftp" => self.builtin_zftp(args),
2633            // zsh/curses module
2634            "zcurses" => self.builtin_zcurses(args),
2635            // zsh/system module
2636            "sysread" => self.builtin_sysread(args),
2637            "syswrite" => self.builtin_syswrite(args),
2638            "syserror" => self.builtin_syserror(args),
2639            "sysopen" => self.builtin_sysopen(args),
2640            "sysseek" => self.builtin_sysseek(args),
2641            // zsh/mapfile module
2642            "mapfile" => 0, // mapfile is a special parameter, not a command
2643            // zsh/param/private
2644            "private" => self.builtin_private(args),
2645            // zsh/attr (extended attributes)
2646            "zgetattr" | "zsetattr" | "zdelattr" | "zlistattr" => {
2647                self.builtin_zattr(cmd_name, args)
2648            }
2649            // Completion helper functions (now implemented in Rust compsys crate)
2650            // These are stubs that return success during non-completion execution
2651            "_arguments" | "_describe" | "_description" | "_message" | "_tags" | "_requested"
2652            | "_all_labels" | "_next_label" | "_files" | "_path_files" | "_directories" | "_cd"
2653            | "_default" | "_dispatch" | "_complete" | "_main_complete" | "_normal"
2654            | "_approximate" | "_correct" | "_expand" | "_history" | "_match" | "_menu"
2655            | "_oldlist" | "_list" | "_prefix" | "_generic" | "_wanted" | "_alternative"
2656            | "_values" | "_sequence" | "_sep_parts" | "_multi_parts" | "_combination"
2657            | "_parameters" | "_command" | "_command_names" | "_commands" | "_functions"
2658            | "_aliases" | "_builtins" | "_jobs" | "_pids" | "_process_names" | "_signals"
2659            | "_users" | "_groups" | "_hosts" | "_domains" | "_urls" | "_email_addresses"
2660            | "_options" | "_contexts" | "_set_options" | "_unset_options" | "_vars"
2661            | "_env_variables" | "_shell_variables" | "_arrays" | "_globflags" | "_globquals"
2662            | "_globqual_delims" | "_subscript" | "_history_modifiers" | "_brace_parameter"
2663            | "_tilde" | "_style" | "_cache_invalid" | "_store_cache" | "_retrieve_cache"
2664            | "_call_function" | "_call_program" | "_pick_variant" | "_setup"
2665            | "_comp_priv_prefix" | "_regex_arguments" | "_regex_words" | "_guard"
2666            | "_gnu_generic" | "_long_options" | "_x_arguments" | "_sub_commands"
2667            | "_cmdstring" | "_cmdambivalent" | "_first" | "_precommand" | "_user_at_host"
2668            | "_user_expand" | "_path_commands" | "_globbed_files" | "_have_glob_qual" => {
2669                // Return success - these functions are for completion context only
2670                // The actual completion logic is in the compsys Rust crate
2671                0
2672            }
2673            _ => {
2674                // ── AOP intercept dispatch ──
2675                // Check if any intercepts match this command name.
2676                // Fast path: skip if no intercepts registered.
2677                if !self.intercepts.is_empty() {
2678                    let full_cmd = if args.is_empty() {
2679                        cmd_name.to_string()
2680                    } else {
2681                        format!("{} {}", cmd_name, args.join(" "))
2682                    };
2683                    if let Some(result) = self.run_intercepts(cmd_name, &full_cmd, args) {
2684                        return result;
2685                    }
2686                }
2687
2688                // Check for function
2689                if let Some(func) = self.functions.get(cmd_name).cloned() {
2690                    return self.call_function(&func, args);
2691                }
2692
2693                // Try autoloading from pending autoload list
2694                if self.maybe_autoload(cmd_name) {
2695                    if let Some(func) = self.functions.get(cmd_name).cloned() {
2696                        return self.call_function(&func, args);
2697                    }
2698                }
2699
2700                // Try autoloading from ZWC
2701                if self.autoload_function(cmd_name).is_some() {
2702                    if let Some(func) = self.functions.get(cmd_name).cloned() {
2703                        return self.call_function(&func, args);
2704                    }
2705                }
2706
2707                // External command
2708                self.execute_external(cmd_name, args, &cmd.redirects)?
2709            }
2710        };
2711
2712        // Restore saved fds
2713        for (fd, saved) in saved_fds.into_iter().rev() {
2714            unsafe {
2715                libc::dup2(saved, fd);
2716                libc::close(saved);
2717            }
2718        }
2719
2720        self.last_status = status;
2721        Ok(status)
2722    }
2723
2724    /// Call a function with positional parameters
2725    #[tracing::instrument(level = "debug", skip_all)]
2726    fn call_function(&mut self, func: &ShellCommand, args: &[String]) -> Result<i32, String> {
2727        // Save current positional params
2728        let saved_params = std::mem::take(&mut self.positional_params);
2729
2730        // Save local variable scope — any `local` declarations during this
2731        // function will be reversed on exit (matches zsh's startparamscope/endparamscope).
2732        let saved_local_vars = self.local_save_stack.len();
2733        self.local_scope_depth += 1;
2734
2735        // Set new positional params
2736        self.positional_params = args.to_vec();
2737
2738        // Execute the function
2739        let result = self.execute_command(func);
2740
2741        // Handle return - clear the flag and use its value
2742        let final_result = if let Some(ret) = self.returning.take() {
2743            self.last_status = ret;
2744            Ok(ret)
2745        } else {
2746            result
2747        };
2748
2749        // Restore local variables (endparamscope)
2750        self.local_scope_depth -= 1;
2751        while self.local_save_stack.len() > saved_local_vars {
2752            if let Some((name, old_val)) = self.local_save_stack.pop() {
2753                match old_val {
2754                    Some(v) => { self.variables.insert(name, v); }
2755                    None => { self.variables.remove(&name); }
2756                }
2757            }
2758        }
2759
2760        // Restore positional params
2761        self.positional_params = saved_params;
2762
2763        final_result
2764    }
2765
2766    fn execute_external(
2767        &mut self,
2768        cmd: &str,
2769        args: &[String],
2770        redirects: &[Redirect],
2771    ) -> Result<i32, String> {
2772        self.execute_external_bg(cmd, args, redirects, false)
2773    }
2774
2775    fn execute_external_bg(
2776        &mut self,
2777        cmd: &str,
2778        args: &[String],
2779        redirects: &[Redirect],
2780        background: bool,
2781    ) -> Result<i32, String> {
2782        tracing::trace!(cmd, bg = background, "exec external");
2783        let mut command = Command::new(cmd);
2784        command.args(args);
2785
2786        // Apply redirections
2787        for redir in redirects {
2788            let target = self.expand_word(&redir.target);
2789            match redir.op {
2790                RedirectOp::Read => match File::open(&target) {
2791                    Ok(f) => {
2792                        command.stdin(Stdio::from(f));
2793                    }
2794                    Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
2795                },
2796                RedirectOp::Write => match File::create(&target) {
2797                    Ok(f) => {
2798                        command.stdout(Stdio::from(f));
2799                    }
2800                    Err(e) => return Err(format!("Cannot create {}: {}", target, e)),
2801                },
2802                RedirectOp::Append => {
2803                    match OpenOptions::new().create(true).append(true).open(&target) {
2804                        Ok(f) => {
2805                            command.stdout(Stdio::from(f));
2806                        }
2807                        Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
2808                    }
2809                }
2810                RedirectOp::WriteBoth => match File::create(&target) {
2811                    Ok(f) => {
2812                        let f2 = f
2813                            .try_clone()
2814                            .map_err(|e| format!("Cannot clone fd: {}", e))?;
2815                        command.stdout(Stdio::from(f));
2816                        command.stderr(Stdio::from(f2));
2817                    }
2818                    Err(e) => return Err(format!("Cannot create {}: {}", target, e)),
2819                },
2820                RedirectOp::AppendBoth => {
2821                    match OpenOptions::new().create(true).append(true).open(&target) {
2822                        Ok(f) => {
2823                            let f2 = f
2824                                .try_clone()
2825                                .map_err(|e| format!("Cannot clone fd: {}", e))?;
2826                            command.stdout(Stdio::from(f));
2827                            command.stderr(Stdio::from(f2));
2828                        }
2829                        Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
2830                    }
2831                }
2832                RedirectOp::HereDoc => {
2833                    // Here-document - provide content as stdin
2834                    if let Some(ref content) = redir.heredoc_content {
2835                        // Expand variables in content (unless delimiter was quoted)
2836                        let expanded = self.expand_string(content);
2837                        command.stdin(Stdio::piped());
2838                        // Store the content to write after spawn
2839                        // For now, create a temp file
2840                        use std::io::Write;
2841                        let mut temp_file = tempfile::NamedTempFile::new()
2842                            .map_err(|e| format!("Cannot create temp file: {}", e))?;
2843                        temp_file
2844                            .write_all(expanded.as_bytes())
2845                            .map_err(|e| format!("Cannot write to temp file: {}", e))?;
2846                        let temp_path = temp_file.into_temp_path();
2847                        let f = File::open(&temp_path)
2848                            .map_err(|e| format!("Cannot open temp file: {}", e))?;
2849                        command.stdin(Stdio::from(f));
2850                    }
2851                }
2852                RedirectOp::HereString => {
2853                    // Here-string - provide target as stdin
2854                    use std::io::Write;
2855                    let content = format!("{}\n", target);
2856                    let mut temp_file = tempfile::NamedTempFile::new()
2857                        .map_err(|e| format!("Cannot create temp file: {}", e))?;
2858                    temp_file
2859                        .write_all(content.as_bytes())
2860                        .map_err(|e| format!("Cannot write to temp file: {}", e))?;
2861                    let temp_path = temp_file.into_temp_path();
2862                    let f = File::open(&temp_path)
2863                        .map_err(|e| format!("Cannot open temp file: {}", e))?;
2864                    command.stdin(Stdio::from(f));
2865                }
2866                _ => {
2867                    // Other redirections handled simply
2868                }
2869            }
2870
2871            // Handle {varname}>file syntax - store FD in variable
2872            if let Some(ref var_name) = redir.fd_var {
2873                // For {varname}>file, we open the file and store the fd number
2874                // This is typically used with exec, but we'll handle it for commands too
2875                #[cfg(unix)]
2876                {
2877                    use std::os::unix::io::AsRawFd;
2878                    let fd = match redir.op {
2879                        RedirectOp::Write | RedirectOp::Append => {
2880                            let f = if redir.op == RedirectOp::Write {
2881                                File::create(&target)
2882                            } else {
2883                                OpenOptions::new().create(true).append(true).open(&target)
2884                            };
2885                            match f {
2886                                Ok(file) => {
2887                                    let raw_fd = file.as_raw_fd();
2888                                    std::mem::forget(file); // Don't close the file
2889                                    raw_fd
2890                                }
2891                                Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
2892                            }
2893                        }
2894                        RedirectOp::Read => match File::open(&target) {
2895                            Ok(file) => {
2896                                let raw_fd = file.as_raw_fd();
2897                                std::mem::forget(file);
2898                                raw_fd
2899                            }
2900                            Err(e) => return Err(format!("Cannot open {}: {}", target, e)),
2901                        },
2902                        _ => continue,
2903                    };
2904                    self.variables.insert(var_name.clone(), fd.to_string());
2905                }
2906            }
2907        }
2908
2909        if background {
2910            match command.spawn() {
2911                Ok(child) => {
2912                    let pid = child.id();
2913                    let cmd_str = format!("{} {}", cmd, args.join(" "));
2914                    let job_id = self.jobs.add_job(child, cmd_str, JobState::Running);
2915                    println!("[{}] {}", job_id, pid);
2916                    Ok(0)
2917                }
2918                Err(e) => {
2919                    if e.kind() == io::ErrorKind::NotFound {
2920                        eprintln!("zshrs: command not found: {}", cmd);
2921                        Ok(127)
2922                    } else {
2923                        Err(format!("zshrs: {}: {}", cmd, e))
2924                    }
2925                }
2926            }
2927        } else {
2928            match command.status() {
2929                Ok(status) => Ok(status.code().unwrap_or(1)),
2930                Err(e) => {
2931                    if e.kind() == io::ErrorKind::NotFound {
2932                        eprintln!("zshrs: command not found: {}", cmd);
2933                        Ok(127)
2934                    } else {
2935                        Err(format!("zshrs: {}: {}", cmd, e))
2936                    }
2937                }
2938            }
2939        }
2940    }
2941
2942    #[tracing::instrument(level = "trace", skip_all, fields(stages = cmds.len()))]
2943    fn execute_pipeline(&mut self, cmds: &[ShellCommand]) -> Result<i32, String> {
2944        if cmds.len() == 1 {
2945            return self.execute_command(&cmds[0]);
2946        }
2947
2948        let mut children: Vec<Child> = Vec::new();
2949        let mut prev_stdout: Option<std::process::ChildStdout> = None;
2950
2951        for (i, cmd) in cmds.iter().enumerate() {
2952            if let ShellCommand::Simple(simple) = cmd {
2953                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
2954                if words.is_empty() {
2955                    continue;
2956                }
2957
2958                let mut command = Command::new(&words[0]);
2959                command.args(&words[1..]);
2960
2961                if let Some(stdout) = prev_stdout.take() {
2962                    command.stdin(Stdio::from(stdout));
2963                }
2964
2965                if i < cmds.len() - 1 {
2966                    command.stdout(Stdio::piped());
2967                }
2968
2969                match command.spawn() {
2970                    Ok(mut child) => {
2971                        prev_stdout = child.stdout.take();
2972                        children.push(child);
2973                    }
2974                    Err(e) => {
2975                        eprintln!("zshrs: {}: {}", words[0], e);
2976                        return Ok(127);
2977                    }
2978                }
2979            }
2980        }
2981
2982        // Wait for all children
2983        let mut last_status = 0;
2984        for mut child in children {
2985            if let Ok(status) = child.wait() {
2986                last_status = status.code().unwrap_or(1);
2987            }
2988        }
2989
2990        Ok(last_status)
2991    }
2992
2993    fn execute_list(&mut self, items: &[(ShellCommand, ListOp)]) -> Result<i32, String> {
2994        for (cmd, op) in items {
2995            // Check if this command should run in background
2996            let background = matches!(op, ListOp::Amp);
2997
2998            let status = if background {
2999                self.execute_command_bg(cmd)?
3000            } else {
3001                self.execute_command(cmd)?
3002            };
3003
3004            // Check for control flow
3005            if self.returning.is_some() || self.breaking > 0 || self.continuing > 0 {
3006                return Ok(status);
3007            }
3008
3009            match op {
3010                ListOp::And => {
3011                    if status != 0 {
3012                        return Ok(status);
3013                    }
3014                }
3015                ListOp::Or => {
3016                    if status == 0 {
3017                        return Ok(0);
3018                    }
3019                }
3020                ListOp::Amp => {
3021                    // Already backgrounded above, continue
3022                }
3023                ListOp::Semi | ListOp::Newline => {
3024                    // Sequential, continue
3025                }
3026            }
3027        }
3028
3029        Ok(self.last_status)
3030    }
3031
3032    fn execute_command_bg(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
3033        // For simple commands, run in background
3034        if let ShellCommand::Simple(simple) = cmd {
3035            if simple.words.is_empty() {
3036                return Ok(0);
3037            }
3038            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
3039            let cmd_name = &words[0];
3040            let args: Vec<String> = words[1..].to_vec();
3041            return self.execute_external_bg(cmd_name, &args, &simple.redirects, true);
3042        }
3043        // For complex commands, just execute normally (could fork in future)
3044        self.execute_command(cmd)
3045    }
3046
3047    #[tracing::instrument(level = "trace", skip_all)]
3048    fn execute_compound(&mut self, compound: &CompoundCommand) -> Result<i32, String> {
3049        match compound {
3050            CompoundCommand::BraceGroup(cmds) => {
3051                for cmd in cmds {
3052                    self.execute_command(cmd)?;
3053                    if self.returning.is_some() {
3054                        break;
3055                    }
3056                }
3057                Ok(self.last_status)
3058            }
3059            CompoundCommand::Subshell(cmds) => {
3060                // Subshell isolates variable changes — save/restore all state.
3061                // In real zsh this forks; we simulate by cloning variables.
3062                let saved_vars = self.variables.clone();
3063                let saved_arrays = self.arrays.clone();
3064                let saved_assoc = self.assoc_arrays.clone();
3065                let saved_params = self.positional_params.clone();
3066
3067                for cmd in cmds {
3068                    self.execute_command(cmd)?;
3069                    if self.returning.is_some() {
3070                        break;
3071                    }
3072                }
3073                let status = self.last_status;
3074
3075                // Restore state — subshell changes are discarded
3076                self.variables = saved_vars;
3077                self.arrays = saved_arrays;
3078                self.assoc_arrays = saved_assoc;
3079                self.positional_params = saved_params;
3080                self.last_status = status;
3081
3082                Ok(status)
3083            }
3084
3085            CompoundCommand::If {
3086                conditions,
3087                else_part,
3088            } => {
3089                for (cond, body) in conditions {
3090                    // Execute condition
3091                    for cmd in cond {
3092                        self.execute_command(cmd)?;
3093                    }
3094
3095                    if self.last_status == 0 {
3096                        // Condition true, execute body
3097                        for cmd in body {
3098                            self.execute_command(cmd)?;
3099                        }
3100                        return Ok(self.last_status);
3101                    }
3102                }
3103
3104                // All conditions false, execute else
3105                if let Some(else_cmds) = else_part {
3106                    for cmd in else_cmds {
3107                        self.execute_command(cmd)?;
3108                    }
3109                }
3110
3111                Ok(self.last_status)
3112            }
3113
3114            CompoundCommand::For { var, words, body } => {
3115                let items: Vec<String> = if let Some(words) = words {
3116                    words
3117                        .iter()
3118                        .flat_map(|w| self.expand_word_split(w))
3119                        .collect()
3120                } else {
3121                    // Iterate over positional parameters
3122                    self.positional_params.clone()
3123                };
3124
3125                for item in items {
3126                    env::set_var(var, &item);
3127                    self.variables.insert(var.clone(), item);
3128
3129                    for cmd in body {
3130                        self.execute_command(cmd)?;
3131                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
3132                            break;
3133                        }
3134                    }
3135
3136                    if self.continuing > 0 {
3137                        self.continuing -= 1;
3138                        if self.continuing > 0 {
3139                            break;
3140                        }
3141                        continue;
3142                    }
3143                    if self.breaking > 0 {
3144                        self.breaking -= 1;
3145                        break;
3146                    }
3147                    if self.returning.is_some() {
3148                        break;
3149                    }
3150                }
3151
3152                Ok(self.last_status)
3153            }
3154
3155            CompoundCommand::ForArith {
3156                init,
3157                cond,
3158                step,
3159                body,
3160            } => {
3161                // C-style for loop: for ((init; cond; step))
3162                // Execute init expression (use evaluate_arithmetic_expr for assignment support)
3163                if !init.is_empty() {
3164                    self.evaluate_arithmetic_expr(init);
3165                }
3166
3167                // Loop while condition is true
3168                loop {
3169                    // Evaluate condition (use eval_arith_expr for comparison result)
3170                    if !cond.is_empty() {
3171                        let cond_result = self.eval_arith_expr(cond);
3172                        if cond_result == 0 {
3173                            break;
3174                        }
3175                    }
3176
3177                    // Execute body
3178                    for cmd in body {
3179                        self.execute_command(cmd)?;
3180                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
3181                            break;
3182                        }
3183                    }
3184
3185                    if self.continuing > 0 {
3186                        self.continuing -= 1;
3187                        if self.continuing > 0 {
3188                            break;
3189                        }
3190                        continue;
3191                    }
3192                    if self.breaking > 0 {
3193                        self.breaking -= 1;
3194                        break;
3195                    }
3196                    if self.returning.is_some() {
3197                        break;
3198                    }
3199
3200                    // Execute step (use evaluate_arithmetic_expr for assignment support like i++)
3201                    if !step.is_empty() {
3202                        self.evaluate_arithmetic_expr(step);
3203                    }
3204                }
3205                Ok(self.last_status)
3206            }
3207
3208            CompoundCommand::While { condition, body } => {
3209                loop {
3210                    for cmd in condition {
3211                        self.execute_command(cmd)?;
3212                        if self.breaking > 0 || self.returning.is_some() {
3213                            break;
3214                        }
3215                    }
3216
3217                    if self.last_status != 0 || self.breaking > 0 || self.returning.is_some() {
3218                        break;
3219                    }
3220
3221                    for cmd in body {
3222                        self.execute_command(cmd)?;
3223                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
3224                            break;
3225                        }
3226                    }
3227
3228                    if self.continuing > 0 {
3229                        self.continuing -= 1;
3230                        if self.continuing > 0 {
3231                            break;
3232                        }
3233                        continue;
3234                    }
3235                    if self.breaking > 0 {
3236                        self.breaking -= 1;
3237                        break;
3238                    }
3239                }
3240                Ok(self.last_status)
3241            }
3242
3243            CompoundCommand::Until { condition, body } => {
3244                loop {
3245                    for cmd in condition {
3246                        self.execute_command(cmd)?;
3247                        if self.breaking > 0 || self.returning.is_some() {
3248                            break;
3249                        }
3250                    }
3251
3252                    if self.last_status == 0 || self.breaking > 0 || self.returning.is_some() {
3253                        break;
3254                    }
3255
3256                    for cmd in body {
3257                        self.execute_command(cmd)?;
3258                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
3259                            break;
3260                        }
3261                    }
3262
3263                    if self.continuing > 0 {
3264                        self.continuing -= 1;
3265                        if self.continuing > 0 {
3266                            break;
3267                        }
3268                        continue;
3269                    }
3270                    if self.breaking > 0 {
3271                        self.breaking -= 1;
3272                        break;
3273                    }
3274                }
3275                Ok(self.last_status)
3276            }
3277
3278            CompoundCommand::Case { word, cases } => {
3279                let value = self.expand_word(word);
3280
3281                for (patterns, body, term) in cases {
3282                    for pattern in patterns {
3283                        let pat = self.expand_word(pattern);
3284                        if self.matches_pattern(&value, &pat) {
3285                            for cmd in body {
3286                                self.execute_command(cmd)?;
3287                            }
3288
3289                            match term {
3290                                CaseTerminator::Break => return Ok(self.last_status),
3291                                CaseTerminator::Fallthrough => {
3292                                    // Continue to next case body
3293                                }
3294                                CaseTerminator::Continue => {
3295                                    // Continue pattern matching
3296                                    break;
3297                                }
3298                            }
3299                        }
3300                    }
3301                }
3302
3303                Ok(self.last_status)
3304            }
3305
3306            CompoundCommand::Select { var, words, body } => {
3307                // Simplified: just use first word
3308                if let Some(words) = words {
3309                    if let Some(first) = words.first() {
3310                        let val = self.expand_word(first);
3311                        env::set_var(var, &val);
3312                        for cmd in body {
3313                            self.execute_command(cmd)?;
3314                        }
3315                    }
3316                }
3317                Ok(self.last_status)
3318            }
3319
3320            CompoundCommand::Repeat { count, body } => {
3321                let n: i64 = self
3322                    .expand_word(&ShellWord::Literal(count.clone()))
3323                    .parse()
3324                    .unwrap_or(0);
3325
3326                for _ in 0..n {
3327                    for cmd in body {
3328                        self.execute_command(cmd)?;
3329                        if self.breaking > 0 || self.continuing > 0 || self.returning.is_some() {
3330                            break;
3331                        }
3332                    }
3333
3334                    if self.continuing > 0 {
3335                        self.continuing -= 1;
3336                        if self.continuing > 0 {
3337                            break;
3338                        }
3339                        continue;
3340                    }
3341                    if self.breaking > 0 {
3342                        self.breaking -= 1;
3343                        break;
3344                    }
3345                    if self.returning.is_some() {
3346                        break;
3347                    }
3348                }
3349
3350                Ok(self.last_status)
3351            }
3352
3353            CompoundCommand::Try {
3354                try_body,
3355                always_body,
3356            } => {
3357                // Port of exectry() from Src/loop.c
3358                // The :try clause
3359                for cmd in try_body {
3360                    if let Err(_e) = self.execute_command(cmd) {
3361                        break;
3362                    }
3363                    if self.returning.is_some() {
3364                        break;
3365                    }
3366                }
3367
3368                // endval = lastval ? lastval : errflag
3369                let endval = self.last_status;
3370
3371                // Save and reset control flow flags for the always clause
3372                let save_returning = self.returning.take();
3373                let save_breaking = self.breaking;
3374                let save_continuing = self.continuing;
3375                self.breaking = 0;
3376                self.continuing = 0;
3377
3378                // The always clause — executes unconditionally
3379                for cmd in always_body {
3380                    let _ = self.execute_command(cmd);
3381                }
3382
3383                // Restore control flow: C uses "if (!retflag) retflag = save"
3384                // i.e. always block's flags take precedence if set
3385                if self.returning.is_none() {
3386                    self.returning = save_returning;
3387                }
3388                if self.breaking == 0 {
3389                    self.breaking = save_breaking;
3390                }
3391                if self.continuing == 0 {
3392                    self.continuing = save_continuing;
3393                }
3394
3395                self.last_status = endval;
3396                Ok(endval)
3397            }
3398
3399            CompoundCommand::Cond(expr) => {
3400                let result = self.eval_cond_expr(expr);
3401                self.last_status = if result { 0 } else { 1 };
3402                Ok(self.last_status)
3403            }
3404
3405            CompoundCommand::Arith(expr) => {
3406                // Evaluate arithmetic expression and set variables
3407                let result = self.evaluate_arithmetic_expr(expr);
3408                // (( )) returns 0 if result is non-zero, 1 if result is zero
3409                self.last_status = if result != 0 { 0 } else { 1 };
3410                Ok(self.last_status)
3411            }
3412
3413            CompoundCommand::Coproc { name, body } => {
3414                // Create pipes for stdin and stdout
3415                let (stdin_read, stdin_write) =
3416                    os_pipe::pipe().map_err(|e| format!("Cannot create pipe: {}", e))?;
3417                let (stdout_read, stdout_write) =
3418                    os_pipe::pipe().map_err(|e| format!("Cannot create pipe: {}", e))?;
3419
3420                // Get the command to run
3421                let cmd_str = match body.as_ref() {
3422                    ShellCommand::Simple(simple) => simple
3423                        .words
3424                        .iter()
3425                        .map(|w| self.expand_word(w))
3426                        .collect::<Vec<_>>()
3427                        .join(" "),
3428                    ShellCommand::Compound(CompoundCommand::BraceGroup(_cmds)) => {
3429                        // Just run as a subshell with the commands
3430                        // For simplicity, we'll use bash -c
3431                        "bash -c 'true'".to_string()
3432                    }
3433                    _ => "true".to_string(),
3434                };
3435
3436                // Fork and run the command in background with redirected stdin/stdout
3437                let parts: Vec<&str> = cmd_str.split_whitespace().collect();
3438                if parts.is_empty() {
3439                    return Ok(0);
3440                }
3441
3442                let mut command = Command::new(parts[0]);
3443                if parts.len() > 1 {
3444                    command.args(&parts[1..]);
3445                }
3446
3447                use std::os::unix::io::{FromRawFd, IntoRawFd};
3448
3449                command.stdin(unsafe { Stdio::from_raw_fd(stdin_read.into_raw_fd()) });
3450                command.stdout(unsafe { Stdio::from_raw_fd(stdout_write.into_raw_fd()) });
3451
3452                match command.spawn() {
3453                    Ok(child) => {
3454                        let pid = child.id();
3455                        let coproc_name = name.clone().unwrap_or_else(|| "COPROC".to_string());
3456
3457                        // Store file descriptors in environment-like variables
3458                        // COPROC[0] = read from coproc (stdout_read)
3459                        // COPROC[1] = write to coproc (stdin_write)
3460                        let read_fd = stdout_read.into_raw_fd();
3461                        let write_fd = stdin_write.into_raw_fd();
3462
3463                        self.arrays.insert(
3464                            coproc_name.clone(),
3465                            vec![read_fd.to_string(), write_fd.to_string()],
3466                        );
3467
3468                        // Also store PID
3469                        self.variables
3470                            .insert(format!("{}_PID", coproc_name), pid.to_string());
3471
3472                        let cmd_str_clone = cmd_str.clone();
3473                        self.jobs.add_job(child, cmd_str_clone, JobState::Running);
3474
3475                        Ok(0)
3476                    }
3477                    Err(e) => {
3478                        if e.kind() == io::ErrorKind::NotFound {
3479                            eprintln!("zshrs: command not found: {}", parts[0]);
3480                            Ok(127)
3481                        } else {
3482                            Err(format!("zshrs: coproc: {}: {}", parts[0], e))
3483                        }
3484                    }
3485                }
3486            }
3487
3488            CompoundCommand::WithRedirects(cmd, redirects) => {
3489                // Execute the command with redirects applied
3490                let mut saved_fds: Vec<(i32, i32)> = Vec::new();
3491
3492                // Set up redirects
3493                for redirect in redirects {
3494                    let fd = redirect.fd.unwrap_or(match redirect.op {
3495                        RedirectOp::Read
3496                        | RedirectOp::HereDoc
3497                        | RedirectOp::HereString
3498                        | RedirectOp::ReadWrite => 0,
3499                        _ => 1,
3500                    });
3501
3502                    let target = self.expand_word(&redirect.target);
3503
3504                    match redirect.op {
3505                        RedirectOp::Write | RedirectOp::Clobber => {
3506                            use std::os::unix::io::IntoRawFd;
3507                            let saved = unsafe { libc::dup(fd) };
3508                            if saved >= 0 {
3509                                saved_fds.push((fd, saved));
3510                            }
3511                            if let Ok(file) = std::fs::File::create(&target) {
3512                                let new_fd = file.into_raw_fd();
3513                                unsafe {
3514                                    libc::dup2(new_fd, fd);
3515                                }
3516                                unsafe {
3517                                    libc::close(new_fd);
3518                                }
3519                            }
3520                        }
3521                        RedirectOp::Append => {
3522                            use std::os::unix::io::IntoRawFd;
3523                            let saved = unsafe { libc::dup(fd) };
3524                            if saved >= 0 {
3525                                saved_fds.push((fd, saved));
3526                            }
3527                            if let Ok(file) = std::fs::OpenOptions::new()
3528                                .create(true)
3529                                .append(true)
3530                                .open(&target)
3531                            {
3532                                let new_fd = file.into_raw_fd();
3533                                unsafe {
3534                                    libc::dup2(new_fd, fd);
3535                                }
3536                                unsafe {
3537                                    libc::close(new_fd);
3538                                }
3539                            }
3540                        }
3541                        RedirectOp::Read => {
3542                            use std::os::unix::io::IntoRawFd;
3543                            let saved = unsafe { libc::dup(fd) };
3544                            if saved >= 0 {
3545                                saved_fds.push((fd, saved));
3546                            }
3547                            if let Ok(file) = std::fs::File::open(&target) {
3548                                let new_fd = file.into_raw_fd();
3549                                unsafe {
3550                                    libc::dup2(new_fd, fd);
3551                                }
3552                                unsafe {
3553                                    libc::close(new_fd);
3554                                }
3555                            }
3556                        }
3557                        RedirectOp::DupWrite | RedirectOp::DupRead => {
3558                            if let Ok(target_fd) = target.parse::<i32>() {
3559                                let saved = unsafe { libc::dup(fd) };
3560                                if saved >= 0 {
3561                                    saved_fds.push((fd, saved));
3562                                }
3563                                unsafe {
3564                                    libc::dup2(target_fd, fd);
3565                                }
3566                            }
3567                        }
3568                        _ => {}
3569                    }
3570                }
3571
3572                // Execute the inner command
3573                let result = self.execute_command(cmd);
3574
3575                // Restore saved fds
3576                for (fd, saved) in saved_fds.into_iter().rev() {
3577                    unsafe {
3578                        libc::dup2(saved, fd);
3579                        libc::close(saved);
3580                    }
3581                }
3582
3583                result
3584            }
3585        }
3586    }
3587
3588    /// Expand a word with brace and glob expansion (for command arguments)
3589    #[tracing::instrument(level = "trace", skip_all)]
3590    fn expand_word_glob(&mut self, word: &ShellWord) -> Vec<String> {
3591        match word {
3592            ShellWord::SingleQuoted(s) => vec![s.clone()],
3593            ShellWord::DoubleQuoted(parts) => {
3594                // Double quotes prevent glob and brace expansion
3595                vec![parts.iter().map(|p| self.expand_word(p)).collect()]
3596            }
3597            _ => {
3598                let expanded = self.expand_word(word);
3599
3600                // First do brace expansion
3601                let brace_expanded = self.expand_braces(&expanded);
3602
3603                // Then glob expansion on each result (unless noglob is set)
3604                let noglob = self.options.get("noglob").copied().unwrap_or(false)
3605                    || self.options.get("GLOB").map(|v| !v).unwrap_or(false);
3606                brace_expanded
3607                    .into_iter()
3608                    .flat_map(|s| {
3609                        if !noglob
3610                            && (s.contains('*')
3611                                || s.contains('?')
3612                                || s.contains('[')
3613                                || self.has_extglob_pattern(&s))
3614                        {
3615                            self.expand_glob(&s)
3616                        } else {
3617                            vec![s]
3618                        }
3619                    })
3620                    .collect()
3621            }
3622        }
3623    }
3624
3625    /// Expand brace patterns like {a,b,c} and {1..10}
3626    fn expand_braces(&self, s: &str) -> Vec<String> {
3627        // Find a brace pattern
3628        let mut depth = 0;
3629        let mut brace_start = None;
3630
3631        for (i, c) in s.char_indices() {
3632            match c {
3633                '{' => {
3634                    if depth == 0 {
3635                        brace_start = Some(i);
3636                    }
3637                    depth += 1;
3638                }
3639                '}' => {
3640                    depth -= 1;
3641                    if depth == 0 {
3642                        if let Some(start) = brace_start {
3643                            let prefix = &s[..start];
3644                            let content = &s[start + 1..i];
3645                            let suffix = &s[i + 1..];
3646
3647                            // Check if this is a sequence {a..b} or a list {a,b,c}
3648                            let expansions = if content.contains("..") {
3649                                self.expand_brace_sequence(content)
3650                            } else if content.contains(',') {
3651                                self.expand_brace_list(content)
3652                            } else {
3653                                // Not a valid brace expansion, return as-is
3654                                return vec![s.to_string()];
3655                            };
3656
3657                            // Combine prefix, expansions, and suffix
3658                            let mut results = Vec::new();
3659                            for exp in expansions {
3660                                let combined = format!("{}{}{}", prefix, exp, suffix);
3661                                // Recursively expand any remaining braces
3662                                results.extend(self.expand_braces(&combined));
3663                            }
3664                            return results;
3665                        }
3666                    }
3667                }
3668                _ => {}
3669            }
3670        }
3671
3672        // No brace expansion found
3673        vec![s.to_string()]
3674    }
3675
3676    /// Expand comma-separated brace list like {a,b,c}
3677    fn expand_brace_list(&self, content: &str) -> Vec<String> {
3678        // Split by comma, but respect nested braces
3679        let mut parts = Vec::new();
3680        let mut current = String::new();
3681        let mut depth = 0;
3682
3683        for c in content.chars() {
3684            match c {
3685                '{' => {
3686                    depth += 1;
3687                    current.push(c);
3688                }
3689                '}' => {
3690                    depth -= 1;
3691                    current.push(c);
3692                }
3693                ',' if depth == 0 => {
3694                    parts.push(current.clone());
3695                    current.clear();
3696                }
3697                _ => current.push(c),
3698            }
3699        }
3700        parts.push(current);
3701
3702        parts
3703    }
3704
3705    /// Expand sequence brace pattern like {1..10} or {a..z}
3706    fn expand_brace_sequence(&self, content: &str) -> Vec<String> {
3707        let parts: Vec<&str> = content.splitn(3, "..").collect();
3708        if parts.len() < 2 {
3709            return vec![content.to_string()];
3710        }
3711
3712        let start = parts[0];
3713        let end = parts[1];
3714        let step: i64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
3715
3716        // Try numeric sequence
3717        if let (Ok(start_num), Ok(end_num)) = (start.parse::<i64>(), end.parse::<i64>()) {
3718            let mut results = Vec::new();
3719            if start_num <= end_num {
3720                let mut i = start_num;
3721                while i <= end_num {
3722                    results.push(i.to_string());
3723                    i += step;
3724                }
3725            } else {
3726                let mut i = start_num;
3727                while i >= end_num {
3728                    results.push(i.to_string());
3729                    i -= step;
3730                }
3731            }
3732            return results;
3733        }
3734
3735        // Try character sequence
3736        if start.len() == 1 && end.len() == 1 {
3737            let start_char = start.chars().next().unwrap();
3738            let end_char = end.chars().next().unwrap();
3739            let mut results = Vec::new();
3740
3741            if start_char <= end_char {
3742                let mut c = start_char;
3743                while c <= end_char {
3744                    results.push(c.to_string());
3745                    c = (c as u8 + step as u8) as char;
3746                    if c as u8 > end_char as u8 {
3747                        break;
3748                    }
3749                }
3750            } else {
3751                let mut c = start_char;
3752                while c >= end_char {
3753                    results.push(c.to_string());
3754                    if (c as u8) < step as u8 {
3755                        break;
3756                    }
3757                    c = (c as u8 - step as u8) as char;
3758                }
3759            }
3760            return results;
3761        }
3762
3763        vec![content.to_string()]
3764    }
3765
3766    /// Expand glob pattern to matching files
3767    fn expand_glob(&self, pattern: &str) -> Vec<String> {
3768        // Check for zsh glob qualifiers at end: *(.) *(/) *(@) etc.
3769        let (glob_pattern, qualifiers) = self.parse_glob_qualifiers(pattern);
3770
3771        // Check for extended glob patterns: ?(pat), *(pat), +(pat), @(pat), !(pat)
3772        if self.has_extglob_pattern(&glob_pattern) {
3773            let expanded = self.expand_extglob(&glob_pattern);
3774            return self.filter_by_qualifiers(expanded, &qualifiers);
3775        }
3776
3777        let nullglob = self.options.get("nullglob").copied().unwrap_or(false);
3778        let dotglob = self.options.get("dotglob").copied().unwrap_or(false);
3779        let nocaseglob = self.options.get("nocaseglob").copied().unwrap_or(false);
3780
3781        // Parallel recursive glob: when pattern contains **/ we split the
3782        // directory walk across worker pool threads — one thread per top-level
3783        // subdirectory.  zsh does this single-threaded via fork+exec which is
3784        // why `echo **/*.rs` is painfully slow on large trees.
3785        let expanded = if glob_pattern.contains("**/") {
3786            self.expand_glob_parallel(&glob_pattern, dotglob, nocaseglob)
3787        } else {
3788            let options = glob::MatchOptions {
3789                case_sensitive: !nocaseglob,
3790                require_literal_separator: false,
3791                require_literal_leading_dot: !dotglob,
3792            };
3793            match glob::glob_with(&glob_pattern, options) {
3794                Ok(paths) => paths
3795                    .filter_map(|p| p.ok())
3796                    .map(|p| p.to_string_lossy().to_string())
3797                    .collect(),
3798                Err(_) => vec![],
3799            }
3800        };
3801
3802        let mut expanded = self.filter_by_qualifiers(expanded, &qualifiers);
3803        expanded.sort();
3804
3805        if expanded.is_empty() {
3806            if nullglob {
3807                vec![]
3808            } else {
3809                vec![pattern.to_string()]
3810            }
3811        } else {
3812            expanded
3813        }
3814    }
3815
3816    /// Parallel recursive glob using the worker pool.
3817    ///
3818    /// Splits `base/**/file_pattern` into per-subdirectory walks, each
3819    /// running on a pool thread via walkdir.  Results merge via channel.
3820    /// This is why `echo **/*.rs` will be 5-10x faster than zsh.
3821    fn expand_glob_parallel(
3822        &self,
3823        pattern: &str,
3824        dotglob: bool,
3825        nocaseglob: bool,
3826    ) -> Vec<String> {
3827        use walkdir::WalkDir;
3828
3829        // Split pattern at the first **/ into (base_dir, file_glob)
3830        // e.g. "src/**/*.rs" → ("src", "*.rs")
3831        //      "**/*.rs"     → (".", "*.rs")
3832        let (base, file_glob) = if let Some(pos) = pattern.find("**/") {
3833            let base = if pos == 0 { "." } else { &pattern[..pos.saturating_sub(1)] };
3834            let rest = &pattern[pos + 3..]; // skip "**/", get "*.rs" or "foo/**/*.rs"
3835            (base.to_string(), rest.to_string())
3836        } else {
3837            return vec![];
3838        };
3839
3840        // If file_glob itself contains **/, fall back to single-threaded glob
3841        // (nested recursive patterns are rare, not worth the complexity)
3842        if file_glob.contains("**/") {
3843            let options = glob::MatchOptions {
3844                case_sensitive: !nocaseglob,
3845                require_literal_separator: false,
3846                require_literal_leading_dot: !dotglob,
3847            };
3848            return match glob::glob_with(pattern, options) {
3849                Ok(paths) => paths
3850                    .filter_map(|p| p.ok())
3851                    .map(|p| p.to_string_lossy().to_string())
3852                    .collect(),
3853                Err(_) => vec![],
3854            };
3855        }
3856
3857        // Build the glob::Pattern for matching filenames
3858        let match_opts = glob::MatchOptions {
3859            case_sensitive: !nocaseglob,
3860            require_literal_separator: false,
3861            require_literal_leading_dot: !dotglob,
3862        };
3863        let file_pat = match glob::Pattern::new(&file_glob) {
3864            Ok(p) => p,
3865            Err(_) => return vec![],
3866        };
3867
3868        // Enumerate top-level entries in base dir to fan out across workers
3869        let top_entries: Vec<std::path::PathBuf> = match std::fs::read_dir(&base) {
3870            Ok(rd) => rd
3871                .filter_map(|e| e.ok())
3872                .map(|e| e.path())
3873                .collect(),
3874            Err(_) => return vec![],
3875        };
3876
3877        // Also check files directly in base (not in subdirs)
3878        let mut results: Vec<String> = Vec::new();
3879        for entry in &top_entries {
3880            if entry.is_file() || entry.is_symlink() {
3881                if let Some(name) = entry.file_name().and_then(|n| n.to_str()) {
3882                    if file_pat.matches_with(name, match_opts) {
3883                        results.push(entry.to_string_lossy().to_string());
3884                    }
3885                }
3886            }
3887        }
3888
3889        // Fan out subdirectory walks to worker pool
3890        let subdirs: Vec<std::path::PathBuf> = top_entries
3891            .into_iter()
3892            .filter(|p| p.is_dir())
3893            .filter(|p| {
3894                dotglob
3895                    || !p
3896                        .file_name()
3897                        .and_then(|n| n.to_str())
3898                        .map(|n| n.starts_with('.'))
3899                        .unwrap_or(false)
3900            })
3901            .collect();
3902
3903        if subdirs.is_empty() {
3904            return results;
3905        }
3906
3907        let (tx, rx) = std::sync::mpsc::channel::<Vec<String>>();
3908
3909        for subdir in &subdirs {
3910            let tx = tx.clone();
3911            let subdir = subdir.clone();
3912            let file_pat = file_pat.clone();
3913            let skip_dot = !dotglob;
3914            self.worker_pool.submit(move || {
3915                let mut matches = Vec::new();
3916                let walker = WalkDir::new(&subdir)
3917                    .follow_links(false)
3918                    .into_iter()
3919                    .filter_entry(move |e| {
3920                        // Skip hidden dirs if !dotglob
3921                        if skip_dot {
3922                            if let Some(name) = e.file_name().to_str() {
3923                                if name.starts_with('.') && e.depth() > 0 {
3924                                    return false;
3925                                }
3926                            }
3927                        }
3928                        true
3929                    });
3930                for entry in walker.filter_map(|e| e.ok()) {
3931                    if entry.file_type().is_file() || entry.file_type().is_symlink() {
3932                        if let Some(name) = entry.file_name().to_str() {
3933                            if file_pat.matches_with(name, match_opts) {
3934                                matches.push(entry.path().to_string_lossy().to_string());
3935                            }
3936                        }
3937                    }
3938                }
3939                let _ = tx.send(matches);
3940            });
3941        }
3942
3943        // Drop our sender so rx knows when all workers are done
3944        drop(tx);
3945
3946        // Collect results from all workers
3947        for batch in rx {
3948            results.extend(batch);
3949        }
3950
3951        results
3952    }
3953
3954    /// Parse zsh glob qualifiers from the end of a pattern
3955    /// Returns (pattern_without_qualifiers, qualifiers_string)
3956    fn parse_glob_qualifiers(&self, pattern: &str) -> (String, String) {
3957        // Check if pattern ends with (...) that looks like qualifiers
3958        // Qualifiers are single chars like . / @ * % or combinations
3959        if !pattern.ends_with(')') {
3960            return (pattern.to_string(), String::new());
3961        }
3962
3963        // Find matching opening paren
3964        let chars: Vec<char> = pattern.chars().collect();
3965        let mut depth = 0;
3966        let mut qual_start = None;
3967
3968        for i in (0..chars.len()).rev() {
3969            match chars[i] {
3970                ')' => depth += 1,
3971                '(' => {
3972                    depth -= 1;
3973                    if depth == 0 {
3974                        qual_start = Some(i);
3975                        break;
3976                    }
3977                }
3978                _ => {}
3979            }
3980        }
3981
3982        if let Some(start) = qual_start {
3983            let qual_content: String = chars[start + 1..chars.len() - 1].iter().collect();
3984
3985            // Check if this looks like glob qualifiers (not extglob)
3986            // Qualifiers are things like: . / @ * % r w x ^ - etc.
3987            // Extglob would have | inside
3988            if !qual_content.contains('|') && self.looks_like_glob_qualifiers(&qual_content) {
3989                let base_pattern: String = chars[..start].iter().collect();
3990                return (base_pattern, qual_content);
3991            }
3992        }
3993
3994        (pattern.to_string(), String::new())
3995    }
3996
3997    /// Check if string looks like glob qualifiers
3998    fn looks_like_glob_qualifiers(&self, s: &str) -> bool {
3999        if s.is_empty() {
4000            return false;
4001        }
4002        // Valid qualifier chars: . / @ = p * % r w x A I E R W X s S t ^ - + :
4003        // Also numbers for depth limits, and things like [1,5] for ranges
4004        let valid_chars = "./@=p*%brwxAIERWXsStfedDLNnMmcaou^-+:0123456789,[]FT";
4005        s.chars()
4006            .all(|c| valid_chars.contains(c) || c.is_whitespace())
4007    }
4008
4009    /// Filter file list by glob qualifiers
4010    /// Prefetch file metadata in parallel across the worker pool.
4011    /// Returns a map from path → (metadata, symlink_metadata).
4012    /// Each batch of files is stat'd on a pool thread.
4013    fn prefetch_metadata(
4014        &self,
4015        files: &[String],
4016    ) -> HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)> {
4017        if files.len() < 32 {
4018            // Small list — serial stat is faster than channel overhead
4019            return files
4020                .iter()
4021                .map(|f| {
4022                    let meta = std::fs::metadata(f).ok();
4023                    let symlink_meta = std::fs::symlink_metadata(f).ok();
4024                    (f.clone(), (meta, symlink_meta))
4025                })
4026                .collect();
4027        }
4028
4029        let pool_size = self.worker_pool.size();
4030        let chunk_size = (files.len() + pool_size - 1) / pool_size;
4031        let (tx, rx) = std::sync::mpsc::channel();
4032
4033        for chunk in files.chunks(chunk_size) {
4034            let tx = tx.clone();
4035            let chunk: Vec<String> = chunk.to_vec();
4036            self.worker_pool.submit(move || {
4037                let batch: Vec<(String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>))> =
4038                    chunk
4039                        .into_iter()
4040                        .map(|f| {
4041                            let meta = std::fs::metadata(&f).ok();
4042                            let symlink_meta = std::fs::symlink_metadata(&f).ok();
4043                            (f, (meta, symlink_meta))
4044                        })
4045                        .collect();
4046                let _ = tx.send(batch);
4047            });
4048        }
4049        drop(tx);
4050
4051        let mut map = HashMap::with_capacity(files.len());
4052        for batch in rx {
4053            for (path, metas) in batch {
4054                map.insert(path, metas);
4055            }
4056        }
4057        map
4058    }
4059
4060    fn filter_by_qualifiers(&self, files: Vec<String>, qualifiers: &str) -> Vec<String> {
4061        if qualifiers.is_empty() {
4062            return files;
4063        }
4064
4065        // Parallel metadata prefetch — all stat syscalls happen on pool threads,
4066        // then filter/sort uses cached metadata with zero syscalls.
4067        let meta_cache = self.prefetch_metadata(&files);
4068
4069        let mut result = files;
4070        let mut negate = false;
4071        let mut chars = qualifiers.chars().peekable();
4072
4073        while let Some(c) = chars.next() {
4074            match c {
4075                // Negation
4076                '^' => negate = !negate,
4077
4078                // File types — all use prefetched metadata cache
4079                '.' => {
4080                    result = result
4081                        .into_iter()
4082                        .filter(|f| {
4083                            let is_file = meta_cache
4084                                .get(f)
4085                                .and_then(|(m, _)| m.as_ref())
4086                                .map(|m| m.is_file())
4087                                .unwrap_or(false);
4088                            if negate { !is_file } else { is_file }
4089                        })
4090                        .collect();
4091                    negate = false;
4092                }
4093                '/' => {
4094                    result = result
4095                        .into_iter()
4096                        .filter(|f| {
4097                            let is_dir = meta_cache
4098                                .get(f)
4099                                .and_then(|(m, _)| m.as_ref())
4100                                .map(|m| m.is_dir())
4101                                .unwrap_or(false);
4102                            if negate { !is_dir } else { is_dir }
4103                        })
4104                        .collect();
4105                    negate = false;
4106                }
4107                '@' => {
4108                    result = result
4109                        .into_iter()
4110                        .filter(|f| {
4111                            let is_link = meta_cache
4112                                .get(f)
4113                                .and_then(|(_, sm)| sm.as_ref())
4114                                .map(|m| m.file_type().is_symlink())
4115                                .unwrap_or(false);
4116                            if negate { !is_link } else { is_link }
4117                        })
4118                        .collect();
4119                    negate = false;
4120                }
4121                '=' => {
4122                    // Sockets
4123                    use std::os::unix::fs::FileTypeExt;
4124                    result = result
4125                        .into_iter()
4126                        .filter(|f| {
4127                            let is_socket = meta_cache
4128                                .get(f)
4129                                .and_then(|(_, sm)| sm.as_ref())
4130                                .map(|m| m.file_type().is_socket())
4131                                .unwrap_or(false);
4132                            if negate { !is_socket } else { is_socket }
4133                        })
4134                        .collect();
4135                    negate = false;
4136                }
4137                'p' => {
4138                    // Named pipes (FIFOs)
4139                    use std::os::unix::fs::FileTypeExt;
4140                    result = result
4141                        .into_iter()
4142                        .filter(|f| {
4143                            let is_fifo = meta_cache
4144                                .get(f)
4145                                .and_then(|(_, sm)| sm.as_ref())
4146                                .map(|m| m.file_type().is_fifo())
4147                                .unwrap_or(false);
4148                            if negate { !is_fifo } else { is_fifo }
4149                        })
4150                        .collect();
4151                    negate = false;
4152                }
4153                '*' => {
4154                    // Executable files
4155                    use std::os::unix::fs::PermissionsExt;
4156                    result = result
4157                        .into_iter()
4158                        .filter(|f| {
4159                            let is_exec = meta_cache
4160                                .get(f)
4161                                .and_then(|(m, _)| m.as_ref())
4162                                .map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
4163                                .unwrap_or(false);
4164                            if negate { !is_exec } else { is_exec }
4165                        })
4166                        .collect();
4167                    negate = false;
4168                }
4169                '%' => {
4170                    // Device files
4171                    use std::os::unix::fs::FileTypeExt;
4172                    let next = chars.peek().copied();
4173                    result = result
4174                        .into_iter()
4175                        .filter(|f| {
4176                            let is_device = meta_cache
4177                                .get(f)
4178                                .and_then(|(_, sm)| sm.as_ref())
4179                                .map(|m| match next {
4180                                    Some('b') => m.file_type().is_block_device(),
4181                                    Some('c') => m.file_type().is_char_device(),
4182                                    _ => {
4183                                        m.file_type().is_block_device()
4184                                            || m.file_type().is_char_device()
4185                                    }
4186                                })
4187                                .unwrap_or(false);
4188                            if negate { !is_device } else { is_device }
4189                        })
4190                        .collect();
4191                    if next == Some('b') || next == Some('c') {
4192                        chars.next();
4193                    }
4194                    negate = false;
4195                }
4196
4197                // Permission qualifiers — all use prefetched metadata cache
4198                'r' => {
4199                    result = self.filter_by_permission(result, 0o400, negate, &meta_cache);
4200                    negate = false;
4201                }
4202                'w' => {
4203                    result = self.filter_by_permission(result, 0o200, negate, &meta_cache);
4204                    negate = false;
4205                }
4206                'x' => {
4207                    result = self.filter_by_permission(result, 0o100, negate, &meta_cache);
4208                    negate = false;
4209                }
4210                'A' => {
4211                    result = self.filter_by_permission(result, 0o040, negate, &meta_cache);
4212                    negate = false;
4213                }
4214                'I' => {
4215                    result = self.filter_by_permission(result, 0o020, negate, &meta_cache);
4216                    negate = false;
4217                }
4218                'E' => {
4219                    result = self.filter_by_permission(result, 0o010, negate, &meta_cache);
4220                    negate = false;
4221                }
4222                'R' => {
4223                    result = self.filter_by_permission(result, 0o004, negate, &meta_cache);
4224                    negate = false;
4225                }
4226                'W' => {
4227                    result = self.filter_by_permission(result, 0o002, negate, &meta_cache);
4228                    negate = false;
4229                }
4230                'X' => {
4231                    result = self.filter_by_permission(result, 0o001, negate, &meta_cache);
4232                    negate = false;
4233                }
4234                's' => {
4235                    result = self.filter_by_permission(result, 0o4000, negate, &meta_cache);
4236                    negate = false;
4237                }
4238                'S' => {
4239                    result = self.filter_by_permission(result, 0o2000, negate, &meta_cache);
4240                    negate = false;
4241                }
4242                't' => {
4243                    result = self.filter_by_permission(result, 0o1000, negate, &meta_cache);
4244                    negate = false;
4245                }
4246
4247                // Full/empty directories
4248                'F' => {
4249                    // Non-empty directories
4250                    result = result
4251                        .into_iter()
4252                        .filter(|f| {
4253                            let path = std::path::Path::new(f);
4254                            let is_nonempty = path.is_dir()
4255                                && std::fs::read_dir(path)
4256                                    .map(|mut d| d.next().is_some())
4257                                    .unwrap_or(false);
4258                            if negate {
4259                                !is_nonempty
4260                            } else {
4261                                is_nonempty
4262                            }
4263                        })
4264                        .collect();
4265                    negate = false;
4266                }
4267
4268                // Ownership — uses prefetched metadata cache
4269                'U' => {
4270                    // Owned by effective UID
4271                    let euid = unsafe { libc::geteuid() };
4272                    result = result
4273                        .into_iter()
4274                        .filter(|f| {
4275                            use std::os::unix::fs::MetadataExt;
4276                            let is_owned = meta_cache
4277                                .get(f)
4278                                .and_then(|(m, _)| m.as_ref())
4279                                .map(|m| m.uid() == euid)
4280                                .unwrap_or(false);
4281                            if negate { !is_owned } else { is_owned }
4282                        })
4283                        .collect();
4284                    negate = false;
4285                }
4286                'G' => {
4287                    // Owned by effective GID
4288                    let egid = unsafe { libc::getegid() };
4289                    result = result
4290                        .into_iter()
4291                        .filter(|f| {
4292                            use std::os::unix::fs::MetadataExt;
4293                            let is_owned = meta_cache
4294                                .get(f)
4295                                .and_then(|(m, _)| m.as_ref())
4296                                .map(|m| m.gid() == egid)
4297                                .unwrap_or(false);
4298                            if negate { !is_owned } else {
4299                                is_owned
4300                            }
4301                        })
4302                        .collect();
4303                    negate = false;
4304                }
4305
4306                // Sorting modifiers
4307                'o' => {
4308                    // Sort by name (ascending) - already default
4309                    if chars.peek() == Some(&'n') {
4310                        chars.next();
4311                        // Sort by name
4312                        result.sort();
4313                    } else if chars.peek() == Some(&'L') {
4314                        chars.next();
4315                        // Sort by size — uses prefetched metadata
4316                        result.sort_by_key(|f| {
4317                            meta_cache.get(f).and_then(|(m, _)| m.as_ref()).map(|m| m.len()).unwrap_or(0)
4318                        });
4319                    } else if chars.peek() == Some(&'m') {
4320                        chars.next();
4321                        // Sort by modification time — uses prefetched metadata
4322                        result.sort_by_key(|f| {
4323                            meta_cache.get(f).and_then(|(m, _)| m.as_ref())
4324                                .and_then(|m| m.modified().ok())
4325                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
4326                        });
4327                    } else if chars.peek() == Some(&'a') {
4328                        chars.next();
4329                        // Sort by access time — uses prefetched metadata
4330                        result.sort_by_key(|f| {
4331                            meta_cache.get(f).and_then(|(m, _)| m.as_ref())
4332                                .and_then(|m| m.accessed().ok())
4333                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
4334                        });
4335                    }
4336                }
4337                'O' => {
4338                    // Reverse sort — uses prefetched metadata
4339                    if chars.peek() == Some(&'n') {
4340                        chars.next();
4341                        result.sort();
4342                        result.reverse();
4343                    } else if chars.peek() == Some(&'L') {
4344                        chars.next();
4345                        result.sort_by_key(|f| {
4346                            meta_cache.get(f).and_then(|(m, _)| m.as_ref()).map(|m| m.len()).unwrap_or(0)
4347                        });
4348                        result.reverse();
4349                    } else if chars.peek() == Some(&'m') {
4350                        chars.next();
4351                        result.sort_by_key(|f| {
4352                            meta_cache.get(f).and_then(|(m, _)| m.as_ref())
4353                                .and_then(|m| m.modified().ok())
4354                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
4355                        });
4356                        result.reverse();
4357                    } else {
4358                        // Just reverse current order
4359                        result.reverse();
4360                    }
4361                }
4362
4363                // Subscript range [n] or [n,m]
4364                '[' => {
4365                    let mut range_str = String::new();
4366                    while let Some(&ch) = chars.peek() {
4367                        if ch == ']' {
4368                            chars.next();
4369                            break;
4370                        }
4371                        range_str.push(chars.next().unwrap());
4372                    }
4373
4374                    if let Some((start, end)) = self.parse_subscript_range(&range_str, result.len())
4375                    {
4376                        result = result.into_iter().skip(start).take(end - start).collect();
4377                    }
4378                }
4379
4380                // Depth limit (for **/)
4381                'D' => {
4382                    // Include dotfiles (handled by dotglob)
4383                }
4384                'N' => {
4385                    // Nullglob for this pattern
4386                }
4387
4388                // Unknown qualifier - ignore
4389                _ => {}
4390            }
4391        }
4392
4393        result
4394    }
4395
4396    /// Filter files by permission bits — uses prefetched metadata cache
4397    fn filter_by_permission(
4398        &self,
4399        files: Vec<String>,
4400        mode: u32,
4401        negate: bool,
4402        meta_cache: &HashMap<String, (Option<std::fs::Metadata>, Option<std::fs::Metadata>)>,
4403    ) -> Vec<String> {
4404        use std::os::unix::fs::PermissionsExt;
4405        files
4406            .into_iter()
4407            .filter(|f| {
4408                let has_perm = meta_cache
4409                    .get(f)
4410                    .and_then(|(m, _)| m.as_ref())
4411                    .map(|m| (m.permissions().mode() & mode) != 0)
4412                    .unwrap_or(false);
4413                if negate { !has_perm } else { has_perm }
4414            })
4415            .collect()
4416    }
4417
4418    /// Parse subscript range like "1" or "1,5" or "-1" or "1,-1"
4419    fn parse_subscript_range(&self, s: &str, len: usize) -> Option<(usize, usize)> {
4420        if s.is_empty() || len == 0 {
4421            return None;
4422        }
4423
4424        let parts: Vec<&str> = s.split(',').collect();
4425
4426        let parse_idx = |idx_str: &str| -> Option<usize> {
4427            let idx: i64 = idx_str.trim().parse().ok()?;
4428            if idx < 0 {
4429                // Negative index from end
4430                let abs = (-idx) as usize;
4431                if abs > len {
4432                    None
4433                } else {
4434                    Some(len - abs)
4435                }
4436            } else if idx == 0 {
4437                Some(0)
4438            } else {
4439                // 1-indexed
4440                Some((idx as usize).saturating_sub(1).min(len))
4441            }
4442        };
4443
4444        match parts.len() {
4445            1 => {
4446                // Single element [n]
4447                let idx = parse_idx(parts[0])?;
4448                Some((idx, idx + 1))
4449            }
4450            2 => {
4451                // Range [n,m]
4452                let start = parse_idx(parts[0])?;
4453                let end = parse_idx(parts[1])?.saturating_add(1);
4454                Some((start.min(end), start.max(end)))
4455            }
4456            _ => None,
4457        }
4458    }
4459
4460    /// Check if pattern contains extended glob syntax
4461    fn has_extglob_pattern(&self, pattern: &str) -> bool {
4462        let chars: Vec<char> = pattern.chars().collect();
4463        for i in 0..chars.len().saturating_sub(1) {
4464            if (chars[i] == '?'
4465                || chars[i] == '*'
4466                || chars[i] == '+'
4467                || chars[i] == '@'
4468                || chars[i] == '!')
4469                && chars[i + 1] == '('
4470            {
4471                return true;
4472            }
4473        }
4474        false
4475    }
4476
4477    /// Convert extended glob pattern to regex
4478    fn extglob_to_regex(&self, pattern: &str) -> String {
4479        let mut regex = String::from("^");
4480        let chars: Vec<char> = pattern.chars().collect();
4481        let mut i = 0;
4482
4483        while i < chars.len() {
4484            let c = chars[i];
4485
4486            // Check for extglob patterns
4487            if i + 1 < chars.len() && chars[i + 1] == '(' {
4488                match c {
4489                    '?' => {
4490                        // ?(pattern) - zero or one occurrence
4491                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
4492                        let inner_regex = self.extglob_inner_to_regex(&inner);
4493                        regex.push_str(&format!("({})?", inner_regex));
4494                        i = end + 1;
4495                        continue;
4496                    }
4497                    '*' => {
4498                        // *(pattern) - zero or more occurrences
4499                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
4500                        let inner_regex = self.extglob_inner_to_regex(&inner);
4501                        regex.push_str(&format!("({})*", inner_regex));
4502                        i = end + 1;
4503                        continue;
4504                    }
4505                    '+' => {
4506                        // +(pattern) - one or more occurrences
4507                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
4508                        let inner_regex = self.extglob_inner_to_regex(&inner);
4509                        regex.push_str(&format!("({})+", inner_regex));
4510                        i = end + 1;
4511                        continue;
4512                    }
4513                    '@' => {
4514                        // @(pattern) - exactly one occurrence
4515                        let (inner, end) = self.extract_extglob_inner(&chars, i + 2);
4516                        let inner_regex = self.extglob_inner_to_regex(&inner);
4517                        regex.push_str(&format!("({})", inner_regex));
4518                        i = end + 1;
4519                        continue;
4520                    }
4521                    '!' => {
4522                        // !(pattern) - handled specially in expand_extglob
4523                        // Just skip this extglob for regex, will do manual filtering
4524                        let (_, end) = self.extract_extglob_inner(&chars, i + 2);
4525                        regex.push_str(".*"); // Match anything, we filter later
4526                        i = end + 1;
4527                        continue;
4528                    }
4529                    _ => {}
4530                }
4531            }
4532
4533            // Handle regular glob characters
4534            match c {
4535                '*' => regex.push_str(".*"),
4536                '?' => regex.push('.'),
4537                '.' => regex.push_str("\\."),
4538                '[' => {
4539                    regex.push('[');
4540                    i += 1;
4541                    while i < chars.len() && chars[i] != ']' {
4542                        if chars[i] == '!' && regex.ends_with('[') {
4543                            regex.push('^');
4544                        } else {
4545                            regex.push(chars[i]);
4546                        }
4547                        i += 1;
4548                    }
4549                    regex.push(']');
4550                }
4551                '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
4552                    regex.push('\\');
4553                    regex.push(c);
4554                }
4555                _ => regex.push(c),
4556            }
4557            i += 1;
4558        }
4559
4560        regex.push('$');
4561        regex
4562    }
4563
4564    /// Extract the inner part of an extglob pattern (until closing paren)
4565    fn extract_extglob_inner(&self, chars: &[char], start: usize) -> (String, usize) {
4566        let mut inner = String::new();
4567        let mut depth = 1;
4568        let mut i = start;
4569
4570        while i < chars.len() && depth > 0 {
4571            if chars[i] == '(' {
4572                depth += 1;
4573            } else if chars[i] == ')' {
4574                depth -= 1;
4575                if depth == 0 {
4576                    return (inner, i);
4577                }
4578            }
4579            inner.push(chars[i]);
4580            i += 1;
4581        }
4582
4583        (inner, i)
4584    }
4585
4586    /// Convert the inner part of extglob (handles | for alternation)
4587    fn extglob_inner_to_regex(&self, inner: &str) -> String {
4588        // Split by | and convert each alternative
4589        let alternatives: Vec<String> = inner
4590            .split('|')
4591            .map(|alt| {
4592                let mut result = String::new();
4593                for c in alt.chars() {
4594                    match c {
4595                        '*' => result.push_str(".*"),
4596                        '?' => result.push('.'),
4597                        '.' => result.push_str("\\."),
4598                        '^' | '$' | '(' | ')' | '{' | '}' | '\\' => {
4599                            result.push('\\');
4600                            result.push(c);
4601                        }
4602                        _ => result.push(c),
4603                    }
4604                }
4605                result
4606            })
4607            .collect();
4608
4609        alternatives.join("|")
4610    }
4611
4612    /// Expand extended glob pattern
4613    fn expand_extglob(&self, pattern: &str) -> Vec<String> {
4614        // Determine directory to search
4615        let (search_dir, file_pattern) = if let Some(last_slash) = pattern.rfind('/') {
4616            (&pattern[..last_slash], &pattern[last_slash + 1..])
4617        } else {
4618            (".", pattern)
4619        };
4620
4621        // Check for !(pattern) - negative matching
4622        if let Some((neg_pat, suffix)) = self.extract_neg_extglob(file_pattern) {
4623            return self.expand_neg_extglob(search_dir, &neg_pat, &suffix, pattern);
4624        }
4625
4626        // Convert file pattern to regex for positive extglob
4627        let regex_str = self.extglob_to_regex(file_pattern);
4628
4629        let re = match cached_regex(&regex_str) {
4630            Some(r) => r,
4631            None => return vec![pattern.to_string()],
4632        };
4633
4634        let mut results = Vec::new();
4635
4636        if let Ok(entries) = std::fs::read_dir(search_dir) {
4637            for entry in entries.flatten() {
4638                let name = entry.file_name().to_string_lossy().to_string();
4639                // Skip hidden files unless pattern starts with .
4640                if name.starts_with('.') && !file_pattern.starts_with('.') {
4641                    continue;
4642                }
4643
4644                if re.is_match(&name) {
4645                    let full_path = if search_dir == "." {
4646                        name
4647                    } else {
4648                        format!("{}/{}", search_dir, name)
4649                    };
4650                    results.push(full_path);
4651                }
4652            }
4653        }
4654
4655        if results.is_empty() {
4656            vec![pattern.to_string()]
4657        } else {
4658            results.sort();
4659            results
4660        }
4661    }
4662
4663    /// Handle !(pattern) negative extglob expansion
4664    fn expand_neg_extglob(
4665        &self,
4666        search_dir: &str,
4667        neg_pat: &str,
4668        suffix: &str,
4669        original_pattern: &str,
4670    ) -> Vec<String> {
4671        let mut results = Vec::new();
4672
4673        if let Ok(entries) = std::fs::read_dir(search_dir) {
4674            for entry in entries.flatten() {
4675                let name = entry.file_name().to_string_lossy().to_string();
4676                // Skip hidden files
4677                if name.starts_with('.') {
4678                    continue;
4679                }
4680
4681                // File must end with suffix
4682                if !name.ends_with(suffix) {
4683                    continue;
4684                }
4685
4686                let basename = &name[..name.len() - suffix.len()];
4687                // Check if basename matches any negated alternative
4688                let alts: Vec<&str> = neg_pat.split('|').collect();
4689                let matches_neg = alts.iter().any(|alt| {
4690                    if alt.contains('*') || alt.contains('?') {
4691                        let alt_re = self.extglob_inner_to_regex(alt);
4692                        let full_pattern = format!("^{}$", alt_re);
4693                        if let Some(r) = cached_regex(&full_pattern) {
4694                            r.is_match(basename)
4695                        } else {
4696                            *alt == basename
4697                        }
4698                    } else {
4699                        *alt == basename
4700                    }
4701                });
4702
4703                if !matches_neg {
4704                    let full_path = if search_dir == "." {
4705                        name
4706                    } else {
4707                        format!("{}/{}", search_dir, name)
4708                    };
4709                    results.push(full_path);
4710                }
4711            }
4712        }
4713
4714        if results.is_empty() {
4715            vec![original_pattern.to_string()]
4716        } else {
4717            results.sort();
4718            results
4719        }
4720    }
4721
4722    /// Extract !(pattern) info from file pattern, returns (inner_pattern, suffix)
4723    fn extract_neg_extglob(&self, pattern: &str) -> Option<(String, String)> {
4724        let chars: Vec<char> = pattern.chars().collect();
4725        if chars.len() >= 3 && chars[0] == '!' && chars[1] == '(' {
4726            let mut depth = 1;
4727            let mut i = 2;
4728            while i < chars.len() && depth > 0 {
4729                if chars[i] == '(' {
4730                    depth += 1;
4731                } else if chars[i] == ')' {
4732                    depth -= 1;
4733                }
4734                i += 1;
4735            }
4736            if depth == 0 {
4737                let inner: String = chars[2..i - 1].iter().collect();
4738                let suffix: String = chars[i..].iter().collect();
4739                return Some((inner, suffix));
4740            }
4741        }
4742        None
4743    }
4744
4745    /// Expand a word with word splitting (for contexts like `for x in $words`)
4746    fn expand_word_split(&mut self, word: &ShellWord) -> Vec<String> {
4747        match word {
4748            ShellWord::Literal(s) => {
4749                // First do brace expansion, then variable expansion on each result
4750                let brace_expanded = self.expand_braces(s);
4751                brace_expanded
4752                    .into_iter()
4753                    .flat_map(|item| self.expand_string_split(&item))
4754                    .collect()
4755            }
4756            ShellWord::SingleQuoted(s) => vec![s.clone()],
4757            ShellWord::DoubleQuoted(parts) => {
4758                // Double quotes prevent word splitting
4759                vec![parts.iter().map(|p| self.expand_word(p)).collect()]
4760            }
4761            ShellWord::Variable(name) => {
4762                let val = env::var(name).unwrap_or_default();
4763                self.split_words(&val)
4764            }
4765            ShellWord::VariableBraced(name, modifier) => {
4766                let val = env::var(name).ok();
4767                let expanded = self.apply_var_modifier(name, val, modifier.as_deref());
4768                self.split_words(&expanded)
4769            }
4770            ShellWord::ArrayVar(name, index) => {
4771                let idx_str = self.expand_word(index);
4772                if idx_str == "@" || idx_str == "*" {
4773                    // ${arr[@]} returns each element as separate word
4774                    self.arrays.get(name).cloned().unwrap_or_default()
4775                } else {
4776                    vec![self.expand_array_access(name, index)]
4777                }
4778            }
4779            ShellWord::Glob(pattern) => match glob::glob(pattern) {
4780                Ok(paths) => {
4781                    let expanded: Vec<String> = paths
4782                        .filter_map(|p| p.ok())
4783                        .map(|p| p.to_string_lossy().to_string())
4784                        .collect();
4785                    if expanded.is_empty() {
4786                        vec![pattern.clone()]
4787                    } else {
4788                        expanded
4789                    }
4790                }
4791                Err(_) => vec![pattern.clone()],
4792            },
4793            ShellWord::CommandSub(_) => {
4794                // Command substitution results must be word-split for array context
4795                let val = self.expand_word(word);
4796                self.split_words(&val)
4797            }
4798            ShellWord::Concat(parts) => {
4799                // Concat in split context — expand and split the result
4800                let val = self.expand_concat_parallel(parts);
4801                self.split_words(&val)
4802            }
4803            _ => vec![self.expand_word(word)],
4804        }
4805    }
4806
4807    /// Expand string with word splitting - returns Vec for array expansions
4808    fn expand_string_split(&mut self, s: &str) -> Vec<String> {
4809        let mut results: Vec<String> = Vec::new();
4810        let mut current = String::new();
4811        let mut chars = s.chars().peekable();
4812
4813        while let Some(c) = chars.next() {
4814            if c == '$' {
4815                if chars.peek() == Some(&'{') {
4816                    chars.next(); // consume '{'
4817                    let mut brace_content = String::new();
4818                    let mut depth = 1;
4819                    while let Some(ch) = chars.next() {
4820                        if ch == '{' {
4821                            depth += 1;
4822                            brace_content.push(ch);
4823                        } else if ch == '}' {
4824                            depth -= 1;
4825                            if depth == 0 {
4826                                break;
4827                            }
4828                            brace_content.push(ch);
4829                        } else {
4830                            brace_content.push(ch);
4831                        }
4832                    }
4833
4834                    // Check if this is an array expansion ${arr[@]} or ${arr[*]}
4835                    if let Some(bracket_start) = brace_content.find('[') {
4836                        let var_name = &brace_content[..bracket_start];
4837                        let bracket_content = &brace_content[bracket_start + 1..];
4838                        if let Some(bracket_end) = bracket_content.find(']') {
4839                            let index = &bracket_content[..bracket_end];
4840                            if (index == "@" || index == "*")
4841                                && bracket_end + 1 == bracket_content.len()
4842                            {
4843                                // This is ${arr[@]} - expand to separate elements
4844                                if !current.is_empty() {
4845                                    results.push(current.clone());
4846                                    current.clear();
4847                                }
4848                                if let Some(arr) = self.arrays.get(var_name) {
4849                                    results.extend(arr.clone());
4850                                }
4851                                continue;
4852                            }
4853                        }
4854                    }
4855
4856                    // Not an array expansion, use normal expansion
4857                    current.push_str(&self.expand_braced_variable(&brace_content));
4858                } else {
4859                    // Simple variable like $var
4860                    let mut var_name = String::new();
4861                    while let Some(&ch) = chars.peek() {
4862                        if ch.is_alphanumeric() || ch == '_' {
4863                            var_name.push(chars.next().unwrap());
4864                        } else {
4865                            break;
4866                        }
4867                    }
4868                    let val = self.get_variable(&var_name);
4869                    // Split this variable's value
4870                    if !current.is_empty() {
4871                        results.push(current.clone());
4872                        current.clear();
4873                    }
4874                    results.extend(self.split_words(&val));
4875                }
4876            } else {
4877                current.push(c);
4878            }
4879        }
4880
4881        if !current.is_empty() {
4882            results.push(current);
4883        }
4884
4885        if results.is_empty() {
4886            results.push(String::new());
4887        }
4888
4889        results
4890    }
4891
4892    /// Split a string into words based on IFS
4893    fn split_words(&self, s: &str) -> Vec<String> {
4894        let ifs = self
4895            .variables
4896            .get("IFS")
4897            .cloned()
4898            .or_else(|| env::var("IFS").ok())
4899            .unwrap_or_else(|| " \t\n".to_string());
4900
4901        if ifs.is_empty() {
4902            return vec![s.to_string()];
4903        }
4904
4905        s.split(|c: char| ifs.contains(c))
4906            .filter(|s| !s.is_empty())
4907            .map(|s| s.to_string())
4908            .collect()
4909    }
4910
4911    #[tracing::instrument(level = "trace", skip_all)]
4912    fn expand_word(&mut self, word: &ShellWord) -> String {
4913        match word {
4914            ShellWord::Literal(s) => {
4915                let expanded = self.expand_string(s);
4916                // Don't glob-expand here, that's done in expand_word_glob
4917                expanded
4918            }
4919            ShellWord::SingleQuoted(s) => s.clone(),
4920            ShellWord::DoubleQuoted(parts) => parts.iter().map(|p| self.expand_word(p)).collect(),
4921            ShellWord::Variable(name) => self.get_variable(name),
4922            ShellWord::VariableBraced(name, modifier) => {
4923                let val = env::var(name).ok();
4924                self.apply_var_modifier(name, val, modifier.as_deref())
4925            }
4926            ShellWord::Tilde(user) => {
4927                if let Some(u) = user {
4928                    // ~user expansion (simplified)
4929                    format!("/home/{}", u)
4930                } else {
4931                    dirs::home_dir()
4932                        .map(|p| p.to_string_lossy().to_string())
4933                        .unwrap_or_else(|| "~".to_string())
4934                }
4935            }
4936            ShellWord::Glob(pattern) => {
4937                // Expand glob
4938                match glob::glob(pattern) {
4939                    Ok(paths) => {
4940                        let expanded: Vec<String> = paths
4941                            .filter_map(|p| p.ok())
4942                            .map(|p| p.to_string_lossy().to_string())
4943                            .collect();
4944                        if expanded.is_empty() {
4945                            pattern.clone()
4946                        } else {
4947                            expanded.join(" ")
4948                        }
4949                    }
4950                    Err(_) => pattern.clone(),
4951                }
4952            }
4953            ShellWord::Concat(parts) => self.expand_concat_parallel(parts),
4954            ShellWord::CommandSub(cmd) => self.execute_command_substitution(cmd),
4955            ShellWord::ProcessSubIn(cmd) => self.execute_process_sub_in(cmd),
4956            ShellWord::ProcessSubOut(cmd) => self.execute_process_sub_out(cmd),
4957            ShellWord::ArithSub(expr) => self.evaluate_arithmetic(expr),
4958            ShellWord::ArrayVar(name, index) => self.expand_array_access(name, index),
4959            ShellWord::ArrayLiteral(elements) => elements
4960                .iter()
4961                .map(|e| self.expand_word(e))
4962                .collect::<Vec<_>>()
4963                .join(" "),
4964        }
4965    }
4966
4967    /// Pre-launch external command substitutions from a word list onto the worker pool.
4968    /// Returns a Vec aligned with `words` — Some(receiver) for pre-launched externals, None otherwise.
4969    fn preflight_command_subs(
4970        &mut self,
4971        words: &[ShellWord],
4972    ) -> Vec<Option<crossbeam_channel::Receiver<String>>> {
4973        use crate::parser::ShellWord;
4974        use std::process::{Command, Stdio};
4975
4976        let mut receivers = Vec::with_capacity(words.len());
4977
4978        // Count external command subs — don't bother with pool overhead for just one
4979        let external_count = words.iter().filter(|w| {
4980            if let ShellWord::CommandSub(cmd) = w {
4981                if let ShellCommand::Simple(simple) = cmd.as_ref() {
4982                    if let Some(first) = simple.words.first() {
4983                        let name = self.expand_word(first);
4984                        return !self.functions.contains_key(&name) && !self.is_builtin(&name);
4985                    }
4986                }
4987            }
4988            false
4989        }).count();
4990
4991        if external_count < 2 {
4992            // Not worth parallelizing — fall through to sequential
4993            return vec![None; words.len()];
4994        }
4995
4996        for word in words {
4997            if let ShellWord::CommandSub(cmd) = word {
4998                if let ShellCommand::Simple(simple) = cmd.as_ref() {
4999                    let first = simple.words.first().map(|w| self.expand_word(w));
5000                    if let Some(ref name) = first {
5001                        if !self.functions.contains_key(name) && !self.is_builtin(name) {
5002                            let expanded: Vec<String> =
5003                                simple.words.iter().map(|w| self.expand_word(w)).collect();
5004                            let rx = self.worker_pool.submit_with_result(move || {
5005                                let output = Command::new(&expanded[0])
5006                                    .args(&expanded[1..])
5007                                    .stdout(Stdio::piped())
5008                                    .stderr(Stdio::inherit())
5009                                    .output();
5010                                match output {
5011                                    Ok(out) => String::from_utf8_lossy(&out.stdout)
5012                                        .trim_end_matches('\n')
5013                                        .to_string(),
5014                                    Err(_) => String::new(),
5015                                }
5016                            });
5017                            receivers.push(Some(rx));
5018                            continue;
5019                        }
5020                    }
5021                }
5022            }
5023            receivers.push(None);
5024        }
5025
5026        receivers
5027    }
5028
5029    /// Expand a Concat word list, launching external command substitutions in parallel.
5030    /// Internal subs (builtins/functions) still run sequentially on the main thread.
5031    fn expand_concat_parallel(&mut self, parts: &[ShellWord]) -> String {
5032        use crate::parser::ShellWord;
5033        use std::process::{Command, Stdio};
5034
5035        // Phase 1: identify external command subs and pre-launch them
5036        let mut preflight: Vec<Option<crossbeam_channel::Receiver<String>>> = Vec::with_capacity(parts.len());
5037
5038        for part in parts {
5039            if let ShellWord::CommandSub(cmd) = part {
5040                if let ShellCommand::Simple(simple) = cmd.as_ref() {
5041                    let first = simple.words.first().map(|w| self.expand_word(w));
5042                    if let Some(ref name) = first {
5043                        if !self.functions.contains_key(name) && !self.is_builtin(name) {
5044                            // External command — pre-launch on background thread
5045                            let words: Vec<String> =
5046                                simple.words.iter().map(|w| self.expand_word(w)).collect();
5047                            let rx = self.worker_pool.submit_with_result(move || {
5048                                let output = Command::new(&words[0])
5049                                    .args(&words[1..])
5050                                    .stdout(Stdio::piped())
5051                                    .stderr(Stdio::inherit())
5052                                    .output();
5053                                match output {
5054                                    Ok(out) => String::from_utf8_lossy(&out.stdout)
5055                                        .trim_end_matches('\n')
5056                                        .to_string(),
5057                                    Err(_) => String::new(),
5058                                }
5059                            });
5060                            preflight.push(Some(rx));
5061                            continue;
5062                        }
5063                    }
5064                }
5065            }
5066            preflight.push(None); // not pre-launched
5067        }
5068
5069        // Phase 2: collect results in order, using pre-launched receivers where available
5070        let mut result = String::new();
5071        for (i, part) in parts.iter().enumerate() {
5072            if let Some(rx) = preflight[i].take() {
5073                // Pre-launched external command sub — collect result
5074                result.push_str(&rx.recv().unwrap_or_default());
5075            } else {
5076                // Everything else — expand sequentially (may be internal sub, variable, literal)
5077                result.push_str(&self.expand_word(part));
5078            }
5079        }
5080        result
5081    }
5082
5083    fn expand_braced_variable(&mut self, content: &str) -> String {
5084        // Handle nested expansion: ${${inner}[subscript]} or ${${inner}modifier}
5085        if content.starts_with("${") {
5086            // Find matching closing brace for inner expansion
5087            let mut depth = 0;
5088            let mut inner_end = 0;
5089            for (i, c) in content.char_indices() {
5090                match c {
5091                    '{' => depth += 1,
5092                    '}' => {
5093                        depth -= 1;
5094                        if depth == 0 {
5095                            inner_end = i;
5096                            break;
5097                        }
5098                    }
5099                    _ => {}
5100                }
5101            }
5102
5103            if inner_end > 0 {
5104                // Expand the inner ${...}
5105                let inner_content = &content[2..inner_end];
5106                let inner_result = self.expand_braced_variable(inner_content);
5107
5108                // Check for subscript or modifier after the inner expansion
5109                let rest = &content[inner_end + 1..];
5110                if rest.starts_with('[') {
5111                    // Apply subscript to result: ${${...}[idx]}
5112                    if let Some(bracket_end) = rest.find(']') {
5113                        let index = &rest[1..bracket_end];
5114                        if let Ok(idx) = index.parse::<i64>() {
5115                            let chars: Vec<char> = inner_result.chars().collect();
5116                            let actual_idx = if idx < 0 {
5117                                (chars.len() as i64 + idx).max(0) as usize
5118                            } else if idx > 0 {
5119                                (idx - 1) as usize
5120                            } else {
5121                                0
5122                            };
5123                            return chars
5124                                .get(actual_idx)
5125                                .map(|c| c.to_string())
5126                                .unwrap_or_default();
5127                        }
5128                    }
5129                }
5130
5131                return inner_result;
5132            }
5133        }
5134
5135        // Handle zsh-style parameter expansion flags ${(flags)var}
5136        if content.starts_with('(') {
5137            if let Some(close_paren) = content.find(')') {
5138                let flags_str = &content[1..close_paren];
5139                let rest = &content[close_paren + 1..];
5140                let flags = self.parse_zsh_flags(flags_str);
5141
5142                // Check for (M) match flag
5143                let has_match_flag = flags.iter().any(|f| matches!(f, ZshParamFlag::Match));
5144
5145                // Handle ${(M)var:#pattern} - pattern filter with flags
5146                if let Some(filter_pos) = rest.find(":#") {
5147                    let var_name = &rest[..filter_pos];
5148                    let pattern = &rest[filter_pos + 2..];
5149
5150                    // Array path: filter each element against pattern
5151                    if let Some(arr) = self.arrays.get(var_name).cloned() {
5152                        let filtered: Vec<String> = if arr.len() >= 1000 {
5153                            tracing::trace!(
5154                                count = arr.len(),
5155                                pattern,
5156                                "using parallel filter (rayon) for large array"
5157                            );
5158                            use rayon::prelude::*;
5159                            let pattern = pattern.to_string();
5160                            arr.into_par_iter()
5161                                .filter(|elem| {
5162                                    let m = Self::glob_match_static(elem, &pattern);
5163                                    if has_match_flag { m } else { !m }
5164                                })
5165                                .collect()
5166                        } else {
5167                            arr.into_iter()
5168                                .filter(|elem| {
5169                                    let m = self.glob_match(elem, pattern);
5170                                    if has_match_flag { m } else { !m }
5171                                })
5172                                .collect()
5173                        };
5174                        return filtered.join(" ");
5175                    }
5176
5177                    // Scalar path: original behavior
5178                    let val = self.get_variable(var_name);
5179                    let matches = self.glob_match(&val, pattern);
5180
5181                    return if has_match_flag {
5182                        if matches { val } else { String::new() }
5183                    } else {
5184                        if matches { String::new() } else { val }
5185                    };
5186                }
5187
5188                // Handle ${(%):-%n} style - empty var with default after flags
5189                // rest could be ":-%n" or ":-default" or "var:-default" or just "var"
5190                let (var_name, default_val) = if rest.starts_with(":-") {
5191                    // Empty variable name with default: ${(%):-default}
5192                    ("", Some(&rest[2..]))
5193                } else if let Some(pos) = rest.find(":-") {
5194                    // Variable with default: ${(%)var:-default}
5195                    (&rest[..pos], Some(&rest[pos + 2..]))
5196                } else if rest.starts_with(':') {
5197                    // Just ":" means empty var name, no default
5198                    ("", None)
5199                } else {
5200                    // Normal variable reference
5201                    let vn = rest
5202                        .split(|c: char| !c.is_alphanumeric() && c != '_')
5203                        .next()
5204                        .unwrap_or("");
5205                    (vn, None)
5206                };
5207
5208                let mut val = self.get_variable(var_name);
5209
5210                // Use default if variable is empty
5211                if val.is_empty() {
5212                    if let Some(def) = default_val {
5213                        // Expand the default value (handles $var and other expansions)
5214                        val = self.expand_string(def);
5215                    }
5216                }
5217
5218                // Apply flags in order
5219                for flag in &flags {
5220                    val = self.apply_zsh_param_flag(&val, var_name, flag);
5221                }
5222                return val;
5223            }
5224        }
5225
5226        // Handle ${#arr[@]} - array length
5227        if content.starts_with('#') {
5228            let rest = &content[1..];
5229            if let Some(bracket_start) = rest.find('[') {
5230                let var_name = &rest[..bracket_start];
5231                let bracket_content = &rest[bracket_start + 1..];
5232                if let Some(bracket_end) = bracket_content.find(']') {
5233                    let index = &bracket_content[..bracket_end];
5234                    if index == "@" || index == "*" {
5235                        // ${#arr[@]} - return array length
5236                        return self
5237                            .arrays
5238                            .get(var_name)
5239                            .map(|arr| arr.len().to_string())
5240                            .unwrap_or_else(|| "0".to_string());
5241                    }
5242                }
5243            }
5244            // ${#arr} - if rest is an array name, return array length
5245            if self.arrays.contains_key(rest) {
5246                return self
5247                    .arrays
5248                    .get(rest)
5249                    .map(|arr| arr.len().to_string())
5250                    .unwrap_or_else(|| "0".to_string());
5251            }
5252            // ${#assoc} - if rest is an assoc array name, return assoc length
5253            if self.assoc_arrays.contains_key(rest) {
5254                return self
5255                    .assoc_arrays
5256                    .get(rest)
5257                    .map(|h| h.len().to_string())
5258                    .unwrap_or_else(|| "0".to_string());
5259            }
5260            // ${#var} - string length
5261            let val = self.get_variable(rest);
5262            return val.len().to_string();
5263        }
5264
5265        // Handle ${+var} and ${+arr[key]} - test if variable/element is set (returns 1 if set, 0 if not)
5266        if content.starts_with('+') {
5267            let rest = &content[1..];
5268
5269            // Check for array/assoc access: ${+arr[key]}
5270            if let Some(bracket_start) = rest.find('[') {
5271                let var_name = &rest[..bracket_start];
5272                let bracket_content = &rest[bracket_start + 1..];
5273                if let Some(bracket_end) = bracket_content.find(']') {
5274                    let key = &bracket_content[..bracket_end];
5275
5276                    // Check special arrays first
5277                    if let Some(val) = self.get_special_array_value(var_name, key) {
5278                        return if val.is_empty() {
5279                            "0".to_string()
5280                        } else {
5281                            "1".to_string()
5282                        };
5283                    }
5284
5285                    // Check user assoc arrays
5286                    if self.assoc_arrays.contains_key(var_name) {
5287                        let expanded_key = self.expand_string(key);
5288                        let has_key = self
5289                            .assoc_arrays
5290                            .get(var_name)
5291                            .map(|a| a.contains_key(&expanded_key))
5292                            .unwrap_or(false);
5293                        return if has_key {
5294                            "1".to_string()
5295                        } else {
5296                            "0".to_string()
5297                        };
5298                    }
5299
5300                    // Check regular arrays
5301                    if let Some(arr) = self.arrays.get(var_name) {
5302                        if let Ok(idx) = key.parse::<usize>() {
5303                            let actual_idx = if idx > 0 { idx - 1 } else { 0 };
5304                            return if arr.get(actual_idx).is_some() {
5305                                "1".to_string()
5306                            } else {
5307                                "0".to_string()
5308                            };
5309                        }
5310                    }
5311
5312                    return "0".to_string();
5313                }
5314            }
5315
5316            // Simple variable: ${+var}
5317            let is_set = self.variables.contains_key(rest)
5318                || self.arrays.contains_key(rest)
5319                || self.assoc_arrays.contains_key(rest)
5320                || std::env::var(rest).is_ok()
5321                || self.functions.contains_key(rest);
5322            return if is_set {
5323                "1".to_string()
5324            } else {
5325                "0".to_string()
5326            };
5327        }
5328
5329        // Handle ${arr[idx]} or ${assoc[key]}
5330        if let Some(bracket_start) = content.find('[') {
5331            let var_name = &content[..bracket_start];
5332            let bracket_content = &content[bracket_start + 1..];
5333            if let Some(bracket_end) = bracket_content.find(']') {
5334                let index = &bracket_content[..bracket_end];
5335
5336                // Check for zsh/parameter special associative arrays (options, commands, etc.)
5337                if let Some(val) = self.get_special_array_value(var_name, index) {
5338                    return val;
5339                }
5340
5341                // Check if it's a user-defined associative array
5342                if self.assoc_arrays.contains_key(var_name) {
5343                    if index == "@" || index == "*" {
5344                        // ${assoc[@]} - return all values
5345                        return self
5346                            .assoc_arrays
5347                            .get(var_name)
5348                            .map(|a| a.values().cloned().collect::<Vec<_>>().join(" "))
5349                            .unwrap_or_default();
5350                    } else {
5351                        // ${assoc[key]} - return value for key
5352                        let key = self.expand_string(index);
5353                        return self
5354                            .assoc_arrays
5355                            .get(var_name)
5356                            .and_then(|a| a.get(&key).cloned())
5357                            .unwrap_or_default();
5358                    }
5359                }
5360
5361                // Regular indexed array
5362                if index == "@" || index == "*" {
5363                    // ${arr[@]} - return all elements
5364                    return self
5365                        .arrays
5366                        .get(var_name)
5367                        .map(|arr| arr.join(" "))
5368                        .unwrap_or_default();
5369                }
5370
5371                // Use the ported subscript module for comprehensive index parsing
5372                use crate::subscript::{
5373                    get_array_by_subscript, get_array_element_by_subscript, getindex,
5374                };
5375                let ksh_arrays = self.options.get("ksh_arrays").copied().unwrap_or(false);
5376
5377                if let Ok(v) = getindex(index, false, ksh_arrays) {
5378                    // Check if it's an array first
5379                    if let Some(arr) = self.arrays.get(var_name) {
5380                        if v.is_all() {
5381                            return arr.join(" ");
5382                        }
5383                        // Check if this is a range (comma in subscript) vs single element
5384                        // For a single element, v.end == v.start + 1 after adjustment
5385                        // But for negative single indices, we need to handle specially
5386                        let is_range = index.contains(',');
5387                        if is_range {
5388                            // Range: ${arr[2,4]} returns elements 2 through 4
5389                            return get_array_by_subscript(arr, &v, ksh_arrays).join(" ");
5390                        } else {
5391                            // Single element (including negative indices like -1)
5392                            return get_array_element_by_subscript(arr, &v, ksh_arrays)
5393                                .unwrap_or_default();
5394                        }
5395                    }
5396
5397                    // Not an array - treat as string subscripting
5398                    let val = self.get_variable(var_name);
5399                    if !val.is_empty() {
5400                        let chars: Vec<char> = val.chars().collect();
5401                        let idx = v.start;
5402                        let actual_idx = if idx < 0 {
5403                            (chars.len() as i64 + idx).max(0) as usize
5404                        } else if idx > 0 {
5405                            (idx - 1) as usize // zsh is 1-indexed
5406                        } else {
5407                            0
5408                        };
5409
5410                        if v.end > v.start + 1 {
5411                            // String range
5412                            let end_idx = if v.end < 0 {
5413                                (chars.len() as i64 + v.end + 1).max(0) as usize
5414                            } else {
5415                                v.end as usize
5416                            };
5417                            let end_idx = end_idx.min(chars.len());
5418                            return chars[actual_idx..end_idx].iter().collect();
5419                        } else {
5420                            return chars
5421                                .get(actual_idx)
5422                                .map(|c| c.to_string())
5423                                .unwrap_or_default();
5424                        }
5425                    }
5426                    return String::new();
5427                }
5428
5429                // Non-numeric index on non-assoc - return empty
5430                return String::new();
5431            }
5432        }
5433
5434        // Handle ${var:-default}, ${var:=default}, ${var:?error}, ${var:+alternate}
5435        if let Some(colon_pos) = content.find(':') {
5436            let var_name = &content[..colon_pos];
5437            let rest = &content[colon_pos + 1..];
5438            let val = self.get_variable(var_name);
5439            let val_opt = if val.is_empty() {
5440                None
5441            } else {
5442                Some(val.clone())
5443            };
5444
5445            if rest.starts_with('-') {
5446                // ${var:-default}
5447                return match val_opt {
5448                    Some(v) if !v.is_empty() => v,
5449                    _ => self.expand_string(&rest[1..]),
5450                };
5451            } else if rest.starts_with('=') {
5452                // ${var:=default}
5453                return match val_opt {
5454                    Some(v) if !v.is_empty() => v,
5455                    _ => {
5456                        let default = self.expand_string(&rest[1..]);
5457                        self.variables.insert(var_name.to_string(), default.clone());
5458                        default
5459                    }
5460                };
5461            } else if rest.starts_with('?') {
5462                // ${var:?error}
5463                return match val_opt {
5464                    Some(v) if !v.is_empty() => v,
5465                    _ => {
5466                        let msg = self.expand_string(&rest[1..]);
5467                        eprintln!("zshrs: {}: {}", var_name, msg);
5468                        String::new()
5469                    }
5470                };
5471            } else if rest.starts_with('+') {
5472                // ${var:+alternate}
5473                return match val_opt {
5474                    Some(v) if !v.is_empty() => self.expand_string(&rest[1..]),
5475                    _ => String::new(),
5476                };
5477            } else if rest.starts_with('#') {
5478                // ${var:#pattern} - filter: remove elements matching pattern
5479                // With (M) flag, keep only matching elements
5480                let pattern = &rest[1..];
5481                // For scalars, return empty if matches, value if not
5482                if self.glob_match(&val, pattern) {
5483                    return String::new();
5484                } else {
5485                    return val;
5486                }
5487            } else if self.is_history_modifier(rest) {
5488                // Handle history-style modifiers: :A, :h, :t, :r, :e, :l, :u, :q, :Q
5489                // These can be chained: ${var:A:h:h}
5490                return self.apply_history_modifiers(&val, rest);
5491            } else if rest
5492                .chars()
5493                .next()
5494                .map(|c| c.is_ascii_digit() || c == '-')
5495                .unwrap_or(false)
5496            {
5497                // ${var:offset} or ${var:offset:length}
5498                let parts: Vec<&str> = rest.splitn(2, ':').collect();
5499                let offset: i64 = parts[0].parse().unwrap_or(0);
5500                let length: Option<usize> = parts.get(1).and_then(|s| s.parse().ok());
5501
5502                let start = if offset < 0 {
5503                    (val.len() as i64 + offset).max(0) as usize
5504                } else {
5505                    (offset as usize).min(val.len())
5506                };
5507
5508                return if let Some(len) = length {
5509                    val.chars().skip(start).take(len).collect()
5510                } else {
5511                    val.chars().skip(start).collect()
5512                };
5513            }
5514        }
5515
5516        // Handle ${var/pattern/replacement} and ${var//pattern/replacement}
5517        // Only if the part before / is a valid variable name
5518        if let Some(slash_pos) = content.find('/') {
5519            let var_name = &content[..slash_pos];
5520            // Variable names must start with letter/underscore and contain only alnum/_
5521            if !var_name.is_empty()
5522                && var_name
5523                    .chars()
5524                    .next()
5525                    .map(|c| c.is_alphabetic() || c == '_')
5526                    .unwrap_or(false)
5527                && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
5528            {
5529                let rest = &content[slash_pos + 1..];
5530                let val = self.get_variable(var_name);
5531
5532                let replace_all = rest.starts_with('/');
5533                let rest = if replace_all { &rest[1..] } else { rest };
5534
5535                let parts: Vec<&str> = rest.splitn(2, '/').collect();
5536                let pattern = parts.get(0).unwrap_or(&"");
5537                let replacement = parts.get(1).unwrap_or(&"");
5538
5539                return if replace_all {
5540                    val.replace(pattern, replacement)
5541                } else {
5542                    val.replacen(pattern, replacement, 1)
5543                };
5544            }
5545        }
5546
5547        // Handle ${var#pattern} and ${var##pattern} - remove prefix
5548        // But only if the # is not at the start (which would be length)
5549        if let Some(hash_pos) = content.find('#') {
5550            if hash_pos > 0 {
5551                let var_name = &content[..hash_pos];
5552                // Make sure var_name looks like a valid variable name
5553                if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
5554                    let rest = &content[hash_pos + 1..];
5555                    let val = self.get_variable(var_name);
5556
5557                    let long = rest.starts_with('#');
5558                    let pattern = if long { &rest[1..] } else { rest };
5559
5560                    // Convert shell glob pattern to regex-style for matching prefixes
5561                    let pattern_regex = regex::escape(pattern)
5562                        .replace(r"\*", ".*")
5563                        .replace(r"\?", ".");
5564                    let full_pattern = format!("^{}", pattern_regex);
5565
5566                    if let Some(re) = cached_regex(&full_pattern) {
5567                        if long {
5568                            // Remove longest prefix match - find all matches and use the longest
5569                            let mut longest_end = 0;
5570                            for m in re.find_iter(&val) {
5571                                if m.end() > longest_end {
5572                                    longest_end = m.end();
5573                                }
5574                            }
5575                            if longest_end > 0 {
5576                                return val[longest_end..].to_string();
5577                            }
5578                        } else {
5579                            // Remove shortest prefix match
5580                            if let Some(m) = re.find(&val) {
5581                                return val[m.end()..].to_string();
5582                            }
5583                        }
5584                    }
5585                    return val;
5586                }
5587            }
5588        }
5589
5590        // Handle ${var%pattern} and ${var%%pattern} - remove suffix
5591        if let Some(pct_pos) = content.find('%') {
5592            if pct_pos > 0 {
5593                let var_name = &content[..pct_pos];
5594                if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
5595                    let rest = &content[pct_pos + 1..];
5596                    let val = self.get_variable(var_name);
5597
5598                    let long = rest.starts_with('%');
5599                    let pattern = if long { &rest[1..] } else { rest };
5600
5601                    // Use glob pattern matching for suffix removal
5602                    if let Ok(glob) = glob::Pattern::new(pattern) {
5603                        if long {
5604                            // Remove longest suffix match - find earliest matching position
5605                            for i in 0..=val.len() {
5606                                if glob.matches(&val[i..]) {
5607                                    return val[..i].to_string();
5608                                }
5609                            }
5610                        } else {
5611                            // Remove shortest suffix match - find latest matching position
5612                            for i in (0..=val.len()).rev() {
5613                                if glob.matches(&val[i..]) {
5614                                    return val[..i].to_string();
5615                                }
5616                            }
5617                        }
5618                    }
5619                    return val;
5620                }
5621            }
5622        }
5623
5624        // Handle ${var^} and ${var^^} - uppercase
5625        if let Some(caret_pos) = content.find('^') {
5626            let var_name = &content[..caret_pos];
5627            let val = self.get_variable(var_name);
5628            let all = content[caret_pos + 1..].starts_with('^');
5629
5630            return if all {
5631                val.to_uppercase()
5632            } else {
5633                let mut chars = val.chars();
5634                match chars.next() {
5635                    Some(first) => first.to_uppercase().to_string() + chars.as_str(),
5636                    None => String::new(),
5637                }
5638            };
5639        }
5640
5641        // Handle ${var,} and ${var,,} - lowercase
5642        if let Some(comma_pos) = content.find(',') {
5643            let var_name = &content[..comma_pos];
5644            let val = self.get_variable(var_name);
5645            let all = content[comma_pos + 1..].starts_with(',');
5646
5647            return if all {
5648                val.to_lowercase()
5649            } else {
5650                let mut chars = val.chars();
5651                match chars.next() {
5652                    Some(first) => first.to_lowercase().to_string() + chars.as_str(),
5653                    None => String::new(),
5654                }
5655            };
5656        }
5657
5658        // Handle ${!prefix*} and ${!prefix@} - expand to variable names with prefix
5659        if content.starts_with('!') {
5660            let rest = &content[1..];
5661            if rest.ends_with('*') || rest.ends_with('@') {
5662                let prefix = &rest[..rest.len() - 1];
5663                let mut matches: Vec<String> = self
5664                    .variables
5665                    .keys()
5666                    .filter(|k| k.starts_with(prefix))
5667                    .cloned()
5668                    .collect();
5669                // Also check arrays
5670                for k in self.arrays.keys() {
5671                    if k.starts_with(prefix) && !matches.contains(k) {
5672                        matches.push(k.clone());
5673                    }
5674                }
5675                matches.sort();
5676                return matches.join(" ");
5677            }
5678
5679            // ${!var} - indirect expansion
5680            let var_name = self.get_variable(rest);
5681            return self.get_variable(&var_name);
5682        }
5683
5684        // Default: just get the variable
5685        self.get_variable(content)
5686    }
5687
5688    fn expand_array_access(&mut self, name: &str, index: &ShellWord) -> String {
5689        use crate::subscript::{get_array_by_subscript, get_array_element_by_subscript, getindex};
5690
5691        let idx_str = self.expand_word(index);
5692        let ksh_arrays = self.options.get("ksh_arrays").copied().unwrap_or(false);
5693
5694        // Use the ported subscript module for index parsing
5695        match getindex(&idx_str, false, ksh_arrays) {
5696            Ok(v) => {
5697                if let Some(arr) = self.arrays.get(name) {
5698                    if v.is_all() {
5699                        arr.join(" ")
5700                    } else if v.start == v.end - 1 {
5701                        // Single element
5702                        get_array_element_by_subscript(arr, &v, ksh_arrays).unwrap_or_default()
5703                    } else {
5704                        // Range
5705                        get_array_by_subscript(arr, &v, ksh_arrays).join(" ")
5706                    }
5707                } else {
5708                    String::new()
5709                }
5710            }
5711            Err(_) => String::new(),
5712        }
5713    }
5714
5715    #[tracing::instrument(level = "trace", skip_all)]
5716    fn expand_string(&mut self, s: &str) -> String {
5717        let mut result = String::new();
5718        let mut chars = s.chars().peekable();
5719
5720        while let Some(c) = chars.next() {
5721            // \x00 prefix marks chars from single quotes - keep them literal
5722            if c == '\x00' {
5723                if let Some(literal_char) = chars.next() {
5724                    result.push(literal_char);
5725                }
5726                continue;
5727            }
5728            if c == '$' {
5729                if chars.peek() == Some(&'(') {
5730                    chars.next(); // consume '('
5731
5732                    // Check for $(( )) arithmetic
5733                    if chars.peek() == Some(&'(') {
5734                        chars.next(); // consume second '('
5735                        let expr = Self::collect_until_double_paren(&mut chars);
5736                        result.push_str(&self.evaluate_arithmetic(&expr));
5737                    } else {
5738                        // Command substitution $(...)
5739                        let cmd_str = Self::collect_until_paren(&mut chars);
5740                        result.push_str(&self.run_command_substitution(&cmd_str));
5741                    }
5742                } else if chars.peek() == Some(&'{') {
5743                    chars.next();
5744                    // Collect the full braced expression including brackets
5745                    let mut brace_content = String::new();
5746                    let mut depth = 1;
5747                    while let Some(c) = chars.next() {
5748                        if c == '{' {
5749                            depth += 1;
5750                            brace_content.push(c);
5751                        } else if c == '}' {
5752                            depth -= 1;
5753                            if depth == 0 {
5754                                break;
5755                            }
5756                            brace_content.push(c);
5757                        } else {
5758                            brace_content.push(c);
5759                        }
5760                    }
5761                    result.push_str(&self.expand_braced_variable(&brace_content));
5762                } else {
5763                    // Check for single-char special vars first: $$, $!, $-
5764                    if matches!(chars.peek(), Some(&'$') | Some(&'!') | Some(&'-')) {
5765                        let sc = chars.next().unwrap();
5766                        result.push_str(&self.get_variable(&sc.to_string()));
5767                        continue;
5768                    }
5769                    // $#name → ${#name} (string/array length)
5770                    if chars.peek() == Some(&'#') {
5771                        let mut peek_iter = chars.clone();
5772                        peek_iter.next(); // skip #
5773                        if peek_iter.peek().map(|c| c.is_alphabetic() || *c == '_').unwrap_or(false) {
5774                            chars.next(); // consume #
5775                            let mut name = String::new();
5776                            while let Some(&c) = chars.peek() {
5777                                if c.is_alphanumeric() || c == '_' {
5778                                    name.push(chars.next().unwrap());
5779                                } else {
5780                                    break;
5781                                }
5782                            }
5783                            // Return length of variable or array
5784                            let len = if let Some(arr) = self.arrays.get(&name) {
5785                                arr.len()
5786                            } else {
5787                                self.get_variable(&name).len()
5788                            };
5789                            result.push_str(&len.to_string());
5790                            continue;
5791                        }
5792                    }
5793                    let mut var_name = String::new();
5794                    while let Some(&c) = chars.peek() {
5795                        if c.is_alphanumeric() || c == '_' || c == '@' || c == '*' || c == '#' || c == '?' {
5796                            var_name.push(chars.next().unwrap());
5797                            // Handle single-char special vars
5798                            if matches!(
5799                                var_name.as_str(),
5800                                "@" | "*"
5801                                    | "#"
5802                                    | "?"
5803                                    | "$"
5804                                    | "!"
5805                                    | "-"
5806                                    | "0"
5807                                    | "1"
5808                                    | "2"
5809                                    | "3"
5810                                    | "4"
5811                                    | "5"
5812                                    | "6"
5813                                    | "7"
5814                                    | "8"
5815                                    | "9"
5816                            ) {
5817                                break;
5818                            }
5819                        } else {
5820                            break;
5821                        }
5822                    }
5823                    result.push_str(&self.get_variable(&var_name));
5824                }
5825            } else if c == '`' {
5826                // Backtick command substitution
5827                let cmd_str: String = chars.by_ref().take_while(|&c| c != '`').collect();
5828                result.push_str(&self.run_command_substitution(&cmd_str));
5829            } else if c == '<' && chars.peek() == Some(&'(') {
5830                // Process substitution <(cmd)
5831                chars.next(); // consume '('
5832                let cmd_str = Self::collect_until_paren(&mut chars);
5833                result.push_str(&self.run_process_sub_in(&cmd_str));
5834            } else if c == '>' && chars.peek() == Some(&'(') {
5835                // Process substitution >(cmd)
5836                chars.next(); // consume '('
5837                let cmd_str = Self::collect_until_paren(&mut chars);
5838                result.push_str(&self.run_process_sub_out(&cmd_str));
5839            } else if c == '~' && result.is_empty() {
5840                if let Some(home) = dirs::home_dir() {
5841                    result.push_str(&home.to_string_lossy());
5842                } else {
5843                    result.push(c);
5844                }
5845            } else {
5846                result.push(c);
5847            }
5848        }
5849
5850        result
5851    }
5852
5853    fn collect_until_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
5854        let mut result = String::new();
5855        let mut depth = 1;
5856
5857        while let Some(c) = chars.next() {
5858            if c == '(' {
5859                depth += 1;
5860                result.push(c);
5861            } else if c == ')' {
5862                depth -= 1;
5863                if depth == 0 {
5864                    break;
5865                }
5866                result.push(c);
5867            } else {
5868                result.push(c);
5869            }
5870        }
5871
5872        result
5873    }
5874
5875    fn collect_until_double_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
5876        let mut result = String::new();
5877        let mut arith_depth = 1; // Tracks $(( ... )) nesting
5878        let mut paren_depth = 0; // Tracks ( ... ) nesting within expression
5879
5880        while let Some(c) = chars.next() {
5881            if c == '(' {
5882                if paren_depth == 0 && chars.peek() == Some(&'(') {
5883                    // Nested $(( - but we need to see if it's really another arithmetic
5884                    // For simplicity, track inner parens
5885                    paren_depth += 1;
5886                    result.push(c);
5887                } else {
5888                    paren_depth += 1;
5889                    result.push(c);
5890                }
5891            } else if c == ')' {
5892                if paren_depth > 0 {
5893                    // Inside nested parens, just close one level
5894                    paren_depth -= 1;
5895                    result.push(c);
5896                } else if chars.peek() == Some(&')') {
5897                    // At top level and seeing )) - this closes our arithmetic
5898                    chars.next();
5899                    arith_depth -= 1;
5900                    if arith_depth == 0 {
5901                        break;
5902                    }
5903                    result.push_str("))");
5904                } else {
5905                    // Single ) at top level - shouldn't happen in valid expression
5906                    result.push(c);
5907                }
5908            } else {
5909                result.push(c);
5910            }
5911        }
5912
5913        result
5914    }
5915
5916    fn run_process_sub_in(&mut self, cmd_str: &str) -> String {
5917        use std::fs;
5918        use std::process::Stdio;
5919
5920        // Parse the command
5921        let mut parser = ShellParser::new(cmd_str);
5922        let commands = match parser.parse_script() {
5923            Ok(cmds) => cmds,
5924            Err(_) => return String::new(),
5925        };
5926
5927        // Create a unique FIFO in temp directory
5928        let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
5929        let fifo_counter = self.process_sub_counter;
5930        self.process_sub_counter += 1;
5931        let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
5932
5933        // Remove if exists, then create FIFO
5934        let _ = fs::remove_file(&fifo_path);
5935        if let Err(_) = nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU) {
5936            return String::new();
5937        }
5938
5939        // Spawn command that writes to the FIFO
5940        let fifo_clone = fifo_path.clone();
5941        if let Some(cmd) = commands.first() {
5942            if let ShellCommand::Simple(simple) = cmd {
5943                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
5944                if !words.is_empty() {
5945                    let cmd_name = words[0].clone();
5946                    let args: Vec<String> = words[1..].to_vec();
5947
5948                    self.worker_pool.submit(move || {
5949                        // Open FIFO for writing (will block until reader connects)
5950                        if let Ok(fifo) = fs::OpenOptions::new().write(true).open(&fifo_clone) {
5951                            let _ = Command::new(&cmd_name)
5952                                .args(&args)
5953                                .stdout(fifo)
5954                                .stderr(Stdio::inherit())
5955                                .status();
5956                        }
5957                        // Clean up FIFO after command completes
5958                        let _ = fs::remove_file(&fifo_clone);
5959                    });
5960                }
5961            }
5962        }
5963
5964        fifo_path
5965    }
5966
5967    fn run_process_sub_out(&mut self, cmd_str: &str) -> String {
5968        use std::fs;
5969        use std::process::Stdio;
5970
5971        // Parse the command
5972        let mut parser = ShellParser::new(cmd_str);
5973        let commands = match parser.parse_script() {
5974            Ok(cmds) => cmds,
5975            Err(_) => return String::new(),
5976        };
5977
5978        // Create a unique FIFO in temp directory
5979        let fifo_path = format!("/tmp/zshrs_psub_{}", std::process::id());
5980        let fifo_counter = self.process_sub_counter;
5981        self.process_sub_counter += 1;
5982        let fifo_path = format!("{}_{}", fifo_path, fifo_counter);
5983
5984        // Remove if exists, then create FIFO
5985        let _ = fs::remove_file(&fifo_path);
5986        if let Err(_) = nix::unistd::mkfifo(fifo_path.as_str(), nix::sys::stat::Mode::S_IRWXU) {
5987            return String::new();
5988        }
5989
5990        // Spawn command that reads from the FIFO
5991        let fifo_clone = fifo_path.clone();
5992        if let Some(cmd) = commands.first() {
5993            if let ShellCommand::Simple(simple) = cmd {
5994                let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
5995                if !words.is_empty() {
5996                    let cmd_name = words[0].clone();
5997                    let args: Vec<String> = words[1..].to_vec();
5998
5999                    self.worker_pool.submit(move || {
6000                        // Open FIFO for reading (will block until writer connects)
6001                        if let Ok(fifo) = fs::File::open(&fifo_clone) {
6002                            let _ = Command::new(&cmd_name)
6003                                .args(&args)
6004                                .stdin(fifo)
6005                                .stdout(Stdio::inherit())
6006                                .stderr(Stdio::inherit())
6007                                .status();
6008                        }
6009                        // Clean up FIFO after command completes
6010                        let _ = fs::remove_file(&fifo_clone);
6011                    });
6012                }
6013            }
6014        }
6015
6016        fifo_path
6017    }
6018
6019    fn run_command_substitution(&mut self, cmd_str: &str) -> String {
6020        use std::process::Stdio;
6021
6022        // Port of getoutput() from Src/exec.c:
6023        // C zsh forks, redirects stdout to a pipe, executes via execode(),
6024        // and the parent reads back the output.  We achieve the same by
6025        // capturing stdout through an in-process pipe.
6026
6027        let mut parser = ShellParser::new(cmd_str);
6028        let commands = match parser.parse_script() {
6029            Ok(cmds) => cmds,
6030            Err(_) => return String::new(),
6031        };
6032
6033        if commands.is_empty() {
6034            return String::new();
6035        }
6036
6037        // Check if this is a simple external-only command (no builtins/functions)
6038        // so we can use the fast path of spawning a child process.
6039        let is_internal = if let ShellCommand::Simple(simple) = &commands[0] {
6040            let first = simple.words.first().map(|w| self.expand_word(w));
6041            if let Some(ref name) = first {
6042                self.functions.contains_key(name)
6043                    || self.is_builtin(name)
6044            } else {
6045                true
6046            }
6047        } else {
6048            true // compound commands are always internal
6049        };
6050
6051        if is_internal {
6052            // Internal execution: capture stdout via a pipe
6053            let (read_fd, write_fd) = {
6054                let mut fds = [0i32; 2];
6055                if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
6056                    return String::new();
6057                }
6058                (fds[0], fds[1])
6059            };
6060
6061            // Save original stdout and redirect to our pipe
6062            let saved_stdout = unsafe { libc::dup(1) };
6063            unsafe { libc::dup2(write_fd, 1); }
6064            unsafe { libc::close(write_fd); }
6065
6066            // Execute all commands
6067            for cmd in &commands {
6068                let _ = self.execute_command(cmd);
6069            }
6070
6071            // Flush stdout so buffered output goes to pipe
6072            use std::io::Write;
6073            let _ = io::stdout().flush();
6074
6075            // Restore stdout
6076            unsafe { libc::dup2(saved_stdout, 1); }
6077            unsafe { libc::close(saved_stdout); }
6078
6079            // Read captured output
6080            use std::os::unix::io::FromRawFd;
6081            let mut output = String::new();
6082            let read_file = unsafe { std::fs::File::from_raw_fd(read_fd) };
6083            use std::io::Read;
6084            let _ = std::io::BufReader::new(read_file).read_to_string(&mut output);
6085
6086            output.trim_end_matches('\n').to_string()
6087        } else {
6088            // External command: spawn child and capture stdout
6089            if let ShellCommand::Simple(simple) = &commands[0] {
6090                let words: Vec<String> =
6091                    simple.words.iter().map(|w| self.expand_word(w)).collect();
6092                if words.is_empty() {
6093                    return String::new();
6094                }
6095
6096                let output = Command::new(&words[0])
6097                    .args(&words[1..])
6098                    .stdout(Stdio::piped())
6099                    .stderr(Stdio::inherit())
6100                    .output();
6101
6102                match output {
6103                    Ok(out) => String::from_utf8_lossy(&out.stdout)
6104                        .trim_end_matches('\n')
6105                        .to_string(),
6106                    Err(_) => String::new(),
6107                }
6108            } else {
6109                String::new()
6110            }
6111        }
6112    }
6113
6114    /// Process substitution <(cmd) - returns FIFO path
6115    fn execute_process_sub_in(&mut self, cmd: &ShellCommand) -> String {
6116        if let ShellCommand::Simple(simple) = cmd {
6117            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
6118            let cmd_str = words.join(" ");
6119            self.run_process_sub_in(&cmd_str)
6120        } else {
6121            String::new()
6122        }
6123    }
6124
6125    /// Process substitution >(cmd) - returns FIFO path
6126    fn execute_process_sub_out(&mut self, cmd: &ShellCommand) -> String {
6127        if let ShellCommand::Simple(simple) = cmd {
6128            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
6129            let cmd_str = words.join(" ");
6130            self.run_process_sub_out(&cmd_str)
6131        } else {
6132            String::new()
6133        }
6134    }
6135
6136    /// Get value from zsh/parameter special arrays (options, commands, functions, etc.)
6137    /// Returns Some(value) if this is a special array access, None otherwise
6138    fn get_special_array_value(&self, array_name: &str, key: &str) -> Option<String> {
6139        match array_name {
6140            // === SHELL OPTIONS ===
6141            "options" => {
6142                if key == "@" || key == "*" {
6143                    // Return all options as "name=on/off" pairs
6144                    let opts: Vec<String> = self
6145                        .options
6146                        .iter()
6147                        .map(|(k, v)| format!("{}={}", k, if *v { "on" } else { "off" }))
6148                        .collect();
6149                    return Some(opts.join(" "));
6150                }
6151                let opt_name = key.to_lowercase().replace('_', "");
6152                let is_on = self.options.get(&opt_name).copied().unwrap_or(false);
6153                Some(if is_on {
6154                    "on".to_string()
6155                } else {
6156                    "off".to_string()
6157                })
6158            }
6159
6160            // === ALIASES ===
6161            "aliases" => {
6162                if key == "@" || key == "*" {
6163                    let vals: Vec<String> = self.aliases.values().cloned().collect();
6164                    return Some(vals.join(" "));
6165                }
6166                Some(self.aliases.get(key).cloned().unwrap_or_default())
6167            }
6168            "galiases" => {
6169                if key == "@" || key == "*" {
6170                    let vals: Vec<String> = self.global_aliases.values().cloned().collect();
6171                    return Some(vals.join(" "));
6172                }
6173                Some(self.global_aliases.get(key).cloned().unwrap_or_default())
6174            }
6175            "saliases" => {
6176                if key == "@" || key == "*" {
6177                    let vals: Vec<String> = self.suffix_aliases.values().cloned().collect();
6178                    return Some(vals.join(" "));
6179                }
6180                Some(self.suffix_aliases.get(key).cloned().unwrap_or_default())
6181            }
6182
6183            // === FUNCTIONS ===
6184            "functions" => {
6185                if key == "@" || key == "*" {
6186                    let names: Vec<String> = self.functions.keys().cloned().collect();
6187                    return Some(names.join(" "));
6188                }
6189                if let Some(body) = self.functions.get(key) {
6190                    Some(format!("{:?}", body))
6191                } else {
6192                    Some(String::new())
6193                }
6194            }
6195            "functions_source" => {
6196                // We don't track source locations, return empty
6197                Some(String::new())
6198            }
6199
6200            // === COMMANDS (command hash table) ===
6201            "commands" => {
6202                if key == "@" || key == "*" {
6203                    return Some(String::new()); // Would need to enumerate PATH
6204                }
6205                // Look up command in PATH
6206                if let Some(path) = self.find_in_path(key) {
6207                    Some(path)
6208                } else {
6209                    Some(String::new())
6210                }
6211            }
6212
6213            // === BUILTINS ===
6214            "builtins" => {
6215                let builtins = Self::get_builtin_names();
6216                if key == "@" || key == "*" {
6217                    return Some(builtins.join(" "));
6218                }
6219                if builtins.contains(&key) {
6220                    Some("defined".to_string())
6221                } else {
6222                    Some(String::new())
6223                }
6224            }
6225
6226            // === PARAMETERS ===
6227            "parameters" => {
6228                if key == "@" || key == "*" {
6229                    let mut names: Vec<String> = self.variables.keys().cloned().collect();
6230                    names.extend(self.arrays.keys().cloned());
6231                    names.extend(self.assoc_arrays.keys().cloned());
6232                    return Some(names.join(" "));
6233                }
6234                // Return type of parameter
6235                if self.assoc_arrays.contains_key(key) {
6236                    Some("association".to_string())
6237                } else if self.arrays.contains_key(key) {
6238                    Some("array".to_string())
6239                } else if self.variables.contains_key(key) || std::env::var(key).is_ok() {
6240                    Some("scalar".to_string())
6241                } else {
6242                    Some(String::new())
6243                }
6244            }
6245
6246            // === NAMED DIRECTORIES ===
6247            "nameddirs" => {
6248                if key == "@" || key == "*" {
6249                    let vals: Vec<String> = self
6250                        .named_dirs
6251                        .values()
6252                        .map(|p| p.display().to_string())
6253                        .collect();
6254                    return Some(vals.join(" "));
6255                }
6256                Some(
6257                    self.named_dirs
6258                        .get(key)
6259                        .map(|p| p.display().to_string())
6260                        .unwrap_or_default(),
6261                )
6262            }
6263
6264            // === USER DIRECTORIES ===
6265            "userdirs" => {
6266                if key == "@" || key == "*" {
6267                    return Some(String::new());
6268                }
6269                // Get home directory for user
6270                #[cfg(unix)]
6271                {
6272                    use std::ffi::CString;
6273                    if let Ok(name) = CString::new(key) {
6274                        unsafe {
6275                            let pwd = libc::getpwnam(name.as_ptr());
6276                            if !pwd.is_null() {
6277                                let dir = std::ffi::CStr::from_ptr((*pwd).pw_dir);
6278                                return Some(dir.to_string_lossy().to_string());
6279                            }
6280                        }
6281                    }
6282                }
6283                Some(String::new())
6284            }
6285
6286            // === USER GROUPS ===
6287            "usergroups" => {
6288                if key == "@" || key == "*" {
6289                    return Some(String::new());
6290                }
6291                // Get GID for group name
6292                #[cfg(unix)]
6293                {
6294                    use std::ffi::CString;
6295                    if let Ok(name) = CString::new(key) {
6296                        unsafe {
6297                            let grp = libc::getgrnam(name.as_ptr());
6298                            if !grp.is_null() {
6299                                return Some((*grp).gr_gid.to_string());
6300                            }
6301                        }
6302                    }
6303                }
6304                Some(String::new())
6305            }
6306
6307            // === DIRECTORY STACK ===
6308            "dirstack" => {
6309                if key == "@" || key == "*" {
6310                    let dirs: Vec<String> = self
6311                        .dir_stack
6312                        .iter()
6313                        .map(|p| p.display().to_string())
6314                        .collect();
6315                    return Some(dirs.join(" "));
6316                }
6317                if let Ok(idx) = key.parse::<usize>() {
6318                    Some(
6319                        self.dir_stack
6320                            .get(idx)
6321                            .map(|p| p.display().to_string())
6322                            .unwrap_or_default(),
6323                    )
6324                } else {
6325                    Some(String::new())
6326                }
6327            }
6328
6329            // === JOBS ===
6330            "jobstates" => {
6331                if key == "@" || key == "*" {
6332                    let states: Vec<String> = self
6333                        .jobs
6334                        .iter()
6335                        .map(|(id, job)| format!("{}:{:?}", id, job.state))
6336                        .collect();
6337                    return Some(states.join(" "));
6338                }
6339                if let Ok(id) = key.parse::<usize>() {
6340                    if let Some(job) = self.jobs.get(id) {
6341                        return Some(format!("{:?}", job.state));
6342                    }
6343                }
6344                Some(String::new())
6345            }
6346            "jobtexts" => {
6347                if key == "@" || key == "*" {
6348                    let texts: Vec<String> = self
6349                        .jobs
6350                        .iter()
6351                        .map(|(_, job)| job.command.clone())
6352                        .collect();
6353                    return Some(texts.join(" "));
6354                }
6355                if let Ok(id) = key.parse::<usize>() {
6356                    if let Some(job) = self.jobs.get(id) {
6357                        return Some(job.command.clone());
6358                    }
6359                }
6360                Some(String::new())
6361            }
6362            "jobdirs" => {
6363                // We don't track job directories separately - return current dir
6364                if key == "@" || key == "*" {
6365                    return Some(String::new());
6366                }
6367                Some(String::new())
6368            }
6369
6370            // === HISTORY ===
6371            "history" => {
6372                if key == "@" || key == "*" {
6373                    // Return recent history
6374                    if let Some(ref engine) = self.history {
6375                        if let Ok(entries) = engine.recent(100) {
6376                            let cmds: Vec<String> =
6377                                entries.iter().map(|e| e.command.clone()).collect();
6378                            return Some(cmds.join("\n"));
6379                        }
6380                    }
6381                    return Some(String::new());
6382                }
6383                if let Ok(num) = key.parse::<usize>() {
6384                    if let Some(ref engine) = self.history {
6385                        if let Ok(Some(entry)) = engine.get_by_offset(num.saturating_sub(1)) {
6386                            return Some(entry.command);
6387                        }
6388                    }
6389                }
6390                Some(String::new())
6391            }
6392            "historywords" => {
6393                // Array of words from history - simplified
6394                Some(String::new())
6395            }
6396
6397            // === MODULES ===
6398            "modules" => {
6399                // zshrs doesn't have loadable modules like zsh
6400                // Return empty or fake "loaded" for common modules
6401                if key == "@" || key == "*" {
6402                    return Some("zsh/parameter zsh/zutil".to_string());
6403                }
6404                match key {
6405                    "zsh/parameter" | "zsh/zutil" | "zsh/complete" | "zsh/complist" => {
6406                        Some("loaded".to_string())
6407                    }
6408                    _ => Some(String::new()),
6409                }
6410            }
6411
6412            // === RESERVED WORDS ===
6413            "reswords" => {
6414                let reswords = [
6415                    "do",
6416                    "done",
6417                    "esac",
6418                    "then",
6419                    "elif",
6420                    "else",
6421                    "fi",
6422                    "for",
6423                    "case",
6424                    "if",
6425                    "while",
6426                    "function",
6427                    "repeat",
6428                    "time",
6429                    "until",
6430                    "select",
6431                    "coproc",
6432                    "nocorrect",
6433                    "foreach",
6434                    "end",
6435                    "in",
6436                ];
6437                if key == "@" || key == "*" {
6438                    return Some(reswords.join(" "));
6439                }
6440                if let Ok(idx) = key.parse::<usize>() {
6441                    Some(reswords.get(idx).map(|s| s.to_string()).unwrap_or_default())
6442                } else {
6443                    Some(String::new())
6444                }
6445            }
6446
6447            // === PATCHARS (characters with special meaning in patterns) ===
6448            "patchars" => {
6449                let patchars = ["?", "*", "[", "]", "^", "#", "~", "(", ")", "|"];
6450                if key == "@" || key == "*" {
6451                    return Some(patchars.join(" "));
6452                }
6453                if let Ok(idx) = key.parse::<usize>() {
6454                    Some(patchars.get(idx).map(|s| s.to_string()).unwrap_or_default())
6455                } else {
6456                    Some(String::new())
6457                }
6458            }
6459
6460            // === FUNCTION CALL STACK ===
6461            "funcstack" | "functrace" | "funcfiletrace" | "funcsourcetrace" => {
6462                // Would need call stack tracking - return empty for now
6463                Some(String::new())
6464            }
6465
6466            // === DISABLED VARIANTS (dis_*) ===
6467            "dis_aliases"
6468            | "dis_galiases"
6469            | "dis_saliases"
6470            | "dis_functions"
6471            | "dis_functions_source"
6472            | "dis_builtins"
6473            | "dis_reswords"
6474            | "dis_patchars" => {
6475                // We don't track disabled items - return empty
6476                Some(String::new())
6477            }
6478
6479            // Not a special array
6480            _ => None,
6481        }
6482    }
6483
6484    /// Get list of all builtin command names
6485    fn get_builtin_names() -> Vec<&'static str> {
6486        vec![
6487            ".",
6488            ":",
6489            "[",
6490            "alias",
6491            "autoload",
6492            "bg",
6493            "bind",
6494            "bindkey",
6495            "break",
6496            "builtin",
6497            "bye",
6498            "caller",
6499            "cd",
6500            "cdreplay",
6501            "chdir",
6502            "clone",
6503            "command",
6504            "compadd",
6505            "comparguments",
6506            "compcall",
6507            "compctl",
6508            "compdef",
6509            "compdescribe",
6510            "compfiles",
6511            "compgen",
6512            "compgroups",
6513            "compinit",
6514            "complete",
6515            "compopt",
6516            "compquote",
6517            "compset",
6518            "comptags",
6519            "comptry",
6520            "compvalues",
6521            "continue",
6522            "coproc",
6523            "declare",
6524            "dirs",
6525            "disable",
6526            "disown",
6527            "echo",
6528            "echotc",
6529            "echoti",
6530            "emulate",
6531            "enable",
6532            "eval",
6533            "exec",
6534            "exit",
6535            "export",
6536            "false",
6537            "fc",
6538            "fg",
6539            "float",
6540            "functions",
6541            "getln",
6542            "getopts",
6543            "hash",
6544            "help",
6545            "history",
6546            "integer",
6547            "jobs",
6548            "kill",
6549            "let",
6550            "limit",
6551            "local",
6552            "log",
6553            "logout",
6554            "mapfile",
6555            "noglob",
6556            "popd",
6557            "print",
6558            "printf",
6559            "private",
6560            "prompt",
6561            "promptinit",
6562            "pushd",
6563            "pushln",
6564            "pwd",
6565            "r",
6566            "read",
6567            "readarray",
6568            "readonly",
6569            "rehash",
6570            "return",
6571            "sched",
6572            "set",
6573            "setopt",
6574            "shift",
6575            "shopt",
6576            "source",
6577            "stat",
6578            "strftime",
6579            "suspend",
6580            "test",
6581            "times",
6582            "trap",
6583            "true",
6584            "ttyctl",
6585            "type",
6586            "typeset",
6587            "ulimit",
6588            "umask",
6589            "unalias",
6590            "unfunction",
6591            "unhash",
6592            "unlimit",
6593            "unset",
6594            "unsetopt",
6595            "vared",
6596            "wait",
6597            "whence",
6598            "where",
6599            "which",
6600            "zcompile",
6601            "zcurses",
6602            "zformat",
6603            "zle",
6604            "zmodload",
6605            "zparseopts",
6606            "zprof",
6607            "zpty",
6608            "zregexparse",
6609            "zsocket",
6610            "zstyle",
6611            "ztcp",
6612            "add-zsh-hook",
6613        ]
6614    }
6615
6616    fn get_variable(&self, name: &str) -> String {
6617        // Handle special parameters
6618        match name {
6619            "" => String::new(), // Empty name returns empty
6620            "$" => std::process::id().to_string(),
6621            "@" | "*" => self.positional_params.join(" "),
6622            "#" => self.positional_params.len().to_string(),
6623            "?" => self.last_status.to_string(),
6624            "0" => self
6625                .variables
6626                .get("0")
6627                .cloned()
6628                .unwrap_or_else(|| env::args().next().unwrap_or_default()),
6629            n if !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) => {
6630                let idx: usize = n.parse().unwrap_or(0);
6631                if idx == 0 {
6632                    env::args().next().unwrap_or_default()
6633                } else {
6634                    self.positional_params
6635                        .get(idx - 1)
6636                        .cloned()
6637                        .unwrap_or_default()
6638                }
6639            }
6640            _ => {
6641                // Check local variables first, then arrays, then env
6642                self.variables
6643                    .get(name)
6644                    .cloned()
6645                    .or_else(|| {
6646                        // In zsh, $arr expands to space-joined array elements
6647                        self.arrays.get(name).map(|a| a.join(" "))
6648                    })
6649                    .or_else(|| env::var(name).ok())
6650                    .unwrap_or_default()
6651            }
6652        }
6653    }
6654
6655    fn apply_var_modifier(
6656        &mut self,
6657        name: &str,
6658        val: Option<String>,
6659        modifier: Option<&VarModifier>,
6660    ) -> String {
6661        match modifier {
6662            None => val.unwrap_or_default(),
6663
6664            // ${var:-word} - use default value
6665            Some(VarModifier::Default(word)) => match &val {
6666                Some(v) if !v.is_empty() => v.clone(),
6667                _ => self.expand_word(word),
6668            },
6669
6670            // ${var:=word} - assign default value
6671            Some(VarModifier::DefaultAssign(word)) => match &val {
6672                Some(v) if !v.is_empty() => v.clone(),
6673                _ => self.expand_word(word),
6674            },
6675
6676            // ${var:?word} - error if null or unset
6677            Some(VarModifier::Error(word)) => match &val {
6678                Some(v) if !v.is_empty() => v.clone(),
6679                _ => {
6680                    let msg = self.expand_word(word);
6681                    eprintln!("zshrs: {}", msg);
6682                    String::new()
6683                }
6684            },
6685
6686            // ${var:+word} - use alternate value
6687            Some(VarModifier::Alternate(word)) => match &val {
6688                Some(v) if !v.is_empty() => self.expand_word(word),
6689                _ => String::new(),
6690            },
6691
6692            // ${#var} - string length
6693            Some(VarModifier::Length) => val
6694                .map(|v| v.len().to_string())
6695                .unwrap_or_else(|| "0".to_string()),
6696
6697            // ${var:offset} or ${var:offset:length} - substring
6698            Some(VarModifier::Substring(offset, length)) => {
6699                let v = val.unwrap_or_default();
6700                let start = if *offset < 0 {
6701                    (v.len() as i64 + offset).max(0) as usize
6702                } else {
6703                    (*offset as usize).min(v.len())
6704                };
6705
6706                if let Some(len) = length {
6707                    let len = (*len as usize).min(v.len().saturating_sub(start));
6708                    v.chars().skip(start).take(len).collect()
6709                } else {
6710                    v.chars().skip(start).collect()
6711                }
6712            }
6713
6714            // ${var#pattern} - remove shortest prefix
6715            Some(VarModifier::RemovePrefix(pattern)) => {
6716                let v = val.unwrap_or_default();
6717                let pat = self.expand_word(pattern);
6718                if v.starts_with(&pat) {
6719                    v[pat.len()..].to_string()
6720                } else {
6721                    v
6722                }
6723            }
6724
6725            // ${var##pattern} - remove longest prefix
6726            Some(VarModifier::RemovePrefixLong(pattern)) => {
6727                let v = val.unwrap_or_default();
6728                let pat = self.expand_word(pattern);
6729                // For glob patterns, find longest match from start
6730                if let Ok(glob) = glob::Pattern::new(&pat) {
6731                    for i in (0..=v.len()).rev() {
6732                        if glob.matches(&v[..i]) {
6733                            return v[i..].to_string();
6734                        }
6735                    }
6736                }
6737                v
6738            }
6739
6740            // ${var%pattern} - remove shortest suffix
6741            Some(VarModifier::RemoveSuffix(pattern)) => {
6742                let v = val.unwrap_or_default();
6743                let pat = self.expand_word(pattern);
6744                // For glob patterns, find shortest match from end
6745                if let Ok(glob) = glob::Pattern::new(&pat) {
6746                    for i in (0..=v.len()).rev() {
6747                        if glob.matches(&v[i..]) {
6748                            return v[..i].to_string();
6749                        }
6750                    }
6751                } else if v.ends_with(&pat) {
6752                    return v[..v.len() - pat.len()].to_string();
6753                }
6754                v
6755            }
6756
6757            // ${var%%pattern} - remove longest suffix
6758            Some(VarModifier::RemoveSuffixLong(pattern)) => {
6759                let v = val.unwrap_or_default();
6760                let pat = self.expand_word(pattern);
6761                // For glob patterns, find longest match from end
6762                if let Ok(glob) = glob::Pattern::new(&pat) {
6763                    for i in 0..=v.len() {
6764                        if glob.matches(&v[i..]) {
6765                            return v[..i].to_string();
6766                        }
6767                    }
6768                }
6769                v
6770            }
6771
6772            // ${var/pattern/replacement} - replace first match
6773            Some(VarModifier::Replace(pattern, replacement)) => {
6774                let v = val.unwrap_or_default();
6775                let pat = self.expand_word(pattern);
6776                let repl = self.expand_word(replacement);
6777                v.replacen(&pat, &repl, 1)
6778            }
6779
6780            // ${var//pattern/replacement} - replace all matches
6781            Some(VarModifier::ReplaceAll(pattern, replacement)) => {
6782                let v = val.unwrap_or_default();
6783                let pat = self.expand_word(pattern);
6784                let repl = self.expand_word(replacement);
6785                v.replace(&pat, &repl)
6786            }
6787
6788            // ${var^} or ${var^^} - uppercase
6789            Some(VarModifier::Upper) => val.map(|v| v.to_uppercase()).unwrap_or_default(),
6790
6791            // ${var,} or ${var,,} - lowercase
6792            Some(VarModifier::Lower) => val.map(|v| v.to_lowercase()).unwrap_or_default(),
6793
6794            // ${(flags)var} - zsh parameter expansion flags
6795            Some(VarModifier::ZshFlags(flags)) => {
6796                let mut result = val.unwrap_or_default();
6797                for flag in flags {
6798                    result = self.apply_zsh_param_flag(&result, name, flag);
6799                }
6800                result
6801            }
6802
6803            // Array-related modifiers are handled elsewhere
6804            Some(VarModifier::ArrayLength)
6805            | Some(VarModifier::ArrayIndex(_))
6806            | Some(VarModifier::ArrayAll) => val.unwrap_or_default(),
6807        }
6808    }
6809
6810    /// Check if a string starts with history modifier characters
6811    fn is_history_modifier(&self, s: &str) -> bool {
6812        if s.is_empty() {
6813            return false;
6814        }
6815        let first = s.chars().next().unwrap();
6816        matches!(
6817            first,
6818            'A' | 'a' | 'h' | 't' | 'r' | 'e' | 'l' | 'u' | 'q' | 'Q' | 'P'
6819        )
6820    }
6821
6822    /// Apply zsh history-style modifiers to a value
6823    /// Modifiers can be chained: :A:h:h
6824    fn apply_history_modifiers(&self, val: &str, modifiers: &str) -> String {
6825        let mut result = val.to_string();
6826        let mut chars = modifiers.chars().peekable();
6827
6828        while let Some(c) = chars.next() {
6829            match c {
6830                ':' => continue,
6831                'A' => {
6832                    if let Ok(abs) = std::fs::canonicalize(&result) {
6833                        result = abs.to_string_lossy().to_string();
6834                    } else if !result.starts_with('/') {
6835                        if let Ok(cwd) = std::env::current_dir() {
6836                            result = cwd.join(&result).to_string_lossy().to_string();
6837                        }
6838                    }
6839                }
6840                'a' => {
6841                    if !result.starts_with('/') {
6842                        if let Ok(cwd) = std::env::current_dir() {
6843                            result = cwd.join(&result).to_string_lossy().to_string();
6844                        }
6845                    }
6846                }
6847                'h' => {
6848                    if let Some(pos) = result.rfind('/') {
6849                        if pos == 0 {
6850                            result = "/".to_string();
6851                        } else {
6852                            result = result[..pos].to_string();
6853                        }
6854                    } else {
6855                        result = ".".to_string();
6856                    }
6857                }
6858                't' => {
6859                    if let Some(pos) = result.rfind('/') {
6860                        result = result[pos + 1..].to_string();
6861                    }
6862                }
6863                'r' => {
6864                    if let Some(dot_pos) = result.rfind('.') {
6865                        let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
6866                        if dot_pos > slash_pos {
6867                            result = result[..dot_pos].to_string();
6868                        }
6869                    }
6870                }
6871                'e' => {
6872                    if let Some(dot_pos) = result.rfind('.') {
6873                        let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
6874                        if dot_pos > slash_pos {
6875                            result = result[dot_pos + 1..].to_string();
6876                        } else {
6877                            result = String::new();
6878                        }
6879                    } else {
6880                        result = String::new();
6881                    }
6882                }
6883                'l' => result = result.to_lowercase(),
6884                'u' => result = result.to_uppercase(),
6885                'q' => result = format!("'{}'", result.replace('\'', "'\\''")),
6886                'Q' => {
6887                    if result.starts_with('\'') && result.ends_with('\'') && result.len() >= 2 {
6888                        result = result[1..result.len() - 1].to_string();
6889                    } else if result.starts_with('"') && result.ends_with('"') && result.len() >= 2
6890                    {
6891                        result = result[1..result.len() - 1].to_string();
6892                    }
6893                }
6894                'P' => {
6895                    if let Ok(real) = std::fs::canonicalize(&result) {
6896                        result = real.to_string_lossy().to_string();
6897                    }
6898                }
6899                _ => break,
6900            }
6901        }
6902        result
6903    }
6904
6905    /// Parse zsh parameter expansion flags from a string like "L", "U", "j:,:"
6906    fn parse_zsh_flags(&self, s: &str) -> Vec<ZshParamFlag> {
6907        let mut flags = Vec::new();
6908        let mut chars = s.chars().peekable();
6909
6910        while let Some(c) = chars.next() {
6911            match c {
6912                'L' => flags.push(ZshParamFlag::Lower),
6913                'U' => flags.push(ZshParamFlag::Upper),
6914                'C' => flags.push(ZshParamFlag::Capitalize),
6915                'j' => {
6916                    // j<delim>sep<delim> — join with separator (delim can be any char)
6917                    if let Some(&delim) = chars.peek() {
6918                        chars.next(); // consume delimiter char
6919                        let mut sep = String::new();
6920                        while let Some(&ch) = chars.peek() {
6921                            if ch == delim {
6922                                chars.next();
6923                                break;
6924                            }
6925                            sep.push(chars.next().unwrap());
6926                        }
6927                        flags.push(ZshParamFlag::Join(sep));
6928                    }
6929                }
6930                'F' => flags.push(ZshParamFlag::JoinNewline),
6931                's' => {
6932                    // s:sep: - split on separator
6933                    if chars.peek() == Some(&':') {
6934                        chars.next();
6935                        let mut sep = String::new();
6936                        while let Some(&ch) = chars.peek() {
6937                            if ch == ':' {
6938                                chars.next();
6939                                break;
6940                            }
6941                            sep.push(chars.next().unwrap());
6942                        }
6943                        flags.push(ZshParamFlag::Split(sep));
6944                    }
6945                }
6946                'f' => flags.push(ZshParamFlag::SplitLines),
6947                'z' => flags.push(ZshParamFlag::SplitWords),
6948                't' => flags.push(ZshParamFlag::Type),
6949                'w' => flags.push(ZshParamFlag::Words),
6950                'b' => flags.push(ZshParamFlag::QuoteBackslash),
6951                'q' => {
6952                    if chars.peek() == Some(&'q') {
6953                        chars.next();
6954                        flags.push(ZshParamFlag::DoubleQuote);
6955                    } else {
6956                        flags.push(ZshParamFlag::Quote);
6957                    }
6958                }
6959                'u' => flags.push(ZshParamFlag::Unique),
6960                'O' => flags.push(ZshParamFlag::Reverse),
6961                'o' => flags.push(ZshParamFlag::Sort),
6962                'n' => flags.push(ZshParamFlag::NumericSort),
6963                'a' => flags.push(ZshParamFlag::IndexSort),
6964                'k' => flags.push(ZshParamFlag::Keys),
6965                'v' => flags.push(ZshParamFlag::Values),
6966                '#' => flags.push(ZshParamFlag::Length),
6967                'c' => flags.push(ZshParamFlag::CountChars),
6968                'e' => flags.push(ZshParamFlag::Expand),
6969                '%' => {
6970                    if chars.peek() == Some(&'%') {
6971                        chars.next();
6972                        flags.push(ZshParamFlag::PromptExpandFull);
6973                    } else {
6974                        flags.push(ZshParamFlag::PromptExpand);
6975                    }
6976                }
6977                'V' => flags.push(ZshParamFlag::Visible),
6978                'D' => flags.push(ZshParamFlag::Directory),
6979                'M' => flags.push(ZshParamFlag::Match),
6980                'R' => flags.push(ZshParamFlag::Remove),
6981                'S' => flags.push(ZshParamFlag::Subscript),
6982                'P' => flags.push(ZshParamFlag::Parameter),
6983                '~' => flags.push(ZshParamFlag::Glob),
6984                'l' => {
6985                    // l:len:fill: - pad left
6986                    if chars.peek() == Some(&':') {
6987                        chars.next();
6988                        let mut len_str = String::new();
6989                        while let Some(&ch) = chars.peek() {
6990                            if ch == ':' {
6991                                chars.next();
6992                                break;
6993                            }
6994                            len_str.push(chars.next().unwrap());
6995                        }
6996                        let mut fill = ' ';
6997                        if let Some(&ch) = chars.peek() {
6998                            if ch != ':' {
6999                                fill = chars.next().unwrap();
7000                                if chars.peek() == Some(&':') {
7001                                    chars.next();
7002                                }
7003                            }
7004                        }
7005                        if let Ok(len) = len_str.parse() {
7006                            flags.push(ZshParamFlag::PadLeft(len, fill));
7007                        }
7008                    }
7009                }
7010                'r' => {
7011                    // r:len:fill: - pad right
7012                    if chars.peek() == Some(&':') {
7013                        chars.next();
7014                        let mut len_str = String::new();
7015                        while let Some(&ch) = chars.peek() {
7016                            if ch == ':' {
7017                                chars.next();
7018                                break;
7019                            }
7020                            len_str.push(chars.next().unwrap());
7021                        }
7022                        let mut fill = ' ';
7023                        if let Some(&ch) = chars.peek() {
7024                            if ch != ':' {
7025                                fill = chars.next().unwrap();
7026                                if chars.peek() == Some(&':') {
7027                                    chars.next();
7028                                }
7029                            }
7030                        }
7031                        if let Ok(len) = len_str.parse() {
7032                            flags.push(ZshParamFlag::PadRight(len, fill));
7033                        }
7034                    }
7035                }
7036                'm' => {
7037                    // Width for padding - parse number if present
7038                    let mut width_str = String::new();
7039                    while let Some(&ch) = chars.peek() {
7040                        if ch.is_ascii_digit() {
7041                            width_str.push(chars.next().unwrap());
7042                        } else {
7043                            break;
7044                        }
7045                    }
7046                    if let Ok(w) = width_str.parse() {
7047                        flags.push(ZshParamFlag::Width(w));
7048                    }
7049                }
7050                _ => {}
7051            }
7052        }
7053        flags
7054    }
7055
7056    /// Apply a single zsh parameter expansion flag
7057    fn apply_zsh_param_flag(&self, val: &str, name: &str, flag: &ZshParamFlag) -> String {
7058        match flag {
7059            ZshParamFlag::Lower => val.to_lowercase(),
7060            ZshParamFlag::Upper => val.to_uppercase(),
7061            ZshParamFlag::Capitalize => val
7062                .split_whitespace()
7063                .map(|word| {
7064                    let mut c = word.chars();
7065                    match c.next() {
7066                        None => String::new(),
7067                        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
7068                    }
7069                })
7070                .collect::<Vec<_>>()
7071                .join(" "),
7072            ZshParamFlag::Join(sep) => {
7073                if let Some(arr) = self.arrays.get(name) {
7074                    arr.join(sep)
7075                } else {
7076                    val.to_string()
7077                }
7078            }
7079            ZshParamFlag::Split(sep) => val.split(sep).collect::<Vec<_>>().join(" "),
7080            ZshParamFlag::SplitLines => val.lines().collect::<Vec<_>>().join(" "),
7081            ZshParamFlag::Type => {
7082                if self.arrays.contains_key(name) {
7083                    "array".to_string()
7084                } else if self.assoc_arrays.contains_key(name) {
7085                    "association".to_string()
7086                } else if self.functions.contains_key(name) {
7087                    "function".to_string()
7088                } else if std::env::var(name).is_ok() || self.variables.contains_key(name) {
7089                    "scalar".to_string()
7090                } else {
7091                    "".to_string()
7092                }
7093            }
7094            ZshParamFlag::Words => val.split_whitespace().collect::<Vec<_>>().join(" "),
7095            ZshParamFlag::Quote => format!("'{}'", val.replace('\'', "'\\''")),
7096            ZshParamFlag::DoubleQuote => format!("\"{}\"", val.replace('"', "\\\"")),
7097            ZshParamFlag::Unique => {
7098                // Unique preserves first-occurrence order, so parallel doesn't help.
7099                // For 1000+ elements, pre-allocate the HashSet for less rehashing.
7100                let words: Vec<&str> = val.split_whitespace().collect();
7101                let mut seen = std::collections::HashSet::with_capacity(
7102                    if words.len() >= 1000 { words.len() } else { 0 },
7103                );
7104                if words.len() >= 1000 {
7105                    tracing::trace!(
7106                        count = words.len(),
7107                        "unique on large array ({} elements)",
7108                        words.len()
7109                    );
7110                }
7111                words
7112                    .into_iter()
7113                    .filter(|s| seen.insert(*s))
7114                    .collect::<Vec<_>>()
7115                    .join(" ")
7116            }
7117            ZshParamFlag::Reverse => {
7118                // (O) flag: reverse sort (sort descending)
7119                let mut words: Vec<&str> = val.split_whitespace().collect();
7120                if words.len() >= 1000 {
7121                    tracing::trace!(
7122                        count = words.len(),
7123                        "using parallel reverse sort (rayon) for large array"
7124                    );
7125                    use rayon::prelude::*;
7126                    words.par_sort_unstable_by(|a, b| b.cmp(a));
7127                } else {
7128                    words.sort_unstable_by(|a, b| b.cmp(a));
7129                }
7130                words.join(" ")
7131            }
7132            ZshParamFlag::Sort => {
7133                let mut words: Vec<&str> = val.split_whitespace().collect();
7134                if words.len() >= 1000 {
7135                    tracing::trace!(
7136                        count = words.len(),
7137                        "using parallel sort (rayon) for large array"
7138                    );
7139                    use rayon::prelude::*;
7140                    words.par_sort_unstable();
7141                } else {
7142                    words.sort_unstable();
7143                }
7144                words.join(" ")
7145            }
7146            ZshParamFlag::NumericSort => {
7147                let mut words: Vec<&str> = val.split_whitespace().collect();
7148                let cmp = |a: &&str, b: &&str| {
7149                    let na: i64 = a.parse().unwrap_or(0);
7150                    let nb: i64 = b.parse().unwrap_or(0);
7151                    na.cmp(&nb)
7152                };
7153                if words.len() >= 1000 {
7154                    tracing::trace!(
7155                        count = words.len(),
7156                        "using parallel numeric sort (rayon) for large array"
7157                    );
7158                    use rayon::prelude::*;
7159                    words.par_sort_unstable_by(cmp);
7160                } else {
7161                    words.sort_unstable_by(cmp);
7162                }
7163                words.join(" ")
7164            }
7165            ZshParamFlag::Keys => {
7166                if let Some(assoc) = self.assoc_arrays.get(name) {
7167                    assoc.keys().cloned().collect::<Vec<_>>().join(" ")
7168                } else {
7169                    String::new()
7170                }
7171            }
7172            ZshParamFlag::Values => {
7173                if let Some(assoc) = self.assoc_arrays.get(name) {
7174                    assoc.values().cloned().collect::<Vec<_>>().join(" ")
7175                } else {
7176                    val.to_string()
7177                }
7178            }
7179            ZshParamFlag::Length => val.len().to_string(),
7180            ZshParamFlag::Head(n) => val
7181                .split_whitespace()
7182                .take(*n)
7183                .collect::<Vec<_>>()
7184                .join(" "),
7185            ZshParamFlag::Tail(n) => {
7186                let words: Vec<&str> = val.split_whitespace().collect();
7187                if words.len() > *n {
7188                    words[words.len() - n..].join(" ")
7189                } else {
7190                    val.to_string()
7191                }
7192            }
7193            ZshParamFlag::JoinNewline => {
7194                if let Some(arr) = self.arrays.get(name) {
7195                    arr.join("\n")
7196                } else {
7197                    val.to_string()
7198                }
7199            }
7200            ZshParamFlag::SplitWords => {
7201                // Shell-style word splitting
7202                val.split_whitespace().collect::<Vec<_>>().join(" ")
7203            }
7204            ZshParamFlag::QuoteBackslash => {
7205                // Quote special pattern chars with backslashes
7206                let mut result = String::new();
7207                for c in val.chars() {
7208                    if "\\*?[]{}()".contains(c) {
7209                        result.push('\\');
7210                    }
7211                    result.push(c);
7212                }
7213                result
7214            }
7215            ZshParamFlag::IndexSort => {
7216                // Array index order - just return as-is (default)
7217                val.to_string()
7218            }
7219            ZshParamFlag::CountChars => {
7220                // Count total characters
7221                val.chars().count().to_string()
7222            }
7223            ZshParamFlag::Expand => {
7224                // Would need mutable self to do expansions
7225                val.to_string()
7226            }
7227            ZshParamFlag::PromptExpand => {
7228                // Expand prompt escapes
7229                self.expand_prompt_string(val)
7230            }
7231            ZshParamFlag::PromptExpandFull => {
7232                // Full prompt expansion
7233                self.expand_prompt_string(val)
7234            }
7235            ZshParamFlag::Visible => {
7236                // Make non-printable characters visible
7237                val.chars()
7238                    .map(|c| {
7239                        if c.is_control() {
7240                            format!("^{}", (c as u8 + 64) as char)
7241                        } else {
7242                            c.to_string()
7243                        }
7244                    })
7245                    .collect()
7246            }
7247            ZshParamFlag::Directory => {
7248                // Substitute leading directory with ~ if it's home
7249                if let Some(home) = dirs::home_dir() {
7250                    let home_str = home.to_string_lossy();
7251                    if val.starts_with(home_str.as_ref()) {
7252                        format!("~{}", &val[home_str.len()..])
7253                    } else {
7254                        val.to_string()
7255                    }
7256                } else {
7257                    val.to_string()
7258                }
7259            }
7260            ZshParamFlag::PadLeft(len, fill) => {
7261                if val.len() >= *len {
7262                    val.to_string()
7263                } else {
7264                    let padding: String = std::iter::repeat(*fill).take(len - val.len()).collect();
7265                    format!("{}{}", padding, val)
7266                }
7267            }
7268            ZshParamFlag::PadRight(len, fill) => {
7269                if val.len() >= *len {
7270                    val.to_string()
7271                } else {
7272                    let padding: String = std::iter::repeat(*fill).take(len - val.len()).collect();
7273                    format!("{}{}", val, padding)
7274                }
7275            }
7276            ZshParamFlag::Width(_) => {
7277                // Width modifier - used with padding, just return value
7278                val.to_string()
7279            }
7280            ZshParamFlag::Match => {
7281                // Match flag - used with pattern operations, just pass through
7282                // Actual matching is handled in the pattern operations below
7283                val.to_string()
7284            }
7285            ZshParamFlag::Remove => {
7286                // Remove flag - complement of Match
7287                val.to_string()
7288            }
7289            ZshParamFlag::Subscript => {
7290                // Subscript scanning
7291                val.to_string()
7292            }
7293            ZshParamFlag::Parameter => {
7294                // Parameter indirection - treat val as parameter name
7295                self.get_variable(val)
7296            }
7297            ZshParamFlag::Glob => {
7298                // Glob patterns in pattern matching
7299                val.to_string()
7300            }
7301        }
7302    }
7303
7304    /// Expand prompt escape sequences using the full prompt module
7305    fn expand_prompt_string(&self, s: &str) -> String {
7306        let ctx = self.build_prompt_context();
7307        expand_prompt(s, &ctx)
7308    }
7309
7310    /// Build a PromptContext from current executor state
7311    fn build_prompt_context(&self) -> PromptContext {
7312        let pwd = env::current_dir()
7313            .map(|p| p.to_string_lossy().to_string())
7314            .unwrap_or_else(|_| "/".to_string());
7315
7316        let home = env::var("HOME").unwrap_or_default();
7317
7318        let user = env::var("USER")
7319            .or_else(|_| env::var("LOGNAME"))
7320            .unwrap_or_else(|_| "user".to_string());
7321
7322        let host = hostname::get()
7323            .map(|h| h.to_string_lossy().to_string())
7324            .unwrap_or_else(|_| "localhost".to_string());
7325
7326        let host_short = host.split('.').next().unwrap_or(&host).to_string();
7327
7328        let shlvl = env::var("SHLVL")
7329            .ok()
7330            .and_then(|s| s.parse().ok())
7331            .unwrap_or(1);
7332
7333        PromptContext {
7334            pwd,
7335            home,
7336            user,
7337            host,
7338            host_short,
7339            tty: String::new(),
7340            lastval: self.last_status,
7341            histnum: self
7342                .history
7343                .as_ref()
7344                .and_then(|h| h.count().ok())
7345                .unwrap_or(1),
7346            shlvl,
7347            num_jobs: self.jobs.list().len() as i32,
7348            is_root: unsafe { libc::geteuid() } == 0,
7349            cmd_stack: Vec::new(),
7350            psvar: self.get_psvar(),
7351            term_width: self.get_term_width(),
7352            lineno: 1,
7353        }
7354    }
7355
7356    fn get_psvar(&self) -> Vec<String> {
7357        if let Some(arr) = self.arrays.get("psvar") {
7358            arr.clone()
7359        } else {
7360            Vec::new()
7361        }
7362    }
7363
7364    fn get_term_width(&self) -> usize {
7365        env::var("COLUMNS")
7366            .ok()
7367            .and_then(|s| s.parse().ok())
7368            .unwrap_or(80)
7369    }
7370
7371    /// Execute a command and capture its output (command substitution)
7372    fn execute_command_substitution(&mut self, cmd: &ShellCommand) -> String {
7373        match self.execute_command_capture(cmd) {
7374            Ok(output) => output.trim_end_matches('\n').to_string(),
7375            Err(_) => String::new(),
7376        }
7377    }
7378
7379    /// Execute a command and capture its stdout
7380    fn execute_command_capture(&mut self, cmd: &ShellCommand) -> Result<String, String> {
7381        // For simple commands, we can use Command directly
7382        if let ShellCommand::Simple(simple) = cmd {
7383            let words: Vec<String> = simple.words.iter().map(|w| self.expand_word(w)).collect();
7384            if words.is_empty() {
7385                return Ok(String::new());
7386            }
7387
7388            let cmd_name = &words[0];
7389            let args = &words[1..];
7390
7391            // Handle some builtins that can return values
7392            match cmd_name.as_str() {
7393                "echo" => {
7394                    let output = args.join(" ");
7395                    return Ok(format!("{}\n", output));
7396                }
7397                "printf" => {
7398                    if !args.is_empty() {
7399                        // Simple printf - just format string with args
7400                        let format = &args[0];
7401                        let result = if args.len() > 1 {
7402                            // Very basic: just handle %s
7403                            let mut out = format.clone();
7404                            for (i, arg) in args[1..].iter().enumerate() {
7405                                out = out.replacen("%s", arg, 1);
7406                                out = out.replacen(&format!("${}", i + 1), arg, 1);
7407                            }
7408                            out
7409                        } else {
7410                            format.clone()
7411                        };
7412                        return Ok(result);
7413                    }
7414                    return Ok(String::new());
7415                }
7416                "pwd" => {
7417                    return Ok(env::current_dir()
7418                        .map(|p| format!("{}\n", p.display()))
7419                        .unwrap_or_default());
7420                }
7421                _ => {}
7422            }
7423
7424            // External command - capture its output
7425            let output = Command::new(cmd_name)
7426                .args(args)
7427                .stdout(Stdio::piped())
7428                .stderr(Stdio::inherit())
7429                .output();
7430
7431            match output {
7432                Ok(output) => {
7433                    self.last_status = output.status.code().unwrap_or(1);
7434                    Ok(String::from_utf8_lossy(&output.stdout).to_string())
7435                }
7436                Err(e) => {
7437                    self.last_status = 127;
7438                    Err(format!("{}: {}", cmd_name, e))
7439                }
7440            }
7441        } else if let ShellCommand::Pipeline(cmds, _negated) = cmd {
7442            // For pipelines, execute and capture output of the last command
7443            // This is simplified - proper implementation would pipe between all
7444            if let Some(last) = cmds.last() {
7445                return self.execute_command_capture(last);
7446            }
7447            Ok(String::new())
7448        } else {
7449            // For compound commands, execute them and return empty
7450            // (complex case - could be expanded later)
7451            let _ = self.execute_command(cmd);
7452            Ok(String::new())
7453        }
7454    }
7455
7456    /// Evaluate arithmetic expression using the full math module
7457    fn evaluate_arithmetic(&mut self, expr: &str) -> String {
7458        let expr = self.expand_string(expr);
7459        let force_float = self.options.get("forcefloat").copied().unwrap_or(false);
7460        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
7461        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
7462
7463        let mut evaluator = MathEval::new(&expr)
7464            .with_string_variables(&self.variables)
7465            .with_force_float(force_float)
7466            .with_c_precedences(c_prec)
7467            .with_octal_zeroes(octal);
7468
7469        match evaluator.evaluate() {
7470            Ok(result) => {
7471                for (k, v) in evaluator.extract_string_variables() {
7472                    self.variables.insert(k.clone(), v.clone());
7473                    env::set_var(&k, &v);
7474                }
7475                match result {
7476                    crate::math::MathNum::Integer(i) => i.to_string(),
7477                    crate::math::MathNum::Float(f) => {
7478                        if f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
7479                            (f as i64).to_string()
7480                        } else {
7481                            f.to_string()
7482                        }
7483                    }
7484                    crate::math::MathNum::Unset => "0".to_string(),
7485                }
7486            }
7487            Err(_) => "0".to_string(),
7488        }
7489    }
7490
7491    fn eval_arith_expr(&mut self, expr: &str) -> i64 {
7492        let expr_expanded = self.expand_string(expr);
7493        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
7494        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
7495
7496        let mut evaluator = MathEval::new(&expr_expanded)
7497            .with_string_variables(&self.variables)
7498            .with_c_precedences(c_prec)
7499            .with_octal_zeroes(octal);
7500
7501        match evaluator.evaluate() {
7502            Ok(result) => {
7503                for (k, v) in evaluator.extract_string_variables() {
7504                    self.variables.insert(k.clone(), v.clone());
7505                    env::set_var(&k, &v);
7506                }
7507                result.to_int()
7508            }
7509            Err(_) => 0,
7510        }
7511    }
7512
7513    fn eval_arith_expr_float(&mut self, expr: &str) -> f64 {
7514        let expr_expanded = self.expand_string(expr);
7515        let force_float = self.options.get("forcefloat").copied().unwrap_or(false);
7516        let c_prec = self.options.get("cprecedences").copied().unwrap_or(false);
7517        let octal = self.options.get("octalzeroes").copied().unwrap_or(false);
7518
7519        let mut evaluator = MathEval::new(&expr_expanded)
7520            .with_string_variables(&self.variables)
7521            .with_force_float(force_float)
7522            .with_c_precedences(c_prec)
7523            .with_octal_zeroes(octal);
7524
7525        match evaluator.evaluate() {
7526            Ok(result) => {
7527                for (k, v) in evaluator.extract_string_variables() {
7528                    self.variables.insert(k.clone(), v.clone());
7529                    env::set_var(&k, &v);
7530                }
7531                result.to_float()
7532            }
7533            Err(_) => 0.0,
7534        }
7535    }
7536
7537    fn matches_pattern(&self, value: &str, pattern: &str) -> bool {
7538        // Simple glob matching
7539        if pattern == "*" {
7540            return true;
7541        }
7542        if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
7543            // Use glob matching for wildcards and character classes
7544            glob::Pattern::new(pattern)
7545                .map(|p| p.matches(value))
7546                .unwrap_or(false)
7547        } else {
7548            value == pattern
7549        }
7550    }
7551
7552    fn eval_cond_expr(&mut self, expr: &CondExpr) -> bool {
7553        match expr {
7554            CondExpr::FileExists(w) => std::path::Path::new(&self.expand_word(w)).exists(),
7555            CondExpr::FileRegular(w) => std::path::Path::new(&self.expand_word(w)).is_file(),
7556            CondExpr::FileDirectory(w) => std::path::Path::new(&self.expand_word(w)).is_dir(),
7557            CondExpr::FileSymlink(w) => std::path::Path::new(&self.expand_word(w)).is_symlink(),
7558            CondExpr::FileReadable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
7559            CondExpr::FileWritable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
7560            CondExpr::FileExecutable(w) => std::path::Path::new(&self.expand_word(w)).exists(),
7561            CondExpr::FileNonEmpty(w) => std::fs::metadata(&self.expand_word(w))
7562                .map(|m| m.len() > 0)
7563                .unwrap_or(false),
7564            CondExpr::StringEmpty(w) => self.expand_word(w).is_empty(),
7565            CondExpr::StringNonEmpty(w) => !self.expand_word(w).is_empty(),
7566            CondExpr::StringEqual(a, b) => {
7567                let left = self.expand_word(a);
7568                let right = self.expand_word(b);
7569                // In [[ ]], == does glob pattern matching on the right side
7570                if right.contains('*') || right.contains('?') || right.contains('[') {
7571                    crate::glob::pattern_match(&right, &left, true, true)
7572                } else {
7573                    left == right
7574                }
7575            }
7576            CondExpr::StringNotEqual(a, b) => {
7577                let left = self.expand_word(a);
7578                let right = self.expand_word(b);
7579                if right.contains('*') || right.contains('?') || right.contains('[') {
7580                    !crate::glob::pattern_match(&right, &left, true, true)
7581                } else {
7582                    left != right
7583                }
7584            }
7585            CondExpr::StringMatch(a, b) => {
7586                let val = self.expand_word(a);
7587                let pattern = self.expand_word(b);
7588                if let Some(re) = cached_regex(&pattern) {
7589                    if let Some(caps) = re.captures(&val) {
7590                        // Set $MATCH to the full match
7591                        if let Some(m) = caps.get(0) {
7592                            self.variables.insert("MATCH".to_string(), m.as_str().to_string());
7593                        }
7594                        // Set $match array with capture groups
7595                        let mut match_arr = Vec::new();
7596                        for i in 1..caps.len() {
7597                            if let Some(g) = caps.get(i) {
7598                                match_arr.push(g.as_str().to_string());
7599                            }
7600                        }
7601                        if !match_arr.is_empty() {
7602                            self.arrays.insert("match".to_string(), match_arr);
7603                        }
7604                        true
7605                    } else {
7606                        self.variables.remove("MATCH");
7607                        self.arrays.remove("match");
7608                        false
7609                    }
7610                } else {
7611                    false
7612                }
7613            }
7614            CondExpr::StringLess(a, b) => self.expand_word(a) < self.expand_word(b),
7615            CondExpr::StringGreater(a, b) => self.expand_word(a) > self.expand_word(b),
7616            CondExpr::NumEqual(a, b) => {
7617                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
7618                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
7619                a_val == b_val
7620            }
7621            CondExpr::NumNotEqual(a, b) => {
7622                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
7623                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
7624                a_val != b_val
7625            }
7626            CondExpr::NumLess(a, b) => {
7627                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
7628                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
7629                a_val < b_val
7630            }
7631            CondExpr::NumLessEqual(a, b) => {
7632                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
7633                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
7634                a_val <= b_val
7635            }
7636            CondExpr::NumGreater(a, b) => {
7637                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
7638                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
7639                a_val > b_val
7640            }
7641            CondExpr::NumGreaterEqual(a, b) => {
7642                let a_val = self.expand_word(a).parse::<i64>().unwrap_or(0);
7643                let b_val = self.expand_word(b).parse::<i64>().unwrap_or(0);
7644                a_val >= b_val
7645            }
7646            CondExpr::Not(inner) => !self.eval_cond_expr(inner),
7647            CondExpr::And(a, b) => self.eval_cond_expr(a) && self.eval_cond_expr(b),
7648            CondExpr::Or(a, b) => self.eval_cond_expr(a) || self.eval_cond_expr(b),
7649        }
7650    }
7651
7652    // Builtins
7653    // Ported from zsh/Src/builtin.c
7654
7655    /// cd builtin - change directory
7656    /// Ported from zsh/Src/builtin.c bin_cd() lines 839-859, cd_get_dest() lines 864-957,
7657    /// cd_do_chdir() lines 967-1081, cd_try_chdir() lines 1116-1181
7658    fn builtin_cd(&mut self, args: &[String]) -> i32 {
7659        // cd [ -qsLP ] [ arg ]
7660        // cd [ -qsLP ] old new
7661        // cd [ -qsLP ] {+|-}n
7662        let mut quiet = false;
7663        let mut use_cdpath = false;
7664        let mut logical = true; // -L is default
7665        let mut positional_args: Vec<&str> = Vec::new();
7666
7667        for arg in args {
7668            if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
7669                // Check if it's a stack index like -2
7670                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
7671                    positional_args.push(arg);
7672                    continue;
7673                }
7674                for ch in arg[1..].chars() {
7675                    match ch {
7676                        'q' => quiet = true,
7677                        's' => use_cdpath = true,
7678                        'L' => logical = true,
7679                        'P' => logical = false,
7680                        _ => {
7681                            eprintln!("cd: bad option: -{}", ch);
7682                            return 1;
7683                        }
7684                    }
7685                }
7686            } else if arg.starts_with('+')
7687                && arg.len() > 1
7688                && arg[1..].chars().all(|c| c.is_ascii_digit())
7689            {
7690                // Stack index like +2
7691                positional_args.push(arg);
7692            } else {
7693                positional_args.push(arg);
7694            }
7695        }
7696
7697        // Handle cd old new (substitution)
7698        if positional_args.len() == 2 {
7699            if let Ok(cwd) = env::current_dir() {
7700                let cwd_str = cwd.to_string_lossy();
7701                let old = positional_args[0];
7702                let new = positional_args[1];
7703                if cwd_str.contains(old) {
7704                    let new_path = cwd_str.replace(old, new);
7705                    if !quiet {
7706                        println!("{}", new_path);
7707                    }
7708                    positional_args = vec![];
7709                    return self.do_cd(&new_path, quiet, use_cdpath, logical);
7710                }
7711            }
7712        }
7713
7714        let path_arg = positional_args.first().map(|s| *s).unwrap_or("~");
7715
7716        // Handle stack indices
7717        if path_arg.starts_with('+') || path_arg.starts_with('-') {
7718            if let Ok(n) = path_arg[1..].parse::<usize>() {
7719                let idx = if path_arg.starts_with('+') {
7720                    n
7721                } else {
7722                    self.dir_stack.len().saturating_sub(n)
7723                };
7724                if let Some(dir) = self.dir_stack.get(idx) {
7725                    let dir_path = dir.to_string_lossy().to_string();
7726                    return self.do_cd(&dir_path, quiet, use_cdpath, logical);
7727                } else {
7728                    eprintln!("cd: no such entry in dir stack");
7729                    return 1;
7730                }
7731            }
7732        }
7733
7734        self.do_cd(path_arg, quiet, use_cdpath, logical)
7735    }
7736
7737    fn do_cd(&mut self, path_arg: &str, quiet: bool, use_cdpath: bool, physical: bool) -> i32 {
7738        let path = if path_arg == "~" || path_arg.is_empty() {
7739            dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
7740        } else if path_arg.starts_with("~/") {
7741            dirs::home_dir()
7742                .unwrap_or_else(|| PathBuf::from("."))
7743                .join(&path_arg[2..])
7744        } else if path_arg == "-" {
7745            if let Ok(oldpwd) = env::var("OLDPWD") {
7746                if !quiet {
7747                    println!("{}", oldpwd);
7748                }
7749                PathBuf::from(oldpwd)
7750            } else {
7751                eprintln!("cd: OLDPWD not set");
7752                return 1;
7753            }
7754        } else if use_cdpath && !path_arg.starts_with('/') && !path_arg.starts_with('.') {
7755            // Search CDPATH
7756            let cdpath = env::var("CDPATH").unwrap_or_default();
7757            let mut found = None;
7758            for dir in cdpath.split(':') {
7759                let candidate = if dir.is_empty() {
7760                    PathBuf::from(path_arg)
7761                } else {
7762                    PathBuf::from(dir).join(path_arg)
7763                };
7764                if candidate.is_dir() {
7765                    found = Some(candidate);
7766                    break;
7767                }
7768            }
7769            found.unwrap_or_else(|| PathBuf::from(path_arg))
7770        } else {
7771            PathBuf::from(path_arg)
7772        };
7773
7774        if let Ok(cwd) = env::current_dir() {
7775            env::set_var("OLDPWD", &cwd);
7776        }
7777
7778        // Resolve symlinks if -P (physical)
7779        let target = if !physical {
7780            if let Ok(resolved) = path.canonicalize() {
7781                resolved
7782            } else {
7783                path.clone()
7784            }
7785        } else {
7786            path.clone()
7787        };
7788
7789        match env::set_current_dir(&target) {
7790            Ok(_) => {
7791                if let Ok(cwd) = env::current_dir() {
7792                    env::set_var("PWD", &cwd);
7793                    self.variables
7794                        .insert("PWD".to_string(), cwd.to_string_lossy().to_string());
7795                }
7796                0
7797            }
7798            Err(e) => {
7799                eprintln!("cd: {}: {}", path.display(), e);
7800                1
7801            }
7802        }
7803    }
7804
7805    fn builtin_pwd(&mut self, _redirects: &[Redirect]) -> i32 {
7806        match env::current_dir() {
7807            Ok(path) => {
7808                println!("{}", path.display());
7809                0
7810            }
7811            Err(e) => {
7812                eprintln!("pwd: {}", e);
7813                1
7814            }
7815        }
7816    }
7817
7818    fn builtin_echo(&mut self, args: &[String], _redirects: &[Redirect]) -> i32 {
7819        let mut newline = true;
7820        let mut interpret_escapes = false;
7821        let mut start = 0;
7822
7823        for (i, arg) in args.iter().enumerate() {
7824            match arg.as_str() {
7825                "-n" => {
7826                    newline = false;
7827                    start = i + 1;
7828                }
7829                "-e" => {
7830                    interpret_escapes = true;
7831                    start = i + 1;
7832                }
7833                "-E" => {
7834                    interpret_escapes = false;
7835                    start = i + 1;
7836                }
7837                _ => break,
7838            }
7839        }
7840
7841        let output = args[start..].join(" ");
7842        if interpret_escapes {
7843            print!("{}", output.replace("\\n", "\n").replace("\\t", "\t"));
7844        } else {
7845            print!("{}", output);
7846        }
7847
7848        if newline {
7849            println!();
7850        }
7851        0
7852    }
7853
7854    fn builtin_export(&mut self, args: &[String]) -> i32 {
7855        for arg in args {
7856            if let Some((key, value)) = arg.split_once('=') {
7857                self.variables.insert(key.to_string(), value.to_string());
7858                env::set_var(key, value);
7859            } else {
7860                // export VAR (no value) — mark existing var as exported
7861                let val = self.get_variable(arg);
7862                env::set_var(arg, &val);
7863            }
7864        }
7865        0
7866    }
7867
7868    fn builtin_unset(&mut self, args: &[String]) -> i32 {
7869        for arg in args {
7870            env::remove_var(arg);
7871            self.variables.remove(arg);
7872        }
7873        0
7874    }
7875
7876    fn builtin_source(&mut self, args: &[String]) -> i32 {
7877        if args.is_empty() {
7878            eprintln!("source: filename argument required");
7879            return 1;
7880        }
7881
7882        let path = &args[0];
7883
7884        // Resolve to absolute path
7885        let abs_path = if path.starts_with('/') {
7886            path.clone()
7887        } else if path.starts_with("~/") {
7888            if let Some(home) = dirs::home_dir() {
7889                home.join(&path[2..]).to_string_lossy().to_string()
7890            } else {
7891                path.clone()
7892            }
7893        } else {
7894            std::env::current_dir()
7895                .map(|cwd| cwd.join(path).to_string_lossy().to_string())
7896                .unwrap_or_else(|_| path.clone())
7897        };
7898
7899        // Save current $0 and set to the sourced file path
7900        let saved_zero = self.variables.get("0").cloned();
7901        self.variables.insert("0".to_string(), abs_path.clone());
7902
7903        let result;
7904
7905        if self.posix_mode {
7906            // --- POSIX mode: plain read + execute, no SQLite, no caching, no threads ---
7907            result = match std::fs::read_to_string(&abs_path) {
7908                Ok(content) => match self.execute_script(&content) {
7909                    Ok(status) => status,
7910                    Err(e) => { eprintln!("source: {}: {}", path, e); 1 }
7911                },
7912                Err(e) => { eprintln!("source: {}: {}", path, e); 1 }
7913            };
7914        } else {
7915            // --- zshrs/zsh mode: plugin cache + AST cache + worker pool ---
7916            let file_path = std::path::Path::new(&abs_path);
7917
7918            // Check plugin cache for side-effect replay
7919            if let Some(ref cache) = self.plugin_cache {
7920                if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(file_path) {
7921                    if let Some(plugin_id) = cache.check(&abs_path, mt_s, mt_ns) {
7922                        if let Ok(delta) = cache.load(plugin_id) {
7923                            let t0 = std::time::Instant::now();
7924                            self.replay_plugin_delta(&delta);
7925                            tracing::info!(
7926                                path = %abs_path,
7927                                replay_us = t0.elapsed().as_micros() as u64,
7928                                funcs = delta.functions.len(),
7929                                aliases = delta.aliases.len(),
7930                                vars = delta.variables.len() + delta.exports.len(),
7931                                "source: cache hit, replayed"
7932                            );
7933                            // Restore $0
7934                            if let Some(z) = saved_zero { self.variables.insert("0".to_string(), z); }
7935                            else { self.variables.remove("0"); }
7936                            return 0;
7937                        }
7938                    }
7939                }
7940            }
7941
7942            // Cache miss — snapshot, execute via AST-cached path, diff, async store
7943            let snapshot = self.snapshot_state();
7944            let t0 = std::time::Instant::now();
7945            tracing::debug!(path = %abs_path, "source: cache miss, executing via AST-cached path");
7946            result = match self.execute_script_file(&abs_path) {
7947                Ok(status) => status,
7948                Err(e) => {
7949                    tracing::warn!(path = %abs_path, error = %e, "source: execution failed");
7950                    eprintln!("source: {}: {}", path, e);
7951                    1
7952                }
7953            };
7954            let source_ms = t0.elapsed().as_millis() as u64;
7955
7956            // Async-store delta to plugin cache on worker pool
7957            if result == 0 {
7958                if let Some((mt_s, mt_ns)) = crate::plugin_cache::file_mtime(file_path) {
7959                    let delta = self.diff_state(&snapshot);
7960                    let store_path = abs_path.clone();
7961                    tracing::info!(
7962                        path = %abs_path, source_ms,
7963                        funcs = delta.functions.len(),
7964                        aliases = delta.aliases.len(),
7965                        vars = delta.variables.len() + delta.exports.len(),
7966                        "source: caching delta on worker"
7967                    );
7968                    let cache_db_path = crate::plugin_cache::default_cache_path();
7969                    self.worker_pool.submit(move || {
7970                        match crate::plugin_cache::PluginCache::open(&cache_db_path) {
7971                            Ok(cache) => {
7972                                if let Err(e) = cache.store(&store_path, mt_s, mt_ns, source_ms, &delta) {
7973                                    tracing::error!(path = %store_path, error = %e, "plugin_cache: store failed");
7974                                } else {
7975                                    tracing::debug!(path = %store_path, "plugin_cache: stored");
7976                                }
7977                            }
7978                            Err(e) => tracing::error!(error = %e, "plugin_cache: open for write failed"),
7979                        }
7980                    });
7981                }
7982            }
7983        }
7984
7985        // Handle return from sourced script
7986        let final_result = if let Some(ret) = self.returning.take() {
7987            ret
7988        } else {
7989            result
7990        };
7991
7992        // Restore $0
7993        if let Some(z) = saved_zero {
7994            self.variables.insert("0".to_string(), z);
7995        } else {
7996            self.variables.remove("0");
7997        }
7998
7999        final_result
8000    }
8001
8002    /// Snapshot executor state before sourcing a plugin (for delta computation).
8003    fn snapshot_state(&self) -> PluginSnapshot {
8004        PluginSnapshot {
8005            functions: self.functions.keys().cloned().collect(),
8006            aliases: self.aliases.keys().cloned().collect(),
8007            global_aliases: self.global_aliases.keys().cloned().collect(),
8008            suffix_aliases: self.suffix_aliases.keys().cloned().collect(),
8009            variables: self.variables.clone(),
8010            arrays: self.arrays.keys().cloned().collect(),
8011            assoc_arrays: self.assoc_arrays.keys().cloned().collect(),
8012            fpath: self.fpath.clone(),
8013            options: self.options.clone(),
8014            hooks: self.hook_functions.clone(),
8015            autoloads: self.autoload_pending.keys().cloned().collect(),
8016        }
8017    }
8018
8019    /// Compute the delta between current state and a previous snapshot.
8020    fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
8021        use crate::plugin_cache::{AliasKind, PluginDelta};
8022        let mut delta = PluginDelta::default();
8023
8024        // New functions — serialize AST to bincode for instant replay
8025        for (name, body) in &self.functions {
8026            if !snap.functions.contains(name) {
8027                if let Ok(bytes) = bincode::serialize(body) {
8028                    delta.functions.push((name.clone(), bytes));
8029                }
8030            }
8031        }
8032
8033        // New aliases
8034        for (name, value) in &self.aliases {
8035            if !snap.aliases.contains(name) {
8036                delta.aliases.push((name.clone(), value.clone(), AliasKind::Regular));
8037            }
8038        }
8039        for (name, value) in &self.global_aliases {
8040            if !snap.global_aliases.contains(name) {
8041                delta.aliases.push((name.clone(), value.clone(), AliasKind::Global));
8042            }
8043        }
8044        for (name, value) in &self.suffix_aliases {
8045            if !snap.suffix_aliases.contains(name) {
8046                delta.aliases.push((name.clone(), value.clone(), AliasKind::Suffix));
8047            }
8048        }
8049
8050        // New/changed variables
8051        for (name, value) in &self.variables {
8052            if name == "0" { continue; } // skip $0 (we set it ourselves)
8053            match snap.variables.get(name) {
8054                Some(old) if old == value => {} // unchanged
8055                _ => {
8056                    // Check if it's also exported
8057                    if env::var(name).ok().as_ref() == Some(value) {
8058                        delta.exports.push((name.clone(), value.clone()));
8059                    } else {
8060                        delta.variables.push((name.clone(), value.clone()));
8061                    }
8062                }
8063            }
8064        }
8065
8066        // New arrays
8067        for (name, values) in &self.arrays {
8068            if !snap.arrays.contains(name) {
8069                delta.arrays.push((name.clone(), values.clone()));
8070            }
8071        }
8072
8073        // New fpath entries
8074        for p in &self.fpath {
8075            if !snap.fpath.contains(p) {
8076                delta.fpath_additions.push(p.to_string_lossy().to_string());
8077            }
8078        }
8079
8080        // Changed options
8081        for (name, value) in &self.options {
8082            match snap.options.get(name) {
8083                Some(old) if old == value => {}
8084                _ => delta.options_changed.push((name.clone(), *value)),
8085            }
8086        }
8087
8088        // New hooks
8089        for (hook, funcs) in &self.hook_functions {
8090            let old_funcs = snap.hooks.get(hook);
8091            for f in funcs {
8092                let is_new = old_funcs.map_or(true, |old| !old.contains(f));
8093                if is_new {
8094                    delta.hooks.push((hook.clone(), f.clone()));
8095                }
8096            }
8097        }
8098
8099        // New autoloads
8100        for (name, flags) in &self.autoload_pending {
8101            if !snap.autoloads.contains(name) {
8102                delta.autoloads.push((name.clone(), format!("{:?}", flags)));
8103            }
8104        }
8105
8106        delta
8107    }
8108
8109    /// Replay a cached plugin delta into the executor state.
8110    fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
8111        use crate::plugin_cache::AliasKind;
8112
8113        // Aliases
8114        for (name, value, kind) in &delta.aliases {
8115            match kind {
8116                AliasKind::Regular => { self.aliases.insert(name.clone(), value.clone()); }
8117                AliasKind::Global => { self.global_aliases.insert(name.clone(), value.clone()); }
8118                AliasKind::Suffix => { self.suffix_aliases.insert(name.clone(), value.clone()); }
8119            }
8120        }
8121
8122        // Variables
8123        for (name, value) in &delta.variables {
8124            self.variables.insert(name.clone(), value.clone());
8125        }
8126
8127        // Exports (set in both variables and process env)
8128        for (name, value) in &delta.exports {
8129            self.variables.insert(name.clone(), value.clone());
8130            env::set_var(name, value);
8131        }
8132
8133        // Arrays
8134        for (name, values) in &delta.arrays {
8135            self.arrays.insert(name.clone(), values.clone());
8136        }
8137
8138        // Fpath additions
8139        for p in &delta.fpath_additions {
8140            let pb = PathBuf::from(p);
8141            if !self.fpath.contains(&pb) {
8142                self.fpath.push(pb);
8143            }
8144        }
8145
8146        // Completions
8147        for (cmd, func) in &delta.completions {
8148            if let Some(ref mut comps) = self.assoc_arrays.get_mut("_comps") {
8149                comps.insert(cmd.clone(), func.clone());
8150            }
8151        }
8152
8153        // Options
8154        for (name, enabled) in &delta.options_changed {
8155            self.options.insert(name.clone(), *enabled);
8156        }
8157
8158        // Hooks
8159        for (hook, func) in &delta.hooks {
8160            self.hook_functions
8161                .entry(hook.clone())
8162                .or_insert_with(Vec::new)
8163                .push(func.clone());
8164        }
8165
8166        // Functions — deserialize bincode bytecode blobs directly into self.functions
8167        for (name, bytes) in &delta.functions {
8168            if let Ok(ast) = bincode::deserialize::<crate::parser::ShellCommand>(bytes) {
8169                self.functions.insert(name.clone(), ast);
8170            }
8171        }
8172    }
8173
8174    fn builtin_exit(&mut self, args: &[String]) -> i32 {
8175        let code = args
8176            .first()
8177            .and_then(|s| s.parse::<i32>().ok())
8178            .unwrap_or(self.last_status);
8179        std::process::exit(code);
8180    }
8181
8182    fn builtin_return(&mut self, args: &[String]) -> i32 {
8183        let status = args
8184            .first()
8185            .and_then(|s| s.parse::<i32>().ok())
8186            .unwrap_or(self.last_status);
8187        self.returning = Some(status);
8188        status
8189    }
8190
8191    fn builtin_test(&mut self, args: &[String]) -> i32 {
8192        if args.is_empty() {
8193            return 1;
8194        }
8195
8196        // Strip trailing "]" when called as `[`
8197        let args: Vec<&str> = args
8198            .iter()
8199            .map(|s| s.as_str())
8200            .filter(|&s| s != "]")
8201            .collect();
8202
8203        // Prefetch metadata for all file paths in the expression — one stat() per unique path
8204        // instead of one stat() per test flag. Avoids 7 serial stat()s for -r -w -x -g -k -u -s.
8205        let mut meta_cache: HashMap<String, Option<std::fs::Metadata>> = HashMap::new();
8206        for arg in &args {
8207            if !arg.starts_with('-') && !arg.starts_with('!') && *arg != "(" && *arg != ")" {
8208                let path_str = arg.to_string();
8209                if !meta_cache.contains_key(&path_str) {
8210                    meta_cache.insert(path_str, std::fs::metadata(arg).ok());
8211                }
8212            }
8213        }
8214
8215        // Helper closure: get metadata from cache or fetch
8216        let get_meta = |path: &str| -> Option<std::fs::Metadata> {
8217            meta_cache.get(path).cloned().unwrap_or_else(|| std::fs::metadata(path).ok())
8218        };
8219
8220        match args.as_slice() {
8221            // String tests
8222            ["-z", s] => {
8223                if s.is_empty() {
8224                    0
8225                } else {
8226                    1
8227                }
8228            }
8229            ["-n", s] => {
8230                if !s.is_empty() {
8231                    0
8232                } else {
8233                    1
8234                }
8235            }
8236
8237            // File existence/type tests
8238            ["-a", path] | ["-e", path] => {
8239                if std::path::Path::new(path).exists() {
8240                    0
8241                } else {
8242                    1
8243                }
8244            }
8245            ["-f", path] => {
8246                if std::path::Path::new(path).is_file() {
8247                    0
8248                } else {
8249                    1
8250                }
8251            }
8252            ["-d", path] => {
8253                if std::path::Path::new(path).is_dir() {
8254                    0
8255                } else {
8256                    1
8257                }
8258            }
8259            ["-b", path] => {
8260                use std::os::unix::fs::FileTypeExt;
8261                if std::fs::symlink_metadata(path)
8262                    .map(|m| m.file_type().is_block_device())
8263                    .unwrap_or(false)
8264                {
8265                    0
8266                } else {
8267                    1
8268                }
8269            }
8270            ["-c", path] => {
8271                use std::os::unix::fs::FileTypeExt;
8272                if std::fs::symlink_metadata(path)
8273                    .map(|m| m.file_type().is_char_device())
8274                    .unwrap_or(false)
8275                {
8276                    0
8277                } else {
8278                    1
8279                }
8280            }
8281            ["-p", path] => {
8282                use std::os::unix::fs::FileTypeExt;
8283                if std::fs::symlink_metadata(path)
8284                    .map(|m| m.file_type().is_fifo())
8285                    .unwrap_or(false)
8286                {
8287                    0
8288                } else {
8289                    1
8290                }
8291            }
8292            ["-S", path] => {
8293                use std::os::unix::fs::FileTypeExt;
8294                if std::fs::symlink_metadata(path)
8295                    .map(|m| m.file_type().is_socket())
8296                    .unwrap_or(false)
8297                {
8298                    0
8299                } else {
8300                    1
8301                }
8302            }
8303            ["-h", path] | ["-L", path] => {
8304                if std::path::Path::new(path).is_symlink() {
8305                    0
8306                } else {
8307                    1
8308                }
8309            }
8310
8311            // File permission tests — all use prefetched metadata (one stat per path)
8312            ["-r", path] => {
8313                use std::os::unix::fs::MetadataExt;
8314                if let Some(meta) = get_meta(path) {
8315                    let mode = meta.mode();
8316                    let uid = unsafe { libc::geteuid() };
8317                    let gid = unsafe { libc::getegid() };
8318                    let readable = if meta.uid() == uid {
8319                        mode & 0o400 != 0
8320                    } else if meta.gid() == gid {
8321                        mode & 0o040 != 0
8322                    } else {
8323                        mode & 0o004 != 0
8324                    };
8325                    if readable { 0 } else { 1 }
8326                } else {
8327                    1
8328                }
8329            }
8330            ["-w", path] => {
8331                use std::os::unix::fs::MetadataExt;
8332                if let Some(meta) = get_meta(path) {
8333                    let mode = meta.mode();
8334                    let uid = unsafe { libc::geteuid() };
8335                    let gid = unsafe { libc::getegid() };
8336                    let writable = if meta.uid() == uid {
8337                        mode & 0o200 != 0
8338                    } else if meta.gid() == gid {
8339                        mode & 0o020 != 0
8340                    } else {
8341                        mode & 0o002 != 0
8342                    };
8343                    if writable { 0 } else { 1 }
8344                } else {
8345                    1
8346                }
8347            }
8348            ["-x", path] => {
8349                use std::os::unix::fs::MetadataExt;
8350                if let Some(meta) = get_meta(path) {
8351                    let mode = meta.mode();
8352                    let uid = unsafe { libc::geteuid() };
8353                    let gid = unsafe { libc::getegid() };
8354                    let executable = if meta.uid() == uid {
8355                        mode & 0o100 != 0
8356                    } else if meta.gid() == gid {
8357                        mode & 0o010 != 0
8358                    } else {
8359                        mode & 0o001 != 0
8360                    };
8361                    if executable { 0 } else { 1 }
8362                } else {
8363                    1
8364                }
8365            }
8366
8367            // Special permission bits — prefetched metadata
8368            ["-g", path] => {
8369                use std::os::unix::fs::MetadataExt;
8370                if get_meta(path).map(|m| m.mode() & 0o2000 != 0).unwrap_or(false) { 0 } else { 1 }
8371            }
8372            ["-k", path] => {
8373                use std::os::unix::fs::MetadataExt;
8374                if get_meta(path).map(|m| m.mode() & 0o1000 != 0).unwrap_or(false) { 0 } else { 1 }
8375            }
8376            ["-u", path] => {
8377                use std::os::unix::fs::MetadataExt;
8378                if get_meta(path)
8379                    .map(|m| m.mode() & 0o4000 != 0)
8380                    .unwrap_or(false)
8381                {
8382                    0
8383                } else {
8384                    1
8385                }
8386            }
8387
8388            // File size — prefetched metadata
8389            ["-s", path] => {
8390                if get_meta(path).map(|m| m.len() > 0).unwrap_or(false) { 0 } else { 1 }
8391            }
8392
8393            // Ownership — prefetched metadata
8394            ["-O", path] => {
8395                use std::os::unix::fs::MetadataExt;
8396                if get_meta(path).map(|m| m.uid() == unsafe { libc::geteuid() }).unwrap_or(false) { 0 } else { 1 }
8397            }
8398            ["-G", path] => {
8399                use std::os::unix::fs::MetadataExt;
8400                if get_meta(path).map(|m| m.gid() == unsafe { libc::getegid() }).unwrap_or(false) { 0 } else { 1 }
8401            }
8402
8403            // File times — prefetched metadata
8404            ["-N", path] => {
8405                use std::os::unix::fs::MetadataExt;
8406                if let Some(meta) = get_meta(path) {
8407                    if meta.mtime() > meta.atime() {
8408                        0
8409                    } else {
8410                        1
8411                    }
8412                } else {
8413                    1
8414                }
8415            }
8416
8417            // Terminal test
8418            ["-t", fd] => {
8419                if let Ok(fd_num) = fd.parse::<i32>() {
8420                    if unsafe { libc::isatty(fd_num) } == 1 {
8421                        0
8422                    } else {
8423                        1
8424                    }
8425                } else {
8426                    1
8427                }
8428            }
8429
8430            // Variable test
8431            ["-v", varname] => {
8432                if self.variables.contains_key(*varname) || std::env::var(varname).is_ok() {
8433                    0
8434                } else {
8435                    1
8436                }
8437            }
8438
8439            // Option test
8440            ["-o", opt] => {
8441                let (name, _) = Self::normalize_option_name(opt);
8442                if self.options.get(&name).copied().unwrap_or(false) {
8443                    0
8444                } else {
8445                    1
8446                }
8447            }
8448
8449            // String comparisons
8450            [a, "=", b] | [a, "==", b] => {
8451                if a == b {
8452                    0
8453                } else {
8454                    1
8455                }
8456            }
8457            [a, "!=", b] => {
8458                if a != b {
8459                    0
8460                } else {
8461                    1
8462                }
8463            }
8464            [a, "<", b] => {
8465                if *a < *b {
8466                    0
8467                } else {
8468                    1
8469                }
8470            }
8471            [a, ">", b] => {
8472                if *a > *b {
8473                    0
8474                } else {
8475                    1
8476                }
8477            }
8478
8479            // Numeric comparisons
8480            [a, "-eq", b] => {
8481                let a: i64 = a.parse().unwrap_or(0);
8482                let b: i64 = b.parse().unwrap_or(0);
8483                if a == b {
8484                    0
8485                } else {
8486                    1
8487                }
8488            }
8489            [a, "-ne", b] => {
8490                let a: i64 = a.parse().unwrap_or(0);
8491                let b: i64 = b.parse().unwrap_or(0);
8492                if a != b {
8493                    0
8494                } else {
8495                    1
8496                }
8497            }
8498            [a, "-lt", b] => {
8499                let a: i64 = a.parse().unwrap_or(0);
8500                let b: i64 = b.parse().unwrap_or(0);
8501                if a < b {
8502                    0
8503                } else {
8504                    1
8505                }
8506            }
8507            [a, "-le", b] => {
8508                let a: i64 = a.parse().unwrap_or(0);
8509                let b: i64 = b.parse().unwrap_or(0);
8510                if a <= b {
8511                    0
8512                } else {
8513                    1
8514                }
8515            }
8516            [a, "-gt", b] => {
8517                let a: i64 = a.parse().unwrap_or(0);
8518                let b: i64 = b.parse().unwrap_or(0);
8519                if a > b {
8520                    0
8521                } else {
8522                    1
8523                }
8524            }
8525            [a, "-ge", b] => {
8526                let a: i64 = a.parse().unwrap_or(0);
8527                let b: i64 = b.parse().unwrap_or(0);
8528                if a >= b {
8529                    0
8530                } else {
8531                    1
8532                }
8533            }
8534
8535            // File comparisons
8536            [f1, "-nt", f2] => {
8537                let m1 = std::fs::metadata(f1).and_then(|m| m.modified()).ok();
8538                let m2 = std::fs::metadata(f2).and_then(|m| m.modified()).ok();
8539                match (m1, m2) {
8540                    (Some(t1), Some(t2)) => {
8541                        if t1 > t2 {
8542                            0
8543                        } else {
8544                            1
8545                        }
8546                    }
8547                    (Some(_), None) => 0,
8548                    _ => 1,
8549                }
8550            }
8551            [f1, "-ot", f2] => {
8552                let m1 = std::fs::metadata(f1).and_then(|m| m.modified()).ok();
8553                let m2 = std::fs::metadata(f2).and_then(|m| m.modified()).ok();
8554                match (m1, m2) {
8555                    (Some(t1), Some(t2)) => {
8556                        if t1 < t2 {
8557                            0
8558                        } else {
8559                            1
8560                        }
8561                    }
8562                    (None, Some(_)) => 0,
8563                    _ => 1,
8564                }
8565            }
8566            [f1, "-ef", f2] => {
8567                use std::os::unix::fs::MetadataExt;
8568                let m1 = std::fs::metadata(f1).ok();
8569                let m2 = std::fs::metadata(f2).ok();
8570                match (m1, m2) {
8571                    (Some(a), Some(b)) => {
8572                        if a.dev() == b.dev() && a.ino() == b.ino() {
8573                            0
8574                        } else {
8575                            1
8576                        }
8577                    }
8578                    _ => 1,
8579                }
8580            }
8581
8582            // Single string test
8583            [s] => {
8584                if !s.is_empty() {
8585                    0
8586                } else {
8587                    1
8588                }
8589            }
8590
8591            _ => 1,
8592        }
8593    }
8594
8595    fn builtin_local(&mut self, args: &[String]) -> i32 {
8596        self.builtin_typeset(args)
8597    }
8598
8599    fn builtin_declare(&mut self, args: &[String]) -> i32 {
8600        self.builtin_typeset(args)
8601    }
8602
8603    fn builtin_typeset(&mut self, args: &[String]) -> i32 {
8604        // Save old values when inside a function scope (local variable support).
8605        // Restored by call_function on function exit.
8606        if self.local_scope_depth > 0 {
8607            for arg in args {
8608                if arg.starts_with('-') || arg.starts_with('+') {
8609                    continue;
8610                }
8611                let name = arg.split('=').next().unwrap_or(arg);
8612                if !name.is_empty() {
8613                    let old_val = self.variables.get(name).cloned();
8614                    self.local_save_stack.push((name.to_string(), old_val));
8615                }
8616            }
8617        }
8618
8619        // typeset [ {+|-}AHUaghlmrtux ] [ {+|-}EFLRZip [ n ] ]
8620        //         [ + ] [ name[=value] ... ]
8621        // typeset -T [ {+|-}Urux ] [ {+|-}LRZp [ n ] ] SCALAR[=value] array
8622        // typeset -f [ {+|-}TUkmtuz ] [ + ] [ name ... ]
8623
8624        let mut is_array = false; // -a
8625        let mut is_assoc = false; // -A
8626        let mut is_export = false; // -x
8627        let mut is_integer = false; // -i
8628        let mut is_readonly = false; // -r
8629        let mut is_lower = false; // -l
8630        let mut is_upper = false; // -u
8631        let mut is_left_pad = false; // -L
8632        let mut is_right_pad = false; // -R
8633        let mut is_zero_pad = false; // -Z
8634        let mut is_float = false; // -F
8635        let mut is_float_exp = false; // -E
8636        let mut is_function = false; // -f
8637        let mut is_global = false; // -g
8638        let mut is_tied = false; // -T
8639        let mut is_hidden = false; // -H
8640        let mut is_hide_val = false; // -h
8641        let mut is_trace = false; // -t
8642        let mut print_mode = false; // -p
8643        let mut pattern_match = false; // -m
8644        let mut list_mode = false; // no args: list all
8645        let mut plus_mode = false; // +x etc: remove attribute
8646        let mut width: Option<usize> = None;
8647        let mut precision: Option<usize> = None;
8648        let mut var_args: Vec<String> = Vec::new();
8649
8650        let mut i = 0;
8651        while i < args.len() {
8652            let arg = &args[i];
8653
8654            if arg == "--" {
8655                i += 1;
8656                while i < args.len() {
8657                    var_args.push(args[i].clone());
8658                    i += 1;
8659                }
8660                break;
8661            }
8662
8663            if arg == "+" {
8664                plus_mode = true;
8665                i += 1;
8666                continue;
8667            }
8668
8669            if arg.starts_with('+') && arg.len() > 1 {
8670                plus_mode = true;
8671                for c in arg[1..].chars() {
8672                    match c {
8673                        'a' => is_array = false,
8674                        'A' => is_assoc = false,
8675                        'x' => is_export = false,
8676                        'i' => is_integer = false,
8677                        'r' => is_readonly = false,
8678                        'l' => is_lower = false,
8679                        'u' => is_upper = false,
8680                        'L' => is_left_pad = false,
8681                        'R' => is_right_pad = false,
8682                        'Z' => is_zero_pad = false,
8683                        'F' => is_float = false,
8684                        'E' => is_float_exp = false,
8685                        'f' => is_function = false,
8686                        'g' => is_global = false,
8687                        'T' => is_tied = false,
8688                        'H' => is_hidden = false,
8689                        'h' => is_hide_val = false,
8690                        't' => is_trace = false,
8691                        'p' => print_mode = false,
8692                        'm' => pattern_match = false,
8693                        _ => {}
8694                    }
8695                }
8696            } else if arg.starts_with('-') && arg.len() > 1 {
8697                let mut chars = arg[1..].chars().peekable();
8698                while let Some(c) = chars.next() {
8699                    match c {
8700                        'a' => is_array = true,
8701                        'A' => is_assoc = true,
8702                        'x' => is_export = true,
8703                        'i' => is_integer = true,
8704                        'r' => is_readonly = true,
8705                        'l' => is_lower = true,
8706                        'u' => is_upper = true,
8707                        'L' => {
8708                            is_left_pad = true;
8709                            // Check for width
8710                            let rest: String = chars.clone().collect();
8711                            if !rest.is_empty()
8712                                && rest
8713                                    .chars()
8714                                    .next()
8715                                    .map(|c| c.is_ascii_digit())
8716                                    .unwrap_or(false)
8717                            {
8718                                let num: String =
8719                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
8720                                width = num.parse().ok();
8721                            }
8722                        }
8723                        'R' => {
8724                            is_right_pad = true;
8725                            let rest: String = chars.clone().collect();
8726                            if !rest.is_empty()
8727                                && rest
8728                                    .chars()
8729                                    .next()
8730                                    .map(|c| c.is_ascii_digit())
8731                                    .unwrap_or(false)
8732                            {
8733                                let num: String =
8734                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
8735                                width = num.parse().ok();
8736                            }
8737                        }
8738                        'Z' => {
8739                            is_zero_pad = true;
8740                            let rest: String = chars.clone().collect();
8741                            if !rest.is_empty()
8742                                && rest
8743                                    .chars()
8744                                    .next()
8745                                    .map(|c| c.is_ascii_digit())
8746                                    .unwrap_or(false)
8747                            {
8748                                let num: String =
8749                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
8750                                width = num.parse().ok();
8751                            }
8752                        }
8753                        'F' => {
8754                            is_float = true;
8755                            let rest: String = chars.clone().collect();
8756                            if !rest.is_empty()
8757                                && rest
8758                                    .chars()
8759                                    .next()
8760                                    .map(|c| c.is_ascii_digit())
8761                                    .unwrap_or(false)
8762                            {
8763                                let num: String =
8764                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
8765                                precision = num.parse().ok();
8766                            }
8767                        }
8768                        'E' => {
8769                            is_float_exp = true;
8770                            let rest: String = chars.clone().collect();
8771                            if !rest.is_empty()
8772                                && rest
8773                                    .chars()
8774                                    .next()
8775                                    .map(|c| c.is_ascii_digit())
8776                                    .unwrap_or(false)
8777                            {
8778                                let num: String =
8779                                    chars.by_ref().take_while(|c| c.is_ascii_digit()).collect();
8780                                precision = num.parse().ok();
8781                            }
8782                        }
8783                        'f' => is_function = true,
8784                        'g' => is_global = true,
8785                        'T' => is_tied = true,
8786                        'H' => is_hidden = true,
8787                        'h' => is_hide_val = true,
8788                        't' => is_trace = true,
8789                        'p' => print_mode = true,
8790                        'm' => pattern_match = true,
8791                        _ => {}
8792                    }
8793                }
8794            } else {
8795                var_args.push(arg.clone());
8796            }
8797            i += 1;
8798        }
8799
8800        let _ = is_global;
8801        let _ = is_tied;
8802        let _ = is_hidden;
8803        let _ = is_hide_val;
8804        let _ = is_trace;
8805        let _ = pattern_match;
8806        let _ = precision;
8807
8808        // If -f (function mode) with no args, list functions
8809        if is_function && var_args.is_empty() {
8810            let mut func_names: Vec<_> = self.functions.keys().cloned().collect();
8811            func_names.sort();
8812            for name in &func_names {
8813                if let Some(func) = self.functions.get(name) {
8814                    if print_mode {
8815                        let body = crate::text::getpermtext(func);
8816                        println!("{} () {{\n\t{}\n}}", name, body.trim());
8817                    } else {
8818                        let body = crate::text::getpermtext(func);
8819                        println!("{} () {{\n\t{}\n}}", name, body.trim());
8820                    }
8821                }
8822            }
8823            return 0;
8824        }
8825
8826        // If -f with args, just show those functions
8827        if is_function {
8828            for name in &var_args {
8829                if let Some(func) = self.functions.get(name) {
8830                    if print_mode {
8831                        let body = crate::text::getpermtext(func);
8832                        println!("{} () {{\n\t{}\n}}", name, body.trim());
8833                    } else {
8834                        let body = crate::text::getpermtext(func);
8835                        println!("{} () {{\n\t{}\n}}", name, body.trim());
8836                    }
8837                }
8838            }
8839            return 0;
8840        }
8841
8842        // No args: list all variables with attributes
8843        if var_args.is_empty() {
8844            list_mode = true;
8845        }
8846
8847        if list_mode {
8848            let mut sorted_names: Vec<_> = self.variables.keys().cloned().collect();
8849            sorted_names.sort();
8850            for name in &sorted_names {
8851                let val = self.variables.get(name).cloned().unwrap_or_default();
8852                let mut attrs = String::new();
8853                if is_export || env::var(name).is_ok() {
8854                    attrs.push('x');
8855                }
8856                let is_arr = self.arrays.contains_key(name);
8857                let is_hash = self.assoc_arrays.contains_key(name);
8858                if is_arr {
8859                    attrs.push('a');
8860                }
8861                if is_hash {
8862                    attrs.push('A');
8863                }
8864                if print_mode {
8865                    // typeset -p: output re-executable code with values
8866                    let prefix = if attrs.is_empty() {
8867                        "typeset".to_string()
8868                    } else {
8869                        format!("typeset -{}", attrs)
8870                    };
8871                    if is_hash {
8872                        if let Some(assoc) = self.assoc_arrays.get(name) {
8873                            let mut pairs: Vec<_> = assoc.iter().collect();
8874                            pairs.sort_by_key(|(k, _)| (*k).clone());
8875                            let formatted: Vec<String> = pairs
8876                                .iter()
8877                                .map(|(k, v)| {
8878                                    format!("[{}]={}", shell_quote_value(k), shell_quote_value(v))
8879                                })
8880                                .collect();
8881                            println!("{} {}=( {} )", prefix, name, formatted.join(" "));
8882                        }
8883                    } else if is_arr {
8884                        if let Some(arr) = self.arrays.get(name) {
8885                            let formatted: Vec<String> =
8886                                arr.iter().map(|v| shell_quote_value(v)).collect();
8887                            println!("{} {}=( {} )", prefix, name, formatted.join(" "));
8888                        }
8889                    } else {
8890                        println!("{} {}={}", prefix, name, shell_quote_value(&val));
8891                    }
8892                } else if is_hide_val {
8893                    println!("{}={}", name, "*".repeat(val.len().min(8)));
8894                } else {
8895                    println!("{}={}", name, val);
8896                }
8897            }
8898            return 0;
8899        }
8900
8901        // Process variable assignments
8902        for arg in var_args {
8903            // Check if this starts an array assignment: "name=(" or "name=(value"
8904            if let Some(eq_pos) = arg.find('=') {
8905                let name = &arg[..eq_pos];
8906                let rest = &arg[eq_pos + 1..];
8907
8908                if rest.starts_with('(') {
8909                    // Array assignment - collect all elements until we find ')'
8910                    let mut elements = Vec::new();
8911                    let current = rest[1..].to_string(); // skip '('
8912
8913                    // Check if closing ) is in this arg
8914                    if let Some(close_pos) = current.find(')') {
8915                        let content = &current[..close_pos];
8916                        if !content.is_empty() {
8917                            elements.extend(content.split_whitespace().map(|s| s.to_string()));
8918                        }
8919                    } else {
8920                        // Single arg with just elements
8921                        if !current.is_empty() {
8922                            let trimmed = current.trim_end_matches(')');
8923                            elements.extend(trimmed.split_whitespace().map(|s| s.to_string()));
8924                        }
8925                    }
8926
8927                    // Set array variable
8928                    if is_assoc {
8929                        let mut assoc = std::collections::HashMap::new();
8930                        let mut iter = elements.iter();
8931                        while let Some(key) = iter.next() {
8932                            if let Some(val) = iter.next() {
8933                                assoc.insert(key.clone(), val.clone());
8934                            }
8935                        }
8936                        self.assoc_arrays.insert(name.to_string(), assoc);
8937                    } else {
8938                        self.arrays.insert(name.to_string(), elements);
8939                    }
8940                    self.variables.insert(name.to_string(), String::new());
8941                } else {
8942                    // Regular assignment - apply transformations
8943                    let mut value = rest.to_string();
8944
8945                    if is_integer {
8946                        // Force integer evaluation
8947                        value = self.evaluate_arithmetic(&value).to_string();
8948                    }
8949                    if is_lower {
8950                        value = value.to_lowercase();
8951                    }
8952                    if is_upper {
8953                        value = value.to_uppercase();
8954                    }
8955                    if let Some(w) = width {
8956                        if is_left_pad {
8957                            value = format!("{:<width$}", value, width = w);
8958                            value.truncate(w);
8959                        } else if is_right_pad || is_zero_pad {
8960                            let pad_char = if is_zero_pad { '0' } else { ' ' };
8961                            if value.len() < w {
8962                                value = format!(
8963                                    "{}{}",
8964                                    pad_char.to_string().repeat(w - value.len()),
8965                                    value
8966                                );
8967                            }
8968                            if value.len() > w {
8969                                value = value[value.len() - w..].to_string();
8970                            }
8971                        }
8972                    }
8973                    if is_float || is_float_exp {
8974                        if let Ok(f) = value.parse::<f64>() {
8975                            let prec = precision.unwrap_or(10);
8976                            value = if is_float_exp {
8977                                format!("{:.prec$e}", f, prec = prec)
8978                            } else {
8979                                format!("{:.prec$}", f, prec = prec)
8980                            };
8981                        }
8982                    }
8983
8984                    self.variables.insert(name.to_string(), value.clone());
8985
8986                    if is_export {
8987                        env::set_var(name, &value);
8988                    }
8989                }
8990            } else if is_array || is_assoc {
8991                // Just declaring the variable
8992                if is_assoc {
8993                    self.assoc_arrays
8994                        .insert(arg.clone(), std::collections::HashMap::new());
8995                } else {
8996                    self.arrays.insert(arg.clone(), Vec::new());
8997                }
8998                self.variables.insert(arg.clone(), String::new());
8999            } else {
9000                self.variables.insert(arg.clone(), String::new());
9001                if is_export {
9002                    env::set_var(&arg, "");
9003                }
9004            }
9005
9006            // Apply readonly flag — must come after the variable is set
9007            if is_readonly {
9008                let name = if let Some(eq_pos) = arg.find('=') {
9009                    arg[..eq_pos].to_string()
9010                } else {
9011                    arg.clone()
9012                };
9013                self.readonly_vars.insert(name);
9014            }
9015        }
9016        0
9017    }
9018
9019    fn builtin_read(&mut self, args: &[String]) -> i32 {
9020        // read [ -rszpqAclneE ] [ -t timeout ] [ -d delim ] [ -k [ num ] ] [ -u fd ]
9021        //      [ name[?prompt] ] [ name ... ]
9022        use std::io::{BufRead, Read as IoRead};
9023
9024        let mut raw_mode = false; // -r: don't interpret backslash escapes
9025        let mut silent = false; // -s: don't echo input
9026        let mut to_history = false; // -z: read from history stack
9027        let mut prompt_str: Option<String> = None; // -p prompt
9028        let mut use_array = false; // -A: read into array
9029        let mut timeout: Option<u64> = None; // -t timeout in seconds
9030        let mut delimiter = '\n'; // -d delim
9031        let mut nchars: Option<usize> = None; // -k num: read exactly num chars
9032        let mut fd = 0; // -u fd: read from fd
9033        let mut quiet = false; // -q: test only, don't assign
9034        let mut var_names: Vec<String> = Vec::new();
9035
9036        let mut i = 0;
9037        while i < args.len() {
9038            let arg = &args[i];
9039
9040            if arg == "--" {
9041                i += 1;
9042                while i < args.len() {
9043                    var_names.push(args[i].clone());
9044                    i += 1;
9045                }
9046                break;
9047            }
9048
9049            if arg.starts_with('-') && arg.len() > 1 {
9050                let mut chars = arg[1..].chars().peekable();
9051                while let Some(ch) = chars.next() {
9052                    match ch {
9053                        'r' => raw_mode = true,
9054                        's' => silent = true,
9055                        'z' => to_history = true,
9056                        'A' => use_array = true,
9057                        'c' | 'l' | 'n' | 'e' | 'E' => {} // TODO
9058                        'q' => quiet = true,
9059                        't' => {
9060                            let rest: String = chars.collect();
9061                            if !rest.is_empty() {
9062                                timeout = rest.parse().ok();
9063                            } else {
9064                                i += 1;
9065                                if i < args.len() {
9066                                    timeout = args[i].parse().ok();
9067                                }
9068                            }
9069                            break;
9070                        }
9071                        'd' => {
9072                            let rest: String = chars.collect();
9073                            if !rest.is_empty() {
9074                                delimiter = rest.chars().next().unwrap_or('\n');
9075                            } else {
9076                                i += 1;
9077                                if i < args.len() {
9078                                    delimiter = args[i].chars().next().unwrap_or('\n');
9079                                }
9080                            }
9081                            break;
9082                        }
9083                        'k' => {
9084                            let rest: String = chars.collect();
9085                            if !rest.is_empty() {
9086                                nchars = Some(rest.parse().unwrap_or(1));
9087                            } else if i + 1 < args.len()
9088                                && args[i + 1].chars().all(|c| c.is_ascii_digit())
9089                            {
9090                                i += 1;
9091                                nchars = Some(args[i].parse().unwrap_or(1));
9092                            } else {
9093                                nchars = Some(1);
9094                            }
9095                            break;
9096                        }
9097                        'u' => {
9098                            let rest: String = chars.collect();
9099                            if !rest.is_empty() {
9100                                fd = rest.parse().unwrap_or(0);
9101                            } else {
9102                                i += 1;
9103                                if i < args.len() {
9104                                    fd = args[i].parse().unwrap_or(0);
9105                                }
9106                            }
9107                            break;
9108                        }
9109                        'p' => {
9110                            let rest: String = chars.collect();
9111                            if !rest.is_empty() {
9112                                prompt_str = Some(rest);
9113                            } else {
9114                                i += 1;
9115                                if i < args.len() {
9116                                    prompt_str = Some(args[i].clone());
9117                                }
9118                            }
9119                            break;
9120                        }
9121                        _ => {}
9122                    }
9123                }
9124            } else {
9125                if let Some(pos) = arg.find('?') {
9126                    var_names.push(arg[..pos].to_string());
9127                    prompt_str = Some(arg[pos + 1..].to_string());
9128                } else {
9129                    var_names.push(arg.clone());
9130                }
9131            }
9132            i += 1;
9133        }
9134
9135        if var_names.is_empty() {
9136            var_names.push("REPLY".to_string());
9137        }
9138
9139        if let Some(ref p) = prompt_str {
9140            eprint!("{}", p);
9141            let _ = std::io::stderr().flush();
9142        }
9143
9144        let _ = to_history;
9145        let _ = fd;
9146        let _ = silent;
9147
9148        let input = if let Some(n) = nchars {
9149            let mut buf = vec![0u8; n];
9150            let stdin = io::stdin();
9151            if let Some(_t) = timeout {
9152                // TODO: proper timeout
9153            }
9154            match stdin.lock().read_exact(&mut buf) {
9155                Ok(_) => String::from_utf8_lossy(&buf).to_string(),
9156                Err(_) => return 1,
9157            }
9158        } else {
9159            let stdin = io::stdin();
9160            let mut input = String::new();
9161            if delimiter == '\n' {
9162                match stdin.lock().read_line(&mut input) {
9163                    Ok(0) => return 1,
9164                    Ok(_) => {}
9165                    Err(_) => return 1,
9166                }
9167            } else {
9168                let mut byte = [0u8; 1];
9169                loop {
9170                    match stdin.lock().read_exact(&mut byte) {
9171                        Ok(_) => {
9172                            let c = byte[0] as char;
9173                            if c == delimiter {
9174                                break;
9175                            }
9176                            input.push(c);
9177                        }
9178                        Err(_) => break,
9179                    }
9180                }
9181            }
9182            input
9183                .trim_end_matches('\n')
9184                .trim_end_matches('\r')
9185                .to_string()
9186        };
9187
9188        let processed = if raw_mode {
9189            input
9190        } else {
9191            input.replace("\\\n", "")
9192        };
9193
9194        if quiet {
9195            return if processed.is_empty() { 1 } else { 0 };
9196        }
9197
9198        if use_array {
9199            let var = &var_names[0];
9200            let words: Vec<String> = processed.split_whitespace().map(String::from).collect();
9201            self.arrays.insert(var.clone(), words);
9202        } else if var_names.len() == 1 {
9203            let var = &var_names[0];
9204            env::set_var(var, &processed);
9205            self.variables.insert(var.clone(), processed);
9206        } else {
9207            let ifs = self
9208                .variables
9209                .get("IFS")
9210                .map(|s| s.as_str())
9211                .unwrap_or(" \t\n");
9212            let words: Vec<&str> = processed
9213                .split(|c| ifs.contains(c))
9214                .filter(|s| !s.is_empty())
9215                .collect();
9216
9217            for (j, var) in var_names.iter().enumerate() {
9218                if j < words.len() {
9219                    if j == var_names.len() - 1 && words.len() > var_names.len() {
9220                        let remaining = words[j..].join(" ");
9221                        env::set_var(var, &remaining);
9222                        self.variables.insert(var.clone(), remaining);
9223                    } else {
9224                        env::set_var(var, words[j]);
9225                        self.variables.insert(var.clone(), words[j].to_string());
9226                    }
9227                } else {
9228                    env::set_var(var, "");
9229                    self.variables.insert(var.clone(), String::new());
9230                }
9231            }
9232        }
9233
9234        0
9235    }
9236
9237    fn builtin_shift(&mut self, args: &[String]) -> i32 {
9238        // shift [ -p ] [ n ] [ name ... ]
9239        // -p: shift from end instead of beginning (pop)
9240        // n: number of elements to shift (default 1)
9241        // name: array names to shift (default: shift positional parameters)
9242
9243        let mut from_end = false;
9244        let mut count = 1usize;
9245        let mut array_names: Vec<String> = Vec::new();
9246
9247        let mut i = 0;
9248        while i < args.len() {
9249            let arg = &args[i];
9250            if arg == "-p" {
9251                from_end = true;
9252            } else if arg.chars().all(|c| c.is_ascii_digit()) {
9253                count = arg.parse().unwrap_or(1);
9254            } else {
9255                array_names.push(arg.clone());
9256            }
9257            i += 1;
9258        }
9259
9260        if array_names.is_empty() {
9261            // Shift positional parameters
9262            if from_end {
9263                for _ in 0..count {
9264                    if !self.positional_params.is_empty() {
9265                        self.positional_params.pop();
9266                    }
9267                }
9268            } else {
9269                for _ in 0..count.min(self.positional_params.len()) {
9270                    self.positional_params.remove(0);
9271                }
9272            }
9273        } else {
9274            // Shift specified arrays
9275            for name in array_names {
9276                if let Some(arr) = self.arrays.get_mut(&name) {
9277                    if from_end {
9278                        for _ in 0..count {
9279                            if !arr.is_empty() {
9280                                arr.pop();
9281                            }
9282                        }
9283                    } else {
9284                        for _ in 0..count {
9285                            if !arr.is_empty() {
9286                                arr.remove(0);
9287                            }
9288                        }
9289                    }
9290                }
9291            }
9292        }
9293
9294        0
9295    }
9296
9297    #[tracing::instrument(level = "debug", skip(self))]
9298    fn builtin_eval(&mut self, args: &[String]) -> i32 {
9299        let code = args.join(" ");
9300        match self.execute_script(&code) {
9301            Ok(status) => status,
9302            Err(e) => {
9303                eprintln!("eval: {}", e);
9304                1
9305            }
9306        }
9307    }
9308
9309    fn builtin_autoload(&mut self, args: &[String]) -> i32 {
9310        // Parse options like zsh: -U (no alias), -z (zsh style), -k (ksh style),
9311        // -X (execute now), -x (export), -r (resolve), -R (resolve recurse),
9312        // -t (trace), -T (trace local), -W (warn nested), -d (use calling dir)
9313        let mut functions = Vec::new();
9314        let mut no_alias = false; // -U
9315        let mut zsh_style = false; // -z
9316        let mut ksh_style = false; // -k
9317        let mut execute_now = false; // -X
9318        let mut resolve = false; // -r
9319        let mut trace = false; // -t
9320        let mut use_caller_dir = false; // -d
9321        let _list_mode = false;
9322
9323        let mut i = 0;
9324        while i < args.len() {
9325            let arg = &args[i];
9326
9327            if arg == "--" {
9328                i += 1;
9329                break;
9330            }
9331
9332            if arg.starts_with('+') {
9333                let flags = &arg[1..];
9334                for c in flags.chars() {
9335                    match c {
9336                        'U' => no_alias = false,
9337                        'z' => zsh_style = false,
9338                        'k' => ksh_style = false,
9339                        't' => trace = false,
9340                        'd' => use_caller_dir = false,
9341                        _ => {}
9342                    }
9343                }
9344            } else if arg.starts_with('-') {
9345                let flags = &arg[1..];
9346                if flags.is_empty() {
9347                    // Just "-" means end of options
9348                    i += 1;
9349                    break;
9350                }
9351                for c in flags.chars() {
9352                    match c {
9353                        'U' => no_alias = true,
9354                        'z' => zsh_style = true,
9355                        'k' => ksh_style = true,
9356                        'X' => execute_now = true,
9357                        'r' | 'R' => resolve = true,
9358                        't' => trace = true,
9359                        'T' => {} // trace local
9360                        'W' => {} // warn nested
9361                        'd' => use_caller_dir = true,
9362                        'w' => {} // wordcode
9363                        'm' => {} // pattern match
9364                        _ => {}
9365                    }
9366                }
9367            } else {
9368                functions.push(arg.clone());
9369            }
9370            i += 1;
9371        }
9372
9373        // Collect remaining args as function names
9374        while i < args.len() {
9375            functions.push(args[i].clone());
9376            i += 1;
9377        }
9378
9379        // If no functions specified, list autoloaded functions
9380        if functions.is_empty() && !execute_now {
9381            for (name, _) in &self.autoload_pending {
9382                if no_alias && zsh_style {
9383                    println!("autoload -Uz {}", name);
9384                } else if no_alias {
9385                    println!("autoload -U {}", name);
9386                } else {
9387                    println!("autoload {}", name);
9388                }
9389            }
9390            return 0;
9391        }
9392
9393        // Handle -X: load and execute function immediately (called from stub)
9394        // When a stub function calls `builtin autoload -Xz`, we load the real function
9395        // and then need to execute it with the original arguments
9396        if execute_now {
9397            for func_name in &functions {
9398                // Load the function from fpath
9399                if let Some(loaded) = self.load_autoload_function(func_name) {
9400                    // Extract body from FunctionDef
9401                    let body = match loaded {
9402                        ShellCommand::FunctionDef(_, body) => (*body).clone(),
9403                        other => other,
9404                    };
9405                    // Replace the stub with the real function
9406                    self.functions.insert(func_name.clone(), body);
9407                    // Remove from pending
9408                    self.autoload_pending.remove(func_name);
9409                } else {
9410                    eprintln!(
9411                        "autoload: {}: function definition file not found",
9412                        func_name
9413                    );
9414                    return 1;
9415                }
9416            }
9417            return 0;
9418        }
9419
9420        // Register functions for autoload - create stub functions
9421        for func_name in &functions {
9422            // Store autoload metadata
9423            let mut flags = AutoloadFlags::empty();
9424            if no_alias {
9425                flags |= AutoloadFlags::NO_ALIAS;
9426            }
9427            if zsh_style {
9428                flags |= AutoloadFlags::ZSH_STYLE;
9429            }
9430            if ksh_style {
9431                flags |= AutoloadFlags::KSH_STYLE;
9432            }
9433            if trace {
9434                flags |= AutoloadFlags::TRACE;
9435            }
9436            if use_caller_dir {
9437                flags |= AutoloadFlags::USE_CALLER_DIR;
9438            }
9439
9440            self.autoload_pending.insert(func_name.clone(), flags);
9441
9442            // Create a stub function: `builtin autoload -Xz funcname && funcname "$@"`
9443            // When called, this loads the real function and re-calls it
9444            let autoload_opts = if zsh_style && no_alias {
9445                "-XUz"
9446            } else if zsh_style {
9447                "-Xz"
9448            } else if no_alias {
9449                "-XU"
9450            } else {
9451                "-X"
9452            };
9453
9454            // The stub: builtin autoload -Xz funcname && funcname "$@"
9455            let stub = ShellCommand::List(vec![
9456                (
9457                    ShellCommand::Simple(SimpleCommand {
9458                        assignments: vec![],
9459                        words: vec![
9460                            ShellWord::Literal("builtin".to_string()),
9461                            ShellWord::Literal("autoload".to_string()),
9462                            ShellWord::Literal(autoload_opts.to_string()),
9463                            ShellWord::Literal(func_name.clone()),
9464                        ],
9465                        redirects: vec![],
9466                    }),
9467                    ListOp::And,
9468                ),
9469                (
9470                    ShellCommand::Simple(SimpleCommand {
9471                        assignments: vec![],
9472                        words: vec![
9473                            ShellWord::Literal(func_name.clone()),
9474                            ShellWord::DoubleQuoted(vec![ShellWord::Variable("@".to_string())]),
9475                        ],
9476                        redirects: vec![],
9477                    }),
9478                    ListOp::Semi,
9479                ),
9480            ]);
9481
9482            self.functions.insert(func_name.clone(), stub);
9483
9484            // If -r or -R, resolve the path now to verify it exists
9485            if resolve {
9486                if self.find_function_file(func_name).is_none() {
9487                    eprintln!(
9488                        "autoload: {}: function definition file not found",
9489                        func_name
9490                    );
9491                }
9492            }
9493        }
9494
9495        // Batch pre-resolution: when multiple autoloads are registered at once
9496        // (common during .zshrc processing), dispatch fpath lookups in parallel
9497        // across the worker pool to pre-read function files into the OS page cache.
9498        if functions.len() >= 4 && !resolve && !execute_now {
9499            let fpath_dirs: Vec<PathBuf> = self.fpath.clone();
9500            let names: Vec<String> = functions.clone();
9501            let pool = std::sync::Arc::clone(&self.worker_pool);
9502
9503            tracing::debug!(
9504                count = names.len(),
9505                fpath_dirs = fpath_dirs.len(),
9506                "batch autoload: pre-resolving fpath lookups on worker pool"
9507            );
9508
9509            // Submit resolution tasks — each worker scans fpath for a subset of names.
9510            // Results are cached in a shared map for later use by load_autoload_function.
9511            let resolved = std::sync::Arc::new(parking_lot::Mutex::new(
9512                HashMap::<String, PathBuf>::with_capacity(names.len()),
9513            ));
9514
9515            for name in names {
9516                let dirs = fpath_dirs.clone();
9517                let resolved = std::sync::Arc::clone(&resolved);
9518                pool.submit(move || {
9519                    for dir in &dirs {
9520                        let path = dir.join(&name);
9521                        if path.exists() && path.is_file() {
9522                            // Pre-read to warm OS page cache (the read result is discarded,
9523                            // but the pages stay in the kernel buffer cache)
9524                            let _ = std::fs::read(&path);
9525                            resolved.lock().insert(name.clone(), path);
9526                            tracing::trace!(func = %name, "autoload batch: pre-resolved");
9527                            break;
9528                        }
9529                    }
9530                });
9531            }
9532        }
9533
9534        0
9535    }
9536
9537    /// Find a function file in fpath
9538    fn find_function_file(&self, name: &str) -> Option<PathBuf> {
9539        for dir in &self.fpath {
9540            let path = dir.join(name);
9541            if path.exists() && path.is_file() {
9542                return Some(path);
9543            }
9544        }
9545        None
9546    }
9547
9548    /// Load an autoloaded function from fpath - reads file and parses it
9549    fn load_autoload_function(&mut self, name: &str) -> Option<ShellCommand> {
9550        // FAST PATH: Try SQLite cache first (no filesystem access)
9551        // Skip in zsh_compat mode - use traditional fpath scanning only
9552        if !self.zsh_compat {
9553            if let Some(ref cache) = self.compsys_cache {
9554                // FASTEST: try cached bytecodes (skip lex+parse+compile entirely)
9555                if let Ok(Some(bc_blob)) = cache.get_autoload_bytecode(name) {
9556                    // Try fusevm::Chunk first (new format — actual bytecodes)
9557                    if let Ok(chunk) = bincode::deserialize::<fusevm::Chunk>(&bc_blob) {
9558                        if !chunk.ops.is_empty() {
9559                            tracing::trace!(name, bytes = bc_blob.len(), ops = chunk.ops.len(), "autoload: bytecode cache hit → VM");
9560                            // Execute directly on fusevm — no parse, no compile
9561                            let mut vm = fusevm::VM::new(chunk);
9562                            let _ = vm.run();
9563                            self.last_status = vm.last_status;
9564                            // Return a no-op so the caller doesn't try to execute again
9565                            return Some(ShellCommand::Simple(crate::parser::SimpleCommand {
9566                                assignments: Vec::new(),
9567                                words: Vec::new(),
9568                                redirects: Vec::new(),
9569                            }));
9570                        }
9571                    }
9572                    // Fallback: try legacy Vec<ShellCommand> format (migration)
9573                    if let Ok(commands) = bincode::deserialize::<Vec<ShellCommand>>(&bc_blob) {
9574                        if !commands.is_empty() {
9575                            tracing::trace!(name, bytes = bc_blob.len(), "autoload: legacy AST cache hit");
9576                            return Some(self.wrap_autoload_commands(name, commands));
9577                        }
9578                    }
9579                }
9580
9581                // FAST: cached source text — parse + compile (still no filesystem access)
9582                if let Ok(Some(body)) = cache.get_autoload_body(name) {
9583                    let mut parser = ShellParser::new(&body);
9584                    if let Ok(commands) = parser.parse_script() {
9585                        if !commands.is_empty() {
9586                            // Compile to bytecodes and cache for next time
9587                            let compiler = crate::shell_compiler::ShellCompiler::new();
9588                            let chunk = compiler.compile(&commands);
9589                            if let Ok(blob) = bincode::serialize(&chunk) {
9590                                let _ = cache.set_autoload_bytecode(name, &blob);
9591                                tracing::trace!(name, bytes = blob.len(), "autoload: bytecodes compiled and cached");
9592                            }
9593                            return Some(self.wrap_autoload_commands(name, commands));
9594                        }
9595                    }
9596                }
9597            }
9598        }
9599
9600        // SLOW PATH: Try ZWC cache (but skip if we're reloading an existing function)
9601        if !self.functions.contains_key(name) {
9602            // Try to load from ZWC files
9603            for dir in &self.fpath.clone() {
9604                // Try dir.zwc (e.g., /path/to/src.zwc for /path/to/src)
9605                let zwc_path = dir.with_extension("zwc");
9606                if zwc_path.exists() {
9607                    // Function name in directory ZWC includes path prefix
9608                    let prefixed_name = format!(
9609                        "{}/{}",
9610                        dir.file_name().and_then(|n| n.to_str()).unwrap_or(""),
9611                        name
9612                    );
9613                    if let Some(func) = self.load_function_from_zwc(&zwc_path, &prefixed_name) {
9614                        return Some(func);
9615                    }
9616                    // Also try without prefix
9617                    if let Some(func) = self.load_function_from_zwc(&zwc_path, name) {
9618                        return Some(func);
9619                    }
9620                }
9621                // Try individual function.zwc
9622                let func_zwc = dir.join(format!("{}.zwc", name));
9623                if func_zwc.exists() {
9624                    if let Some(func) = self.load_function_from_zwc(&func_zwc, name) {
9625                        return Some(func);
9626                    }
9627                }
9628            }
9629        }
9630
9631        // SLOWEST PATH: Find the function file in fpath
9632        let path = self.find_function_file(name)?;
9633
9634        // Read the file
9635        let content = std::fs::read_to_string(&path).ok()?;
9636
9637        // Parse the content
9638        let mut parser = ShellParser::new(&content);
9639
9640        if let Ok(commands) = parser.parse_script() {
9641            if commands.is_empty() {
9642                return None;
9643            }
9644
9645            // Check if it's a single function definition for this name (ksh style)
9646            if commands.len() == 1 {
9647                if let ShellCommand::FunctionDef(ref fn_name, _) = commands[0] {
9648                    if fn_name == name {
9649                        return Some(commands[0].clone());
9650                    }
9651                }
9652            }
9653
9654            // Otherwise, the file contents become the function body (zsh style)
9655            // Wrap all commands in a List
9656            let body = if commands.len() == 1 {
9657                commands.into_iter().next().unwrap()
9658            } else {
9659                // Convert to List with Semi separators
9660                let list_cmds: Vec<(ShellCommand, ListOp)> =
9661                    commands.into_iter().map(|c| (c, ListOp::Semi)).collect();
9662                ShellCommand::List(list_cmds)
9663            };
9664
9665            return Some(ShellCommand::FunctionDef(name.to_string(), Box::new(body)));
9666        }
9667
9668        None
9669    }
9670
9671    /// Convert parsed commands into a FunctionDef, handling ksh vs zsh style.
9672    fn wrap_autoload_commands(&self, name: &str, commands: Vec<ShellCommand>) -> ShellCommand {
9673        // ksh style: file contains a single function definition for this name
9674        if commands.len() == 1 {
9675            if let ShellCommand::FunctionDef(ref fn_name, _) = commands[0] {
9676                if fn_name == name {
9677                    return commands.into_iter().next().unwrap();
9678                }
9679            }
9680        }
9681        // zsh style: file body IS the function body
9682        let body = if commands.len() == 1 {
9683            commands.into_iter().next().unwrap()
9684        } else {
9685            let list_cmds: Vec<(ShellCommand, ListOp)> =
9686                commands.into_iter().map(|c| (c, ListOp::Semi)).collect();
9687            ShellCommand::List(list_cmds)
9688        };
9689        ShellCommand::FunctionDef(name.to_string(), Box::new(body))
9690    }
9691
9692    /// Check if a function is autoload pending and load it if so
9693    pub fn maybe_autoload(&mut self, name: &str) -> bool {
9694        if self.autoload_pending.contains_key(name) {
9695            if let Some(func) = self.load_autoload_function(name) {
9696                // For FunctionDef, extract the body and store it
9697                let to_store = match func {
9698                    ShellCommand::FunctionDef(_, body) => (*body).clone(),
9699                    other => other,
9700                };
9701                self.functions.insert(name.to_string(), to_store);
9702                self.autoload_pending.remove(name);
9703                return true;
9704            }
9705        }
9706        false
9707    }
9708
9709    fn builtin_jobs(&mut self, args: &[String]) -> i32 {
9710        // jobs [ -dlprsZ ] [ job ... ]
9711        // -l: long format (show PID)
9712        // -p: print process group IDs only
9713        // -d: show directory from which job was started
9714        // -r: show running jobs only
9715        // -s: show stopped jobs only
9716        // -Z: set process name (not relevant here)
9717
9718        let mut long_format = false;
9719        let mut pids_only = false;
9720        let mut show_dir = false;
9721        let mut running_only = false;
9722        let mut stopped_only = false;
9723        let mut job_ids: Vec<usize> = Vec::new();
9724
9725        for arg in args {
9726            if arg.starts_with('-') {
9727                for c in arg[1..].chars() {
9728                    match c {
9729                        'l' => long_format = true,
9730                        'p' => pids_only = true,
9731                        'd' => show_dir = true,
9732                        'r' => running_only = true,
9733                        's' => stopped_only = true,
9734                        'Z' => {} // ignore
9735                        _ => {}
9736                    }
9737                }
9738            } else if arg.starts_with('%') {
9739                if let Ok(id) = arg[1..].parse::<usize>() {
9740                    job_ids.push(id);
9741                }
9742            } else if let Ok(id) = arg.parse::<usize>() {
9743                job_ids.push(id);
9744            }
9745        }
9746
9747        // Reap finished jobs first
9748        for job in self.jobs.reap_finished() {
9749            if !running_only && !stopped_only {
9750                if pids_only {
9751                    println!("{}", job.pid);
9752                } else {
9753                    println!("[{}]  Done                    {}", job.id, job.command);
9754                }
9755            }
9756        }
9757
9758        // List jobs (optionally filtered)
9759        for job in self.jobs.list() {
9760            // Filter by specific job IDs if provided
9761            if !job_ids.is_empty() && !job_ids.contains(&job.id) {
9762                continue;
9763            }
9764
9765            // Filter by state
9766            if running_only && job.state != JobState::Running {
9767                continue;
9768            }
9769            if stopped_only && job.state != JobState::Stopped {
9770                continue;
9771            }
9772
9773            if pids_only {
9774                println!("{}", job.pid);
9775                continue;
9776            }
9777
9778            let marker = if job.is_current { "+" } else { "-" };
9779            let state = match job.state {
9780                JobState::Running => "running",
9781                JobState::Stopped => "suspended",
9782                JobState::Done => "done",
9783            };
9784
9785            if long_format {
9786                println!(
9787                    "[{}]{} {:6} {}  {}",
9788                    job.id, marker, job.pid, state, job.command
9789                );
9790            } else {
9791                println!("[{}]{} {}  {}", job.id, marker, state, job.command);
9792            }
9793
9794            if show_dir {
9795                if let Ok(cwd) = env::current_dir() {
9796                    println!("    (pwd: {})", cwd.display());
9797                }
9798            }
9799        }
9800        0
9801    }
9802
9803    fn builtin_fg(&mut self, args: &[String]) -> i32 {
9804        let job_id = if let Some(arg) = args.first() {
9805            // Parse %N or just N
9806            let s = arg.trim_start_matches('%');
9807            match s.parse::<usize>() {
9808                Ok(id) => Some(id),
9809                Err(_) => {
9810                    eprintln!("fg: {}: no such job", arg);
9811                    return 1;
9812                }
9813            }
9814        } else {
9815            self.jobs.current().map(|j| j.id)
9816        };
9817
9818        let Some(id) = job_id else {
9819            eprintln!("fg: no current job");
9820            return 1;
9821        };
9822
9823        let Some(job) = self.jobs.get(id) else {
9824            eprintln!("fg: %{}: no such job", id);
9825            return 1;
9826        };
9827
9828        let pid = job.pid;
9829        let cmd = job.command.clone();
9830        println!("{}", cmd);
9831
9832        // Continue the job
9833        if let Err(e) = continue_job(pid) {
9834            eprintln!("fg: {}", e);
9835            return 1;
9836        }
9837
9838        // Wait for it
9839        match wait_for_job(pid) {
9840            Ok(status) => {
9841                self.jobs.remove(id);
9842                status
9843            }
9844            Err(e) => {
9845                eprintln!("fg: {}", e);
9846                1
9847            }
9848        }
9849    }
9850
9851    fn builtin_bg(&mut self, args: &[String]) -> i32 {
9852        let job_id = if let Some(arg) = args.first() {
9853            let s = arg.trim_start_matches('%');
9854            match s.parse::<usize>() {
9855                Ok(id) => Some(id),
9856                Err(_) => {
9857                    eprintln!("bg: {}: no such job", arg);
9858                    return 1;
9859                }
9860            }
9861        } else {
9862            self.jobs.current().map(|j| j.id)
9863        };
9864
9865        let Some(id) = job_id else {
9866            eprintln!("bg: no current job");
9867            return 1;
9868        };
9869
9870        let Some(job) = self.jobs.get_mut(id) else {
9871            eprintln!("bg: %{}: no such job", id);
9872            return 1;
9873        };
9874
9875        let pid = job.pid;
9876        let cmd = job.command.clone();
9877
9878        if let Err(e) = continue_job(pid) {
9879            eprintln!("bg: {}", e);
9880            return 1;
9881        }
9882
9883        job.state = JobState::Running;
9884        println!("[{}] {} &", id, cmd);
9885        0
9886    }
9887
9888    fn builtin_kill(&mut self, args: &[String]) -> i32 {
9889        // kill [ -s signal_name | -n signal_number | -sig ] job ...
9890        // kill -l [ sig ... ]
9891        use crate::jobs::send_signal;
9892        use nix::sys::signal::Signal;
9893
9894        if args.is_empty() {
9895            eprintln!("kill: usage: kill [-s signal | -n num | -sig] pid ...");
9896            eprintln!("       kill -l [sig ...]");
9897            return 1;
9898        }
9899
9900        // Signal name/number mapping
9901        let signal_map: &[(&str, i32, Signal)] = &[
9902            ("HUP", 1, Signal::SIGHUP),
9903            ("INT", 2, Signal::SIGINT),
9904            ("QUIT", 3, Signal::SIGQUIT),
9905            ("ILL", 4, Signal::SIGILL),
9906            ("TRAP", 5, Signal::SIGTRAP),
9907            ("ABRT", 6, Signal::SIGABRT),
9908            ("BUS", 7, Signal::SIGBUS),
9909            ("FPE", 8, Signal::SIGFPE),
9910            ("KILL", 9, Signal::SIGKILL),
9911            ("USR1", 10, Signal::SIGUSR1),
9912            ("SEGV", 11, Signal::SIGSEGV),
9913            ("USR2", 12, Signal::SIGUSR2),
9914            ("PIPE", 13, Signal::SIGPIPE),
9915            ("ALRM", 14, Signal::SIGALRM),
9916            ("TERM", 15, Signal::SIGTERM),
9917            ("CHLD", 17, Signal::SIGCHLD),
9918            ("CONT", 18, Signal::SIGCONT),
9919            ("STOP", 19, Signal::SIGSTOP),
9920            ("TSTP", 20, Signal::SIGTSTP),
9921            ("TTIN", 21, Signal::SIGTTIN),
9922            ("TTOU", 22, Signal::SIGTTOU),
9923            ("URG", 23, Signal::SIGURG),
9924            ("XCPU", 24, Signal::SIGXCPU),
9925            ("XFSZ", 25, Signal::SIGXFSZ),
9926            ("VTALRM", 26, Signal::SIGVTALRM),
9927            ("PROF", 27, Signal::SIGPROF),
9928            ("WINCH", 28, Signal::SIGWINCH),
9929            ("IO", 29, Signal::SIGIO),
9930            ("SYS", 31, Signal::SIGSYS),
9931        ];
9932
9933        let mut sig = Signal::SIGTERM;
9934        let mut pids: Vec<String> = Vec::new();
9935        let mut list_mode = false;
9936        let mut list_args: Vec<String> = Vec::new();
9937
9938        let mut i = 0;
9939        while i < args.len() {
9940            let arg = &args[i];
9941
9942            if arg == "-l" || arg == "-L" {
9943                list_mode = true;
9944                // Remaining args are signal numbers to translate
9945                list_args = args[i + 1..].to_vec();
9946                break;
9947            } else if arg == "-s" {
9948                // -s signal_name
9949                i += 1;
9950                if i >= args.len() {
9951                    eprintln!("kill: -s requires an argument");
9952                    return 1;
9953                }
9954                let sig_name = args[i].to_uppercase();
9955                let sig_name = sig_name.strip_prefix("SIG").unwrap_or(&sig_name);
9956                if let Some((_, _, s)) = signal_map.iter().find(|(name, _, _)| *name == sig_name) {
9957                    sig = *s;
9958                } else {
9959                    eprintln!("kill: invalid signal: {}", args[i]);
9960                    return 1;
9961                }
9962            } else if arg == "-n" {
9963                // -n signal_number
9964                i += 1;
9965                if i >= args.len() {
9966                    eprintln!("kill: -n requires an argument");
9967                    return 1;
9968                }
9969                let num: i32 = match args[i].parse() {
9970                    Ok(n) => n,
9971                    Err(_) => {
9972                        eprintln!("kill: invalid signal number: {}", args[i]);
9973                        return 1;
9974                    }
9975                };
9976                if let Some((_, _, s)) = signal_map.iter().find(|(_, n, _)| *n == num) {
9977                    sig = *s;
9978                } else {
9979                    eprintln!("kill: invalid signal number: {}", num);
9980                    return 1;
9981                }
9982            } else if arg.starts_with('-') && arg.len() > 1 {
9983                // -SIGNAL or -NUM
9984                let sig_str = &arg[1..];
9985                let sig_upper = sig_str.to_uppercase();
9986                let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
9987
9988                // Try as number first
9989                if let Ok(num) = sig_str.parse::<i32>() {
9990                    if let Some((_, _, s)) = signal_map.iter().find(|(_, n, _)| *n == num) {
9991                        sig = *s;
9992                    } else {
9993                        eprintln!("kill: invalid signal: {}", arg);
9994                        return 1;
9995                    }
9996                } else if let Some((_, _, s)) =
9997                    signal_map.iter().find(|(name, _, _)| *name == sig_name)
9998                {
9999                    sig = *s;
10000                } else {
10001                    eprintln!("kill: invalid signal: {}", arg);
10002                    return 1;
10003                }
10004            } else {
10005                pids.push(arg.clone());
10006            }
10007            i += 1;
10008        }
10009
10010        // Handle -l (list signals)
10011        if list_mode {
10012            if list_args.is_empty() {
10013                // List all signals
10014                for (name, num, _) in signal_map {
10015                    println!("{:2}) SIG{}", num, name);
10016                }
10017            } else {
10018                // Translate signal numbers to names or vice versa
10019                for arg in &list_args {
10020                    if let Ok(num) = arg.parse::<i32>() {
10021                        // Number -> name
10022                        if let Some((name, _, _)) = signal_map.iter().find(|(_, n, _)| *n == num) {
10023                            println!("{}", name);
10024                        } else {
10025                            eprintln!("kill: unknown signal: {}", num);
10026                        }
10027                    } else {
10028                        // Name -> number
10029                        let sig_upper = arg.to_uppercase();
10030                        let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
10031                        if let Some((_, num, _)) =
10032                            signal_map.iter().find(|(name, _, _)| *name == sig_name)
10033                        {
10034                            println!("{}", num);
10035                        } else {
10036                            eprintln!("kill: unknown signal: {}", arg);
10037                        }
10038                    }
10039                }
10040            }
10041            return 0;
10042        }
10043
10044        if pids.is_empty() {
10045            eprintln!("kill: usage: kill [-s signal | -n num | -sig] pid ...");
10046            return 1;
10047        }
10048
10049        let mut status = 0;
10050        for arg in &pids {
10051            // Handle %job syntax
10052            if arg.starts_with('%') {
10053                let id: usize = match arg[1..].parse() {
10054                    Ok(id) => id,
10055                    Err(_) => {
10056                        eprintln!("kill: {}: no such job", arg);
10057                        status = 1;
10058                        continue;
10059                    }
10060                };
10061                if let Some(job) = self.jobs.get(id) {
10062                    if let Err(e) = send_signal(job.pid, sig) {
10063                        eprintln!("kill: {}", e);
10064                        status = 1;
10065                    }
10066                } else {
10067                    eprintln!("kill: {}: no such job", arg);
10068                    status = 1;
10069                }
10070            } else {
10071                // Direct PID
10072                let pid: u32 = match arg.parse() {
10073                    Ok(p) => p,
10074                    Err(_) => {
10075                        eprintln!("kill: {}: invalid pid", arg);
10076                        status = 1;
10077                        continue;
10078                    }
10079                };
10080                if let Err(e) = send_signal(pid as i32, sig) {
10081                    eprintln!("kill: {}", e);
10082                    status = 1;
10083                }
10084            }
10085        }
10086        status
10087    }
10088
10089    fn builtin_disown(&mut self, args: &[String]) -> i32 {
10090        if args.is_empty() {
10091            // Disown current job
10092            if let Some(job) = self.jobs.current() {
10093                let id = job.id;
10094                self.jobs.remove(id);
10095            }
10096            return 0;
10097        }
10098
10099        for arg in args {
10100            let s = arg.trim_start_matches('%');
10101            if let Ok(id) = s.parse::<usize>() {
10102                self.jobs.remove(id);
10103            } else {
10104                eprintln!("disown: {}: no such job", arg);
10105            }
10106        }
10107        0
10108    }
10109
10110    fn builtin_wait(&mut self, args: &[String]) -> i32 {
10111        if args.is_empty() {
10112            // Wait for all jobs
10113            let ids: Vec<usize> = self.jobs.list().iter().map(|j| j.id).collect();
10114            for id in ids {
10115                if let Some(mut job) = self.jobs.remove(id) {
10116                    if let Some(ref mut child) = job.child {
10117                        let _ = wait_for_child(child);
10118                    }
10119                }
10120            }
10121            return 0;
10122        }
10123
10124        let mut status = 0;
10125        for arg in args {
10126            if arg.starts_with('%') {
10127                let id: usize = match arg[1..].parse() {
10128                    Ok(id) => id,
10129                    Err(_) => {
10130                        eprintln!("wait: {}: no such job", arg);
10131                        status = 127;
10132                        continue;
10133                    }
10134                };
10135                if let Some(mut job) = self.jobs.remove(id) {
10136                    if let Some(ref mut child) = job.child {
10137                        match wait_for_child(child) {
10138                            Ok(s) => status = s,
10139                            Err(e) => {
10140                                eprintln!("wait: {}", e);
10141                                status = 127;
10142                            }
10143                        }
10144                    }
10145                } else {
10146                    eprintln!("wait: {}: no such job", arg);
10147                    status = 127;
10148                }
10149            } else {
10150                let pid: u32 = match arg.parse() {
10151                    Ok(p) => p,
10152                    Err(_) => {
10153                        eprintln!("wait: {}: invalid pid", arg);
10154                        status = 127;
10155                        continue;
10156                    }
10157                };
10158                match wait_for_job(pid as i32) {
10159                    Ok(s) => status = s,
10160                    Err(e) => {
10161                        eprintln!("wait: {}", e);
10162                        status = 127;
10163                    }
10164                }
10165            }
10166        }
10167        status
10168    }
10169
10170    fn builtin_suspend(&self, args: &[String]) -> i32 {
10171        let mut force = false;
10172        for arg in args {
10173            if arg == "-f" {
10174                force = true;
10175            }
10176        }
10177
10178        #[cfg(unix)]
10179        {
10180            use nix::sys::signal::{kill, Signal};
10181            use nix::unistd::getppid;
10182
10183            // Check if we're a login shell (parent is init/PID 1)
10184            let ppid = getppid();
10185            if !force && ppid == nix::unistd::Pid::from_raw(1) {
10186                eprintln!("suspend: cannot suspend a login shell");
10187                return 1;
10188            }
10189
10190            // Send SIGTSTP to ourselves
10191            let pid = nix::unistd::getpid();
10192            if let Err(e) = kill(pid, Signal::SIGTSTP) {
10193                eprintln!("suspend: {}", e);
10194                return 1;
10195            }
10196            0
10197        }
10198
10199        #[cfg(not(unix))]
10200        {
10201            eprintln!("suspend: not supported on this platform");
10202            1
10203        }
10204    }
10205}
10206
10207impl Default for ShellExecutor {
10208    fn default() -> Self {
10209        Self::new()
10210    }
10211}
10212
10213#[cfg(test)]
10214mod tests {
10215    use super::*;
10216
10217    #[test]
10218    fn test_simple_echo() {
10219        let mut exec = ShellExecutor::new();
10220        let status = exec.execute_script("true").unwrap();
10221        assert_eq!(status, 0);
10222    }
10223
10224    #[test]
10225    fn test_if_true() {
10226        let mut exec = ShellExecutor::new();
10227        let status = exec.execute_script("if true; then true; fi").unwrap();
10228        assert_eq!(status, 0);
10229    }
10230
10231    #[test]
10232    fn test_if_false() {
10233        let mut exec = ShellExecutor::new();
10234        let status = exec
10235            .execute_script("if false; then true; else false; fi")
10236            .unwrap();
10237        assert_eq!(status, 1);
10238    }
10239
10240    #[test]
10241    fn test_for_loop() {
10242        let mut exec = ShellExecutor::new();
10243        exec.execute_script("for i in a b c; do true; done")
10244            .unwrap();
10245        assert_eq!(exec.last_status, 0);
10246    }
10247
10248    #[test]
10249    fn test_and_list() {
10250        let mut exec = ShellExecutor::new();
10251        let status = exec.execute_script("true && true").unwrap();
10252        assert_eq!(status, 0);
10253
10254        let status = exec.execute_script("true && false").unwrap();
10255        assert_eq!(status, 1);
10256    }
10257
10258    #[test]
10259    fn test_or_list() {
10260        let mut exec = ShellExecutor::new();
10261        let status = exec.execute_script("false || true").unwrap();
10262        assert_eq!(status, 0);
10263    }
10264}
10265
10266impl ShellExecutor {
10267    fn builtin_history(&self, args: &[String]) -> i32 {
10268        let Some(ref engine) = self.history else {
10269            eprintln!("history: history engine not available");
10270            return 1;
10271        };
10272
10273        // Parse options
10274        let mut count = 20usize;
10275        let mut show_all = false;
10276        let mut search_query = None;
10277
10278        let mut i = 0;
10279        while i < args.len() {
10280            match args[i].as_str() {
10281                "-c" | "--clear" => {
10282                    // Clear history - need mutable access
10283                    eprintln!("history: clear not supported in this mode");
10284                    return 1;
10285                }
10286                "-a" | "--all" => show_all = true,
10287                "-n" => {
10288                    if i + 1 < args.len() {
10289                        i += 1;
10290                        count = args[i].parse().unwrap_or(20);
10291                    }
10292                }
10293                s if s.starts_with('-') && s[1..].chars().all(|c| c.is_ascii_digit()) => {
10294                    count = s[1..].parse().unwrap_or(20);
10295                }
10296                s if s.chars().all(|c| c.is_ascii_digit()) => {
10297                    count = s.parse().unwrap_or(20);
10298                }
10299                s => {
10300                    search_query = Some(s.to_string());
10301                }
10302            }
10303            i += 1;
10304        }
10305
10306        if show_all {
10307            count = 10000;
10308        }
10309
10310        let entries = if let Some(ref q) = search_query {
10311            engine.search(q, count)
10312        } else {
10313            engine.recent(count)
10314        };
10315
10316        match entries {
10317            Ok(entries) => {
10318                // Print in chronological order (reverse the results since recent() is newest-first)
10319                for entry in entries.into_iter().rev() {
10320                    println!("{:>6}  {}", entry.id, entry.command);
10321                }
10322                0
10323            }
10324            Err(e) => {
10325                eprintln!("history: {}", e);
10326                1
10327            }
10328        }
10329    }
10330
10331    /// fc builtin - fix command (history manipulation)
10332    /// Ported from zsh/Src/builtin.c bin_fc() lines 1426-1700
10333    /// Options: -l (list), -n (no numbers), -r (reverse), -d/-f/-E/-i/-t (time formats),
10334    /// -D (duration), -e editor, -m pattern, -R/-W/-A (read/write/append history file),
10335    /// -p/-P (push/pop history stack), -I (skip old), -L (local), -s (substitute)
10336    fn builtin_fc(&mut self, args: &[String]) -> i32 {
10337        let Some(ref engine) = self.history else {
10338            eprintln!("fc: history engine not available");
10339            return 1;
10340        };
10341
10342        // Parse options
10343        let mut list_mode = false;
10344        let mut no_numbers = false;
10345        let mut reverse = false;
10346        let mut show_time = false;
10347        let mut show_duration = false;
10348        let mut editor: Option<String> = None;
10349        let mut read_file = false;
10350        let mut write_file = false;
10351        let mut append_file = false;
10352        let mut substitute_mode = false;
10353        let mut positional: Vec<&str> = Vec::new();
10354        let mut substitutions: Vec<(String, String)> = Vec::new();
10355
10356        let mut i = 0;
10357        while i < args.len() {
10358            let arg = &args[i];
10359            if arg == "--" {
10360                i += 1;
10361                while i < args.len() {
10362                    positional.push(&args[i]);
10363                    i += 1;
10364                }
10365                break;
10366            }
10367            if arg.starts_with('-') && arg.len() > 1 {
10368                let chars: Vec<char> = arg[1..].chars().collect();
10369                let mut j = 0;
10370                while j < chars.len() {
10371                    match chars[j] {
10372                        'l' => list_mode = true,
10373                        'n' => no_numbers = true,
10374                        'r' => reverse = true,
10375                        'd' | 'f' | 'E' | 'i' => show_time = true,
10376                        'D' => show_duration = true,
10377                        'R' => read_file = true,
10378                        'W' => write_file = true,
10379                        'A' => append_file = true,
10380                        's' => substitute_mode = true,
10381                        'e' => {
10382                            if j + 1 < chars.len() {
10383                                editor = Some(chars[j + 1..].iter().collect());
10384                                break;
10385                            } else {
10386                                i += 1;
10387                                if i < args.len() {
10388                                    editor = Some(args[i].clone());
10389                                }
10390                            }
10391                        }
10392                        't' => {
10393                            show_time = true;
10394                            if j + 1 < chars.len() {
10395                                break;
10396                            } else {
10397                                i += 1;
10398                            }
10399                        }
10400                        'p' | 'P' | 'a' | 'I' | 'L' | 'm' => {} // Handled but no-op for now
10401                        _ => {
10402                            if chars[j].is_ascii_digit() {
10403                                positional.push(arg);
10404                                break;
10405                            }
10406                        }
10407                    }
10408                    j += 1;
10409                }
10410            } else if arg.contains('=') && !list_mode {
10411                if let Some((old, new)) = arg.split_once('=') {
10412                    substitutions.push((old.to_string(), new.to_string()));
10413                }
10414            } else {
10415                positional.push(arg);
10416            }
10417            i += 1;
10418        }
10419
10420        // Handle file operations (read/write/append)
10421        // Note: HistoryEngine uses SQLite, so file ops are simplified
10422        if read_file || write_file || append_file {
10423            let filename = positional.first().map(|s| *s).unwrap_or("~/.zsh_history");
10424            let path = if filename.starts_with("~/") {
10425                dirs::home_dir()
10426                    .map(|h| h.join(&filename[2..]))
10427                    .unwrap_or_else(|| std::path::PathBuf::from(filename))
10428            } else {
10429                std::path::PathBuf::from(filename)
10430            };
10431
10432            if read_file {
10433                // Read plain text history file and import
10434                if let Ok(contents) = std::fs::read_to_string(&path) {
10435                    for line in contents.lines() {
10436                        if !line.is_empty() && !line.starts_with('#') && !line.starts_with(':') {
10437                            let _ = engine.add(line, None);
10438                        }
10439                    }
10440                } else {
10441                    eprintln!("fc: cannot read {}", path.display());
10442                    return 1;
10443                }
10444            } else if write_file || append_file {
10445                // Export history to plain text file
10446                let mode = if append_file {
10447                    std::fs::OpenOptions::new()
10448                        .create(true)
10449                        .append(true)
10450                        .open(&path)
10451                } else {
10452                    std::fs::File::create(&path)
10453                };
10454                match mode {
10455                    Ok(mut file) => {
10456                        use std::io::Write;
10457                        if let Ok(entries) = engine.recent(10000) {
10458                            for entry in entries.iter().rev() {
10459                                let _ = writeln!(file, ": {}:0;{}", entry.timestamp, entry.command);
10460                            }
10461                        }
10462                    }
10463                    Err(e) => {
10464                        eprintln!("fc: cannot write {}: {}", path.display(), e);
10465                        return 1;
10466                    }
10467                }
10468            }
10469            return 0;
10470        }
10471
10472        // List mode (fc -l)
10473        if list_mode || args.is_empty() {
10474            let (first, last) = match positional.len() {
10475                0 => (-16i64, -1i64),
10476                1 => {
10477                    let n = positional[0].parse::<i64>().unwrap_or(-16);
10478                    (n, -1)
10479                }
10480                _ => {
10481                    let f = positional[0].parse::<i64>().unwrap_or(-16);
10482                    let l = positional[1].parse::<i64>().unwrap_or(-1);
10483                    (f, l)
10484                }
10485            };
10486
10487            let count = if first < 0 { (-first) as usize } else { 16 };
10488            match engine.recent(count.max(100)) {
10489                Ok(mut entries) => {
10490                    if reverse {
10491                        entries.reverse();
10492                    }
10493                    for entry in entries.iter().rev().take(count) {
10494                        if no_numbers {
10495                            println!("{}", entry.command);
10496                        } else if show_time {
10497                            println!(
10498                                "{:>6}  {:>10}  {}",
10499                                entry.id, entry.timestamp, entry.command
10500                            );
10501                        } else if show_duration {
10502                            println!(
10503                                "{:>6}  {:>5}  {}",
10504                                entry.id,
10505                                entry.duration_ms.unwrap_or(0),
10506                                entry.command
10507                            );
10508                        } else {
10509                            println!("{:>6}  {}", entry.id, entry.command);
10510                        }
10511                    }
10512                    0
10513                }
10514                Err(e) => {
10515                    eprintln!("fc: {}", e);
10516                    1
10517                }
10518            }
10519        } else if substitute_mode || !substitutions.is_empty() {
10520            // Substitution mode: fc -s old=new
10521            match engine.get_by_offset(0) {
10522                Ok(Some(entry)) => {
10523                    let mut cmd = entry.command.clone();
10524                    for (old, new) in &substitutions {
10525                        cmd = cmd.replace(old, new);
10526                    }
10527                    println!("{}", cmd);
10528                    self.execute_script(&cmd).unwrap_or(1)
10529                }
10530                Ok(None) => {
10531                    eprintln!("fc: no command to re-execute");
10532                    1
10533                }
10534                Err(e) => {
10535                    eprintln!("fc: {}", e);
10536                    1
10537                }
10538            }
10539        } else if editor.as_deref() == Some("-") {
10540            // fc -e -: re-execute last command without editor
10541            match engine.get_by_offset(0) {
10542                Ok(Some(entry)) => {
10543                    println!("{}", entry.command);
10544                    self.execute_script(&entry.command).unwrap_or(1)
10545                }
10546                Ok(None) => {
10547                    eprintln!("fc: no command to re-execute");
10548                    1
10549                }
10550                Err(e) => {
10551                    eprintln!("fc: {}", e);
10552                    1
10553                }
10554            }
10555        } else if let Some(arg) = positional.first() {
10556            if arg.starts_with('-') || arg.starts_with('+') {
10557                // fc -N or fc +N: re-execute Nth command
10558                let n: usize = arg[1..].parse().unwrap_or(1);
10559                let offset = if arg.starts_with('-') { n - 1 } else { n };
10560                match engine.get_by_offset(offset) {
10561                    Ok(Some(entry)) => {
10562                        println!("{}", entry.command);
10563                        self.execute_script(&entry.command).unwrap_or(1)
10564                    }
10565                    Ok(None) => {
10566                        eprintln!("fc: event not found");
10567                        1
10568                    }
10569                    Err(e) => {
10570                        eprintln!("fc: {}", e);
10571                        1
10572                    }
10573                }
10574            } else {
10575                // Try to find command by prefix
10576                match engine.search_prefix(arg, 1) {
10577                    Ok(entries) if !entries.is_empty() => {
10578                        println!("{}", entries[0].command);
10579                        self.execute_script(&entries[0].command).unwrap_or(1)
10580                    }
10581                    Ok(_) => {
10582                        eprintln!("fc: event not found: {}", arg);
10583                        1
10584                    }
10585                    Err(e) => {
10586                        eprintln!("fc: {}", e);
10587                        1
10588                    }
10589                }
10590            }
10591        } else {
10592            // Default: edit and execute last command
10593            match engine.get_by_offset(0) {
10594                Ok(Some(entry)) => {
10595                    println!("{}", entry.command);
10596                    self.execute_script(&entry.command).unwrap_or(1)
10597                }
10598                Ok(None) => {
10599                    eprintln!("fc: no command to re-execute");
10600                    1
10601                }
10602                Err(e) => {
10603                    eprintln!("fc: {}", e);
10604                    1
10605                }
10606            }
10607        }
10608    }
10609
10610    fn builtin_trap(&mut self, args: &[String]) -> i32 {
10611        if args.is_empty() {
10612            // List all traps
10613            for (sig, action) in &self.traps {
10614                println!("trap -- '{}' {}", action, sig);
10615            }
10616            return 0;
10617        }
10618
10619        // trap -l: list signal names
10620        if args.len() == 1 && args[0] == "-l" {
10621            let signals = [
10622                "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
10623                "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN",
10624                "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS",
10625            ];
10626            for (i, sig) in signals.iter().enumerate() {
10627                print!("{:2}) SIG{:<8}", i + 1, sig);
10628                if (i + 1) % 5 == 0 {
10629                    println!();
10630                }
10631            }
10632            println!();
10633            return 0;
10634        }
10635
10636        // trap -p [sigspec...]: print trap commands
10637        if args.len() >= 1 && args[0] == "-p" {
10638            let signals = if args.len() > 1 {
10639                &args[1..]
10640            } else {
10641                &[] as &[String]
10642            };
10643            if signals.is_empty() {
10644                for (sig, action) in &self.traps {
10645                    println!("trap -- '{}' {}", action, sig);
10646                }
10647            } else {
10648                for sig in signals {
10649                    if let Some(action) = self.traps.get(sig) {
10650                        println!("trap -- '{}' {}", action, sig);
10651                    }
10652                }
10653            }
10654            return 0;
10655        }
10656
10657        // trap '' signal: reset to default
10658        // trap action signal...: set trap
10659        // trap signal: print current action for signal
10660        if args.len() == 1 {
10661            // Print trap for this signal
10662            let sig = &args[0];
10663            if let Some(action) = self.traps.get(sig) {
10664                println!("trap -- '{}' {}", action, sig);
10665            }
10666            return 0;
10667        }
10668
10669        let action = &args[0];
10670        let signals = &args[1..];
10671
10672        for sig in signals {
10673            let sig_upper = sig.to_uppercase();
10674            let sig_name = if sig_upper.starts_with("SIG") {
10675                sig_upper[3..].to_string()
10676            } else {
10677                sig_upper.clone()
10678            };
10679
10680            if action.is_empty() || action == "-" {
10681                // Reset to default
10682                self.traps.remove(&sig_name);
10683            } else {
10684                self.traps.insert(sig_name, action.clone());
10685            }
10686        }
10687
10688        0
10689    }
10690
10691    /// Execute trap handlers for a signal
10692    pub fn run_trap(&mut self, signal: &str) {
10693        if let Some(action) = self.traps.get(signal).cloned() {
10694            let _ = self.execute_script(&action);
10695        }
10696    }
10697
10698    fn builtin_alias(&mut self, args: &[String]) -> i32 {
10699        // alias [ {+|-}gmrsL ] [ name[=value] ... ]
10700        // -g: global alias (expanded anywhere in command line)
10701        // -s: suffix alias (file.ext expands to "handler file.ext")
10702        // -r: regular alias (default)
10703        // -m: pattern match mode
10704        // -L: list in form suitable for reinput
10705        // +g/+s/+r: print aliases of that type
10706
10707        let mut is_global = false;
10708        let mut is_suffix = false;
10709        let mut list_form = false;
10710        let mut pattern_match = false;
10711        let mut print_global = false;
10712        let mut print_suffix = false;
10713        let mut print_regular = false;
10714        let mut positional_args = Vec::new();
10715
10716        let mut i = 0;
10717        while i < args.len() {
10718            let arg = &args[i];
10719            if arg.starts_with('+') && arg.len() > 1 {
10720                // +g, +s, +r: print aliases of that type
10721                for ch in arg[1..].chars() {
10722                    match ch {
10723                        'g' => print_global = true,
10724                        's' => print_suffix = true,
10725                        'r' => print_regular = true,
10726                        'L' => list_form = true,
10727                        'm' => pattern_match = true,
10728                        _ => {}
10729                    }
10730                }
10731            } else if arg.starts_with('-') && arg != "-" {
10732                for ch in arg[1..].chars() {
10733                    match ch {
10734                        'g' => is_global = true,
10735                        's' => is_suffix = true,
10736                        'L' => list_form = true,
10737                        'm' => pattern_match = true,
10738                        'r' => {} // regular alias (default)
10739                        _ => {
10740                            eprintln!("zshrs: alias: bad option: -{}", ch);
10741                            return 1;
10742                        }
10743                    }
10744                }
10745            } else {
10746                positional_args.push(arg.clone());
10747            }
10748            i += 1;
10749        }
10750
10751        // If +g/+s/+r used, list those types
10752        if print_global || print_suffix || print_regular {
10753            if print_regular {
10754                for (name, value) in &self.aliases {
10755                    if list_form {
10756                        println!("alias {}='{}'", name, value);
10757                    } else {
10758                        println!("{}='{}'", name, value);
10759                    }
10760                }
10761            }
10762            if print_global {
10763                for (name, value) in &self.global_aliases {
10764                    if list_form {
10765                        println!("alias -g {}='{}'", name, value);
10766                    } else {
10767                        println!("{}='{}'", name, value);
10768                    }
10769                }
10770            }
10771            if print_suffix {
10772                for (name, value) in &self.suffix_aliases {
10773                    if list_form {
10774                        println!("alias -s {}='{}'", name, value);
10775                    } else {
10776                        println!("{}='{}'", name, value);
10777                    }
10778                }
10779            }
10780            return 0;
10781        }
10782
10783        if positional_args.is_empty() {
10784            // List aliases
10785            let prefix = if is_suffix {
10786                "alias -s "
10787            } else if is_global {
10788                "alias -g "
10789            } else {
10790                "alias "
10791            };
10792            let alias_map: Vec<(String, String)> = if is_suffix {
10793                self.suffix_aliases
10794                    .iter()
10795                    .map(|(k, v)| (k.clone(), v.clone()))
10796                    .collect()
10797            } else if is_global {
10798                self.global_aliases
10799                    .iter()
10800                    .map(|(k, v)| (k.clone(), v.clone()))
10801                    .collect()
10802            } else {
10803                self.aliases
10804                    .iter()
10805                    .map(|(k, v)| (k.clone(), v.clone()))
10806                    .collect()
10807            };
10808            for (name, value) in alias_map {
10809                if list_form {
10810                    println!("{}{}='{}'", prefix, name, value);
10811                } else {
10812                    println!("{}='{}'", name, value);
10813                }
10814            }
10815            return 0;
10816        }
10817
10818        for arg in &positional_args {
10819            if let Some(eq_pos) = arg.find('=') {
10820                // Define alias: name=value
10821                let name = &arg[..eq_pos];
10822                let value = &arg[eq_pos + 1..];
10823                if is_suffix {
10824                    self.suffix_aliases
10825                        .insert(name.to_string(), value.to_string());
10826                } else if is_global {
10827                    self.global_aliases
10828                        .insert(name.to_string(), value.to_string());
10829                } else {
10830                    self.aliases.insert(name.to_string(), value.to_string());
10831                }
10832            } else if pattern_match {
10833                // -m: pattern match mode - list matching aliases
10834                let pattern = arg.replace("*", ".*").replace("?", ".");
10835                let re = regex::Regex::new(&format!("^{}$", pattern));
10836
10837                let alias_map: &HashMap<String, String> = if is_suffix {
10838                    &self.suffix_aliases
10839                } else if is_global {
10840                    &self.global_aliases
10841                } else {
10842                    &self.aliases
10843                };
10844
10845                let prefix = if is_suffix {
10846                    "alias -s "
10847                } else if is_global {
10848                    "alias -g "
10849                } else {
10850                    "alias "
10851                };
10852
10853                for (name, value) in alias_map {
10854                    let matches = if let Ok(ref r) = re {
10855                        r.is_match(name)
10856                    } else {
10857                        name.contains(arg.as_str())
10858                    };
10859                    if matches {
10860                        if list_form {
10861                            println!("{}{}='{}'", prefix, name, value);
10862                        } else {
10863                            println!("{}='{}'", name, value);
10864                        }
10865                    }
10866                }
10867            } else {
10868                // Print alias - look up directly without holding borrow
10869                let value = if is_suffix {
10870                    self.suffix_aliases.get(arg.as_str()).cloned()
10871                } else if is_global {
10872                    self.global_aliases.get(arg.as_str()).cloned()
10873                } else {
10874                    self.aliases.get(arg.as_str()).cloned()
10875                };
10876                if let Some(v) = value {
10877                    println!("{}='{}'", arg, v);
10878                } else {
10879                    eprintln!("zshrs: alias: {}: not found", arg);
10880                    return 1;
10881                }
10882            }
10883        }
10884        0
10885    }
10886
10887    fn builtin_unalias(&mut self, args: &[String]) -> i32 {
10888        if args.is_empty() {
10889            eprintln!("zshrs: unalias: usage: unalias [-agsm] name [name ...]");
10890            return 1;
10891        }
10892
10893        let mut is_global = false;
10894        let mut is_suffix = false;
10895        let mut remove_all = false;
10896        let mut positional_args = Vec::new();
10897
10898        for arg in args {
10899            if arg.starts_with('-') && arg != "-" {
10900                for ch in arg[1..].chars() {
10901                    match ch {
10902                        'a' => remove_all = true,
10903                        'g' => is_global = true,
10904                        's' => is_suffix = true,
10905                        'm' => {} // pattern match, ignore for now
10906                        _ => {
10907                            eprintln!("zshrs: unalias: bad option: -{}", ch);
10908                            return 1;
10909                        }
10910                    }
10911                }
10912            } else {
10913                positional_args.push(arg.clone());
10914            }
10915        }
10916
10917        if remove_all {
10918            if is_suffix {
10919                self.suffix_aliases.clear();
10920            } else if is_global {
10921                self.global_aliases.clear();
10922            } else {
10923                // -a without -g/-s clears all three
10924                self.aliases.clear();
10925                self.global_aliases.clear();
10926                self.suffix_aliases.clear();
10927            }
10928            return 0;
10929        }
10930
10931        if positional_args.is_empty() {
10932            eprintln!("zshrs: unalias: usage: unalias [-agsm] name [name ...]");
10933            return 1;
10934        }
10935
10936        for name in positional_args {
10937            let removed = if is_suffix {
10938                self.suffix_aliases.remove(&name).is_some()
10939            } else if is_global {
10940                self.global_aliases.remove(&name).is_some()
10941            } else {
10942                self.aliases.remove(&name).is_some()
10943            };
10944            if !removed {
10945                eprintln!("zshrs: unalias: {}: not found", name);
10946                return 1;
10947            }
10948        }
10949        0
10950    }
10951
10952    fn builtin_set(&mut self, args: &[String]) -> i32 {
10953        if args.is_empty() {
10954            // List all variables and their values (zsh behavior)
10955            let mut vars: Vec<_> = self.variables.iter().collect();
10956            vars.sort_by_key(|(k, _)| *k);
10957            for (k, v) in vars {
10958                println!("{}={}", k, shell_quote(v));
10959            }
10960            // Also print arrays
10961            let mut arrs: Vec<_> = self.arrays.iter().collect();
10962            arrs.sort_by_key(|(k, _)| *k);
10963            for (k, v) in arrs {
10964                let quoted: Vec<String> = v.iter().map(|s| shell_quote(s)).collect();
10965                println!("{}=( {} )", k, quoted.join(" "));
10966            }
10967            return 0;
10968        }
10969
10970        // Check for "+" alone - print just variable names
10971        if args.len() == 1 && args[0] == "+" {
10972            let mut names: Vec<_> = self.variables.keys().collect();
10973            names.extend(self.arrays.keys());
10974            names.sort();
10975            names.dedup();
10976            for name in names {
10977                println!("{}", name);
10978            }
10979            return 0;
10980        }
10981
10982        let mut iter = args.iter().peekable();
10983        let mut set_array: Option<bool> = None; // Some(true) = -A, Some(false) = +A
10984        let mut array_name: Option<String> = None;
10985        let mut sort_asc = false;
10986        let mut sort_desc = false;
10987
10988        while let Some(arg) = iter.next() {
10989            match arg.as_str() {
10990                "-o" => {
10991                    // -o with no arg: print all options in "option on/off" format
10992                    if iter.peek().is_none()
10993                        || iter
10994                            .peek()
10995                            .map(|s| s.starts_with('-') || s.starts_with('+'))
10996                            .unwrap_or(false)
10997                    {
10998                        self.print_options_table();
10999                        continue;
11000                    }
11001                    if let Some(opt) = iter.next() {
11002                        let (name, enable) = Self::normalize_option_name(opt);
11003                        self.options.insert(name, enable);
11004                    }
11005                }
11006                "+o" => {
11007                    // +o with no arg: print options in re-entrant format
11008                    if iter.peek().is_none()
11009                        || iter
11010                            .peek()
11011                            .map(|s| s.starts_with('-') || s.starts_with('+'))
11012                            .unwrap_or(false)
11013                    {
11014                        self.print_options_reentrant();
11015                        continue;
11016                    }
11017                    if let Some(opt) = iter.next() {
11018                        let (name, enable) = Self::normalize_option_name(opt);
11019                        self.options.insert(name, !enable);
11020                    }
11021                }
11022                "-A" => {
11023                    set_array = Some(true);
11024                    if let Some(name) = iter.next() {
11025                        if !name.starts_with('-') && !name.starts_with('+') {
11026                            array_name = Some(name.clone());
11027                        }
11028                    }
11029                    if array_name.is_none() {
11030                        // Print all arrays with values
11031                        let mut arrs: Vec<_> = self.arrays.iter().collect();
11032                        arrs.sort_by_key(|(k, _)| *k);
11033                        for (k, v) in arrs {
11034                            let quoted: Vec<String> = v.iter().map(|s| shell_quote(s)).collect();
11035                            println!("{}=( {} )", k, quoted.join(" "));
11036                        }
11037                        return 0;
11038                    }
11039                }
11040                "+A" => {
11041                    set_array = Some(false);
11042                    if let Some(name) = iter.next() {
11043                        if !name.starts_with('-') && !name.starts_with('+') {
11044                            array_name = Some(name.clone());
11045                        }
11046                    }
11047                    if array_name.is_none() {
11048                        // Print array names only
11049                        let mut names: Vec<_> = self.arrays.keys().collect();
11050                        names.sort();
11051                        for name in names {
11052                            println!("{}", name);
11053                        }
11054                        return 0;
11055                    }
11056                }
11057                "-s" => sort_asc = true,
11058                "+s" => sort_desc = true,
11059                "-e" => {
11060                    self.options.insert("errexit".to_string(), true);
11061                }
11062                "+e" => {
11063                    self.options.insert("errexit".to_string(), false);
11064                }
11065                "-x" => {
11066                    self.options.insert("xtrace".to_string(), true);
11067                }
11068                "+x" => {
11069                    self.options.insert("xtrace".to_string(), false);
11070                }
11071                "-u" => {
11072                    self.options.insert("nounset".to_string(), true);
11073                }
11074                "+u" => {
11075                    self.options.insert("nounset".to_string(), false);
11076                }
11077                "-v" => {
11078                    self.options.insert("verbose".to_string(), true);
11079                }
11080                "+v" => {
11081                    self.options.insert("verbose".to_string(), false);
11082                }
11083                "-n" => {
11084                    self.options.insert("exec".to_string(), false);
11085                }
11086                "+n" => {
11087                    self.options.insert("exec".to_string(), true);
11088                }
11089                "-f" => {
11090                    self.options.insert("glob".to_string(), false);
11091                }
11092                "+f" => {
11093                    self.options.insert("glob".to_string(), true);
11094                }
11095                "-m" => {
11096                    self.options.insert("monitor".to_string(), true);
11097                }
11098                "+m" => {
11099                    self.options.insert("monitor".to_string(), false);
11100                }
11101                "-C" => {
11102                    self.options.insert("clobber".to_string(), false);
11103                }
11104                "+C" => {
11105                    self.options.insert("clobber".to_string(), true);
11106                }
11107                "-b" => {
11108                    self.options.insert("notify".to_string(), true);
11109                }
11110                "+b" => {
11111                    self.options.insert("notify".to_string(), false);
11112                }
11113                "--" => {
11114                    let remaining: Vec<String> = iter.cloned().collect();
11115                    if let Some(ref name) = array_name {
11116                        let mut values = remaining;
11117                        if sort_asc {
11118                            values.sort();
11119                        } else if sort_desc {
11120                            values.sort();
11121                            values.reverse();
11122                        }
11123                        if set_array == Some(true) {
11124                            self.arrays.insert(name.clone(), values);
11125                        } else {
11126                            // +A: replace initial elements
11127                            let arr = self.arrays.entry(name.clone()).or_default();
11128                            for (i, v) in values.into_iter().enumerate() {
11129                                if i < arr.len() {
11130                                    arr[i] = v;
11131                                } else {
11132                                    arr.push(v);
11133                                }
11134                            }
11135                        }
11136                    } else if remaining.is_empty() {
11137                        // "set --" with nothing after unsets positional params
11138                        self.positional_params.clear();
11139                    } else {
11140                        let mut values = remaining;
11141                        if sort_asc {
11142                            values.sort();
11143                        } else if sort_desc {
11144                            values.sort();
11145                            values.reverse();
11146                        }
11147                        self.positional_params = values;
11148                    }
11149                    return 0;
11150                }
11151                _ => {
11152                    // Handle single-letter options like -ex (multiple options)
11153                    if arg.starts_with('-') && arg.len() > 1 {
11154                        for c in arg[1..].chars() {
11155                            match c {
11156                                'e' => {
11157                                    self.options.insert("errexit".to_string(), true);
11158                                }
11159                                'x' => {
11160                                    self.options.insert("xtrace".to_string(), true);
11161                                }
11162                                'u' => {
11163                                    self.options.insert("nounset".to_string(), true);
11164                                }
11165                                'v' => {
11166                                    self.options.insert("verbose".to_string(), true);
11167                                }
11168                                'n' => {
11169                                    self.options.insert("exec".to_string(), false);
11170                                }
11171                                'f' => {
11172                                    self.options.insert("glob".to_string(), false);
11173                                }
11174                                'm' => {
11175                                    self.options.insert("monitor".to_string(), true);
11176                                }
11177                                'C' => {
11178                                    self.options.insert("clobber".to_string(), false);
11179                                }
11180                                'b' => {
11181                                    self.options.insert("notify".to_string(), true);
11182                                }
11183                                _ => {
11184                                    eprintln!("zshrs: set: -{}: invalid option", c);
11185                                    return 1;
11186                                }
11187                            }
11188                        }
11189                        continue;
11190                    }
11191                    if arg.starts_with('+') && arg.len() > 1 {
11192                        for c in arg[1..].chars() {
11193                            match c {
11194                                'e' => {
11195                                    self.options.insert("errexit".to_string(), false);
11196                                }
11197                                'x' => {
11198                                    self.options.insert("xtrace".to_string(), false);
11199                                }
11200                                'u' => {
11201                                    self.options.insert("nounset".to_string(), false);
11202                                }
11203                                'v' => {
11204                                    self.options.insert("verbose".to_string(), false);
11205                                }
11206                                'n' => {
11207                                    self.options.insert("exec".to_string(), true);
11208                                }
11209                                'f' => {
11210                                    self.options.insert("glob".to_string(), true);
11211                                }
11212                                'm' => {
11213                                    self.options.insert("monitor".to_string(), false);
11214                                }
11215                                'C' => {
11216                                    self.options.insert("clobber".to_string(), true);
11217                                }
11218                                'b' => {
11219                                    self.options.insert("notify".to_string(), false);
11220                                }
11221                                _ => {
11222                                    eprintln!("zshrs: set: +{}: invalid option", c);
11223                                    return 1;
11224                                }
11225                            }
11226                        }
11227                        continue;
11228                    }
11229                    // Treat as positional params
11230                    let mut values: Vec<String> =
11231                        std::iter::once(arg.clone()).chain(iter.cloned()).collect();
11232                    if sort_asc {
11233                        values.sort();
11234                    } else if sort_desc {
11235                        values.sort();
11236                        values.reverse();
11237                    }
11238                    if let Some(ref name) = array_name {
11239                        if set_array == Some(true) {
11240                            self.arrays.insert(name.clone(), values);
11241                        } else {
11242                            let arr = self.arrays.entry(name.clone()).or_default();
11243                            for (i, v) in values.into_iter().enumerate() {
11244                                if i < arr.len() {
11245                                    arr[i] = v;
11246                                } else {
11247                                    arr.push(v);
11248                                }
11249                            }
11250                        }
11251                    } else {
11252                        self.positional_params = values;
11253                    }
11254                    return 0;
11255                }
11256            }
11257        }
11258        0
11259    }
11260
11261    fn default_on_options() -> &'static [&'static str] {
11262        &[
11263            "aliases",
11264            "alwayslastprompt",
11265            "appendhistory",
11266            "autolist",
11267            "automenu",
11268            "autoparamkeys",
11269            "autoparamslash",
11270            "autoremoveslash",
11271            "badpattern",
11272            "banghist",
11273            "bareglobqual",
11274            "beep",
11275            "bgnice",
11276            "caseglob",
11277            "casematch",
11278            "checkjobs",
11279            "checkrunningjobs",
11280            "clobber",
11281            "debugbeforecmd",
11282            "equals",
11283            "evallineno",
11284            "exec",
11285            "flowcontrol",
11286            "functionargzero",
11287            "glob",
11288            "globalexport",
11289            "globalrcs",
11290            "hashcmds",
11291            "hashdirs",
11292            "hashlistall",
11293            "histbeep",
11294            "histsavebycopy",
11295            "hup",
11296            "interactive",
11297            "listambiguous",
11298            "listbeep",
11299            "listtypes",
11300            "monitor",
11301            "multibyte",
11302            "multifuncdef",
11303            "multios",
11304            "nomatch",
11305            "notify",
11306            "promptcr",
11307            "promptpercent",
11308            "promptsp",
11309            "rcs",
11310            "shinstdin",
11311            "shortloops",
11312            "unset",
11313            "zle",
11314        ]
11315    }
11316
11317    fn print_options_table(&self) {
11318        let mut opts: Vec<_> = Self::all_zsh_options().to_vec();
11319        opts.sort();
11320        let defaults_on = Self::default_on_options();
11321        for &opt in &opts {
11322            let enabled = self.options.get(opt).copied().unwrap_or(false);
11323            let is_default_on = defaults_on.contains(&opt);
11324            // zsh format: for default-ON options, show "noOPTION off" when on, "noOPTION on" when off
11325            // for default-OFF options, show "OPTION off" when off, "OPTION on" when on
11326            let (display_name, display_state) = if is_default_on {
11327                (format!("no{}", opt), if enabled { "off" } else { "on" })
11328            } else {
11329                (opt.to_string(), if enabled { "on" } else { "off" })
11330            };
11331            println!("{:<22}{}", display_name, display_state);
11332        }
11333    }
11334
11335    fn print_options_reentrant(&self) {
11336        let mut opts: Vec<_> = Self::all_zsh_options().to_vec();
11337        opts.sort();
11338        let defaults_on = Self::default_on_options();
11339        for &opt in &opts {
11340            let enabled = self.options.get(opt).copied().unwrap_or(false);
11341            let is_default_on = defaults_on.contains(&opt);
11342            // zsh format: use noOPTION for default-on options
11343            let (display_name, use_minus) = if is_default_on {
11344                (format!("no{}", opt), !enabled)
11345            } else {
11346                (opt.to_string(), enabled)
11347            };
11348            if use_minus {
11349                println!("set -o {}", display_name);
11350            } else {
11351                println!("set +o {}", display_name);
11352            }
11353        }
11354    }
11355
11356    /// caller - display call stack (bash)
11357    fn builtin_caller(&self, args: &[String]) -> i32 {
11358        let depth: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
11359        // In a real implementation, we'd track the call stack
11360        // For now, show basic info
11361        if depth == 0 {
11362            println!("1 main");
11363        } else {
11364            println!("{} main", depth);
11365        }
11366        0
11367    }
11368
11369    /// doctor - diagnostic report of shell health, caches, and performance
11370    fn builtin_doctor(&self, _args: &[String]) -> i32 {
11371        let green = |s: &str| format!("\x1b[32m{}\x1b[0m", s);
11372        let red = |s: &str| format!("\x1b[31m{}\x1b[0m", s);
11373        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
11374        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
11375        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
11376
11377        println!("{}", bold("zshrs doctor"));
11378        println!("{}", dim(&"=".repeat(60)));
11379        println!();
11380
11381        // --- Environment ---
11382        println!("{}", bold("Environment"));
11383        println!("  version:    zshrs {}", env!("CARGO_PKG_VERSION"));
11384        println!("  pid:        {}", std::process::id());
11385        let cwd = env::current_dir()
11386            .map(|p| p.to_string_lossy().to_string())
11387            .unwrap_or_else(|_| "?".to_string());
11388        println!("  cwd:        {}", cwd);
11389        println!("  shell:      {}", env::var("SHELL").unwrap_or_else(|_| "?".to_string()));
11390        println!("  pool size:  {}", self.worker_pool.size());
11391        println!("  pool done:  {} tasks completed", self.worker_pool.completed());
11392        println!("  pool queue: {} pending", self.worker_pool.queue_depth());
11393        println!();
11394
11395        // --- Config ---
11396        println!("{}", bold("Config"));
11397        let config_path = crate::config::config_path();
11398        if config_path.exists() {
11399            println!("  {}  {}", green("*"), config_path.display());
11400        } else {
11401            println!("  {}  {} {}", dim("-"), config_path.display(), dim("(using defaults)"));
11402        }
11403        println!();
11404
11405        // --- PATH ---
11406        println!("{}", bold("PATH"));
11407        let path_var = env::var("PATH").unwrap_or_default();
11408        let path_dirs: Vec<&str> = path_var.split(':').filter(|s| !s.is_empty()).collect();
11409        let path_ok = path_dirs.iter().filter(|d| std::path::Path::new(d).is_dir()).count();
11410        let path_missing = path_dirs.len() - path_ok;
11411        println!("  directories: {} total, {} {}, {} {}",
11412            path_dirs.len(),
11413            path_ok, green("valid"),
11414            path_missing, if path_missing > 0 { red("missing") } else { green("missing") },
11415        );
11416        println!("  hash table:  {} entries", self.command_hash.len());
11417        println!();
11418
11419        // --- FPATH ---
11420        println!("{}", bold("FPATH"));
11421        println!("  directories: {}", self.fpath.len());
11422        let fpath_ok = self.fpath.iter().filter(|d| d.is_dir()).count();
11423        let fpath_missing = self.fpath.len() - fpath_ok;
11424        if fpath_missing > 0 {
11425            println!("  {} {} missing fpath directories", red("!"), fpath_missing);
11426        }
11427        println!("  functions:   {} loaded", self.functions.len());
11428        println!("  autoload:    {} pending", self.autoload_pending.len());
11429        println!();
11430
11431        // --- SQLite Caches ---
11432        println!("{}", bold("SQLite Caches"));
11433        if let Some(ref engine) = self.history {
11434            let count = engine.count().unwrap_or(0);
11435            println!("  history:     {} entries  {}", count, green("OK"));
11436        } else {
11437            println!("  history:     {}", yellow("not initialized"));
11438        }
11439
11440        if let Some(ref cache) = self.compsys_cache {
11441            let count = compsys::cache_entry_count(cache);
11442            println!("  compsys:     {} completions  {}", count, green("OK"));
11443
11444            // Check bytecode blob coverage
11445            if let Ok(missing) = cache.get_autoloads_missing_bytecode() {
11446                if missing.is_empty() {
11447                    println!("  bytecode cache:   {}", green("all functions compiled to bytecode"));
11448                } else {
11449                    println!("  bytecode cache:   {} functions {}", missing.len(), yellow("missing bytecode blobs"));
11450                }
11451            }
11452        } else {
11453            println!("  compsys:     {}", yellow("no cache"));
11454        }
11455
11456        if let Some(ref cache) = self.plugin_cache {
11457            let (plugins, functions) = cache.stats();
11458            println!("  plugins:     {} plugins, {} cached functions  {}", plugins, functions, green("OK"));
11459        } else {
11460            println!("  plugins:     {}", yellow("no cache"));
11461        }
11462        println!();
11463
11464        // --- Shell State ---
11465        println!("{}", bold("Shell State"));
11466        println!("  aliases:     {}", self.aliases.len());
11467        println!("  global:      {} aliases", self.global_aliases.len());
11468        println!("  suffix:      {} aliases", self.suffix_aliases.len());
11469        println!("  variables:   {}", self.variables.len());
11470        println!("  arrays:      {}", self.arrays.len());
11471        println!("  assoc:       {}", self.assoc_arrays.len());
11472        println!("  options:     {} set", self.options.iter().filter(|(_, v)| **v).count());
11473        println!("  traps:       {} active", self.traps.len());
11474        println!("  hooks:       {} registered", self.hook_functions.values().map(|v| v.len()).sum::<usize>());
11475        println!();
11476
11477        // --- Log ---
11478        println!("{}", bold("Log"));
11479        let log_path = crate::log::log_path();
11480        if log_path.exists() {
11481            let size = std::fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
11482            println!("  {}  {} bytes", log_path.display(), size);
11483        } else {
11484            println!("  {}", dim("no log file yet"));
11485        }
11486        println!();
11487
11488        // --- Profiling ---
11489        println!("{}", bold("Profiling"));
11490        println!("  chrome tracing: {}", if crate::log::profiling_enabled() { green("enabled") } else { dim("disabled") });
11491        println!("  flamegraph:     {}", if crate::log::flamegraph_enabled() { green("enabled") } else { dim("disabled") });
11492        println!("  prometheus:     {}", if crate::log::prometheus_enabled() { green("enabled") } else { dim("disabled") });
11493        println!();
11494
11495        0
11496    }
11497
11498    /// dbview — browse zshrs SQLite cache tables without SQL.
11499    ///
11500    /// Usage:
11501    ///   dbview                      — list all tables and row counts
11502    ///   dbview autoloads             — dump autoloads table (name, source, body len, ast len)
11503    ///   dbview autoloads _git        — show single row by name
11504    ///   dbview comps                 — dump comps table
11505    ///   dbview history               — recent history entries
11506    ///   dbview history <pattern>     — search history
11507    ///   dbview plugins               — plugin cache entries
11508    ///   dbview executables            — PATH executables cache
11509    ///   dbview <table> --count       — just the count
11510    fn builtin_dbview(&self, args: &[String]) -> i32 {
11511        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
11512        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
11513        let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
11514        let green = |s: &str| format!("\x1b[32m{}\x1b[0m", s);
11515        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
11516
11517        if args.is_empty() {
11518            // List all tables with row counts
11519            println!("{}", bold("zshrs SQLite caches"));
11520            println!();
11521
11522            if let Some(ref cache) = self.compsys_cache {
11523                println!("  {} {}", bold("compsys.db"), dim("(completion cache)"));
11524                if let Ok(n) = cache.count_table("autoloads") {
11525                    let bc_count = cache.count_table_where("autoloads", "bytecode IS NOT NULL").unwrap_or(0);
11526                    println!("    autoloads:    {:>6} rows  ({} compiled)", n, bc_count);
11527                }
11528                if let Ok(n) = cache.count_table("comps") { println!("    comps:        {:>6} rows", n); }
11529                if let Ok(n) = cache.count_table("services") { println!("    services:     {:>6} rows", n); }
11530                if let Ok(n) = cache.count_table("patcomps") { println!("    patcomps:     {:>6} rows", n); }
11531                if let Ok(n) = cache.count_table("executables") { println!("    executables:  {:>6} rows", n); }
11532                if let Ok(n) = cache.count_table("zstyles") { println!("    zstyles:      {:>6} rows", n); }
11533                println!();
11534            }
11535
11536            if let Some(ref engine) = self.history {
11537                println!("  {} {}", bold("history.db"), dim("(command history)"));
11538                if let Ok(n) = engine.count() { println!("    entries:      {:>6} rows", n); }
11539                println!();
11540            }
11541
11542            if let Some(ref cache) = self.plugin_cache {
11543                let (plugins, functions) = cache.stats();
11544                println!("  {} {}", bold("plugins.db"), dim("(plugin source cache)"));
11545                println!("    plugins:      {:>6} rows", plugins);
11546                println!("    functions:    {:>6} rows", functions);
11547                println!();
11548            }
11549
11550            println!("  Usage: {} <table> [name] [--count]", cyan("dbview"));
11551            return 0;
11552        }
11553
11554        let table = args[0].as_str();
11555        let filter = args.get(1).map(|s| s.as_str());
11556        let count_only = args.iter().any(|a| a == "--count" || a == "-c");
11557
11558        match table {
11559            "autoloads" => {
11560                let Some(ref cache) = self.compsys_cache else {
11561                    eprintln!("dbview: no compsys cache");
11562                    return 1;
11563                };
11564
11565                if count_only {
11566                    let n = cache.count_table("autoloads").unwrap_or(0);
11567                    println!("{}", n);
11568                    return 0;
11569                }
11570
11571                if let Some(name) = filter {
11572                    // Single row lookup
11573                    match cache.get_autoload(name) {
11574                        Ok(Some(stub)) => {
11575                            println!("{}", bold(&format!("autoload: {}", name)));
11576                            println!("  source:   {}", stub.source);
11577                            println!("  body:     {} bytes", stub.body.as_ref().map(|b| b.len()).unwrap_or(0));
11578                            match cache.get_autoload_bytecode(name) {
11579                                Ok(Some(blob)) => println!("  bytecode: {} {} bytes", green("YES"), blob.len()),
11580                                _ => println!("  bytecode: {}", yellow("NULL")),
11581                            }
11582                            // Show first few lines of body
11583                            if let Some(ref body) = stub.body {
11584                                println!("  preview:");
11585                                for (i, line) in body.lines().take(10).enumerate() {
11586                                    println!("    {:>3}: {}", i + 1, dim(line));
11587                                }
11588                                let total = body.lines().count();
11589                                if total > 10 {
11590                                    println!("    {} ({} more lines)", dim("..."), total - 10);
11591                                }
11592                            }
11593                        }
11594                        _ => {
11595                            eprintln!("dbview: autoload '{}' not found", name);
11596                            return 1;
11597                        }
11598                    }
11599                    return 0;
11600                }
11601
11602                // Dump all autoloads
11603                let conn = &cache.conn();
11604                match conn.prepare("SELECT name, source, length(body), length(bytecode) FROM autoloads ORDER BY name LIMIT 200") {
11605                    Ok(mut stmt) => {
11606                        let rows = stmt.query_map([], |row| {
11607                            Ok((
11608                                row.get::<_, String>(0)?,
11609                                row.get::<_, String>(1)?,
11610                                row.get::<_, Option<i64>>(2)?,
11611                                row.get::<_, Option<i64>>(3)?,
11612                            ))
11613                        });
11614                        if let Ok(rows) = rows {
11615                            println!("{:<40} {:>8} {:>8}  {}", bold("NAME"), bold("BODY"), bold("BYTECODE"), bold("SOURCE"));
11616                            let mut count = 0;
11617                            for row in rows.flatten() {
11618                                let (name, source, body_len, ast_len) = row;
11619                                let ast_str = match ast_len {
11620                                    Some(n) => green(&format!("{:>8}", n)),
11621                                    None => yellow(&format!("{:>8}", "NULL")),
11622                                };
11623                                let body_str = match body_len {
11624                                    Some(n) => format!("{:>8}", n),
11625                                    None => dim("NULL").to_string(),
11626                                };
11627                                // Truncate source path for display
11628                                let src_short = if source.len() > 50 {
11629                                    format!("...{}", &source[source.len() - 47..])
11630                                } else {
11631                                    source
11632                                };
11633                                println!("{:<40} {} {}  {}", name, body_str, ast_str, dim(&src_short));
11634                                count += 1;
11635                            }
11636                            println!("\n{} rows shown (LIMIT 200)", count);
11637                        }
11638                    }
11639                    Err(e) => {
11640                        eprintln!("dbview: query failed: {}", e);
11641                        return 1;
11642                    }
11643                }
11644            }
11645
11646            "comps" => {
11647                let Some(ref cache) = self.compsys_cache else {
11648                    eprintln!("dbview: no compsys cache");
11649                    return 1;
11650                };
11651                if count_only {
11652                    println!("{}", cache.count_table("comps").unwrap_or(0));
11653                    return 0;
11654                }
11655                let conn = cache.conn();
11656                let query = if let Some(pat) = filter {
11657                    format!("SELECT command, function FROM comps WHERE command LIKE '%{}%' ORDER BY command LIMIT 100", pat)
11658                } else {
11659                    "SELECT command, function FROM comps ORDER BY command LIMIT 100".to_string()
11660                };
11661                match conn.prepare(&query) {
11662                    Ok(mut stmt) => {
11663                        println!("{:<40} {}", bold("COMMAND"), bold("FUNCTION"));
11664                        let rows = stmt.query_map([], |row| {
11665                            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
11666                        });
11667                        if let Ok(rows) = rows {
11668                            for row in rows.flatten() {
11669                                println!("{:<40} {}", row.0, cyan(&row.1));
11670                            }
11671                        }
11672                    }
11673                    Err(e) => { eprintln!("dbview: {}", e); return 1; }
11674                }
11675            }
11676
11677            "executables" => {
11678                let Some(ref cache) = self.compsys_cache else {
11679                    eprintln!("dbview: no compsys cache");
11680                    return 1;
11681                };
11682                if count_only {
11683                    println!("{}", cache.count_table("executables").unwrap_or(0));
11684                    return 0;
11685                }
11686                let conn = cache.conn();
11687                let query = if let Some(pat) = filter {
11688                    format!("SELECT name, path FROM executables WHERE name LIKE '%{}%' ORDER BY name LIMIT 100", pat)
11689                } else {
11690                    "SELECT name, path FROM executables ORDER BY name LIMIT 100".to_string()
11691                };
11692                match conn.prepare(&query) {
11693                    Ok(mut stmt) => {
11694                        println!("{:<30} {}", bold("NAME"), bold("PATH"));
11695                        let rows = stmt.query_map([], |row| {
11696                            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
11697                        });
11698                        if let Ok(rows) = rows {
11699                            for row in rows.flatten() {
11700                                println!("{:<30} {}", row.0, dim(&row.1));
11701                            }
11702                        }
11703                    }
11704                    Err(e) => { eprintln!("dbview: {}", e); return 1; }
11705                }
11706            }
11707
11708            "history" => {
11709                let Some(ref engine) = self.history else {
11710                    eprintln!("dbview: no history engine");
11711                    return 1;
11712                };
11713                if count_only {
11714                    println!("{}", engine.count().unwrap_or(0));
11715                    return 0;
11716                }
11717                if let Some(pat) = filter {
11718                    if let Ok(entries) = engine.search(pat, 20) {
11719                        for e in entries {
11720                            println!("  {} {} {}", dim(&e.timestamp.to_string()), cyan(&e.command), dim(&format!("[{}]", e.exit_code.unwrap_or(0))));
11721                        }
11722                    }
11723                } else if let Ok(entries) = engine.recent(20) {
11724                    for e in entries {
11725                        println!("  {} {} {}", dim(&e.timestamp.to_string()), cyan(&e.command), dim(&format!("[{}]", e.exit_code.unwrap_or(0))));
11726                    }
11727                }
11728            }
11729
11730            "plugins" => {
11731                let Some(ref cache) = self.plugin_cache else {
11732                    eprintln!("dbview: no plugin cache");
11733                    return 1;
11734                };
11735                let (plugins, functions) = cache.stats();
11736                println!("{} plugins, {} cached functions", plugins, functions);
11737            }
11738
11739            _ => {
11740                eprintln!("dbview: unknown table '{}'. Available: autoloads, comps, executables, history, plugins", table);
11741                return 1;
11742            }
11743        }
11744
11745        0
11746    }
11747
11748    /// profile — in-process command profiling with nanosecond accuracy.
11749    ///
11750    /// Unlike `time` (which measures one command) or `zprof` (which only
11751    /// profiles function calls), `profile` traces every execute_command,
11752    /// expansion, glob, and builtin dispatch inside the block.
11753    ///
11754    /// Usage:
11755    ///   profile { commands }     — profile a block
11756    ///   profile -s 'script'     — profile a script string
11757    ///   profile -f func         — profile a function call
11758    ///   profile --clear         — clear accumulated profile data
11759    ///   profile --dump          — show accumulated profile data
11760    fn builtin_profile(&mut self, args: &[String]) -> i32 {
11761        let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
11762        let dim = |s: &str| format!("\x1b[2m{}\x1b[0m", s);
11763        let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
11764        let yellow = |s: &str| format!("\x1b[33m{}\x1b[0m", s);
11765
11766        if args.is_empty() {
11767            println!("Usage: profile {{ commands }}");
11768            println!("       profile -s 'script string'");
11769            println!("       profile -f function_name [args...]");
11770            println!("       profile --clear");
11771            println!("       profile --dump");
11772            return 0;
11773        }
11774
11775        if args[0] == "--clear" {
11776            self.profiler = crate::zprof::Profiler::new();
11777            println!("profile data cleared");
11778            return 0;
11779        }
11780
11781        if args[0] == "--dump" {
11782            let (_, output) = crate::zprof::builtin_zprof(
11783                &mut self.profiler,
11784                &crate::zprof::ZprofOptions { clear: false },
11785            );
11786            if !output.is_empty() {
11787                print!("{}", output);
11788            } else {
11789                println!("{}", dim("no profile data"));
11790            }
11791            return 0;
11792        }
11793
11794        // Determine what to profile
11795        let code = if args[0] == "-s" {
11796            // profile -s 'script string'
11797            if args.len() < 2 {
11798                eprintln!("profile: -s requires a script string");
11799                return 1;
11800            }
11801            args[1..].join(" ")
11802        } else if args[0] == "-f" {
11803            // profile -f func_name [args...]
11804            if args.len() < 2 {
11805                eprintln!("profile: -f requires a function name");
11806                return 1;
11807            }
11808            args[1..].join(" ")
11809        } else {
11810            // profile { commands } — args is the block body
11811            args.join(" ")
11812        };
11813
11814        // Enable profiling, run, collect results
11815        let was_enabled = self.profiling_enabled;
11816        self.profiling_enabled = true;
11817        self.profiler = crate::zprof::Profiler::new(); // fresh data for this run
11818
11819        let t0 = std::time::Instant::now();
11820        let result = self.execute_script(&code);
11821        let elapsed = t0.elapsed();
11822        let status = match result {
11823            Ok(s) => s,
11824            Err(e) => {
11825                eprintln!("profile: {}", e);
11826                1
11827            }
11828        };
11829
11830        // Collect timing data
11831        println!();
11832        println!("{}", bold("profile results"));
11833        println!("{}", dim(&"─".repeat(60)));
11834        let dur_str = if elapsed.as_secs() > 0 {
11835            format!("{:.3}s", elapsed.as_secs_f64())
11836        } else if elapsed.as_millis() > 0 {
11837            format!("{:.3}ms", elapsed.as_secs_f64() * 1000.0)
11838        } else {
11839            format!("{:.1}µs", elapsed.as_secs_f64() * 1_000_000.0)
11840        };
11841        println!("  total:     {}", cyan(&dur_str));
11842        println!("  status:    {}", status);
11843        println!();
11844
11845        // Show function-level breakdown from profiler
11846        let (_, output) = crate::zprof::builtin_zprof(
11847            &mut self.profiler,
11848            &crate::zprof::ZprofOptions { clear: false },
11849        );
11850        if !output.is_empty() {
11851            println!("{}", bold("function breakdown"));
11852            print!("{}", output);
11853        }
11854
11855        // Per-command breakdown from tracing (if tracing is at debug level)
11856        println!();
11857        println!("  {} set ZSHRS_LOG=trace for per-command tracing", yellow("tip:"));
11858        println!("  {} output: {}", dim("log"), dim(&crate::log::log_path().display().to_string()));
11859
11860        self.profiling_enabled = was_enabled;
11861        status
11862    }
11863
11864    // ═══════════════════════════════════════════════════════════════════
11865    // AOP INTERCEPT — the killer builtin
11866    // ═══════════════════════════════════════════════════════════════════
11867
11868    /// Check intercepts for a command. Returns Some(result) if an around
11869    /// advice fully handled the command, None to proceed normally.
11870    fn run_intercepts(
11871        &mut self,
11872        cmd_name: &str,
11873        full_cmd: &str,
11874        args: &[String],
11875    ) -> Option<Result<i32, String>> {
11876        // Collect matching intercepts (clone to avoid borrow issues)
11877        let matching: Vec<Intercept> = self
11878            .intercepts
11879            .iter()
11880            .filter(|i| intercept_matches(&i.pattern, cmd_name, full_cmd))
11881            .cloned()
11882            .collect();
11883
11884        if matching.is_empty() {
11885            return None;
11886        }
11887
11888        // Set INTERCEPT_NAME and INTERCEPT_ARGS for advice code
11889        self.variables.insert("INTERCEPT_NAME".to_string(), cmd_name.to_string());
11890        self.variables.insert("INTERCEPT_ARGS".to_string(), args.join(" "));
11891        self.variables.insert("INTERCEPT_CMD".to_string(), full_cmd.to_string());
11892
11893        // Run before advice
11894        for advice in matching.iter().filter(|i| matches!(i.kind, AdviceKind::Before)) {
11895            let _ = self.execute_advice(&advice.code);
11896        }
11897
11898        // Check for around advice — first match wins
11899        let around = matching.iter().find(|i| matches!(i.kind, AdviceKind::Around));
11900
11901        let t0 = std::time::Instant::now();
11902
11903        let result = if let Some(advice) = around {
11904            // Around advice: set INTERCEPT_PROCEED flag, run advice code.
11905            // If advice calls `intercept_proceed`, the original command runs.
11906            self.variables.insert("__intercept_proceed".to_string(), "0".to_string());
11907            let advice_result = self.execute_advice(&advice.code);
11908
11909            // Check if intercept_proceed was called
11910            let proceeded = self.variables.get("__intercept_proceed")
11911                .map(|v| v == "1")
11912                .unwrap_or(false);
11913
11914            if proceeded {
11915                // The original command was already executed inside the advice
11916                advice_result
11917            } else {
11918                // Advice didn't call proceed — command was suppressed
11919                advice_result
11920            }
11921        } else {
11922            // No around advice — run the original command.
11923            // We return None to let the normal dispatch continue.
11924            // But we still need after advice to fire, so we can't return None here
11925            // if there are after advices. Run the command ourselves.
11926            let has_after = matching.iter().any(|i| matches!(i.kind, AdviceKind::After));
11927            if !has_after {
11928                // Only before advice, no after — let normal dispatch continue
11929                return None;
11930            }
11931
11932            // Has after advice — we must run the command and then run after advice
11933            self.run_original_command(cmd_name, args)
11934        };
11935
11936        let elapsed = t0.elapsed();
11937
11938        // Set timing variable for after advice
11939        let ms = elapsed.as_secs_f64() * 1000.0;
11940        self.variables.insert("INTERCEPT_MS".to_string(), format!("{:.3}", ms));
11941        self.variables.insert("INTERCEPT_US".to_string(), format!("{:.0}", ms * 1000.0));
11942
11943        // Run after advice
11944        for advice in matching.iter().filter(|i| matches!(i.kind, AdviceKind::After)) {
11945            let _ = self.execute_advice(&advice.code);
11946        }
11947
11948        // Clean up
11949        self.variables.remove("INTERCEPT_NAME");
11950        self.variables.remove("INTERCEPT_ARGS");
11951        self.variables.remove("INTERCEPT_CMD");
11952        self.variables.remove("INTERCEPT_MS");
11953        self.variables.remove("INTERCEPT_US");
11954        self.variables.remove("__intercept_proceed");
11955
11956        Some(result)
11957    }
11958
11959    /// Execute the original command (used by around/after intercept dispatch).
11960    /// Execute advice code — dispatches @ prefix to stryke (fat binary),
11961    /// everything else to the shell parser. No fork. Machine code speed.
11962    fn execute_advice(&mut self, code: &str) -> Result<i32, String> {
11963        let code = code.trim();
11964        if code.starts_with('@') {
11965            let stryke_code = code.trim_start_matches('@').trim();
11966            if let Some(status) = crate::try_stryke_dispatch(stryke_code) {
11967                self.last_status = status;
11968                return Ok(status);
11969            }
11970            // No stryke handler (thin binary) — fall through to shell
11971        }
11972        self.execute_script(code)
11973    }
11974
11975    fn run_original_command(&mut self, cmd_name: &str, args: &[String]) -> Result<i32, String> {
11976        // Try function
11977        if let Some(func) = self.functions.get(cmd_name).cloned() {
11978            return self.call_function(&func, args);
11979        }
11980        if self.maybe_autoload(cmd_name) {
11981            if let Some(func) = self.functions.get(cmd_name).cloned() {
11982                return self.call_function(&func, args);
11983            }
11984        }
11985        // External command
11986        self.execute_external(cmd_name, &args.to_vec(), &[])
11987    }
11988
11989    /// intercept builtin — register AOP advice on commands.
11990    ///
11991    /// Usage:
11992    ///   intercept before <pattern> { code }
11993    ///   intercept after <pattern> { code }
11994    ///   intercept around <pattern> { code }
11995    ///   intercept list                       — show all intercepts
11996    ///   intercept remove <id>                — remove by ID
11997    ///   intercept clear                      — remove all
11998    fn builtin_intercept(&mut self, args: &[String]) -> i32 {
11999        if args.is_empty() {
12000            println!("Usage: intercept <before|after|around> <pattern> {{ code }}");
12001            println!("       intercept list | remove <id> | clear");
12002            return 0;
12003        }
12004
12005        match args[0].as_str() {
12006            "list" => {
12007                if self.intercepts.is_empty() {
12008                    println!("no intercepts registered");
12009                } else {
12010                    let bold = |s: &str| format!("\x1b[1m{}\x1b[0m", s);
12011                    let cyan = |s: &str| format!("\x1b[36m{}\x1b[0m", s);
12012                    println!("{:>4}  {:<8}  {:<20}  {}", bold("ID"), bold("KIND"), bold("PATTERN"), bold("CODE"));
12013                    for i in &self.intercepts {
12014                        let kind = match i.kind {
12015                            AdviceKind::Before => "before",
12016                            AdviceKind::After => "after",
12017                            AdviceKind::Around => "around",
12018                        };
12019                        let code_preview = if i.code.len() > 40 {
12020                            format!("{}...", &i.code[..37])
12021                        } else {
12022                            i.code.clone()
12023                        };
12024                        println!("{:>4}  {:<8}  {:<20}  {}", cyan(&i.id.to_string()), kind, i.pattern, code_preview);
12025                    }
12026                }
12027                0
12028            }
12029            "clear" => {
12030                let count = self.intercepts.len();
12031                self.intercepts.clear();
12032                println!("cleared {} intercepts", count);
12033                0
12034            }
12035            "remove" => {
12036                if args.len() < 2 {
12037                    eprintln!("intercept remove: requires ID");
12038                    return 1;
12039                }
12040                if let Ok(id) = args[1].parse::<u32>() {
12041                    let before = self.intercepts.len();
12042                    self.intercepts.retain(|i| i.id != id);
12043                    if self.intercepts.len() < before {
12044                        println!("removed intercept {}", id);
12045                        0
12046                    } else {
12047                        eprintln!("intercept: no intercept with ID {}", id);
12048                        1
12049                    }
12050                } else {
12051                    eprintln!("intercept remove: invalid ID");
12052                    1
12053                }
12054            }
12055            "before" | "after" | "around" => {
12056                let kind = match args[0].as_str() {
12057                    "before" => AdviceKind::Before,
12058                    "after" => AdviceKind::After,
12059                    "around" => AdviceKind::Around,
12060                    _ => unreachable!(),
12061                };
12062
12063                if args.len() < 3 {
12064                    eprintln!("intercept {}: requires <pattern> {{ code }}", args[0]);
12065                    return 1;
12066                }
12067
12068                let pattern = args[1].clone();
12069                // Join remaining args as the code (handles { code } or 'code')
12070                let code = args[2..].join(" ");
12071                // Strip surrounding braces if present
12072                let code = code.trim().to_string();
12073                let code = if code.starts_with('{') && code.ends_with('}') {
12074                    code[1..code.len() - 1].trim().to_string()
12075                } else {
12076                    code
12077                };
12078
12079                let id = self.intercepts.iter().map(|i| i.id).max().unwrap_or(0) + 1;
12080                self.intercepts.push(Intercept {
12081                    pattern,
12082                    kind: kind.clone(),
12083                    code: code.clone(),
12084                    id,
12085                });
12086
12087                let kind_str = match kind {
12088                    AdviceKind::Before => "before",
12089                    AdviceKind::After => "after",
12090                    AdviceKind::Around => "around",
12091                };
12092                println!("intercept #{}: {} {} → {}", id, kind_str, self.intercepts.last().unwrap().pattern,
12093                    if code.len() > 50 { format!("{}...", &code[..47]) } else { code });
12094                0
12095            }
12096            _ => {
12097                eprintln!("intercept: unknown subcommand '{}'. Use before|after|around|list|remove|clear", args[0]);
12098                1
12099            }
12100        }
12101    }
12102
12103    /// intercept_proceed — called from around advice to execute the original command.
12104    fn builtin_intercept_proceed(&mut self, _args: &[String]) -> i32 {
12105        self.variables.insert("__intercept_proceed".to_string(), "1".to_string());
12106        // Run the original command using saved INTERCEPT_NAME/INTERCEPT_ARGS
12107        let cmd_name = self.variables.get("INTERCEPT_NAME").cloned().unwrap_or_default();
12108        let args_str = self.variables.get("INTERCEPT_ARGS").cloned().unwrap_or_default();
12109        let args: Vec<String> = if args_str.is_empty() {
12110            Vec::new()
12111        } else {
12112            args_str.split_whitespace().map(|s| s.to_string()).collect()
12113        };
12114        match self.run_original_command(&cmd_name, &args) {
12115            Ok(status) => status,
12116            Err(e) => {
12117                eprintln!("intercept_proceed: {}", e);
12118                1
12119            }
12120        }
12121    }
12122
12123    // ═══════════════════════════════════════════════════════════════════
12124    // CONCURRENT PRIMITIVES — ship work to the worker pool from shell
12125    // No stryke dependency. Pure zshrs. Thin binary gets full parallelism.
12126    // ═══════════════════════════════════════════════════════════════════
12127
12128    /// async { cmd } — run command on worker pool, return job ID immediately.
12129    /// Output captured in background, retrieve with `await $id`.
12130    ///
12131    /// Usage:
12132    ///   id=$(async 'sleep 2; echo done')
12133    ///   ... do other work ...
12134    ///   result=$(await $id)
12135    fn builtin_async(&mut self, args: &[String]) -> i32 {
12136        if args.is_empty() {
12137            eprintln!("async: requires a command string");
12138            return 1;
12139        }
12140
12141        let code = args.join(" ");
12142        let id = self.next_async_id;
12143        self.next_async_id += 1;
12144
12145        let (tx, rx) = crossbeam_channel::bounded::<(i32, String)>(1);
12146        let pool = std::sync::Arc::clone(&self.worker_pool);
12147
12148        pool.submit(move || {
12149            // Execute in a subprocess to capture stdout
12150            use std::process::{Command, Stdio};
12151            let output = Command::new("sh")
12152                .args(["-c", &code])
12153                .stdout(Stdio::piped())
12154                .stderr(Stdio::inherit())
12155                .output();
12156            match output {
12157                Ok(out) => {
12158                    let stdout = String::from_utf8_lossy(&out.stdout).to_string();
12159                    let status = out.status.code().unwrap_or(1);
12160                    let _ = tx.send((status, stdout));
12161                }
12162                Err(_) => {
12163                    let _ = tx.send((127, String::new()));
12164                }
12165            }
12166        });
12167
12168        self.async_jobs.insert(id, rx);
12169        // Print the job ID so it can be captured: id=$(async 'cmd')
12170        println!("{}", id);
12171        0
12172    }
12173
12174    /// await $id — block until async job completes, print its stdout, return its status.
12175    ///
12176    /// Usage:
12177    ///   id=$(async 'expensive_command')
12178    ///   await $id    # blocks until done, prints output
12179    ///   echo $?      # exit status of the async command
12180    fn builtin_await(&mut self, args: &[String]) -> i32 {
12181        if args.is_empty() {
12182            eprintln!("await: requires a job ID");
12183            return 1;
12184        }
12185
12186        let id: u32 = match args[0].parse() {
12187            Ok(n) => n,
12188            Err(_) => {
12189                eprintln!("await: invalid job ID '{}'", args[0]);
12190                return 1;
12191            }
12192        };
12193
12194        let rx = match self.async_jobs.remove(&id) {
12195            Some(rx) => rx,
12196            None => {
12197                eprintln!("await: no async job with ID {}", id);
12198                return 1;
12199            }
12200        };
12201
12202        // Block until the job completes
12203        match rx.recv() {
12204            Ok((status, stdout)) => {
12205                if !stdout.is_empty() {
12206                    print!("{}", stdout);
12207                }
12208                self.last_status = status;
12209                status
12210            }
12211            Err(_) => {
12212                eprintln!("await: job {} died without result", id);
12213                1
12214            }
12215        }
12216    }
12217
12218    /// pmap 'cmd {}' arg1 arg2 arg3 — parallel map across worker pool.
12219    /// Runs `cmd` for each argument, replacing `{}` with the argument.
12220    /// Output is collected in order. Returns 0 if all succeed.
12221    ///
12222    /// Usage:
12223    ///   pmap 'gzip {}' *.log
12224    ///   pmap 'echo {}' a b c d
12225    ///   ls *.rs | pmap 'wc -l {}'
12226    fn builtin_pmap(&mut self, args: &[String]) -> i32 {
12227        if args.len() < 2 {
12228            eprintln!("pmap: requires 'command {{}}' followed by arguments");
12229            return 1;
12230        }
12231
12232        let template = &args[0];
12233        let items = &args[1..];
12234
12235        // Ship each item to the pool
12236        let mut receivers = Vec::with_capacity(items.len());
12237        for item in items {
12238            let cmd = template.replace("{}", item);
12239            let rx = self.worker_pool.submit_with_result(move || {
12240                use std::process::{Command, Stdio};
12241                let output = Command::new("sh")
12242                    .args(["-c", &cmd])
12243                    .stdout(Stdio::piped())
12244                    .stderr(Stdio::inherit())
12245                    .output();
12246                match output {
12247                    Ok(out) => (
12248                        out.status.code().unwrap_or(1),
12249                        String::from_utf8_lossy(&out.stdout).to_string(),
12250                    ),
12251                    Err(_) => (127, String::new()),
12252                }
12253            });
12254            receivers.push(rx);
12255        }
12256
12257        // Collect results in order
12258        let mut any_fail = false;
12259        for rx in receivers {
12260            if let Ok((status, stdout)) = rx.recv() {
12261                if !stdout.is_empty() {
12262                    print!("{}", stdout);
12263                }
12264                if status != 0 {
12265                    any_fail = true;
12266                }
12267            }
12268        }
12269
12270        if any_fail { 1 } else { 0 }
12271    }
12272
12273    /// pgrep 'pattern' arg1 arg2 ... — parallel grep/filter across worker pool.
12274    /// Runs the pattern command for each argument, prints args where command succeeds.
12275    ///
12276    /// Usage:
12277    ///   pgrep 'test -f {}' /path/a /path/b /path/c
12278    ///   pgrep 'grep -q TODO {}' *.rs
12279    fn builtin_pgrep(&mut self, args: &[String]) -> i32 {
12280        if args.len() < 2 {
12281            eprintln!("pgrep: requires 'test_command {{}}' followed by arguments");
12282            return 1;
12283        }
12284
12285        let template = &args[0];
12286        let items = &args[1..];
12287
12288        let mut receivers: Vec<(String, crossbeam_channel::Receiver<bool>)> = Vec::with_capacity(items.len());
12289        for item in items {
12290            let cmd = template.replace("{}", item);
12291            let rx = self.worker_pool.submit_with_result(move || {
12292                use std::process::{Command, Stdio};
12293                Command::new("sh")
12294                    .args(["-c", &cmd])
12295                    .stdout(Stdio::null())
12296                    .stderr(Stdio::null())
12297                    .status()
12298                    .map(|s| s.success())
12299                    .unwrap_or(false)
12300            });
12301            receivers.push((item.clone(), rx));
12302        }
12303
12304        for (item, rx) in receivers {
12305            if let Ok(true) = rx.recv() {
12306                println!("{}", item);
12307            }
12308        }
12309
12310        0
12311    }
12312
12313    /// peach 'cmd {}' arg1 arg2 ... — parallel for-each, no output ordering.
12314    /// Like pmap but doesn't collect output — fire-and-forget, print as completed.
12315    ///
12316    /// Usage:
12317    ///   peach 'convert {} {}.png' *.svg
12318    ///   peach 'rsync -a {} remote:{}' dir1 dir2 dir3
12319    fn builtin_peach(&mut self, args: &[String]) -> i32 {
12320        if args.len() < 2 {
12321            eprintln!("peach: requires 'command {{}}' followed by arguments");
12322            return 1;
12323        }
12324
12325        let template = &args[0];
12326        let items = &args[1..];
12327
12328        let (tx, rx) = crossbeam_channel::unbounded::<(String, i32, String)>();
12329
12330        for item in items {
12331            let cmd = template.replace("{}", item);
12332            let item_clone = item.clone();
12333            let tx = tx.clone();
12334            self.worker_pool.submit(move || {
12335                use std::process::{Command, Stdio};
12336                let output = Command::new("sh")
12337                    .args(["-c", &cmd])
12338                    .stdout(Stdio::piped())
12339                    .stderr(Stdio::inherit())
12340                    .output();
12341                match output {
12342                    Ok(out) => {
12343                        let stdout = String::from_utf8_lossy(&out.stdout).to_string();
12344                        let status = out.status.code().unwrap_or(1);
12345                        let _ = tx.send((item_clone, status, stdout));
12346                    }
12347                    Err(_) => {
12348                        let _ = tx.send((item_clone, 127, String::new()));
12349                    }
12350                }
12351            });
12352        }
12353        drop(tx);
12354
12355        let mut any_fail = false;
12356        for (_, status, stdout) in rx {
12357            if !stdout.is_empty() {
12358                print!("{}", stdout);
12359            }
12360            if status != 0 {
12361                any_fail = true;
12362            }
12363        }
12364
12365        if any_fail { 1 } else { 0 }
12366    }
12367
12368    /// barrier cmd1 ::: cmd2 ::: cmd3 — run commands in parallel, wait for ALL to complete.
12369    /// Returns the worst (highest) exit status.
12370    ///
12371    /// Usage:
12372    ///   barrier 'make -C proj1' ::: 'make -C proj2' ::: 'make -C proj3'
12373    ///   barrier 'npm test' ::: 'cargo test' ::: 'pytest'
12374    fn builtin_barrier(&mut self, args: &[String]) -> i32 {
12375        if args.is_empty() {
12376            eprintln!("barrier: requires commands separated by :::");
12377            return 1;
12378        }
12379
12380        // Split on ::: delimiter
12381        let mut commands: Vec<String> = Vec::new();
12382        let mut current = String::new();
12383        for arg in args {
12384            if arg == ":::" {
12385                if !current.is_empty() {
12386                    commands.push(current.trim().to_string());
12387                    current.clear();
12388                }
12389            } else {
12390                if !current.is_empty() {
12391                    current.push(' ');
12392                }
12393                current.push_str(arg);
12394            }
12395        }
12396        if !current.is_empty() {
12397            commands.push(current.trim().to_string());
12398        }
12399
12400        if commands.is_empty() {
12401            return 0;
12402        }
12403
12404        // Ship all to pool
12405        let mut receivers = Vec::with_capacity(commands.len());
12406        for cmd in &commands {
12407            let cmd = cmd.clone();
12408            let rx = self.worker_pool.submit_with_result(move || {
12409                use std::process::{Command, Stdio};
12410                Command::new("sh")
12411                    .args(["-c", &cmd])
12412                    .stdout(Stdio::inherit())
12413                    .stderr(Stdio::inherit())
12414                    .status()
12415                    .map(|s| s.code().unwrap_or(1))
12416                    .unwrap_or(127)
12417            });
12418            receivers.push(rx);
12419        }
12420
12421        // Wait for all — return worst status
12422        let mut worst = 0i32;
12423        for rx in receivers {
12424            if let Ok(status) = rx.recv() {
12425                if status > worst {
12426                    worst = status;
12427                }
12428            }
12429        }
12430
12431        self.last_status = worst;
12432        worst
12433    }
12434
12435    /// help - display help for builtins (bash)
12436    fn builtin_help(&self, args: &[String]) -> i32 {
12437        if args.is_empty() {
12438            println!("zshrs shell builtins:");
12439            println!("");
12440            println!("  alias, bg, bind, break, builtin, cd, command, continue,");
12441            println!("  declare, dirs, disown, echo, enable, eval, exec, exit,");
12442            println!("  export, false, fc, fg, getopts, hash, help, history,");
12443            println!("  jobs, kill, let, local, logout, popd, printf, pushd,");
12444            println!("  pwd, read, readonly, return, set, shift, shopt, source,");
12445            println!("  suspend, test, times, trap, true, type, typeset, ulimit,");
12446            println!("  umask, unalias, unset, wait, whence, where, which");
12447            println!("");
12448            println!("Type 'help name' for more information about 'name'.");
12449            return 0;
12450        }
12451
12452        let cmd = &args[0];
12453        match cmd.as_str() {
12454            "cd" => println!("cd: cd [-L|-P] [dir]\n    Change the shell working directory."),
12455            "echo" => println!("echo: echo [-neE] [arg ...]\n    Write arguments to standard output."),
12456            "export" => println!("export: export [-fn] [name[=value] ...]\n    Set export attribute for shell variables."),
12457            "alias" => println!("alias: alias [-p] [name[=value] ...]\n    Define or display aliases."),
12458            "history" => println!("history: history [-c] [-d offset] [n]\n    Display or manipulate the history list."),
12459            "jobs" => println!("jobs: jobs [-lnprs] [jobspec ...]\n    Display status of jobs."),
12460            "kill" => println!("kill: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n    Send a signal to a job."),
12461            "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."),
12462            "set" => println!("set: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]\n    Set or unset values of shell options and positional parameters."),
12463            "test" | "[" => println!("test: test [expr]\n    Evaluate conditional expression."),
12464            "type" => println!("type: type [-afptP] name [name ...]\n    Display information about command type."),
12465            _ => println!("{}: no help available", cmd),
12466        }
12467        0
12468    }
12469
12470    /// readarray/mapfile - read lines into array (bash)
12471    fn builtin_readarray(&mut self, args: &[String]) -> i32 {
12472        use std::io::{BufRead, BufReader};
12473
12474        let mut array_name = "MAPFILE".to_string();
12475        let mut delimiter = '\n';
12476        let mut count = 0usize; // 0 = unlimited
12477        let mut skip = 0usize;
12478        let mut strip_trailing = false;
12479        let mut callback: Option<String> = None;
12480        let mut callback_quantum = 0usize;
12481
12482        let mut i = 0;
12483        while i < args.len() {
12484            match args[i].as_str() {
12485                "-d" => {
12486                    i += 1;
12487                    if i < args.len() && !args[i].is_empty() {
12488                        delimiter = args[i].chars().next().unwrap_or('\n');
12489                    }
12490                }
12491                "-n" => {
12492                    i += 1;
12493                    if i < args.len() {
12494                        count = args[i].parse().unwrap_or(0);
12495                    }
12496                }
12497                "-O" => {
12498                    i += 1;
12499                    // Origin - start index (ignored, we always start at 0)
12500                }
12501                "-s" => {
12502                    i += 1;
12503                    if i < args.len() {
12504                        skip = args[i].parse().unwrap_or(0);
12505                    }
12506                }
12507                "-t" => strip_trailing = true,
12508                "-C" => {
12509                    i += 1;
12510                    if i < args.len() {
12511                        callback = Some(args[i].clone());
12512                    }
12513                }
12514                "-c" => {
12515                    i += 1;
12516                    if i < args.len() {
12517                        callback_quantum = args[i].parse().unwrap_or(5000);
12518                    }
12519                }
12520                "-u" => {
12521                    i += 1;
12522                    // fd - ignored, we read from stdin
12523                }
12524                s if !s.starts_with('-') => {
12525                    array_name = s.to_string();
12526                }
12527                _ => {}
12528            }
12529            i += 1;
12530        }
12531
12532        let stdin = std::io::stdin();
12533        let reader = BufReader::new(stdin.lock());
12534        let mut lines = Vec::new();
12535        let mut line_count = 0usize;
12536
12537        for line_result in reader.lines() {
12538            if let Ok(mut line) = line_result {
12539                line_count += 1;
12540
12541                if line_count <= skip {
12542                    continue;
12543                }
12544
12545                if strip_trailing {
12546                    while line.ends_with('\n') || line.ends_with('\r') {
12547                        line.pop();
12548                    }
12549                }
12550
12551                lines.push(line);
12552
12553                if count > 0 && lines.len() >= count {
12554                    break;
12555                }
12556            }
12557        }
12558
12559        self.arrays.insert(array_name, lines);
12560        let _ = (callback, callback_quantum);
12561        0
12562    }
12563
12564    fn builtin_shopt(&mut self, args: &[String]) -> i32 {
12565        if args.is_empty() {
12566            // List all shell options
12567            for (opt, val) in &self.options {
12568                println!("shopt {} {}", if *val { "-s" } else { "-u" }, opt);
12569            }
12570            return 0;
12571        }
12572
12573        let mut set = None;
12574        let mut opts = Vec::new();
12575
12576        for arg in args {
12577            match arg.as_str() {
12578                "-s" => set = Some(true),
12579                "-u" => set = Some(false),
12580                "-p" => {
12581                    // Print option status
12582                    for opt in &opts {
12583                        let val = self.options.get(opt).copied().unwrap_or(false);
12584                        println!("shopt {} {}", if val { "-s" } else { "-u" }, opt);
12585                    }
12586                    return 0;
12587                }
12588                _ => opts.push(arg.clone()),
12589            }
12590        }
12591
12592        if let Some(enable) = set {
12593            for opt in &opts {
12594                self.options.insert(opt.clone(), enable);
12595            }
12596        } else {
12597            // Query options
12598            for opt in &opts {
12599                let val = self.options.get(opt).copied().unwrap_or(false);
12600                println!("shopt {} {}", if val { "-s" } else { "-u" }, opt);
12601            }
12602        }
12603        0
12604    }
12605
12606    /// zsh-compatible setopt builtin
12607    fn builtin_setopt(&mut self, args: &[String]) -> i32 {
12608        if args.is_empty() {
12609            // List options that differ from compiled-in defaults (zsh behavior)
12610            // For default-ON options: show "noOPTION" if currently OFF
12611            // For default-OFF options: show "OPTION" if currently ON
12612            let defaults_on = Self::default_on_options();
12613            let mut diff_opts: Vec<String> = Vec::new();
12614
12615            for &opt in Self::all_zsh_options() {
12616                let enabled = self.options.get(opt).copied().unwrap_or(false);
12617                let is_default_on = defaults_on.contains(&opt);
12618
12619                if is_default_on && !enabled {
12620                    // Default ON but currently OFF -> show noOPTION
12621                    diff_opts.push(format!("no{}", opt));
12622                } else if !is_default_on && enabled {
12623                    // Default OFF but currently ON -> show OPTION
12624                    diff_opts.push(opt.to_string());
12625                }
12626            }
12627            diff_opts.sort();
12628            for opt in diff_opts {
12629                println!("{}", opt);
12630            }
12631            return 0;
12632        }
12633
12634        let mut use_pattern = false;
12635        let mut iter = args.iter().peekable();
12636
12637        while let Some(arg) = iter.next() {
12638            match arg.as_str() {
12639                "-m" => use_pattern = true,
12640                "-o" => {
12641                    // -o option_name: set option
12642                    if let Some(opt) = iter.next() {
12643                        let (name, enable) = Self::normalize_option_name(opt);
12644                        self.options.insert(name, enable);
12645                    }
12646                }
12647                "+o" => {
12648                    // +o option_name: unset option
12649                    if let Some(opt) = iter.next() {
12650                        let (name, enable) = Self::normalize_option_name(opt);
12651                        self.options.insert(name, !enable);
12652                    }
12653                }
12654                _ => {
12655                    if use_pattern {
12656                        // Match pattern against all options
12657                        for opt in Self::all_zsh_options() {
12658                            if Self::option_matches_pattern(opt, arg) {
12659                                self.options.insert(opt.to_string(), true);
12660                            }
12661                        }
12662                    } else {
12663                        let (name, enable) = Self::normalize_option_name(arg);
12664                        // Verify it's a valid option (zsh doesn't error on bad names in setopt)
12665                        self.options.insert(name, enable);
12666                    }
12667                }
12668            }
12669        }
12670        0
12671    }
12672
12673    /// zsh-compatible unsetopt builtin
12674    fn builtin_unsetopt(&mut self, args: &[String]) -> i32 {
12675        if args.is_empty() {
12676            // List all options in the format you'd pass to unsetopt to disable them
12677            // For default-ON options: show "noOPTION" (to turn it off)
12678            // For default-OFF options: show "OPTION" (already off, but this is what you'd type)
12679            let defaults_on = Self::default_on_options();
12680            let mut all_opts: Vec<String> = Vec::new();
12681
12682            for &opt in Self::all_zsh_options() {
12683                let is_default_on = defaults_on.contains(&opt);
12684                if is_default_on {
12685                    all_opts.push(format!("no{}", opt));
12686                } else {
12687                    all_opts.push(opt.to_string());
12688                }
12689            }
12690            all_opts.sort();
12691            for opt in all_opts {
12692                println!("{}", opt);
12693            }
12694            return 0;
12695        }
12696
12697        let mut use_pattern = false;
12698        let mut iter = args.iter().peekable();
12699
12700        while let Some(arg) = iter.next() {
12701            match arg.as_str() {
12702                "-m" => use_pattern = true,
12703                "-o" => {
12704                    // -o option_name: unset option
12705                    if let Some(opt) = iter.next() {
12706                        let (name, enable) = Self::normalize_option_name(opt);
12707                        self.options.insert(name, !enable);
12708                    }
12709                }
12710                "+o" => {
12711                    // +o option_name: set option (opposite in unsetopt)
12712                    if let Some(opt) = iter.next() {
12713                        let (name, enable) = Self::normalize_option_name(opt);
12714                        self.options.insert(name, enable);
12715                    }
12716                }
12717                _ => {
12718                    if use_pattern {
12719                        for opt in Self::all_zsh_options() {
12720                            if Self::option_matches_pattern(opt, arg) {
12721                                self.options.insert(opt.to_string(), false);
12722                            }
12723                        }
12724                    } else {
12725                        let (name, enable) = Self::normalize_option_name(arg);
12726                        // unsetopt turns OFF the option (or ON if "no" prefix)
12727                        self.options.insert(name, !enable);
12728                    }
12729                }
12730            }
12731        }
12732        0
12733    }
12734
12735    fn builtin_getopts(&mut self, args: &[String]) -> i32 {
12736        if args.len() < 2 {
12737            eprintln!("zshrs: getopts: usage: getopts optstring name [arg ...]");
12738            return 1;
12739        }
12740
12741        let optstring = &args[0];
12742        let varname = &args[1];
12743        let opt_args: Vec<&str> = if args.len() > 2 {
12744            args[2..].iter().map(|s| s.as_str()).collect()
12745        } else {
12746            self.positional_params.iter().map(|s| s.as_str()).collect()
12747        };
12748
12749        // Get current OPTIND
12750        let optind: usize = self
12751            .variables
12752            .get("OPTIND")
12753            .and_then(|s| s.parse().ok())
12754            .unwrap_or(1);
12755
12756        if optind > opt_args.len() {
12757            self.variables.insert(varname.to_string(), "?".to_string());
12758            return 1;
12759        }
12760
12761        let current_arg = opt_args[optind - 1];
12762
12763        if !current_arg.starts_with('-') || current_arg == "-" {
12764            self.variables.insert(varname.to_string(), "?".to_string());
12765            return 1;
12766        }
12767
12768        if current_arg == "--" {
12769            self.variables
12770                .insert("OPTIND".to_string(), (optind + 1).to_string());
12771            self.variables.insert(varname.to_string(), "?".to_string());
12772            return 1;
12773        }
12774
12775        // Get current option position within the argument
12776        let optpos: usize = self
12777            .variables
12778            .get("_OPTPOS")
12779            .and_then(|s| s.parse().ok())
12780            .unwrap_or(1);
12781
12782        let opt_char = current_arg.chars().nth(optpos);
12783
12784        if let Some(c) = opt_char {
12785            // Look up option in optstring
12786            let opt_idx = optstring.find(c);
12787
12788            match opt_idx {
12789                Some(idx) => {
12790                    // Check if option takes an argument
12791                    let takes_arg = optstring.chars().nth(idx + 1) == Some(':');
12792
12793                    if takes_arg {
12794                        // Get argument
12795                        let arg = if optpos + 1 < current_arg.len() {
12796                            // Argument is rest of current arg
12797                            current_arg[optpos + 1..].to_string()
12798                        } else if optind < opt_args.len() {
12799                            // Argument is next arg
12800                            self.variables
12801                                .insert("OPTIND".to_string(), (optind + 2).to_string());
12802                            self.variables.remove("_OPTPOS");
12803                            opt_args[optind].to_string()
12804                        } else {
12805                            // Missing argument
12806                            self.variables.insert(varname.to_string(), "?".to_string());
12807                            if !optstring.starts_with(':') {
12808                                eprintln!("zshrs: getopts: option requires an argument -- {}", c);
12809                            }
12810                            self.variables.insert("OPTARG".to_string(), c.to_string());
12811                            return 1;
12812                        };
12813
12814                        self.variables.insert("OPTARG".to_string(), arg);
12815                        self.variables
12816                            .insert("OPTIND".to_string(), (optind + 1).to_string());
12817                        self.variables.remove("_OPTPOS");
12818                    } else {
12819                        // No argument needed
12820                        if optpos + 1 < current_arg.len() {
12821                            // More options in this arg
12822                            self.variables
12823                                .insert("_OPTPOS".to_string(), (optpos + 1).to_string());
12824                        } else {
12825                            // Move to next arg
12826                            self.variables
12827                                .insert("OPTIND".to_string(), (optind + 1).to_string());
12828                            self.variables.remove("_OPTPOS");
12829                        }
12830                    }
12831
12832                    self.variables.insert(varname.to_string(), c.to_string());
12833                    0
12834                }
12835                None => {
12836                    // Unknown option
12837                    if !optstring.starts_with(':') {
12838                        eprintln!("zshrs: getopts: illegal option -- {}", c);
12839                    }
12840                    self.variables.insert(varname.to_string(), "?".to_string());
12841                    self.variables.insert("OPTARG".to_string(), c.to_string());
12842
12843                    // Advance to next option/arg
12844                    if optpos + 1 < current_arg.len() {
12845                        self.variables
12846                            .insert("_OPTPOS".to_string(), (optpos + 1).to_string());
12847                    } else {
12848                        self.variables
12849                            .insert("OPTIND".to_string(), (optind + 1).to_string());
12850                        self.variables.remove("_OPTPOS");
12851                    }
12852                    0
12853                }
12854            }
12855        } else {
12856            // No more options in current arg
12857            self.variables
12858                .insert("OPTIND".to_string(), (optind + 1).to_string());
12859            self.variables.remove("_OPTPOS");
12860            self.variables.insert(varname.to_string(), "?".to_string());
12861            1
12862        }
12863    }
12864
12865    fn builtin_type(&mut self, args: &[String]) -> i32 {
12866        if args.is_empty() {
12867            return 0;
12868        }
12869
12870        let mut show_all = false;
12871        let mut path_only = false;
12872        let mut silent = false;
12873        let mut show_type = false;
12874        let mut names = Vec::new();
12875
12876        let mut iter = args.iter();
12877        while let Some(arg) = iter.next() {
12878            if arg.starts_with('-') && arg.len() > 1 {
12879                for c in arg[1..].chars() {
12880                    match c {
12881                        'a' => show_all = true,
12882                        'p' => path_only = true,
12883                        'P' => path_only = true,
12884                        's' => silent = true,
12885                        't' => show_type = true,
12886                        'f' => {} // ignore functions (we still show them)
12887                        'w' => {} // like -t but different format
12888                        _ => {}
12889                    }
12890                }
12891            } else {
12892                names.push(arg.clone());
12893            }
12894        }
12895
12896        if names.is_empty() {
12897            return 0;
12898        }
12899
12900        let mut status = 0;
12901        for name in &names {
12902            let mut found_any = false;
12903
12904            // Check for alias (skip if -p)
12905            if !path_only && self.aliases.contains_key(name) {
12906                found_any = true;
12907                if !silent {
12908                    if show_type {
12909                        println!("alias");
12910                    } else {
12911                        println!(
12912                            "{} is aliased to `{}'",
12913                            name,
12914                            self.aliases.get(name).unwrap()
12915                        );
12916                    }
12917                }
12918                if !show_all {
12919                    continue;
12920                }
12921            }
12922
12923            // Check for function (skip if -p)
12924            if !path_only && self.functions.contains_key(name) {
12925                found_any = true;
12926                if !silent {
12927                    if show_type {
12928                        println!("function");
12929                    } else {
12930                        println!("{} is a shell function", name);
12931                    }
12932                }
12933                if !show_all {
12934                    continue;
12935                }
12936            }
12937
12938            // Check for builtin (skip if -p)
12939            if !path_only && (self.is_builtin(name) || name == ":" || name == "[") {
12940                found_any = true;
12941                if !silent {
12942                    if show_type {
12943                        println!("builtin");
12944                    } else {
12945                        println!("{} is a shell builtin", name);
12946                    }
12947                }
12948                if !show_all {
12949                    continue;
12950                }
12951            }
12952
12953            // Check for external command in PATH
12954            if let Ok(path_env) = std::env::var("PATH") {
12955                for dir in path_env.split(':') {
12956                    let full_path = format!("{}/{}", dir, name);
12957                    if std::path::Path::new(&full_path).exists() {
12958                        found_any = true;
12959                        if !silent {
12960                            if show_type {
12961                                println!("file");
12962                            } else {
12963                                println!("{} is {}", name, full_path);
12964                            }
12965                        }
12966                        if !show_all {
12967                            break;
12968                        }
12969                    }
12970                }
12971            }
12972
12973            if !found_any {
12974                if !silent {
12975                    eprintln!("zshrs: type: {}: not found", name);
12976                }
12977                status = 1;
12978            }
12979        }
12980        status
12981    }
12982
12983    fn builtin_hash(&mut self, args: &[String]) -> i32 {
12984        // hash [ -Ldfmrv ] [ name[=value] ] ...
12985        // hash -r clears the hash table
12986        // hash -d manages named directories
12987        // hash -f fills the table with all PATH commands
12988        // hash -m matches patterns
12989        // hash -v verbose
12990        // hash -L list in form suitable for reinput
12991
12992        let mut dir_mode = false;
12993        let mut rehash = false;
12994        let mut fill_all = false;
12995        let mut pattern_match = false;
12996        let mut verbose = false;
12997        let mut list_form = false;
12998        let mut names = Vec::new();
12999
13000        let mut i = 0;
13001        while i < args.len() {
13002            let arg = &args[i];
13003            if arg.starts_with('-') && arg.len() > 1 {
13004                for ch in arg[1..].chars() {
13005                    match ch {
13006                        'd' => dir_mode = true,
13007                        'r' => rehash = true,
13008                        'f' => fill_all = true,
13009                        'm' => pattern_match = true,
13010                        'v' => verbose = true,
13011                        'L' => list_form = true,
13012                        _ => {}
13013                    }
13014                }
13015            } else {
13016                names.push(arg.clone());
13017            }
13018            i += 1;
13019        }
13020
13021        // -r: clear hash table
13022        if rehash && !dir_mode && names.is_empty() {
13023            self.command_hash.clear();
13024            return 0;
13025        }
13026
13027        // -f: fill hash table with all commands in PATH
13028        if fill_all {
13029            if let Ok(path_var) = env::var("PATH") {
13030                for dir in path_var.split(':') {
13031                    if let Ok(entries) = std::fs::read_dir(dir) {
13032                        for entry in entries.flatten() {
13033                            if let Ok(ft) = entry.file_type() {
13034                                if ft.is_file() || ft.is_symlink() {
13035                                    if let Some(name) = entry.file_name().to_str() {
13036                                        let path = entry.path().to_string_lossy().to_string();
13037                                        self.command_hash.insert(name.to_string(), path);
13038                                    }
13039                                }
13040                            }
13041                        }
13042                    }
13043                }
13044            }
13045            return 0;
13046        }
13047
13048        if dir_mode {
13049            // Named directories mode (hash -d)
13050            if names.is_empty() {
13051                // List named directories
13052                for (name, path) in &self.named_dirs {
13053                    if list_form {
13054                        println!("hash -d {}={}", name, path.display());
13055                    } else if verbose {
13056                        println!("{}={}", name, path.display());
13057                    } else {
13058                        println!("{}={}", name, path.display());
13059                    }
13060                }
13061                return 0;
13062            }
13063
13064            if rehash {
13065                // Remove named directories
13066                if pattern_match {
13067                    // -m: pattern matching
13068                    let to_remove: Vec<String> = self
13069                        .named_dirs
13070                        .keys()
13071                        .filter(|k| {
13072                            names.iter().any(|pat| {
13073                                let pattern = pat.replace("*", ".*").replace("?", ".");
13074                                regex::Regex::new(&format!("^{}$", pattern))
13075                                    .map(|r| r.is_match(k))
13076                                    .unwrap_or(false)
13077                            })
13078                        })
13079                        .cloned()
13080                        .collect();
13081                    for name in to_remove {
13082                        self.named_dirs.remove(&name);
13083                    }
13084                } else {
13085                    for name in &names {
13086                        self.named_dirs.remove(name);
13087                    }
13088                }
13089                return 0;
13090            }
13091
13092            // Add named directories
13093            for name in &names {
13094                if let Some((n, p)) = name.split_once('=') {
13095                    self.add_named_dir(n, p);
13096                } else {
13097                    eprintln!("hash: -d: {} not in name=value format", name);
13098                    return 1;
13099                }
13100            }
13101            return 0;
13102        }
13103
13104        // Regular hash - command path lookup
13105        if names.is_empty() {
13106            // List all hashed commands
13107            for (name, path) in &self.command_hash {
13108                if list_form {
13109                    println!("hash {}={}", name, path);
13110                } else {
13111                    println!("{}={}", name, path);
13112                }
13113            }
13114            return 0;
13115        }
13116
13117        for name in &names {
13118            if let Some((cmd, path)) = name.split_once('=') {
13119                // Explicit assignment
13120                self.command_hash.insert(cmd.to_string(), path.to_string());
13121                if verbose {
13122                    println!("{}={}", cmd, path);
13123                }
13124            } else if let Some(path) = self.find_in_path(name) {
13125                // Look up in PATH and hash it
13126                self.command_hash.insert(name.clone(), path.clone());
13127                if verbose {
13128                    println!("{}={}", name, path);
13129                }
13130            } else {
13131                eprintln!("zshrs: hash: {}: not found", name);
13132                return 1;
13133            }
13134        }
13135        0
13136    }
13137
13138    /// add-zsh-hook builtin - add function to hook
13139    fn builtin_add_zsh_hook(&mut self, args: &[String]) -> i32 {
13140        // add-zsh-hook [-d] hook function
13141        if args.len() < 2 {
13142            eprintln!("usage: add-zsh-hook [-d] hook function");
13143            return 1;
13144        }
13145
13146        let (delete, hook, func) = if args[0] == "-d" {
13147            if args.len() < 3 {
13148                eprintln!("usage: add-zsh-hook -d hook function");
13149                return 1;
13150            }
13151            (true, &args[1], &args[2])
13152        } else {
13153            (false, &args[0], &args[1])
13154        };
13155
13156        if delete {
13157            // Remove function from hook
13158            if let Some(funcs) = self.hook_functions.get_mut(hook.as_str()) {
13159                funcs.retain(|f| f != func);
13160            }
13161        } else {
13162            // Add function to hook
13163            self.add_hook(hook, func);
13164        }
13165        0
13166    }
13167
13168    fn builtin_command(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
13169        // command [ -pvV ] simple command
13170        // -p: use default PATH
13171        // -v: print path (like which)
13172        // -V: verbose description (like type)
13173        let mut use_default_path = false;
13174        let mut print_path = false;
13175        let mut verbose = false;
13176        let mut positional_args: Vec<&str> = Vec::new();
13177
13178        let mut i = 0;
13179        while i < args.len() {
13180            let arg = &args[i];
13181            if arg.starts_with('-') && arg.len() > 1 && positional_args.is_empty() {
13182                for ch in arg[1..].chars() {
13183                    match ch {
13184                        'p' => use_default_path = true,
13185                        'v' => print_path = true,
13186                        'V' => verbose = true,
13187                        '-' => {
13188                            // -- ends options
13189                            i += 1;
13190                            break;
13191                        }
13192                        _ => {
13193                            eprintln!("command: bad option: -{}", ch);
13194                            return 1;
13195                        }
13196                    }
13197                }
13198            } else {
13199                positional_args.push(arg);
13200            }
13201            i += 1;
13202        }
13203
13204        // Add remaining args after --
13205        while i < args.len() {
13206            positional_args.push(&args[i]);
13207            i += 1;
13208        }
13209
13210        if positional_args.is_empty() {
13211            return 0;
13212        }
13213
13214        let cmd = positional_args[0];
13215
13216        // -v or -V: just print info about command
13217        if print_path || verbose {
13218            // Search PATH for command
13219            let path_var = if use_default_path {
13220                "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string()
13221            } else {
13222                env::var("PATH").unwrap_or_default()
13223            };
13224
13225            for dir in path_var.split(':') {
13226                let full_path = PathBuf::from(dir).join(cmd);
13227                if full_path.exists() && full_path.is_file() {
13228                    if verbose {
13229                        println!("{} is {}", cmd, full_path.display());
13230                    } else {
13231                        println!("{}", full_path.display());
13232                    }
13233                    return 0;
13234                }
13235            }
13236
13237            if verbose {
13238                eprintln!("{} not found", cmd);
13239            }
13240            return 1;
13241        }
13242
13243        // Execute as external command (bypassing functions and aliases)
13244        let cmd_args: Vec<String> = positional_args[1..].iter().map(|s| s.to_string()).collect();
13245
13246        if use_default_path {
13247            // Temporarily set PATH
13248            let old_path = env::var("PATH").ok();
13249            env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin");
13250            let result = self
13251                .execute_external(
13252                    cmd,
13253                    &cmd_args
13254                        .iter()
13255                        .map(|s| s.as_str())
13256                        .collect::<Vec<_>>()
13257                        .join(" ")
13258                        .split_whitespace()
13259                        .map(String::from)
13260                        .collect::<Vec<_>>(),
13261                    redirects,
13262                )
13263                .unwrap_or(127);
13264            if let Some(p) = old_path {
13265                env::set_var("PATH", p);
13266            }
13267            result
13268        } else {
13269            self.execute_external(cmd, &cmd_args, redirects)
13270                .unwrap_or(127)
13271        }
13272    }
13273
13274    fn builtin_builtin(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
13275        // Run builtin, bypassing functions and aliases
13276        if args.is_empty() {
13277            return 0;
13278        }
13279
13280        let cmd = &args[0];
13281        let cmd_args = &args[1..];
13282
13283        match cmd.as_str() {
13284            "cd" => self.builtin_cd(cmd_args),
13285            "pwd" => self.builtin_pwd(redirects),
13286            "echo" => self.builtin_echo(cmd_args, redirects),
13287            "export" => self.builtin_export(cmd_args),
13288            "unset" => self.builtin_unset(cmd_args),
13289            "exit" => self.builtin_exit(cmd_args),
13290            "return" => self.builtin_return(cmd_args),
13291            "true" => 0,
13292            "false" => 1,
13293            ":" => 0,
13294            "test" | "[" => self.builtin_test(cmd_args),
13295            "local" => self.builtin_local(cmd_args),
13296            "declare" | "typeset" => self.builtin_declare(cmd_args),
13297            "read" => self.builtin_read(cmd_args),
13298            "shift" => self.builtin_shift(cmd_args),
13299            "eval" => self.builtin_eval(cmd_args),
13300            "alias" => self.builtin_alias(cmd_args),
13301            "unalias" => self.builtin_unalias(cmd_args),
13302            "set" => self.builtin_set(cmd_args),
13303            "shopt" => self.builtin_shopt(cmd_args),
13304            "getopts" => self.builtin_getopts(cmd_args),
13305            "type" => self.builtin_type(cmd_args),
13306            "hash" => self.builtin_hash(cmd_args),
13307            "add-zsh-hook" => self.builtin_add_zsh_hook(cmd_args),
13308            "autoload" => self.builtin_autoload(cmd_args),
13309            "source" | "." => self.builtin_source(cmd_args),
13310            "functions" => self.builtin_functions(cmd_args),
13311            "zle" => self.builtin_zle(cmd_args),
13312            "bindkey" => self.builtin_bindkey(cmd_args),
13313            "setopt" => self.builtin_setopt(cmd_args),
13314            "unsetopt" => self.builtin_unsetopt(cmd_args),
13315            "emulate" => self.builtin_emulate(cmd_args),
13316            "zstyle" => self.builtin_zstyle(cmd_args),
13317            "compadd" => self.builtin_compadd(cmd_args),
13318            "compset" => self.builtin_compset(cmd_args),
13319            "compdef" => self.builtin_compdef(cmd_args),
13320            "compinit" => self.builtin_compinit(cmd_args),
13321            "cdreplay" => self.builtin_cdreplay(cmd_args),
13322            "zmodload" => self.builtin_zmodload(cmd_args),
13323            "zcompile" => self.builtin_zcompile(cmd_args),
13324            "zformat" => self.builtin_zformat(cmd_args),
13325            "zprof" => self.builtin_zprof(cmd_args),
13326            "print" => self.builtin_print(cmd_args),
13327            "printf" => self.builtin_printf(cmd_args),
13328            "command" => self.builtin_command(cmd_args, redirects),
13329            "whence" => self.builtin_whence(cmd_args),
13330            "which" => self.builtin_which(cmd_args),
13331            "where" => self.builtin_where(cmd_args),
13332            "fc" => self.builtin_fc(cmd_args),
13333            "history" => self.builtin_history(cmd_args),
13334            "dirs" => self.builtin_dirs(cmd_args),
13335            "pushd" => self.builtin_pushd(cmd_args),
13336            "popd" => self.builtin_popd(cmd_args),
13337            "bg" => self.builtin_bg(cmd_args),
13338            "fg" => self.builtin_fg(cmd_args),
13339            "jobs" => self.builtin_jobs(cmd_args),
13340            "kill" => self.builtin_kill(cmd_args),
13341            "wait" => self.builtin_wait(cmd_args),
13342            "trap" => self.builtin_trap(cmd_args),
13343            "umask" => self.builtin_umask(cmd_args),
13344            "ulimit" => self.builtin_ulimit(cmd_args),
13345            "times" => self.builtin_times(cmd_args),
13346            "let" => self.builtin_let(cmd_args),
13347            "integer" => self.builtin_integer(cmd_args),
13348            "float" => self.builtin_float(cmd_args),
13349            "readonly" => self.builtin_readonly(cmd_args),
13350            _ => {
13351                eprintln!("zshrs: builtin: {}: not a shell builtin", cmd);
13352                1
13353            }
13354        }
13355    }
13356
13357    fn builtin_let(&mut self, args: &[String]) -> i32 {
13358        if args.is_empty() {
13359            return 1;
13360        }
13361
13362        let mut result = 0i64;
13363        for expr in args {
13364            result = self.evaluate_arithmetic_expr(expr);
13365        }
13366
13367        // let returns 1 if last expression evaluates to 0, 0 otherwise
13368        if result == 0 {
13369            1
13370        } else {
13371            0
13372        }
13373    }
13374
13375    /// Generate completion candidates
13376    fn builtin_compgen(&self, args: &[String]) -> i32 {
13377        let mut i = 0;
13378        let mut prefix = String::new();
13379        let mut actions = Vec::new();
13380        let mut wordlist = None;
13381        let mut globpat = None;
13382
13383        while i < args.len() {
13384            match args[i].as_str() {
13385                "-W" => {
13386                    i += 1;
13387                    if i < args.len() {
13388                        wordlist = Some(args[i].clone());
13389                    }
13390                }
13391                "-G" => {
13392                    i += 1;
13393                    if i < args.len() {
13394                        globpat = Some(args[i].clone());
13395                    }
13396                }
13397                "-a" => actions.push("alias"),
13398                "-b" => actions.push("builtin"),
13399                "-c" => actions.push("command"),
13400                "-d" => actions.push("directory"),
13401                "-e" => actions.push("export"),
13402                "-f" => actions.push("file"),
13403                "-j" => actions.push("job"),
13404                "-k" => actions.push("keyword"),
13405                "-u" => actions.push("user"),
13406                "-v" => actions.push("variable"),
13407                s if !s.starts_with('-') => prefix = s.to_string(),
13408                _ => {}
13409            }
13410            i += 1;
13411        }
13412
13413        let mut results = Vec::new();
13414
13415        // Generate based on actions
13416        for action in actions {
13417            match action {
13418                "alias" => {
13419                    for name in self.aliases.keys() {
13420                        if name.starts_with(&prefix) {
13421                            results.push(name.clone());
13422                        }
13423                    }
13424                }
13425                "builtin" => {
13426                    for name in [
13427                        "cd", "pwd", "echo", "export", "unset", "source", "exit", "return", "true",
13428                        "false", ":", "test", "[", "local", "declare", "jobs", "fg", "bg", "kill",
13429                        "disown", "wait", "alias", "unalias", "set", "shopt",
13430                    ] {
13431                        if name.starts_with(&prefix) {
13432                            results.push(name.to_string());
13433                        }
13434                    }
13435                }
13436                "directory" => {
13437                    if let Ok(entries) = std::fs::read_dir(".") {
13438                        for entry in entries.flatten() {
13439                            if let Ok(ft) = entry.file_type() {
13440                                if ft.is_dir() {
13441                                    let name = entry.file_name().to_string_lossy().to_string();
13442                                    if name.starts_with(&prefix) {
13443                                        results.push(name);
13444                                    }
13445                                }
13446                            }
13447                        }
13448                    }
13449                }
13450                "file" => {
13451                    if let Ok(entries) = std::fs::read_dir(".") {
13452                        for entry in entries.flatten() {
13453                            let name = entry.file_name().to_string_lossy().to_string();
13454                            if name.starts_with(&prefix) {
13455                                results.push(name);
13456                            }
13457                        }
13458                    }
13459                }
13460                "variable" => {
13461                    for name in self.variables.keys() {
13462                        if name.starts_with(&prefix) {
13463                            results.push(name.clone());
13464                        }
13465                    }
13466                    for name in std::env::vars().map(|(k, _)| k) {
13467                        if name.starts_with(&prefix) && !results.contains(&name) {
13468                            results.push(name);
13469                        }
13470                    }
13471                }
13472                _ => {}
13473            }
13474        }
13475
13476        // Handle wordlist
13477        if let Some(words) = wordlist {
13478            for word in words.split_whitespace() {
13479                if word.starts_with(&prefix) {
13480                    results.push(word.to_string());
13481                }
13482            }
13483        }
13484
13485        // Handle glob pattern
13486        if let Some(_pattern) = globpat {
13487            let full_pattern = format!("{}*", prefix);
13488            if let Ok(paths) = glob::glob(&full_pattern) {
13489                for path in paths.flatten() {
13490                    results.push(path.to_string_lossy().to_string());
13491                }
13492            }
13493        }
13494
13495        results.sort();
13496        results.dedup();
13497        for r in results {
13498            println!("{}", r);
13499        }
13500        0
13501    }
13502
13503    /// Define completion spec for a command
13504    fn builtin_complete(&mut self, args: &[String]) -> i32 {
13505        if args.is_empty() {
13506            // List all completion specs
13507            for (cmd, spec) in &self.completions {
13508                let mut parts = vec!["complete".to_string()];
13509                for action in &spec.actions {
13510                    parts.push(format!("-{}", action));
13511                }
13512                if let Some(ref w) = spec.wordlist {
13513                    parts.push("-W".to_string());
13514                    parts.push(format!("'{}'", w));
13515                }
13516                if let Some(ref f) = spec.function {
13517                    parts.push("-F".to_string());
13518                    parts.push(f.clone());
13519                }
13520                if let Some(ref c) = spec.command {
13521                    parts.push("-C".to_string());
13522                    parts.push(c.clone());
13523                }
13524                parts.push(cmd.clone());
13525                println!("{}", parts.join(" "));
13526            }
13527            return 0;
13528        }
13529
13530        let mut spec = CompSpec::default();
13531        let mut commands = Vec::new();
13532        let mut i = 0;
13533
13534        while i < args.len() {
13535            match args[i].as_str() {
13536                "-W" => {
13537                    i += 1;
13538                    if i < args.len() {
13539                        spec.wordlist = Some(args[i].clone());
13540                    }
13541                }
13542                "-F" => {
13543                    i += 1;
13544                    if i < args.len() {
13545                        spec.function = Some(args[i].clone());
13546                    }
13547                }
13548                "-C" => {
13549                    i += 1;
13550                    if i < args.len() {
13551                        spec.command = Some(args[i].clone());
13552                    }
13553                }
13554                "-G" => {
13555                    i += 1;
13556                    if i < args.len() {
13557                        spec.globpat = Some(args[i].clone());
13558                    }
13559                }
13560                "-P" => {
13561                    i += 1;
13562                    if i < args.len() {
13563                        spec.prefix = Some(args[i].clone());
13564                    }
13565                }
13566                "-S" => {
13567                    i += 1;
13568                    if i < args.len() {
13569                        spec.suffix = Some(args[i].clone());
13570                    }
13571                }
13572                "-a" => spec.actions.push("a".to_string()),
13573                "-b" => spec.actions.push("b".to_string()),
13574                "-c" => spec.actions.push("c".to_string()),
13575                "-d" => spec.actions.push("d".to_string()),
13576                "-e" => spec.actions.push("e".to_string()),
13577                "-f" => spec.actions.push("f".to_string()),
13578                "-j" => spec.actions.push("j".to_string()),
13579                "-r" => {
13580                    // Remove completion spec
13581                    i += 1;
13582                    while i < args.len() {
13583                        self.completions.remove(&args[i]);
13584                        i += 1;
13585                    }
13586                    return 0;
13587                }
13588                s if !s.starts_with('-') => commands.push(s.to_string()),
13589                _ => {}
13590            }
13591            i += 1;
13592        }
13593
13594        for cmd in commands {
13595            self.completions.insert(cmd, spec.clone());
13596        }
13597        0
13598    }
13599
13600    /// Modify completion options
13601    fn builtin_compopt(&mut self, args: &[String]) -> i32 {
13602        // Basic stub - just accept the options
13603        let _ = args;
13604        0
13605    }
13606
13607    /// zsh compadd - add completion matches
13608    fn builtin_compadd(&mut self, args: &[String]) -> i32 {
13609        // Basic stub for zsh completion system
13610        // In a full implementation, this would add completion candidates
13611        let _ = args;
13612        0
13613    }
13614
13615    /// zsh compset - modify completion prefix/suffix
13616    fn builtin_compset(&mut self, args: &[String]) -> i32 {
13617        // Basic stub for zsh completion system
13618        let _ = args;
13619        0
13620    }
13621
13622    /// compdef - register completion functions for commands
13623    /// Usage: compdef _git git
13624    ///        compdef _docker docker docker-compose
13625    ///        compdef -d git  # delete
13626    fn builtin_compdef(&mut self, args: &[String]) -> i32 {
13627        if let Some(cache) = &mut self.compsys_cache {
13628            compsys::compdef::compdef_execute(cache, args)
13629        } else {
13630            // No cache - defer for cdreplay (zinit turbo mode)
13631            self.deferred_compdefs.push(args.to_vec());
13632            0
13633        }
13634    }
13635
13636    /// compinit - initialize the completion system
13637    /// Scans fpath for completion functions and registers them
13638    #[tracing::instrument(level = "info", skip(self))]
13639    fn builtin_compinit(&mut self, args: &[String]) -> i32 {
13640        // Parse options
13641        // -C: use cache if valid (skip fpath scan)
13642        // -D: don't dump (don't write .zcompdump)
13643        // -d file: specify dump file
13644        // -u: use insecure dirs anyway  -i: silently ignore insecure dirs
13645        // -q: quiet
13646        let mut quiet = false;
13647        let mut no_dump = false;
13648        let mut dump_file: Option<String> = None;
13649        let mut use_cache = false;
13650        let mut ignore_insecure = false;
13651        let mut use_insecure = false;
13652
13653        let mut i = 0;
13654        while i < args.len() {
13655            match args[i].as_str() {
13656                "-q" => quiet = true,
13657                "-C" => use_cache = true,
13658                "-D" => no_dump = true,
13659                "-d" => {
13660                    i += 1;
13661                    if i < args.len() {
13662                        dump_file = Some(args[i].clone());
13663                    }
13664                }
13665                "-u" => use_insecure = true,
13666                "-i" => ignore_insecure = true,
13667                _ => {}
13668            }
13669            i += 1;
13670        }
13671
13672        // Run compaudit with SQLite cache (unless -u skips it entirely)
13673        if !use_insecure && !self.posix_mode {
13674            if let Some(ref cache) = self.plugin_cache {
13675                let insecure = cache.compaudit_cached(&self.fpath);
13676                if !insecure.is_empty() && !ignore_insecure {
13677                    if !quiet {
13678                        eprintln!("compinit: insecure directories:");
13679                        for d in &insecure {
13680                            eprintln!("  {}", d);
13681                        }
13682                        eprintln!("compinit: run with -i to ignore or -u to use anyway");
13683                    }
13684                    return 1;
13685                }
13686            }
13687        }
13688
13689        // ZSH COMPAT MODE: Use traditional zsh algorithm (fpath scan, .zcompdump, no SQLite)
13690        if self.zsh_compat {
13691            return self.compinit_compat(quiet, no_dump, dump_file, use_cache);
13692        }
13693
13694        // ZSHRS MODE: Use SQLite cache with function bodies
13695
13696        // Try to use existing cache if -C and cache is valid
13697        if use_cache {
13698            if let Some(cache) = &self.compsys_cache {
13699                if compsys::cache_is_valid(cache) {
13700                    // Load from cache instead of rescanning
13701                    if let Ok(result) = compsys::load_from_cache(cache) {
13702                        if !quiet {
13703                            tracing::info!(
13704                                comps = result.comps.len(),
13705                                "compinit: using cached completions"
13706                            );
13707                        }
13708                        self.assoc_arrays.insert("_comps".to_string(), result.comps);
13709                        self.assoc_arrays
13710                            .insert("_services".to_string(), result.services);
13711                        self.assoc_arrays
13712                            .insert("_patcomps".to_string(), result.patcomps);
13713
13714                        // Background: fill bytecode blobs for any autoloads that have body but no ast.
13715                        // This populates the cache so subsequent autoload calls skip parsing.
13716                        if let Some(ref cache) = self.compsys_cache {
13717                            if let Ok(missing) = cache.count_autoloads_missing_bytecode() {
13718                                if missing > 0 {
13719                                    tracing::info!(
13720                                        count = missing,
13721                                        "compinit: backfilling bytecode blobs on worker pool"
13722                                    );
13723                                    let cache_path = compsys::cache::default_cache_path();
13724                                    let total_missing = missing;
13725                                    self.worker_pool.submit(move || {
13726                                        let mut cache = match compsys::cache::CompsysCache::open(&cache_path) {
13727                                            Ok(c) => c,
13728                                            Err(_) => return,
13729                                        };
13730                                        // Loop in batches of 100: fetch 100 bodies from SQLite,
13731                                        // parse them, write bytecode blobs back, repeat until none left.
13732                                        // Peak memory: ~100 function bodies + ASTs at a time.
13733                                        let mut total_cached = 0usize;
13734                                        loop {
13735                                            let stubs = match cache.get_autoloads_missing_bytecode_batch(100) {
13736                                                Ok(s) if !s.is_empty() => s,
13737                                                _ => break,
13738                                            };
13739                                            let mut batch: Vec<(String, Vec<u8>)> = Vec::with_capacity(stubs.len());
13740                                            for (name, body) in &stubs {
13741                                                let mut parser = crate::parser::ShellParser::new(body);
13742                                                if let Ok(commands) = parser.parse_script() {
13743                                                    if !commands.is_empty() {
13744                                                        let compiler = crate::shell_compiler::ShellCompiler::new();
13745                                                        let chunk = compiler.compile(&commands);
13746                                                        if let Ok(blob) = bincode::serialize(&chunk) {
13747                                                            batch.push((name.clone(), blob));
13748                                                        }
13749                                                    }
13750                                                }
13751                                            }
13752                                            total_cached += batch.len();
13753                                            if let Err(e) = cache.set_autoload_bytecodes_bulk(&batch) {
13754                                                tracing::warn!(error = %e, "compinit: bytecode backfill batch failed");
13755                                                break;
13756                                            }
13757                                            // If we got fewer than 100 results, we're done
13758                                            if stubs.len() < 100 {
13759                                                break;
13760                                            }
13761                                        }
13762                                        tracing::info!(
13763                                            cached = total_cached,
13764                                            total = total_missing,
13765                                            "compinit: bytecode backfill complete"
13766                                        );
13767                                    });
13768                                }
13769                            }
13770                        }
13771
13772                        return 0;
13773                    }
13774                }
13775            }
13776        }
13777
13778        // Ship compinit to worker pool — no ad-hoc thread spawn.
13779        // The heavy work (scan + SQLite write) runs on a pool thread.
13780        // Results are merged into shell state lazily via drain_compinit_bg().
13781        let fpath = self.fpath.clone();
13782        let fpath_count = fpath.len();
13783        let pool_size = self.worker_pool.size();
13784        let (tx, rx) = std::sync::mpsc::channel();
13785        let bg_start = std::time::Instant::now();
13786        tracing::info!(
13787            fpath_dirs = fpath_count,
13788            worker_pool = pool_size,
13789            "compinit: shipping to worker pool"
13790        );
13791        self.worker_pool.submit(move || {
13792                tracing::debug!("compinit-bg: thread started");
13793                let cache_path = compsys::cache::default_cache_path();
13794                if let Some(parent) = cache_path.parent() {
13795                    let _ = std::fs::create_dir_all(parent);
13796                }
13797                // Remove old DB to start fresh
13798                let _ = std::fs::remove_file(&cache_path);
13799                let _ = std::fs::remove_file(format!("{}-shm", cache_path.display()));
13800                let _ = std::fs::remove_file(format!("{}-wal", cache_path.display()));
13801
13802                let mut cache = match compsys::cache::CompsysCache::open(&cache_path) {
13803                    Ok(c) => c,
13804                    Err(e) => {
13805                        tracing::error!("compinit: failed to create cache: {}", e);
13806                        return;
13807                    }
13808                };
13809
13810                let result = match compsys::build_cache_from_fpath(&fpath, &mut cache) {
13811                    Ok(r) => r,
13812                    Err(e) => {
13813                        tracing::error!("compinit: scan failed: {}", e);
13814                        return;
13815                    }
13816                };
13817
13818                tracing::info!(
13819                    functions = result.files_scanned,
13820                    comps = result.comps.len(),
13821                    dirs = result.dirs_scanned,
13822                    ms = result.scan_time_ms,
13823                    "compinit: background scan complete"
13824                );
13825
13826                // Pre-parse function bodies and cache bytecode blobs.
13827                // Stream: parse one → serialize → write → drop. Never accumulate.
13828                // 16k functions × ~10KB AST = OOM if held in memory.
13829                let parse_start = std::time::Instant::now();
13830                let mut parse_ok = 0usize;
13831                let mut parse_fail = 0usize;
13832                let mut no_body = 0usize;
13833                let batch_size = 100;
13834                let mut batch: Vec<(String, Vec<u8>)> = Vec::with_capacity(batch_size);
13835
13836                for file in &result.files {
13837                    if let Some(ref body) = file.body {
13838                        let mut parser = crate::parser::ShellParser::new(body);
13839                        match parser.parse_script() {
13840                            Ok(commands) if !commands.is_empty() => {
13841                                // Compile AST → fusevm bytecodes, then serialize the Chunk
13842                                let compiler = crate::shell_compiler::ShellCompiler::new();
13843                                let chunk = compiler.compile(&commands);
13844                                if let Ok(blob) = bincode::serialize(&chunk) {
13845                                    batch.push((file.name.clone(), blob));
13846                                    parse_ok += 1;
13847                                    if batch.len() >= batch_size {
13848                                        let _ = cache.set_autoload_bytecodes_bulk(&batch);
13849                                        batch.clear();
13850                                    }
13851                                }
13852                            }
13853                            Ok(_) => { parse_fail += 1; }
13854                            Err(_) => { parse_fail += 1; }
13855                        }
13856                    } else {
13857                        no_body += 1;
13858                    }
13859                }
13860                // Flush remaining
13861                if !batch.is_empty() {
13862                    let _ = cache.set_autoload_bytecodes_bulk(&batch);
13863                    batch.clear();
13864                }
13865
13866                tracing::info!(
13867                    cached = parse_ok,
13868                    failed = parse_fail,
13869                    no_body = no_body,
13870                    total = result.files.len(),
13871                    ms = parse_start.elapsed().as_millis() as u64,
13872                    "compinit: bytecode blobs cached"
13873                );
13874
13875                let _ = tx.send(CompInitBgResult { result, cache });
13876            });
13877
13878        self.compinit_pending = Some((rx, bg_start));
13879        0
13880    }
13881
13882    /// Non-blocking drain of background compinit results.
13883    /// Call this before any completion lookup (prompt, tab-complete, etc.).
13884    /// If the background thread hasn't finished yet, this is a no-op.
13885    pub fn drain_compinit_bg(&mut self) {
13886        if let Some((rx, start)) = self.compinit_pending.take() {
13887            match rx.try_recv() {
13888                Ok(bg) => {
13889                    let comps = bg.result.comps.len();
13890                    self.assoc_arrays
13891                        .insert("_comps".to_string(), bg.result.comps);
13892                    self.assoc_arrays
13893                        .insert("_services".to_string(), bg.result.services);
13894                    self.assoc_arrays
13895                        .insert("_patcomps".to_string(), bg.result.patcomps);
13896                    self.compsys_cache = Some(bg.cache);
13897                    tracing::info!(
13898                        wall_ms = start.elapsed().as_millis() as u64,
13899                        comps,
13900                        "compinit: background results merged"
13901                    );
13902                }
13903                Err(std::sync::mpsc::TryRecvError::Empty) => {
13904                    // Not ready yet — put the receiver back for next poll
13905                    self.compinit_pending = Some((rx, start));
13906                }
13907                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
13908                    tracing::warn!("compinit: background thread died without sending results");
13909                }
13910            }
13911        }
13912    }
13913
13914    /// Traditional zsh compinit (--zsh-compat mode)
13915    /// Uses fpath scanning, .zcompdump files, no SQLite
13916    fn compinit_compat(
13917        &mut self,
13918        quiet: bool,
13919        no_dump: bool,
13920        dump_file: Option<String>,
13921        use_cache: bool,
13922    ) -> i32 {
13923        let zdotdir = self
13924            .variables
13925            .get("ZDOTDIR")
13926            .cloned()
13927            .or_else(|| std::env::var("ZDOTDIR").ok())
13928            .unwrap_or_else(|| std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()));
13929
13930        let dump_path = dump_file
13931            .map(PathBuf::from)
13932            .unwrap_or_else(|| PathBuf::from(&zdotdir).join(".zcompdump"));
13933
13934        // -C: Try to use existing .zcompdump if valid
13935        if use_cache && dump_path.exists() {
13936            if compsys::check_dump(&dump_path, &self.fpath, "zshrs-0.1.0") {
13937                // Valid dump - source it to load _comps
13938                // For now, just rescan (proper impl would source the dump file)
13939                if !quiet {
13940                    tracing::info!("compinit: .zcompdump valid, rescanning for compat");
13941                }
13942            }
13943        }
13944
13945        // Full fpath scan (traditional zsh algorithm)
13946        let result = compsys::compinit(&self.fpath);
13947
13948        if !quiet {
13949            tracing::info!(
13950                functions = result.files_scanned,
13951                comps = result.comps.len(),
13952                dirs = result.dirs_scanned,
13953                ms = result.scan_time_ms,
13954                "compinit: fpath scan complete"
13955            );
13956        }
13957
13958        // Write .zcompdump unless -D
13959        if !no_dump {
13960            let _ = compsys::compdump(&result, &dump_path, "zshrs-0.1.0");
13961        }
13962
13963        // Set up _comps associative array
13964        self.assoc_arrays
13965            .insert("_comps".to_string(), result.comps.clone());
13966        self.assoc_arrays
13967            .insert("_services".to_string(), result.services.clone());
13968        self.assoc_arrays
13969            .insert("_patcomps".to_string(), result.patcomps.clone());
13970
13971        // No SQLite cache in compat mode
13972        self.compsys_cache = None;
13973
13974        0
13975    }
13976
13977    /// cdreplay - replay deferred compdef calls (zinit turbo mode)
13978    /// Usage: cdreplay [-q]
13979    fn builtin_cdreplay(&mut self, args: &[String]) -> i32 {
13980        let quiet = args.contains(&"-q".to_string());
13981
13982        if self.deferred_compdefs.is_empty() {
13983            return 0;
13984        }
13985
13986        let deferred = std::mem::take(&mut self.deferred_compdefs);
13987        let count = deferred.len();
13988
13989        if let Some(cache) = &mut self.compsys_cache {
13990            for compdef_args in deferred {
13991                compsys::compdef::compdef_execute(cache, &compdef_args);
13992            }
13993        }
13994
13995        if !quiet {
13996            eprintln!("cdreplay: replayed {} compdef calls", count);
13997        }
13998
13999        0
14000    }
14001
14002    /// zsh zstyle - configure styles for completion
14003    fn builtin_zstyle(&mut self, args: &[String]) -> i32 {
14004        if args.is_empty() {
14005            // List all styles
14006            for (pattern, style, values) in self.style_table.list(None) {
14007                println!("zstyle '{}' {} {}", pattern, style, values.join(" "));
14008            }
14009            return 0;
14010        }
14011
14012        // Handle options
14013        if args[0].starts_with('-') {
14014            match args[0].as_str() {
14015                "-d" => {
14016                    // Delete style
14017                    let pattern = args.get(1).map(|s| s.as_str());
14018                    let style = args.get(2).map(|s| s.as_str());
14019                    self.style_table.delete(pattern, style);
14020                    return 0;
14021                }
14022                "-g" => {
14023                    // Get style into array
14024                    if args.len() >= 4 {
14025                        let array_name = &args[1];
14026                        let context = &args[2];
14027                        let style = &args[3];
14028                        if let Some(values) = self.style_table.get(context, style) {
14029                            self.arrays.insert(array_name.clone(), values.to_vec());
14030                            return 0;
14031                        }
14032                    }
14033                    return 1;
14034                }
14035                "-s" => {
14036                    // Get style as scalar
14037                    if args.len() >= 4 {
14038                        let var_name = &args[1];
14039                        let context = &args[2];
14040                        let style = &args[3];
14041                        let sep = args.get(4).map(|s| s.as_str()).unwrap_or(" ");
14042                        if let Some(values) = self.style_table.get(context, style) {
14043                            self.variables.insert(var_name.clone(), values.join(sep));
14044                            return 0;
14045                        }
14046                    }
14047                    return 1;
14048                }
14049                "-t" => {
14050                    // Test style (check if true/yes)
14051                    if args.len() >= 3 {
14052                        let context = &args[1];
14053                        let style = &args[2];
14054                        return if self.style_table.test_bool(context, style).unwrap_or(false) {
14055                            0
14056                        } else {
14057                            1
14058                        };
14059                    }
14060                    return 1;
14061                }
14062                "-L" => {
14063                    // List in re-usable format
14064                    for (pattern, style, values) in self.style_table.list(None) {
14065                        let values_str = values
14066                            .iter()
14067                            .map(|v| format!("'{}'", v.replace('\'', "'\\''")))
14068                            .collect::<Vec<_>>()
14069                            .join(" ");
14070                        println!("zstyle '{}' {} {}", pattern, style, values_str);
14071                    }
14072                    return 0;
14073                }
14074                _ => {}
14075            }
14076        }
14077
14078        // Set style: zstyle pattern style values...
14079        if args.len() >= 2 {
14080            let pattern = &args[0];
14081            let style = &args[1];
14082            let values: Vec<String> = args[2..].to_vec();
14083            self.style_table.set(pattern, style, values.clone(), false);
14084
14085            // Write to SQLite cache for completion lookups
14086            if let Some(cache) = &self.compsys_cache {
14087                let _ = cache.set_zstyle(pattern, style, &values, false);
14088            }
14089
14090            // Also update legacy zstyles for backward compat
14091            let existing = self
14092                .zstyles
14093                .iter_mut()
14094                .find(|s| s.pattern == *pattern && s.style == *style);
14095            if let Some(s) = existing {
14096                s.values = args[2..].to_vec();
14097            } else {
14098                self.zstyles.push(ZStyle {
14099                    pattern: pattern.clone(),
14100                    style: style.clone(),
14101                    values: args[2..].to_vec(),
14102                });
14103            }
14104        }
14105        0
14106    }
14107
14108    /// Tie a parameter to a GDBM database
14109    /// Usage: ztie -d db/gdbm -f /path/to/db.gdbm [-r] PARAM_NAME
14110    fn builtin_ztie(&mut self, args: &[String]) -> i32 {
14111        use crate::db_gdbm;
14112
14113        let mut db_type: Option<String> = None;
14114        let mut file_path: Option<String> = None;
14115        let mut readonly = false;
14116        let mut param_args: Vec<String> = Vec::new();
14117
14118        let mut i = 0;
14119        while i < args.len() {
14120            match args[i].as_str() {
14121                "-d" => {
14122                    if i + 1 < args.len() {
14123                        db_type = Some(args[i + 1].clone());
14124                        i += 2;
14125                    } else {
14126                        eprintln!("ztie: -d requires an argument");
14127                        return 1;
14128                    }
14129                }
14130                "-f" => {
14131                    if i + 1 < args.len() {
14132                        file_path = Some(args[i + 1].clone());
14133                        i += 2;
14134                    } else {
14135                        eprintln!("ztie: -f requires an argument");
14136                        return 1;
14137                    }
14138                }
14139                "-r" => {
14140                    readonly = true;
14141                    i += 1;
14142                }
14143                arg if arg.starts_with('-') => {
14144                    eprintln!("ztie: bad option: {}", arg);
14145                    return 1;
14146                }
14147                _ => {
14148                    param_args.push(args[i].clone());
14149                    i += 1;
14150                }
14151            }
14152        }
14153
14154        match db_gdbm::ztie(
14155            &param_args,
14156            readonly,
14157            db_type.as_deref(),
14158            file_path.as_deref(),
14159        ) {
14160            Ok(()) => 0,
14161            Err(e) => {
14162                eprintln!("ztie: {}", e);
14163                1
14164            }
14165        }
14166    }
14167
14168    /// Untie a parameter from its GDBM database
14169    /// Usage: zuntie [-u] PARAM_NAME...
14170    fn builtin_zuntie(&mut self, args: &[String]) -> i32 {
14171        use crate::db_gdbm;
14172
14173        let mut force_unset = false;
14174        let mut param_args: Vec<String> = Vec::new();
14175
14176        for arg in args {
14177            match arg.as_str() {
14178                "-u" => force_unset = true,
14179                a if a.starts_with('-') => {
14180                    eprintln!("zuntie: bad option: {}", a);
14181                    return 1;
14182                }
14183                _ => param_args.push(arg.clone()),
14184            }
14185        }
14186
14187        if param_args.is_empty() {
14188            eprintln!("zuntie: not enough arguments");
14189            return 1;
14190        }
14191
14192        match db_gdbm::zuntie(&param_args, force_unset) {
14193            Ok(()) => 0,
14194            Err(e) => {
14195                eprintln!("zuntie: {}", e);
14196                1
14197            }
14198        }
14199    }
14200
14201    /// Get the path of a tied GDBM database
14202    /// Usage: zgdbmpath PARAM_NAME
14203    /// Sets $REPLY to the path
14204    fn builtin_zgdbmpath(&mut self, args: &[String]) -> i32 {
14205        use crate::db_gdbm;
14206
14207        if args.is_empty() {
14208            eprintln!(
14209                "zgdbmpath: parameter name (whose path is to be written to $REPLY) is required"
14210            );
14211            return 1;
14212        }
14213
14214        match db_gdbm::zgdbmpath(&args[0]) {
14215            Ok(path) => {
14216                self.variables.insert("REPLY".to_string(), path.clone());
14217                std::env::set_var("REPLY", &path);
14218                0
14219            }
14220            Err(e) => {
14221                eprintln!("zgdbmpath: {}", e);
14222                1
14223            }
14224        }
14225    }
14226
14227    /// Push directory onto stack and cd to it
14228    fn builtin_pushd(&mut self, args: &[String]) -> i32 {
14229        // pushd [ -qsLP ] [ arg ]
14230        // pushd [ -qsLP ] old new
14231        // pushd [ -qsLP ] {+|-}n
14232        // -q: quiet (don't print stack)
14233        // -s: no symlink resolution (use -L cd behavior)
14234        // -L: logical directory (resolve .. before symlinks)
14235        // -P: physical directory (resolve symlinks)
14236
14237        let mut quiet = false;
14238        let mut physical = false;
14239        let mut positional_args: Vec<String> = Vec::new();
14240
14241        for arg in args {
14242            if arg.starts_with('-') && arg.len() > 1 {
14243                // Check if it's a stack index
14244                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
14245                    positional_args.push(arg.clone());
14246                    continue;
14247                }
14248                for ch in arg[1..].chars() {
14249                    match ch {
14250                        'q' => quiet = true,
14251                        's' => physical = false,
14252                        'L' => physical = false,
14253                        'P' => physical = true,
14254                        _ => {}
14255                    }
14256                }
14257            } else if arg.starts_with('+') {
14258                positional_args.push(arg.clone());
14259            } else {
14260                positional_args.push(arg.clone());
14261            }
14262        }
14263
14264        let current = match std::env::current_dir() {
14265            Ok(p) => p,
14266            Err(e) => {
14267                eprintln!("pushd: {}", e);
14268                return 1;
14269            }
14270        };
14271
14272        if positional_args.is_empty() {
14273            // Swap top two directories
14274            if self.dir_stack.is_empty() {
14275                eprintln!("pushd: no other directory");
14276                return 1;
14277            }
14278            let target = self.dir_stack.pop().unwrap();
14279            self.dir_stack.push(current.clone());
14280
14281            let resolved = if physical {
14282                target.canonicalize().unwrap_or(target.clone())
14283            } else {
14284                target.clone()
14285            };
14286
14287            if let Err(e) = std::env::set_current_dir(&resolved) {
14288                eprintln!("pushd: {}: {}", target.display(), e);
14289                self.dir_stack.pop();
14290                self.dir_stack.push(target);
14291                return 1;
14292            }
14293            if !quiet {
14294                self.print_dir_stack();
14295            }
14296            return 0;
14297        }
14298
14299        let arg = &positional_args[0];
14300
14301        // Handle +N and -N for rotating the stack
14302        if arg.starts_with('+') || arg.starts_with('-') {
14303            if let Ok(n) = arg[1..].parse::<usize>() {
14304                let total = self.dir_stack.len() + 1;
14305                if n >= total {
14306                    eprintln!("pushd: {}: directory stack index out of range", arg);
14307                    return 1;
14308                }
14309                // Rotate stack
14310                let rotate_pos = if arg.starts_with('+') { n } else { total - n };
14311                let mut full_stack = vec![current.clone()];
14312                full_stack.extend(self.dir_stack.iter().cloned());
14313                full_stack.rotate_left(rotate_pos);
14314
14315                let target = full_stack.remove(0);
14316                self.dir_stack = full_stack;
14317
14318                let resolved = if physical {
14319                    target.canonicalize().unwrap_or(target.clone())
14320                } else {
14321                    target.clone()
14322                };
14323
14324                if let Err(e) = std::env::set_current_dir(&resolved) {
14325                    eprintln!("pushd: {}: {}", target.display(), e);
14326                    return 1;
14327                }
14328                if !quiet {
14329                    self.print_dir_stack();
14330                }
14331                return 0;
14332            }
14333        }
14334
14335        // Regular directory push
14336        let target = PathBuf::from(arg);
14337        let resolved = if physical {
14338            target.canonicalize().unwrap_or(target.clone())
14339        } else {
14340            target.clone()
14341        };
14342
14343        self.dir_stack.push(current);
14344        if let Err(e) = std::env::set_current_dir(&resolved) {
14345            eprintln!("pushd: {}: {}", arg, e);
14346            self.dir_stack.pop();
14347            return 1;
14348        }
14349        if !quiet {
14350            self.print_dir_stack();
14351        }
14352        0
14353    }
14354
14355    /// Pop directory from stack and cd to it
14356    fn builtin_popd(&mut self, args: &[String]) -> i32 {
14357        // popd [ -qsLP ] [ {+|-}n ]
14358        // -q: quiet (don't print stack)
14359        // -s: no symlink resolution
14360        // -L: logical directory
14361        // -P: physical directory
14362
14363        let mut quiet = false;
14364        let mut physical = false;
14365        let mut stack_index: Option<String> = None;
14366
14367        for arg in args {
14368            if arg.starts_with('-') && arg.len() > 1 {
14369                // Check if it's a stack index
14370                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
14371                    stack_index = Some(arg.clone());
14372                    continue;
14373                }
14374                for ch in arg[1..].chars() {
14375                    match ch {
14376                        'q' => quiet = true,
14377                        's' => physical = false,
14378                        'L' => physical = false,
14379                        'P' => physical = true,
14380                        _ => {}
14381                    }
14382                }
14383            } else if arg.starts_with('+') {
14384                stack_index = Some(arg.clone());
14385            }
14386        }
14387
14388        if self.dir_stack.is_empty() {
14389            eprintln!("popd: directory stack empty");
14390            return 1;
14391        }
14392
14393        // Handle +N and -N
14394        if let Some(arg) = stack_index {
14395            if arg.starts_with('+') || arg.starts_with('-') {
14396                if let Ok(n) = arg[1..].parse::<usize>() {
14397                    let total = self.dir_stack.len() + 1;
14398                    if n >= total {
14399                        eprintln!("popd: {}: directory stack index out of range", arg);
14400                        return 1;
14401                    }
14402                    let remove_pos = if arg.starts_with('+') {
14403                        n
14404                    } else {
14405                        total - 1 - n
14406                    };
14407                    if remove_pos == 0 {
14408                        // Remove current and cd to next
14409                        let target = self.dir_stack.remove(0);
14410                        let resolved = if physical {
14411                            target.canonicalize().unwrap_or(target.clone())
14412                        } else {
14413                            target.clone()
14414                        };
14415                        if let Err(e) = std::env::set_current_dir(&resolved) {
14416                            eprintln!("popd: {}: {}", target.display(), e);
14417                            return 1;
14418                        }
14419                    } else {
14420                        self.dir_stack.remove(remove_pos - 1);
14421                    }
14422                    if !quiet {
14423                        self.print_dir_stack();
14424                    }
14425                    return 0;
14426                }
14427            }
14428        }
14429
14430        let target = self.dir_stack.pop().unwrap();
14431        let resolved = if physical {
14432            target.canonicalize().unwrap_or(target.clone())
14433        } else {
14434            target.clone()
14435        };
14436        if let Err(e) = std::env::set_current_dir(&resolved) {
14437            eprintln!("popd: {}: {}", target.display(), e);
14438            self.dir_stack.push(target);
14439            return 1;
14440        }
14441        if !quiet {
14442            self.print_dir_stack();
14443        }
14444        0
14445    }
14446
14447    /// Display directory stack
14448    fn builtin_dirs(&mut self, args: &[String]) -> i32 {
14449        // dirs [ -c ] [ -l ] [ -p ] [ -v ] [ arg ... ]
14450        // -c: clear the directory stack
14451        // -l: full pathnames (don't use ~)
14452        // -p: print one entry per line
14453        // -v: verbose (numbered list)
14454
14455        let mut clear = false;
14456        let mut full_paths = false;
14457        let mut per_line = false;
14458        let mut verbose = false;
14459        let mut indices: Vec<i32> = Vec::new();
14460
14461        for arg in args {
14462            if arg.starts_with('-') && arg.len() > 1 {
14463                // Check if it's a negative index like -2
14464                if arg[1..].chars().all(|c| c.is_ascii_digit()) {
14465                    if let Ok(n) = arg.parse::<i32>() {
14466                        indices.push(n);
14467                        continue;
14468                    }
14469                }
14470                for ch in arg[1..].chars() {
14471                    match ch {
14472                        'c' => clear = true,
14473                        'l' => full_paths = true,
14474                        'p' => per_line = true,
14475                        'v' => verbose = true,
14476                        _ => {}
14477                    }
14478                }
14479            } else if arg.starts_with('+') && arg.len() > 1 {
14480                if let Ok(n) = arg[1..].parse::<i32>() {
14481                    indices.push(n);
14482                }
14483            } else {
14484                // Could be a number
14485                if let Ok(n) = arg.parse::<i32>() {
14486                    indices.push(n);
14487                }
14488            }
14489        }
14490
14491        if clear {
14492            self.dir_stack.clear();
14493            return 0;
14494        }
14495
14496        let current = std::env::current_dir().unwrap_or_default();
14497        let home = dirs::home_dir().unwrap_or_default();
14498
14499        let format_path = |p: &std::path::Path| -> String {
14500            let path_str = p.to_string_lossy().to_string();
14501            if !full_paths {
14502                let home_str = home.to_string_lossy();
14503                if path_str.starts_with(home_str.as_ref()) {
14504                    return format!("~{}", &path_str[home_str.len()..]);
14505                }
14506            }
14507            path_str
14508        };
14509
14510        // If specific indices requested
14511        if !indices.is_empty() {
14512            let stack_len = self.dir_stack.len() + 1; // +1 for current dir
14513            for idx in indices {
14514                let actual_idx = if idx >= 0 {
14515                    idx as usize
14516                } else {
14517                    stack_len.saturating_sub((-idx) as usize)
14518                };
14519
14520                if actual_idx == 0 {
14521                    println!("{}", format_path(&current));
14522                } else if actual_idx <= self.dir_stack.len() {
14523                    // Stack is reversed, so index from end
14524                    let stack_idx = self.dir_stack.len() - actual_idx;
14525                    if let Some(dir) = self.dir_stack.get(stack_idx) {
14526                        println!("{}", format_path(dir));
14527                    }
14528                }
14529            }
14530            return 0;
14531        }
14532
14533        if verbose {
14534            println!(" 0  {}", format_path(&current));
14535            for (i, dir) in self.dir_stack.iter().rev().enumerate() {
14536                println!("{:2}  {}", i + 1, format_path(dir));
14537            }
14538        } else if per_line {
14539            println!("{}", format_path(&current));
14540            for dir in self.dir_stack.iter().rev() {
14541                println!("{}", format_path(dir));
14542            }
14543        } else {
14544            let mut parts = vec![format_path(&current)];
14545            for dir in self.dir_stack.iter().rev() {
14546                parts.push(format_path(dir));
14547            }
14548            println!("{}", parts.join(" "));
14549        }
14550        0
14551    }
14552
14553    fn print_dir_stack(&self) {
14554        let current = std::env::current_dir().unwrap_or_default();
14555        let mut parts = vec![current.to_string_lossy().to_string()];
14556        for dir in self.dir_stack.iter().rev() {
14557            parts.push(dir.to_string_lossy().to_string());
14558        }
14559        println!("{}", parts.join(" "));
14560    }
14561
14562    /// printf builtin - format and print data (zsh/bash compatible)
14563    fn builtin_printf(&self, args: &[String]) -> i32 {
14564        if args.is_empty() {
14565            eprintln!("printf: usage: printf format [arguments]");
14566            return 1;
14567        }
14568
14569        let format = &args[0];
14570        let format_args = &args[1..];
14571        let mut arg_idx = 0;
14572        let mut output = String::new();
14573        let mut chars = format.chars().peekable();
14574
14575        while let Some(c) = chars.next() {
14576            if c == '\\' {
14577                match chars.next() {
14578                    Some('n') => output.push('\n'),
14579                    Some('t') => output.push('\t'),
14580                    Some('r') => output.push('\r'),
14581                    Some('\\') => output.push('\\'),
14582                    Some('a') => output.push('\x07'),
14583                    Some('b') => output.push('\x08'),
14584                    Some('e') | Some('E') => output.push('\x1b'),
14585                    Some('f') => output.push('\x0c'),
14586                    Some('v') => output.push('\x0b'),
14587                    Some('"') => output.push('"'),
14588                    Some('\'') => output.push('\''),
14589                    Some('0') => {
14590                        let mut octal = String::new();
14591                        while octal.len() < 3 {
14592                            if let Some(&d) = chars.peek() {
14593                                if d >= '0' && d <= '7' {
14594                                    octal.push(d);
14595                                    chars.next();
14596                                } else {
14597                                    break;
14598                                }
14599                            } else {
14600                                break;
14601                            }
14602                        }
14603                        if octal.is_empty() {
14604                            output.push('\0');
14605                        } else if let Ok(val) = u8::from_str_radix(&octal, 8) {
14606                            output.push(val as char);
14607                        }
14608                    }
14609                    Some('x') => {
14610                        let mut hex = String::new();
14611                        while hex.len() < 2 {
14612                            if let Some(&d) = chars.peek() {
14613                                if d.is_ascii_hexdigit() {
14614                                    hex.push(d);
14615                                    chars.next();
14616                                } else {
14617                                    break;
14618                                }
14619                            } else {
14620                                break;
14621                            }
14622                        }
14623                        if !hex.is_empty() {
14624                            if let Ok(val) = u8::from_str_radix(&hex, 16) {
14625                                output.push(val as char);
14626                            }
14627                        }
14628                    }
14629                    Some('u') => {
14630                        let mut hex = String::new();
14631                        while hex.len() < 4 {
14632                            if let Some(&d) = chars.peek() {
14633                                if d.is_ascii_hexdigit() {
14634                                    hex.push(d);
14635                                    chars.next();
14636                                } else {
14637                                    break;
14638                                }
14639                            } else {
14640                                break;
14641                            }
14642                        }
14643                        if !hex.is_empty() {
14644                            if let Ok(val) = u32::from_str_radix(&hex, 16) {
14645                                if let Some(c) = char::from_u32(val) {
14646                                    output.push(c);
14647                                }
14648                            }
14649                        }
14650                    }
14651                    Some('U') => {
14652                        let mut hex = String::new();
14653                        while hex.len() < 8 {
14654                            if let Some(&d) = chars.peek() {
14655                                if d.is_ascii_hexdigit() {
14656                                    hex.push(d);
14657                                    chars.next();
14658                                } else {
14659                                    break;
14660                                }
14661                            } else {
14662                                break;
14663                            }
14664                        }
14665                        if !hex.is_empty() {
14666                            if let Ok(val) = u32::from_str_radix(&hex, 16) {
14667                                if let Some(c) = char::from_u32(val) {
14668                                    output.push(c);
14669                                }
14670                            }
14671                        }
14672                    }
14673                    Some('c') => {
14674                        print!("{}", output);
14675                        return 0;
14676                    }
14677                    Some(other) => {
14678                        output.push('\\');
14679                        output.push(other);
14680                    }
14681                    None => output.push('\\'),
14682                }
14683            } else if c == '%' {
14684                if chars.peek() == Some(&'%') {
14685                    chars.next();
14686                    output.push('%');
14687                    continue;
14688                }
14689
14690                let mut flags = String::new();
14691                while let Some(&f) = chars.peek() {
14692                    if f == '-' || f == '+' || f == ' ' || f == '#' || f == '0' {
14693                        flags.push(f);
14694                        chars.next();
14695                    } else {
14696                        break;
14697                    }
14698                }
14699
14700                let mut width = String::new();
14701                if chars.peek() == Some(&'*') {
14702                    chars.next();
14703                    if arg_idx < format_args.len() {
14704                        width = format_args[arg_idx].clone();
14705                        arg_idx += 1;
14706                    }
14707                } else {
14708                    while let Some(&d) = chars.peek() {
14709                        if d.is_ascii_digit() {
14710                            width.push(d);
14711                            chars.next();
14712                        } else {
14713                            break;
14714                        }
14715                    }
14716                }
14717
14718                let mut precision = String::new();
14719                if chars.peek() == Some(&'.') {
14720                    chars.next();
14721                    if chars.peek() == Some(&'*') {
14722                        chars.next();
14723                        if arg_idx < format_args.len() {
14724                            precision = format_args[arg_idx].clone();
14725                            arg_idx += 1;
14726                        }
14727                    } else {
14728                        while let Some(&d) = chars.peek() {
14729                            if d.is_ascii_digit() {
14730                                precision.push(d);
14731                                chars.next();
14732                            } else {
14733                                break;
14734                            }
14735                        }
14736                    }
14737                }
14738
14739                let specifier = chars.next().unwrap_or('s');
14740                let arg = if arg_idx < format_args.len() {
14741                    let a = &format_args[arg_idx];
14742                    arg_idx += 1;
14743                    a.clone()
14744                } else {
14745                    String::new()
14746                };
14747
14748                let width_val: usize = width.parse().unwrap_or(0);
14749                let prec_val: Option<usize> = if precision.is_empty() {
14750                    None
14751                } else {
14752                    precision.parse().ok()
14753                };
14754                let left_align = flags.contains('-');
14755                let zero_pad = flags.contains('0') && !left_align;
14756                let plus_sign = flags.contains('+');
14757                let space_sign = flags.contains(' ') && !plus_sign;
14758                let alt_form = flags.contains('#');
14759
14760                match specifier {
14761                    's' => {
14762                        let mut s = arg;
14763                        if let Some(p) = prec_val {
14764                            s = s.chars().take(p).collect();
14765                        }
14766                        if width_val > s.len() {
14767                            if left_align {
14768                                output.push_str(&s);
14769                                output.push_str(&" ".repeat(width_val - s.len()));
14770                            } else {
14771                                output.push_str(&" ".repeat(width_val - s.len()));
14772                                output.push_str(&s);
14773                            }
14774                        } else {
14775                            output.push_str(&s);
14776                        }
14777                    }
14778                    'b' => {
14779                        let expanded = self.expand_printf_escapes(&arg);
14780                        if let Some(p) = prec_val {
14781                            let s: String = expanded.chars().take(p).collect();
14782                            output.push_str(&s);
14783                        } else {
14784                            output.push_str(&expanded);
14785                        }
14786                    }
14787                    'c' => {
14788                        if let Some(ch) = arg.chars().next() {
14789                            output.push(ch);
14790                        }
14791                    }
14792                    'q' => {
14793                        output.push('\'');
14794                        for ch in arg.chars() {
14795                            if ch == '\'' {
14796                                output.push_str("'\\''");
14797                            } else {
14798                                output.push(ch);
14799                            }
14800                        }
14801                        output.push('\'');
14802                    }
14803                    'd' | 'i' => {
14804                        let val: i64 = if arg.starts_with("0x") || arg.starts_with("0X") {
14805                            i64::from_str_radix(&arg[2..], 16).unwrap_or(0)
14806                        } else if arg.starts_with("0") && arg.len() > 1 && !arg.contains('.') {
14807                            i64::from_str_radix(&arg[1..], 8).unwrap_or(0)
14808                        } else if arg.starts_with('\'') || arg.starts_with('"') {
14809                            arg.chars().nth(1).map(|c| c as i64).unwrap_or(0)
14810                        } else {
14811                            arg.parse().unwrap_or(0)
14812                        };
14813
14814                        let sign = if val < 0 {
14815                            "-"
14816                        } else if plus_sign {
14817                            "+"
14818                        } else if space_sign {
14819                            " "
14820                        } else {
14821                            ""
14822                        };
14823                        let abs_val = val.abs();
14824                        let num_str = abs_val.to_string();
14825                        let total_len = sign.len() + num_str.len();
14826
14827                        if width_val > total_len {
14828                            if left_align {
14829                                output.push_str(sign);
14830                                output.push_str(&num_str);
14831                                output.push_str(&" ".repeat(width_val - total_len));
14832                            } else if zero_pad {
14833                                output.push_str(sign);
14834                                output.push_str(&"0".repeat(width_val - total_len));
14835                                output.push_str(&num_str);
14836                            } else {
14837                                output.push_str(&" ".repeat(width_val - total_len));
14838                                output.push_str(sign);
14839                                output.push_str(&num_str);
14840                            }
14841                        } else {
14842                            output.push_str(sign);
14843                            output.push_str(&num_str);
14844                        }
14845                    }
14846                    'u' => {
14847                        let val: u64 = if arg.starts_with("0x") || arg.starts_with("0X") {
14848                            u64::from_str_radix(&arg[2..], 16).unwrap_or(0)
14849                        } else if arg.starts_with("0") && arg.len() > 1 {
14850                            u64::from_str_radix(&arg[1..], 8).unwrap_or(0)
14851                        } else {
14852                            arg.parse().unwrap_or(0)
14853                        };
14854                        let num_str = val.to_string();
14855                        if width_val > num_str.len() {
14856                            if left_align {
14857                                output.push_str(&num_str);
14858                                output.push_str(&" ".repeat(width_val - num_str.len()));
14859                            } else if zero_pad {
14860                                output.push_str(&"0".repeat(width_val - num_str.len()));
14861                                output.push_str(&num_str);
14862                            } else {
14863                                output.push_str(&" ".repeat(width_val - num_str.len()));
14864                                output.push_str(&num_str);
14865                            }
14866                        } else {
14867                            output.push_str(&num_str);
14868                        }
14869                    }
14870                    'o' => {
14871                        let val: u64 = arg.parse().unwrap_or(0);
14872                        let num_str = format!("{:o}", val);
14873                        let prefix = if alt_form && val != 0 { "0" } else { "" };
14874                        let total_len = prefix.len() + num_str.len();
14875                        if width_val > total_len {
14876                            if left_align {
14877                                output.push_str(prefix);
14878                                output.push_str(&num_str);
14879                                output.push_str(&" ".repeat(width_val - total_len));
14880                            } else {
14881                                output.push_str(&" ".repeat(width_val - total_len));
14882                                output.push_str(prefix);
14883                                output.push_str(&num_str);
14884                            }
14885                        } else {
14886                            output.push_str(prefix);
14887                            output.push_str(&num_str);
14888                        }
14889                    }
14890                    'x' => {
14891                        let val: u64 = arg.parse().unwrap_or(0);
14892                        let num_str = format!("{:x}", val);
14893                        let prefix = if alt_form && val != 0 { "0x" } else { "" };
14894                        let total_len = prefix.len() + num_str.len();
14895                        if width_val > total_len {
14896                            if left_align {
14897                                output.push_str(prefix);
14898                                output.push_str(&num_str);
14899                                output.push_str(&" ".repeat(width_val - total_len));
14900                            } else {
14901                                output.push_str(&" ".repeat(width_val - total_len));
14902                                output.push_str(prefix);
14903                                output.push_str(&num_str);
14904                            }
14905                        } else {
14906                            output.push_str(prefix);
14907                            output.push_str(&num_str);
14908                        }
14909                    }
14910                    'X' => {
14911                        let val: u64 = arg.parse().unwrap_or(0);
14912                        let num_str = format!("{:X}", val);
14913                        let prefix = if alt_form && val != 0 { "0X" } else { "" };
14914                        let total_len = prefix.len() + num_str.len();
14915                        if width_val > total_len {
14916                            if left_align {
14917                                output.push_str(prefix);
14918                                output.push_str(&num_str);
14919                                output.push_str(&" ".repeat(width_val - total_len));
14920                            } else {
14921                                output.push_str(&" ".repeat(width_val - total_len));
14922                                output.push_str(prefix);
14923                                output.push_str(&num_str);
14924                            }
14925                        } else {
14926                            output.push_str(prefix);
14927                            output.push_str(&num_str);
14928                        }
14929                    }
14930                    'e' | 'E' => {
14931                        let val: f64 = arg.parse().unwrap_or(0.0);
14932                        let prec = prec_val.unwrap_or(6);
14933                        let formatted = if specifier == 'e' {
14934                            format!("{:.prec$e}", val, prec = prec)
14935                        } else {
14936                            format!("{:.prec$E}", val, prec = prec)
14937                        };
14938                        if width_val > formatted.len() {
14939                            if left_align {
14940                                output.push_str(&formatted);
14941                                output.push_str(&" ".repeat(width_val - formatted.len()));
14942                            } else {
14943                                output.push_str(&" ".repeat(width_val - formatted.len()));
14944                                output.push_str(&formatted);
14945                            }
14946                        } else {
14947                            output.push_str(&formatted);
14948                        }
14949                    }
14950                    'f' | 'F' => {
14951                        let val: f64 = arg.parse().unwrap_or(0.0);
14952                        let prec = prec_val.unwrap_or(6);
14953                        let sign = if val < 0.0 {
14954                            "-"
14955                        } else if plus_sign {
14956                            "+"
14957                        } else if space_sign {
14958                            " "
14959                        } else {
14960                            ""
14961                        };
14962                        let formatted = format!("{:.prec$}", val.abs(), prec = prec);
14963                        let total = sign.len() + formatted.len();
14964                        if width_val > total {
14965                            if left_align {
14966                                output.push_str(sign);
14967                                output.push_str(&formatted);
14968                                output.push_str(&" ".repeat(width_val - total));
14969                            } else if zero_pad {
14970                                output.push_str(sign);
14971                                output.push_str(&"0".repeat(width_val - total));
14972                                output.push_str(&formatted);
14973                            } else {
14974                                output.push_str(&" ".repeat(width_val - total));
14975                                output.push_str(sign);
14976                                output.push_str(&formatted);
14977                            }
14978                        } else {
14979                            output.push_str(sign);
14980                            output.push_str(&formatted);
14981                        }
14982                    }
14983                    'g' | 'G' => {
14984                        let val: f64 = arg.parse().unwrap_or(0.0);
14985                        let prec = prec_val.unwrap_or(6).max(1);
14986                        let formatted = if specifier == 'g' {
14987                            format!("{:.prec$}", val, prec = prec)
14988                        } else {
14989                            format!("{:.prec$}", val, prec = prec).to_uppercase()
14990                        };
14991                        output.push_str(&formatted);
14992                    }
14993                    'a' | 'A' => {
14994                        let val: f64 = arg.parse().unwrap_or(0.0);
14995                        let formatted = float_to_hex(val, specifier == 'A');
14996                        output.push_str(&formatted);
14997                    }
14998                    _ => {
14999                        output.push('%');
15000                        output.push(specifier);
15001                    }
15002                }
15003            } else {
15004                output.push(c);
15005            }
15006        }
15007
15008        print!("{}", output);
15009        0
15010    }
15011
15012    fn expand_printf_escapes(&self, s: &str) -> String {
15013        let mut result = String::new();
15014        let mut chars = s.chars().peekable();
15015        while let Some(c) = chars.next() {
15016            if c == '\\' {
15017                match chars.next() {
15018                    Some('n') => result.push('\n'),
15019                    Some('t') => result.push('\t'),
15020                    Some('r') => result.push('\r'),
15021                    Some('\\') => result.push('\\'),
15022                    Some('a') => result.push('\x07'),
15023                    Some('b') => result.push('\x08'),
15024                    Some('e') | Some('E') => result.push('\x1b'),
15025                    Some('f') => result.push('\x0c'),
15026                    Some('v') => result.push('\x0b'),
15027                    Some('0') => {
15028                        let mut octal = String::new();
15029                        while octal.len() < 3 {
15030                            if let Some(&d) = chars.peek() {
15031                                if d >= '0' && d <= '7' {
15032                                    octal.push(d);
15033                                    chars.next();
15034                                } else {
15035                                    break;
15036                                }
15037                            } else {
15038                                break;
15039                            }
15040                        }
15041                        if octal.is_empty() {
15042                            result.push('\0');
15043                        } else if let Ok(val) = u8::from_str_radix(&octal, 8) {
15044                            result.push(val as char);
15045                        }
15046                    }
15047                    Some('c') => break,
15048                    Some(other) => {
15049                        result.push('\\');
15050                        result.push(other);
15051                    }
15052                    None => result.push('\\'),
15053                }
15054            } else {
15055                result.push(c);
15056            }
15057        }
15058        result
15059    }
15060
15061    fn evaluate_arithmetic_expr(&mut self, expr: &str) -> i64 {
15062        self.eval_arith_expr(expr)
15063    }
15064
15065    // ═══════════════════════════════════════════════════════════════════════════
15066    // Additional zsh builtins
15067    // ═══════════════════════════════════════════════════════════════════════════
15068
15069    /// break - exit from for/while/until loop
15070    fn builtin_break(&mut self, args: &[String]) -> i32 {
15071        let levels: i32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
15072        self.breaking = levels.max(1);
15073        0
15074    }
15075
15076    /// continue - skip to next iteration of loop
15077    fn builtin_continue(&mut self, args: &[String]) -> i32 {
15078        let levels: i32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
15079        self.continuing = levels.max(1);
15080        0
15081    }
15082
15083    /// disable - disable shell builtins, aliases, functions
15084    fn builtin_disable(&mut self, args: &[String]) -> i32 {
15085        let mut disable_aliases = false;
15086        let mut disable_builtins = false;
15087        let mut disable_functions = false;
15088        let mut names = Vec::new();
15089
15090        let mut iter = args.iter();
15091        while let Some(arg) = iter.next() {
15092            match arg.as_str() {
15093                "-a" => disable_aliases = true,
15094                "-f" => disable_functions = true,
15095                "-r" => disable_builtins = true,
15096                _ if arg.starts_with('-') => {}
15097                _ => names.push(arg.clone()),
15098            }
15099        }
15100
15101        // Default to builtins if no flags
15102        if !disable_aliases && !disable_functions {
15103            disable_builtins = true;
15104        }
15105
15106        for name in names {
15107            if disable_aliases {
15108                self.aliases.remove(&name);
15109            }
15110            if disable_functions {
15111                self.functions.remove(&name);
15112            }
15113            if disable_builtins {
15114                // Store disabled builtins
15115                self.options.insert(format!("_disabled_{}", name), true);
15116            }
15117        }
15118        0
15119    }
15120
15121    /// enable - enable disabled shell builtins
15122    fn builtin_enable(&mut self, args: &[String]) -> i32 {
15123        for arg in args {
15124            if !arg.starts_with('-') {
15125                self.options.remove(&format!("_disabled_{}", arg));
15126            }
15127        }
15128        0
15129    }
15130
15131    /// emulate - set up zsh emulation mode
15132    fn builtin_emulate(&mut self, args: &[String]) -> i32 {
15133        // emulate [ -lLR ] [ {zsh|sh|ksh|csh} [ flags ... ] ]
15134        // flags can include: -c arg, -o opt, +o opt
15135        let mut local_mode = false;
15136        let mut reset_mode = false;
15137        let mut list_mode = false;
15138        let mut mode: Option<String> = None;
15139        let mut command_arg: Option<String> = None;
15140        let mut extra_set_opts: Vec<String> = Vec::new();
15141        let mut extra_unset_opts: Vec<String> = Vec::new();
15142
15143        let mut i = 0;
15144        while i < args.len() {
15145            let arg = &args[i];
15146
15147            if arg == "-c" {
15148                // -c arg: evaluate arg in emulation mode
15149                i += 1;
15150                if i < args.len() {
15151                    command_arg = Some(args[i].clone());
15152                } else {
15153                    eprintln!("emulate: -c requires an argument");
15154                    return 1;
15155                }
15156            } else if arg == "-o" {
15157                // -o opt: set option
15158                i += 1;
15159                if i < args.len() {
15160                    extra_set_opts.push(args[i].clone());
15161                } else {
15162                    eprintln!("emulate: -o requires an argument");
15163                    return 1;
15164                }
15165            } else if arg == "+o" {
15166                // +o opt: unset option
15167                i += 1;
15168                if i < args.len() {
15169                    extra_unset_opts.push(args[i].clone());
15170                } else {
15171                    eprintln!("emulate: +o requires an argument");
15172                    return 1;
15173                }
15174            } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
15175                // Parse combined flags like -LR
15176                for ch in arg[1..].chars() {
15177                    match ch {
15178                        'L' => local_mode = true,
15179                        'R' => reset_mode = true,
15180                        'l' => list_mode = true,
15181                        _ => {
15182                            eprintln!("emulate: bad option: -{}", ch);
15183                            return 1;
15184                        }
15185                    }
15186                }
15187            } else if arg.starts_with('+') && arg.len() > 1 {
15188                // +X flags (unset single-letter options)
15189                for ch in arg[1..].chars() {
15190                    // Map single-letter to option name if needed
15191                    extra_unset_opts.push(ch.to_string());
15192                }
15193            } else if mode.is_none() {
15194                mode = Some(arg.clone());
15195            }
15196            i += 1;
15197        }
15198
15199        // -L and -c are mutually exclusive
15200        if local_mode && command_arg.is_some() {
15201            eprintln!("emulate: -L and -c are mutually exclusive");
15202            return 1;
15203        }
15204
15205        // No argument: print current emulation mode
15206        if mode.is_none() && !list_mode {
15207            let current = self
15208                .variables
15209                .get("EMULATE")
15210                .cloned()
15211                .unwrap_or_else(|| "zsh".to_string());
15212            println!("{}", current);
15213            return 0;
15214        }
15215
15216        let mode = mode.unwrap_or_else(|| "zsh".to_string());
15217
15218        // Get the options that would be set for this mode
15219        let (set_opts, unset_opts) = Self::emulate_mode_options(&mode, reset_mode);
15220
15221        // -l: just list the options, don't apply
15222        if list_mode {
15223            for opt in &set_opts {
15224                println!("{}", opt);
15225            }
15226            for opt in &unset_opts {
15227                println!("no{}", opt);
15228            }
15229            if local_mode {
15230                println!("localoptions");
15231                println!("localpatterns");
15232                println!("localtraps");
15233            }
15234            return 0;
15235        }
15236
15237        // Save current state if -c is used
15238        let saved_options = if command_arg.is_some() {
15239            Some(self.options.clone())
15240        } else {
15241            None
15242        };
15243        let saved_emulate = if command_arg.is_some() {
15244            self.variables.get("EMULATE").cloned()
15245        } else {
15246            None
15247        };
15248
15249        // Apply the emulation
15250        self.variables.insert("EMULATE".to_string(), mode.clone());
15251
15252        // Set options for this mode
15253        for opt in &set_opts {
15254            let opt_name = opt.to_lowercase().replace('_', "");
15255            self.options.insert(opt_name, true);
15256        }
15257        for opt in &unset_opts {
15258            let opt_name = opt.to_lowercase().replace('_', "");
15259            self.options.insert(opt_name, false);
15260        }
15261
15262        // Apply extra -o / +o options
15263        for opt in &extra_set_opts {
15264            let opt_name = opt.to_lowercase().replace('_', "");
15265            self.options.insert(opt_name, true);
15266        }
15267        for opt in &extra_unset_opts {
15268            let opt_name = opt.to_lowercase().replace('_', "");
15269            self.options.insert(opt_name, false);
15270        }
15271
15272        // -L: set local options/traps
15273        if local_mode {
15274            self.options.insert("localoptions".to_string(), true);
15275            self.options.insert("localpatterns".to_string(), true);
15276            self.options.insert("localtraps".to_string(), true);
15277        }
15278
15279        // -c arg: execute command then restore
15280        let result = if let Some(cmd) = command_arg {
15281            let status = self.execute_script(&cmd).unwrap_or(1);
15282
15283            // Restore saved state
15284            if let Some(opts) = saved_options {
15285                self.options = opts;
15286            }
15287            if let Some(emu) = saved_emulate {
15288                self.variables.insert("EMULATE".to_string(), emu);
15289            } else {
15290                self.variables.remove("EMULATE");
15291            }
15292
15293            status
15294        } else {
15295            0
15296        };
15297
15298        result
15299    }
15300
15301    /// Get options to set/unset for an emulation mode
15302    fn emulate_mode_options(mode: &str, reset: bool) -> (Vec<&'static str>, Vec<&'static str>) {
15303        match mode {
15304            "zsh" => {
15305                if reset {
15306                    // Full reset: return to zsh defaults
15307                    (
15308                        vec![
15309                            "aliases",
15310                            "alwayslastprompt",
15311                            "autolist",
15312                            "automenu",
15313                            "autoparamslash",
15314                            "autoremoveslash",
15315                            "banghist",
15316                            "bareglobqual",
15317                            "completeinword",
15318                            "extendedhistory",
15319                            "functionargzero",
15320                            "glob",
15321                            "hashcmds",
15322                            "hashdirs",
15323                            "histexpand",
15324                            "histignoredups",
15325                            "interactivecomments",
15326                            "listambiguous",
15327                            "listtypes",
15328                            "multios",
15329                            "nomatch",
15330                            "notify",
15331                            "promptpercent",
15332                            "promptsubst",
15333                        ],
15334                        vec![
15335                            "ksharrays",
15336                            "kshglob",
15337                            "shwordsplit",
15338                            "shglob",
15339                            "posixbuiltins",
15340                            "posixidentifiers",
15341                            "posixstrings",
15342                            "bsdecho",
15343                            "ignorebraces",
15344                        ],
15345                    )
15346                } else {
15347                    // Minimal changes for portability
15348                    (vec!["functionargzero"], vec!["ksharrays", "shwordsplit"])
15349                }
15350            }
15351            "sh" => {
15352                let set = vec![
15353                    "ksharrays",
15354                    "shwordsplit",
15355                    "posixbuiltins",
15356                    "shglob",
15357                    "shfileexpansion",
15358                    "globsubst",
15359                    "interactivecomments",
15360                    "rmstarsilent",
15361                    "bsdecho",
15362                    "ignorebraces",
15363                ];
15364                let unset = vec![
15365                    "badpattern",
15366                    "banghist",
15367                    "bgnice",
15368                    "equals",
15369                    "functionargzero",
15370                    "globalexport",
15371                    "multios",
15372                    "nomatch",
15373                    "notify",
15374                    "promptpercent",
15375                ];
15376                (set, unset)
15377            }
15378            "ksh" => {
15379                let set = vec![
15380                    "ksharrays",
15381                    "kshglob",
15382                    "shwordsplit",
15383                    "posixbuiltins",
15384                    "kshoptionprint",
15385                    "localoptions",
15386                    "promptbang",
15387                    "promptsubst",
15388                    "singlelinezle",
15389                    "interactivecomments",
15390                ];
15391                let unset = vec![
15392                    "badpattern",
15393                    "banghist",
15394                    "bgnice",
15395                    "equals",
15396                    "functionargzero",
15397                    "globalexport",
15398                    "multios",
15399                    "nomatch",
15400                    "notify",
15401                    "promptpercent",
15402                ];
15403                (set, unset)
15404            }
15405            "csh" => {
15406                // C shell emulation (limited)
15407                (vec!["cshnullglob", "cshjunkiequotes"], vec!["nomatch"])
15408            }
15409            "bash" => {
15410                let set = vec![
15411                    "ksharrays",
15412                    "shwordsplit",
15413                    "interactivecomments",
15414                    "shfileexpansion",
15415                    "globsubst",
15416                ];
15417                let unset = vec![
15418                    "badpattern",
15419                    "banghist",
15420                    "functionargzero",
15421                    "multios",
15422                    "nomatch",
15423                    "notify",
15424                    "promptpercent",
15425                ];
15426                (set, unset)
15427            }
15428            _ => (vec![], vec![]),
15429        }
15430    }
15431
15432    /// exec - replace the shell with a command
15433    fn builtin_exec(&mut self, args: &[String]) -> i32 {
15434        // exec [ -c ] [ -l ] [ -a argv0 ] [ command [ arg ... ] ]
15435        // -c: clear environment
15436        // -l: place - at front of argv[0] (login shell)
15437        // -a argv0: set argv[0] to specified name
15438
15439        let mut clear_env = false;
15440        let mut login_shell = false;
15441        let mut argv0: Option<String> = None;
15442        let mut cmd_args: Vec<String> = Vec::new();
15443
15444        let mut i = 0;
15445        while i < args.len() {
15446            let arg = &args[i];
15447
15448            if arg == "-c" && cmd_args.is_empty() {
15449                clear_env = true;
15450            } else if arg == "-l" && cmd_args.is_empty() {
15451                login_shell = true;
15452            } else if arg == "-a" && cmd_args.is_empty() {
15453                i += 1;
15454                if i < args.len() {
15455                    argv0 = Some(args[i].clone());
15456                }
15457            } else if arg.starts_with('-') && cmd_args.is_empty() {
15458                // Combined flags like -cl
15459                for ch in arg[1..].chars() {
15460                    match ch {
15461                        'c' => clear_env = true,
15462                        'l' => login_shell = true,
15463                        'a' => {
15464                            i += 1;
15465                            if i < args.len() {
15466                                argv0 = Some(args[i].clone());
15467                            }
15468                        }
15469                        _ => {}
15470                    }
15471                }
15472            } else {
15473                cmd_args.push(arg.clone());
15474            }
15475            i += 1;
15476        }
15477
15478        if cmd_args.is_empty() {
15479            // No command: just modify shell's environment
15480            if clear_env {
15481                for (key, _) in env::vars() {
15482                    env::remove_var(&key);
15483                }
15484            }
15485            return 0;
15486        }
15487
15488        let cmd = &cmd_args[0];
15489        let rest_args: Vec<&str> = cmd_args[1..].iter().map(|s| s.as_str()).collect();
15490
15491        // Determine argv[0]
15492        let effective_argv0 = if let Some(a0) = argv0 {
15493            a0
15494        } else if login_shell {
15495            format!("-{}", cmd)
15496        } else {
15497            cmd.clone()
15498        };
15499
15500        use std::os::unix::process::CommandExt;
15501        let mut command = std::process::Command::new(cmd);
15502        command.arg0(&effective_argv0);
15503        command.args(&rest_args);
15504
15505        if clear_env {
15506            command.env_clear();
15507        }
15508
15509        let err = command.exec();
15510        eprintln!("exec: {}: {}", cmd, err);
15511        1
15512    }
15513
15514    /// float - declare floating point variables
15515    fn builtin_float(&mut self, args: &[String]) -> i32 {
15516        for arg in args {
15517            if arg.starts_with('-') {
15518                continue;
15519            }
15520            if let Some(eq_pos) = arg.find('=') {
15521                let name = &arg[..eq_pos];
15522                let value = &arg[eq_pos + 1..];
15523                let float_val: f64 = value.parse().unwrap_or(0.0);
15524                self.variables
15525                    .insert(name.to_string(), float_val.to_string());
15526                self.options.insert(format!("_float_{}", name), true);
15527            } else {
15528                self.variables.insert(arg.clone(), "0.0".to_string());
15529                self.options.insert(format!("_float_{}", arg), true);
15530            }
15531        }
15532        0
15533    }
15534
15535    /// integer - declare integer variables
15536    fn builtin_integer(&mut self, args: &[String]) -> i32 {
15537        for arg in args {
15538            if arg.starts_with('-') {
15539                continue;
15540            }
15541            if let Some(eq_pos) = arg.find('=') {
15542                let name = &arg[..eq_pos];
15543                let value = &arg[eq_pos + 1..];
15544                let int_val: i64 = value.parse().unwrap_or(0);
15545                self.variables.insert(name.to_string(), int_val.to_string());
15546                self.options.insert(format!("_integer_{}", name), true);
15547            } else {
15548                self.variables.insert(arg.clone(), "0".to_string());
15549                self.options.insert(format!("_integer_{}", arg), true);
15550            }
15551        }
15552        0
15553    }
15554
15555    /// functions - list or manipulate function definitions
15556    fn builtin_functions(&self, args: &[String]) -> i32 {
15557        let mut list_only = false;
15558        let mut show_trace = false;
15559        let mut names: Vec<&str> = Vec::new();
15560
15561        for arg in args {
15562            match arg.as_str() {
15563                "-l" => list_only = true,
15564                "-t" => show_trace = true,
15565                _ if arg.starts_with('-') => {}
15566                _ => names.push(arg),
15567            }
15568        }
15569
15570        if names.is_empty() {
15571            // List all functions
15572            let mut func_names: Vec<_> = self.functions.keys().collect();
15573            func_names.sort();
15574            for name in func_names {
15575                if list_only {
15576                    println!("{}", name);
15577                } else if let Some(func) = self.functions.get(name) {
15578                    let body = crate::text::getpermtext(func);
15579                    println!("{} () {{\n\t{}\n}}", name, body.trim());
15580                }
15581            }
15582        } else {
15583            // Show specific functions
15584            for name in names {
15585                if let Some(func) = self.functions.get(name) {
15586                    if show_trace {
15587                        println!("functions -t {}", name);
15588                    } else {
15589                        let body = crate::text::getpermtext(func);
15590                        println!("{} () {{\n\t{}\n}}", name, body.trim());
15591                    }
15592                } else {
15593                    eprintln!("functions: no such function: {}", name);
15594                    return 1;
15595                }
15596            }
15597        }
15598        0
15599    }
15600
15601    /// print - zsh print builtin with many options
15602    fn builtin_print(&mut self, args: &[String]) -> i32 {
15603        // print [ -abcDilmnNoOpPrsSz ] [ -u n ] [ -f format ] [ -C cols ]
15604        //       [ -v name ] [ -xX tabstop ] [ -R [ -en ]] [ arg ... ]
15605        let mut no_newline = false;
15606        let mut one_per_line = false;
15607        let mut interpret_escapes = true; // zsh default is to interpret
15608        let mut raw_mode = false;
15609        let mut prompt_expand = false;
15610        let mut fd: i32 = 1; // stdout
15611        let mut columns = 0usize;
15612        let mut null_terminate = false;
15613        let mut push_to_stack = false;
15614        let mut add_to_history = false;
15615        let mut sort_asc = false;
15616        let mut sort_desc = false;
15617        let mut named_dir_subst = false;
15618        let mut store_var: Option<String> = None;
15619        let mut format_string: Option<String> = None;
15620        let mut output_args: Vec<String> = Vec::new();
15621
15622        let mut i = 0;
15623        while i < args.len() {
15624            let arg = &args[i];
15625
15626            if arg == "--" {
15627                i += 1;
15628                while i < args.len() {
15629                    output_args.push(args[i].clone());
15630                    i += 1;
15631                }
15632                break;
15633            }
15634
15635            if arg.starts_with('-')
15636                && arg.len() > 1
15637                && !arg
15638                    .chars()
15639                    .nth(1)
15640                    .map(|c| c.is_ascii_digit())
15641                    .unwrap_or(false)
15642            {
15643                let mut chars = arg[1..].chars().peekable();
15644                while let Some(ch) = chars.next() {
15645                    match ch {
15646                        'n' => no_newline = true,
15647                        'l' => one_per_line = true,
15648                        'r' => {
15649                            raw_mode = true;
15650                            interpret_escapes = false;
15651                        }
15652                        'R' => {
15653                            raw_mode = true;
15654                            interpret_escapes = false;
15655                        }
15656                        'e' => interpret_escapes = true,
15657                        'E' => interpret_escapes = false,
15658                        'P' => prompt_expand = true,
15659                        'N' => null_terminate = true,
15660                        'z' => push_to_stack = true,
15661                        's' => add_to_history = true,
15662                        'o' => sort_asc = true,
15663                        'O' => sort_desc = true,
15664                        'D' => named_dir_subst = true,
15665                        'c' => columns = 1,
15666                        'a' | 'b' | 'i' | 'm' | 'p' | 'S' | 'x' | 'X' => {} // TODO
15667                        'u' => {
15668                            // -u n: output to fd n
15669                            let rest: String = chars.collect();
15670                            if !rest.is_empty() {
15671                                fd = rest.parse().unwrap_or(1);
15672                            } else {
15673                                i += 1;
15674                                if i < args.len() {
15675                                    fd = args[i].parse().unwrap_or(1);
15676                                }
15677                            }
15678                            break;
15679                        }
15680                        'C' => {
15681                            // -C n: n columns
15682                            let rest: String = chars.collect();
15683                            if !rest.is_empty() {
15684                                columns = rest.parse().unwrap_or(0);
15685                            } else {
15686                                i += 1;
15687                                if i < args.len() {
15688                                    columns = args[i].parse().unwrap_or(0);
15689                                }
15690                            }
15691                            break;
15692                        }
15693                        'v' => {
15694                            // -v name: store in variable
15695                            let rest: String = chars.collect();
15696                            if !rest.is_empty() {
15697                                store_var = Some(rest);
15698                            } else {
15699                                i += 1;
15700                                if i < args.len() {
15701                                    store_var = Some(args[i].clone());
15702                                }
15703                            }
15704                            break;
15705                        }
15706                        'f' => {
15707                            // -f format: printf-style format
15708                            let rest: String = chars.collect();
15709                            if !rest.is_empty() {
15710                                format_string = Some(rest);
15711                            } else {
15712                                i += 1;
15713                                if i < args.len() {
15714                                    format_string = Some(args[i].clone());
15715                                }
15716                            }
15717                            break;
15718                        }
15719                        _ => {}
15720                    }
15721                }
15722            } else {
15723                output_args.push(arg.clone());
15724            }
15725            i += 1;
15726        }
15727
15728        let _ = push_to_stack; // TODO: implement push to buffer stack
15729        let _ = fd; // TODO: implement fd selection
15730
15731        // Sort if requested
15732        if sort_asc {
15733            output_args.sort();
15734        } else if sort_desc {
15735            output_args.sort_by(|a, b| b.cmp(a));
15736        }
15737
15738        // Handle -f format
15739        if let Some(fmt) = format_string {
15740            let output = self.printf_format(&fmt, &output_args);
15741            if let Some(var) = store_var {
15742                self.variables.insert(var, output);
15743            } else {
15744                print!("{}", output);
15745            }
15746            return 0;
15747        }
15748
15749        // Process output
15750        let processed: Vec<String> = output_args
15751            .iter()
15752            .map(|s| {
15753                let mut result = s.clone();
15754                if prompt_expand {
15755                    result = self.expand_prompt_string(&result);
15756                }
15757                if interpret_escapes && !raw_mode {
15758                    result = self.expand_printf_escapes(&result);
15759                }
15760                if named_dir_subst {
15761                    // Replace home dir with ~
15762                    if let Ok(home) = env::var("HOME") {
15763                        if result.starts_with(&home) {
15764                            result = format!("~{}", &result[home.len()..]);
15765                        }
15766                    }
15767                    // Replace named dirs
15768                    for (name, path) in &self.named_dirs {
15769                        let path_str = path.to_string_lossy();
15770                        if result.starts_with(path_str.as_ref()) {
15771                            result = format!("~{}{}", name, &result[path_str.len()..]);
15772                            break;
15773                        }
15774                    }
15775                }
15776                result
15777            })
15778            .collect();
15779
15780        // Determine separator and terminator
15781        let separator = if one_per_line { "\n" } else { " " };
15782        let terminator = if null_terminate {
15783            "\0"
15784        } else if no_newline {
15785            ""
15786        } else {
15787            "\n"
15788        };
15789
15790        // Build output
15791        let output = if one_per_line {
15792            processed.join("\n")
15793        } else if columns > 0 {
15794            // Column output - calculate column widths
15795            let mut result = String::new();
15796            let num_items = processed.len();
15797            let rows = (num_items + columns - 1) / columns;
15798            for row in 0..rows {
15799                let mut row_items = Vec::new();
15800                for col in 0..columns {
15801                    let idx = row + col * rows;
15802                    if idx < num_items {
15803                        row_items.push(processed[idx].as_str());
15804                    }
15805                }
15806                result.push_str(&row_items.join("\t"));
15807                if row < rows - 1 {
15808                    result.push('\n');
15809                }
15810            }
15811            result
15812        } else {
15813            processed.join(separator)
15814        };
15815
15816        // Add to history if -s
15817        if add_to_history {
15818            if let Some(ref mut engine) = self.history {
15819                let _ = engine.add(&output, None);
15820            }
15821        }
15822
15823        // Store in variable or print
15824        if let Some(var) = store_var {
15825            self.variables.insert(var, output);
15826        } else {
15827            print!("{}{}", output, terminator);
15828        }
15829
15830        0
15831    }
15832
15833    fn printf_format(&self, format: &str, args: &[String]) -> String {
15834        let mut result = String::new();
15835        let mut arg_idx = 0;
15836        let mut chars = format.chars().peekable();
15837
15838        while let Some(ch) = chars.next() {
15839            if ch == '%' {
15840                if chars.peek() == Some(&'%') {
15841                    chars.next();
15842                    result.push('%');
15843                    continue;
15844                }
15845
15846                // Parse format specifier
15847                let mut spec = String::from("%");
15848
15849                // Flags
15850                while let Some(&c) = chars.peek() {
15851                    if c == '-' || c == '+' || c == ' ' || c == '#' || c == '0' {
15852                        spec.push(c);
15853                        chars.next();
15854                    } else {
15855                        break;
15856                    }
15857                }
15858
15859                // Width
15860                while let Some(&c) = chars.peek() {
15861                    if c.is_ascii_digit() {
15862                        spec.push(c);
15863                        chars.next();
15864                    } else {
15865                        break;
15866                    }
15867                }
15868
15869                // Precision
15870                if chars.peek() == Some(&'.') {
15871                    spec.push('.');
15872                    chars.next();
15873                    while let Some(&c) = chars.peek() {
15874                        if c.is_ascii_digit() {
15875                            spec.push(c);
15876                            chars.next();
15877                        } else {
15878                            break;
15879                        }
15880                    }
15881                }
15882
15883                // Conversion specifier
15884                if let Some(conv) = chars.next() {
15885                    let arg = args.get(arg_idx).map(|s| s.as_str()).unwrap_or("");
15886                    arg_idx += 1;
15887
15888                    match conv {
15889                        's' => result.push_str(arg),
15890                        'd' | 'i' => {
15891                            let n: i64 = arg.parse().unwrap_or(0);
15892                            result.push_str(&n.to_string());
15893                        }
15894                        'u' => {
15895                            let n: u64 = arg.parse().unwrap_or(0);
15896                            result.push_str(&n.to_string());
15897                        }
15898                        'x' => {
15899                            let n: i64 = arg.parse().unwrap_or(0);
15900                            result.push_str(&format!("{:x}", n));
15901                        }
15902                        'X' => {
15903                            let n: i64 = arg.parse().unwrap_or(0);
15904                            result.push_str(&format!("{:X}", n));
15905                        }
15906                        'o' => {
15907                            let n: i64 = arg.parse().unwrap_or(0);
15908                            result.push_str(&format!("{:o}", n));
15909                        }
15910                        'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
15911                            let n: f64 = arg.parse().unwrap_or(0.0);
15912                            result.push_str(&format!("{}", n));
15913                        }
15914                        'c' => {
15915                            if let Some(c) = arg.chars().next() {
15916                                result.push(c);
15917                            }
15918                        }
15919                        'b' => {
15920                            result.push_str(&self.expand_printf_escapes(arg));
15921                        }
15922                        'n' => result.push('\n'),
15923                        _ => {
15924                            result.push('%');
15925                            result.push(conv);
15926                        }
15927                    }
15928                }
15929            } else {
15930                result.push(ch);
15931            }
15932        }
15933
15934        result
15935    }
15936
15937    /// whence - show how a command would be interpreted
15938    fn builtin_whence(&self, args: &[String]) -> i32 {
15939        // whence [ -vcwfpamsS ] [ -x num ] name ...
15940        // -v: verbose (like type)
15941        // -c: csh-style output
15942        // -w: print word type (alias, builtin, command, function, hashed, reserved, none)
15943        // -f: skip functions
15944        // -p: search path only
15945        // -a: show all matches
15946        // -m: pattern match with glob
15947        // -s: show symlink resolution
15948        // -S: show steps of symlink resolution
15949        // -x num: expand tabs to num spaces
15950
15951        let mut verbose = false;
15952        let mut csh_style = false;
15953        let mut word_type = false;
15954        let mut skip_functions = false;
15955        let mut path_only = false;
15956        let mut show_all = false;
15957        let mut pattern_mode = false;
15958        let mut show_symlink = false;
15959        let mut show_symlink_steps = false;
15960        let mut tab_expand: Option<usize> = None;
15961        let mut names: Vec<&str> = Vec::new();
15962
15963        let mut i = 0;
15964        while i < args.len() {
15965            let arg = &args[i];
15966
15967            if arg == "--" {
15968                i += 1;
15969                while i < args.len() {
15970                    names.push(&args[i]);
15971                    i += 1;
15972                }
15973                break;
15974            }
15975
15976            if arg.starts_with('-') && arg.len() > 1 {
15977                let mut chars = arg[1..].chars().peekable();
15978                while let Some(ch) = chars.next() {
15979                    match ch {
15980                        'v' => verbose = true,
15981                        'c' => csh_style = true,
15982                        'w' => word_type = true,
15983                        'f' => skip_functions = true,
15984                        'p' => path_only = true,
15985                        'a' => show_all = true,
15986                        'm' => pattern_mode = true,
15987                        's' => show_symlink = true,
15988                        'S' => show_symlink_steps = true,
15989                        'x' => {
15990                            // -x num: tab expansion
15991                            let rest: String = chars.collect();
15992                            if !rest.is_empty() {
15993                                tab_expand = rest.parse().ok();
15994                            } else {
15995                                i += 1;
15996                                if i < args.len() {
15997                                    tab_expand = args[i].parse().ok();
15998                                }
15999                            }
16000                            break;
16001                        }
16002                        _ => {}
16003                    }
16004                }
16005            } else {
16006                names.push(arg);
16007            }
16008            i += 1;
16009        }
16010
16011        let _ = csh_style; // TODO: implement csh-style output
16012        let _ = pattern_mode; // TODO: implement glob pattern matching
16013        let _ = tab_expand;
16014
16015        let mut status = 0;
16016        for name in names {
16017            let mut found = false;
16018            let mut word = "none";
16019
16020            if !path_only {
16021                // Check reserved words
16022                if self.is_reserved_word(name) {
16023                    found = true;
16024                    word = "reserved";
16025                    if word_type {
16026                        println!("{}: {}", name, word);
16027                    } else if verbose {
16028                        println!("{} is a reserved word", name);
16029                    } else {
16030                        println!("{}", name);
16031                    }
16032                    if !show_all {
16033                        continue;
16034                    }
16035                }
16036
16037                // Check aliases
16038                if let Some(alias_val) = self.aliases.get(name) {
16039                    found = true;
16040                    word = "alias";
16041                    if word_type {
16042                        println!("{}: {}", name, word);
16043                    } else if verbose {
16044                        println!("{} is an alias for {}", name, alias_val);
16045                    } else {
16046                        println!("{}", alias_val);
16047                    }
16048                    if !show_all {
16049                        continue;
16050                    }
16051                }
16052
16053                // Check functions (unless -f)
16054                if !skip_functions && self.functions.contains_key(name) {
16055                    found = true;
16056                    word = "function";
16057                    if word_type {
16058                        println!("{}: {}", name, word);
16059                    } else if verbose {
16060                        println!("{} is a shell function", name);
16061                    } else {
16062                        println!("{}", name);
16063                    }
16064                    if !show_all {
16065                        continue;
16066                    }
16067                }
16068
16069                // Check builtins
16070                if self.is_builtin(name) {
16071                    found = true;
16072                    word = "builtin";
16073                    if word_type {
16074                        println!("{}: {}", name, word);
16075                    } else if verbose {
16076                        println!("{} is a shell builtin", name);
16077                    } else {
16078                        println!("{}", name);
16079                    }
16080                    if !show_all {
16081                        continue;
16082                    }
16083                }
16084
16085                // Check hashed commands (named_dirs can serve as a command hash)
16086                // The hash builtin adds to named_dirs for now
16087                if let Some(path) = self.named_dirs.get(name) {
16088                    found = true;
16089                    word = "hashed";
16090                    if word_type {
16091                        println!("{}: {}", name, word);
16092                    } else if verbose {
16093                        println!("{} is hashed ({})", name, path.display());
16094                    } else {
16095                        println!("{}", path.display());
16096                    }
16097                    if !show_all {
16098                        continue;
16099                    }
16100                }
16101            }
16102
16103            // Check PATH
16104            if let Some(path) = self.find_in_path(name) {
16105                found = true;
16106                word = "command";
16107
16108                // Handle symlink resolution
16109                let display_path = if show_symlink || show_symlink_steps {
16110                    let p = std::path::Path::new(&path);
16111                    if show_symlink_steps {
16112                        let mut current = p.to_path_buf();
16113                        let mut steps = vec![path.clone()];
16114                        while let Ok(target) = std::fs::read_link(&current) {
16115                            let resolved = if target.is_absolute() {
16116                                target.clone()
16117                            } else {
16118                                current
16119                                    .parent()
16120                                    .unwrap_or(std::path::Path::new("/"))
16121                                    .join(&target)
16122                            };
16123                            steps.push(resolved.to_string_lossy().to_string());
16124                            current = resolved;
16125                        }
16126                        steps.join(" -> ")
16127                    } else {
16128                        match p.canonicalize() {
16129                            Ok(resolved) => format!("{} -> {}", path, resolved.display()),
16130                            Err(_) => path.clone(),
16131                        }
16132                    }
16133                } else {
16134                    path.clone()
16135                };
16136
16137                if word_type {
16138                    println!("{}: {}", name, word);
16139                } else if verbose {
16140                    println!("{} is {}", name, display_path);
16141                } else {
16142                    println!("{}", display_path);
16143                }
16144            }
16145
16146            if !found {
16147                if word_type {
16148                    println!("{}: none", name);
16149                } else if verbose {
16150                    println!("{} not found", name);
16151                }
16152                status = 1;
16153            }
16154        }
16155        status
16156    }
16157
16158    fn is_reserved_word(&self, name: &str) -> bool {
16159        matches!(
16160            name,
16161            "if" | "then"
16162                | "else"
16163                | "elif"
16164                | "fi"
16165                | "case"
16166                | "esac"
16167                | "for"
16168                | "select"
16169                | "while"
16170                | "until"
16171                | "do"
16172                | "done"
16173                | "in"
16174                | "function"
16175                | "time"
16176                | "coproc"
16177                | "{"
16178                | "}"
16179                | "!"
16180                | "[["
16181                | "]]"
16182                | "(("
16183                | "))"
16184        )
16185    }
16186
16187    /// where - show all locations of a command
16188    fn builtin_where(&self, args: &[String]) -> i32 {
16189        // where is like whence -ca
16190        let mut new_args = vec!["-a".to_string(), "-v".to_string()];
16191        new_args.extend(args.iter().cloned());
16192        self.builtin_whence(&new_args)
16193    }
16194
16195    /// which - show path of command
16196    fn builtin_which(&self, args: &[String]) -> i32 {
16197        // which is like whence -c
16198        let mut new_args = vec!["-c".to_string()];
16199        new_args.extend(args.iter().cloned());
16200        self.builtin_whence(&new_args)
16201    }
16202
16203    /// Helper to check if name is a builtin
16204    /// O(1) builtin check via static HashSet — replaces 130+ arm linear match
16205    fn is_builtin(&self, name: &str) -> bool {
16206        BUILTIN_SET.contains(name) || name.starts_with('_')
16207    }
16208
16209    /// Helper to find command in PATH — checks command_hash first for O(1) hit
16210    fn find_in_path(&self, name: &str) -> Option<String> {
16211        // O(1) hash table lookup from rehash
16212        if let Some(path) = self.command_hash.get(name) {
16213            return Some(path.clone());
16214        }
16215        // Fallback: linear PATH walk
16216        let path_var = env::var("PATH").unwrap_or_default();
16217        for dir in path_var.split(':') {
16218            let full_path = format!("{}/{}", dir, name);
16219            if std::path::Path::new(&full_path).exists() {
16220                return Some(full_path);
16221            }
16222        }
16223        None
16224    }
16225
16226    /// ulimit - get/set resource limits
16227    fn builtin_ulimit(&self, args: &[String]) -> i32 {
16228        use libc::{getrlimit, rlimit, setrlimit};
16229        use libc::{RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE};
16230        use libc::{RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK};
16231
16232        let mut resource = RLIMIT_FSIZE; // default: file size
16233        let mut hard = false;
16234        let mut soft = true;
16235        let mut value: Option<u64> = None;
16236
16237        let mut iter = args.iter();
16238        while let Some(arg) = iter.next() {
16239            match arg.as_str() {
16240                "-H" => {
16241                    hard = true;
16242                    soft = false;
16243                }
16244                "-S" => {
16245                    soft = true;
16246                    hard = false;
16247                }
16248                "-a" => {
16249                    // Print all limits
16250                    self.print_all_limits(soft);
16251                    return 0;
16252                }
16253                "-c" => resource = RLIMIT_CORE,
16254                "-d" => resource = RLIMIT_DATA,
16255                "-f" => resource = RLIMIT_FSIZE,
16256                "-n" => resource = RLIMIT_NOFILE,
16257                "-s" => resource = RLIMIT_STACK,
16258                "-t" => resource = RLIMIT_CPU,
16259                "-u" => resource = RLIMIT_NPROC,
16260                "-v" => resource = RLIMIT_AS,
16261                "-m" => resource = RLIMIT_RSS,
16262                "unlimited" => value = Some(libc::RLIM_INFINITY as u64),
16263                _ if !arg.starts_with('-') => {
16264                    value = arg.parse().ok();
16265                }
16266                _ => {}
16267            }
16268        }
16269
16270        let mut rlim = rlimit {
16271            rlim_cur: 0,
16272            rlim_max: 0,
16273        };
16274        unsafe {
16275            if getrlimit(resource, &mut rlim) != 0 {
16276                eprintln!("ulimit: cannot get limit");
16277                return 1;
16278            }
16279        }
16280
16281        if let Some(v) = value {
16282            // Set limit
16283            if soft {
16284                rlim.rlim_cur = v as libc::rlim_t;
16285            }
16286            if hard {
16287                rlim.rlim_max = v as libc::rlim_t;
16288            }
16289            unsafe {
16290                if setrlimit(resource, &rlim) != 0 {
16291                    eprintln!("ulimit: cannot set limit");
16292                    return 1;
16293                }
16294            }
16295        } else {
16296            // Print limit
16297            let limit = if hard { rlim.rlim_max } else { rlim.rlim_cur };
16298            if limit == libc::RLIM_INFINITY as libc::rlim_t {
16299                println!("unlimited");
16300            } else {
16301                println!("{}", limit);
16302            }
16303        }
16304        0
16305    }
16306
16307    fn print_all_limits(&self, soft: bool) {
16308        use libc::{getrlimit, rlimit};
16309        use libc::{RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE};
16310        use libc::{RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK};
16311
16312        let limits = [
16313            (RLIMIT_CORE, "core file size", "blocks", 512),
16314            (RLIMIT_DATA, "data seg size", "kbytes", 1024),
16315            (RLIMIT_FSIZE, "file size", "blocks", 512),
16316            (RLIMIT_NOFILE, "open files", "", 1),
16317            (RLIMIT_STACK, "stack size", "kbytes", 1024),
16318            (RLIMIT_CPU, "cpu time", "seconds", 1),
16319            (RLIMIT_NPROC, "max user processes", "", 1),
16320            (RLIMIT_AS, "virtual memory", "kbytes", 1024),
16321            (RLIMIT_RSS, "max memory size", "kbytes", 1024),
16322        ];
16323
16324        for (resource, name, unit, divisor) in limits {
16325            let mut rlim = rlimit {
16326                rlim_cur: 0,
16327                rlim_max: 0,
16328            };
16329            unsafe {
16330                if getrlimit(resource, &mut rlim) == 0 {
16331                    let limit = if soft { rlim.rlim_cur } else { rlim.rlim_max };
16332                    let unit_str = if unit.is_empty() {
16333                        ""
16334                    } else {
16335                        &format!("({})", unit)
16336                    };
16337                    if limit == libc::RLIM_INFINITY as libc::rlim_t {
16338                        println!("{:25} {} unlimited", name, unit_str);
16339                    } else {
16340                        println!("{:25} {} {}", name, unit_str, limit / divisor);
16341                    }
16342                }
16343            }
16344        }
16345    }
16346
16347    /// limit - csh-style resource limits
16348    fn builtin_limit(&self, args: &[String]) -> i32 {
16349        // Delegate to ulimit with csh-style names
16350        if args.is_empty() {
16351            // Print all resource limits in csh format
16352            use libc::{getrlimit, rlimit, RLIM_INFINITY};
16353            let resources = [
16354                (libc::RLIMIT_CPU, "cputime", 1, "seconds"),
16355                (libc::RLIMIT_FSIZE, "filesize", 1024, "kB"),
16356                (libc::RLIMIT_DATA, "datasize", 1024, "kB"),
16357                (libc::RLIMIT_STACK, "stacksize", 1024, "kB"),
16358                (libc::RLIMIT_CORE, "coredumpsize", 1024, "kB"),
16359                (libc::RLIMIT_RSS, "memoryuse", 1024, "kB"),
16360                #[cfg(target_os = "linux")]
16361                (libc::RLIMIT_NPROC, "maxproc", 1, ""),
16362                (libc::RLIMIT_NOFILE, "descriptors", 1, ""),
16363            ];
16364            for (res, name, divisor, unit) in resources {
16365                let mut rl: rlimit = unsafe { std::mem::zeroed() };
16366                unsafe { getrlimit(res, &mut rl); }
16367                let val = if rl.rlim_cur == RLIM_INFINITY as u64 {
16368                    "unlimited".to_string()
16369                } else {
16370                    let v = rl.rlim_cur as u64 / divisor;
16371                    if unit.is_empty() { format!("{}", v) } else { format!("{}{}", v, unit) }
16372                };
16373                println!("{:<16}{}", name, val);
16374            }
16375            return 0;
16376        }
16377        self.builtin_ulimit(args)
16378    }
16379
16380    /// unlimit - remove resource limits
16381    fn builtin_unlimit(&self, args: &[String]) -> i32 {
16382        let mut new_args = args.to_vec();
16383        new_args.push("unlimited".to_string());
16384        self.builtin_ulimit(&new_args)
16385    }
16386
16387    /// umask - get/set file creation mask
16388    fn builtin_umask(&self, args: &[String]) -> i32 {
16389        use libc::umask;
16390
16391        let mut symbolic = false;
16392        let mut value: Option<&str> = None;
16393
16394        for arg in args {
16395            match arg.as_str() {
16396                "-S" => symbolic = true,
16397                _ if !arg.starts_with('-') => value = Some(arg),
16398                _ => {}
16399            }
16400        }
16401
16402        if let Some(v) = value {
16403            // Set umask
16404            if let Ok(mask) = u32::from_str_radix(v, 8) {
16405                unsafe {
16406                    umask(mask as libc::mode_t);
16407                }
16408            } else {
16409                eprintln!("umask: invalid mask: {}", v);
16410                return 1;
16411            }
16412        } else {
16413            // Get umask
16414            let mask = unsafe {
16415                let m = umask(0);
16416                umask(m);
16417                m
16418            };
16419            if symbolic {
16420                let u = 7 - ((mask >> 6) & 7);
16421                let g = 7 - ((mask >> 3) & 7);
16422                let o = 7 - (mask & 7);
16423                println!(
16424                    "u={}{}{}g={}{}{}o={}{}{}",
16425                    if u & 4 != 0 { "r" } else { "" },
16426                    if u & 2 != 0 { "w" } else { "" },
16427                    if u & 1 != 0 { "x" } else { "" },
16428                    if g & 4 != 0 { "r" } else { "" },
16429                    if g & 2 != 0 { "w" } else { "" },
16430                    if g & 1 != 0 { "x" } else { "" },
16431                    if o & 4 != 0 { "r" } else { "" },
16432                    if o & 2 != 0 { "w" } else { "" },
16433                    if o & 1 != 0 { "x" } else { "" },
16434                );
16435            } else {
16436                println!("{:04o}", mask);
16437            }
16438        }
16439        0
16440    }
16441
16442    /// rehash - rebuild command hash table
16443    fn builtin_rehash(&mut self, args: &[String]) -> i32 {
16444        // rehash [ -d ] [ -f ] [ -v ]
16445        // -d: rehash named directories
16446        // -f: force rehash of all commands in PATH
16447        // -v: verbose (print each command being hashed)
16448
16449        let mut rehash_dirs = false;
16450        let mut force = false;
16451        let mut verbose = false;
16452
16453        for arg in args {
16454            if arg.starts_with('-') {
16455                for ch in arg[1..].chars() {
16456                    match ch {
16457                        'd' => rehash_dirs = true,
16458                        'f' => force = true,
16459                        'v' => verbose = true,
16460                        _ => {}
16461                    }
16462                }
16463            }
16464        }
16465
16466        if rehash_dirs {
16467            // Rebuild named directories from special params like ~user
16468            // For now just clear and rebuild from HOME
16469            self.named_dirs.clear();
16470            if let Ok(home) = env::var("HOME") {
16471                self.named_dirs.insert(String::new(), PathBuf::from(&home)); // ~ without name
16472            }
16473            return 0;
16474        }
16475
16476        // Clear command hash table
16477        self.command_hash.clear();
16478
16479        if force {
16480            // Parallel PATH scan — each PATH dir on a pool thread.
16481            // zsh does this single-threaded; we fan out across workers.
16482            if let Ok(path_var) = env::var("PATH") {
16483                let dirs: Vec<String> = path_var
16484                    .split(':')
16485                    .filter(|s| !s.is_empty())
16486                    .map(|s| s.to_string())
16487                    .collect();
16488
16489                let (tx, rx) = std::sync::mpsc::channel::<Vec<(String, String)>>();
16490
16491                for dir in dirs {
16492                    let tx = tx.clone();
16493                    self.worker_pool.submit(move || {
16494                        let mut batch = Vec::new();
16495                        if let Ok(entries) = std::fs::read_dir(&dir) {
16496                            for entry in entries.flatten() {
16497                                if let Ok(ft) = entry.file_type() {
16498                                    if ft.is_file() || ft.is_symlink() {
16499                                        if let Some(name) = entry.file_name().to_str() {
16500                                            let path =
16501                                                entry.path().to_string_lossy().to_string();
16502                                            batch.push((name.to_string(), path));
16503                                        }
16504                                    }
16505                                }
16506                            }
16507                        }
16508                        let _ = tx.send(batch);
16509                    });
16510                }
16511                drop(tx);
16512
16513                for batch in rx {
16514                    for (name, path) in batch {
16515                        if verbose {
16516                            println!("{}={}", name, path);
16517                        }
16518                        self.command_hash.insert(name, path);
16519                    }
16520                }
16521            }
16522        }
16523
16524        0
16525    }
16526
16527    /// unhash - remove entries from hash table
16528    fn builtin_unhash(&mut self, args: &[String]) -> i32 {
16529        let mut remove_aliases = false;
16530        let mut remove_functions = false;
16531        let mut remove_dirs = false;
16532        let mut names: Vec<&str> = Vec::new();
16533
16534        for arg in args {
16535            match arg.as_str() {
16536                "-a" => remove_aliases = true,
16537                "-f" => remove_functions = true,
16538                "-d" => remove_dirs = true,
16539                "-m" => {} // pattern matching (TODO)
16540                _ if arg.starts_with('-') => {}
16541                _ => names.push(arg),
16542            }
16543        }
16544
16545        for name in names {
16546            if remove_aliases {
16547                self.aliases.remove(name);
16548            }
16549            if remove_functions {
16550                self.functions.remove(name);
16551            }
16552            if remove_dirs {
16553                // Remove from named directories (TODO)
16554            }
16555        }
16556        0
16557    }
16558
16559    /// times - print accumulated user and system times
16560    fn builtin_times(&self, _args: &[String]) -> i32 {
16561        use libc::{getrusage, rusage, RUSAGE_CHILDREN, RUSAGE_SELF};
16562
16563        let mut self_usage: rusage = unsafe { std::mem::zeroed() };
16564        let mut child_usage: rusage = unsafe { std::mem::zeroed() };
16565
16566        unsafe {
16567            getrusage(RUSAGE_SELF, &mut self_usage);
16568            getrusage(RUSAGE_CHILDREN, &mut child_usage);
16569        }
16570
16571        let self_user =
16572            self_usage.ru_utime.tv_sec as f64 + self_usage.ru_utime.tv_usec as f64 / 1_000_000.0;
16573        let self_sys =
16574            self_usage.ru_stime.tv_sec as f64 + self_usage.ru_stime.tv_usec as f64 / 1_000_000.0;
16575        let child_user =
16576            child_usage.ru_utime.tv_sec as f64 + child_usage.ru_utime.tv_usec as f64 / 1_000_000.0;
16577        let child_sys =
16578            child_usage.ru_stime.tv_sec as f64 + child_usage.ru_stime.tv_usec as f64 / 1_000_000.0;
16579
16580        println!("{:.3}s {:.3}s", self_user, self_sys);
16581        println!("{:.3}s {:.3}s", child_user, child_sys);
16582        0
16583    }
16584
16585    /// zmodload - load/unload zsh modules (stub)
16586    fn builtin_zmodload(&mut self, args: &[String]) -> i32 {
16587        let mut list_loaded = false;
16588        let mut unload = false;
16589        let mut modules: Vec<&str> = Vec::new();
16590
16591        for arg in args {
16592            match arg.as_str() {
16593                "-l" | "-L" => list_loaded = true,
16594                "-u" => unload = true,
16595                "-a" | "-b" | "-c" | "-d" | "-e" | "-f" | "-i" | "-p" | "-s" => {}
16596                _ if arg.starts_with('-') => {}
16597                _ => modules.push(arg),
16598            }
16599        }
16600
16601        if list_loaded || modules.is_empty() {
16602            // List loaded modules (stub - we don't really have modules)
16603            println!("zsh/complete");
16604            println!("zsh/complist");
16605            println!("zsh/parameter");
16606            println!("zsh/zutil");
16607            return 0;
16608        }
16609
16610        for module in modules {
16611            if unload {
16612                // Unload module (stub)
16613                self.options.remove(&format!("_module_{}", module));
16614            } else {
16615                // Load module (stub)
16616                self.options.insert(format!("_module_{}", module), true);
16617            }
16618        }
16619        0
16620    }
16621
16622    /// r - redo last command (alias for fc -e -)
16623    fn builtin_r(&mut self, args: &[String]) -> i32 {
16624        let mut fc_args = vec!["-e".to_string(), "-".to_string()];
16625        fc_args.extend(args.iter().cloned());
16626        self.builtin_fc(&fc_args)
16627    }
16628
16629    /// ttyctl - control terminal settings
16630    fn builtin_ttyctl(&self, args: &[String]) -> i32 {
16631        for arg in args {
16632            match arg.as_str() {
16633                "-f" => {
16634                    // Freeze terminal settings
16635                    // In a full implementation, this would save terminal state
16636                }
16637                "-u" => {
16638                    // Unfreeze terminal settings
16639                }
16640                _ => {}
16641            }
16642        }
16643        0
16644    }
16645
16646    /// noglob - run command without globbing
16647    fn builtin_noglob(&mut self, args: &[String], redirects: &[Redirect]) -> i32 {
16648        if args.is_empty() {
16649            return 0;
16650        }
16651
16652        // Temporarily disable globbing
16653        let saved = self.options.get("noglob").cloned();
16654        self.options.insert("noglob".to_string(), true);
16655
16656        // Execute the command
16657        let status = self.builtin_command(args, redirects);
16658
16659        // Restore globbing state
16660        if let Some(v) = saved {
16661            self.options.insert("noglob".to_string(), v);
16662        } else {
16663            self.options.remove("noglob");
16664        }
16665
16666        status
16667    }
16668
16669    // ═══════════════════════════════════════════════════════════════════════════
16670    // zsh module builtins
16671    // ═══════════════════════════════════════════════════════════════════════════
16672
16673    /// zstat - file status (zsh/stat module)
16674    fn builtin_zstat(&self, args: &[String]) -> i32 {
16675        use std::os::unix::fs::MetadataExt;
16676        use std::os::unix::fs::PermissionsExt;
16677
16678        let mut show_all = true;
16679        let mut symbolic_mode = false;
16680        let mut show_link = false;
16681        let mut _as_array = false;
16682        let mut _array_name = String::new();
16683        let mut format_time = String::new();
16684        let mut elements: Vec<String> = Vec::new();
16685        let mut files: Vec<&str> = Vec::new();
16686
16687        let mut iter = args.iter().peekable();
16688        while let Some(arg) = iter.next() {
16689            match arg.as_str() {
16690                "-s" => symbolic_mode = true,
16691                "-L" => show_link = true,
16692                "-N" => {} // Don't resolve symlinks
16693                "-n" => {} // Numeric user/group
16694                "-o" => show_all = false,
16695                "-A" => {
16696                    _as_array = true;
16697                    if let Some(name) = iter.next() {
16698                        _array_name = name.clone();
16699                    }
16700                }
16701                "-F" => {
16702                    if let Some(fmt) = iter.next() {
16703                        format_time = fmt.clone();
16704                    }
16705                }
16706                s if s.starts_with('+') => {
16707                    elements.push(s[1..].to_string());
16708                    show_all = false;
16709                }
16710                s if !s.starts_with('-') => files.push(s),
16711                _ => {}
16712            }
16713        }
16714
16715        if files.is_empty() {
16716            eprintln!("zstat: no files specified");
16717            return 1;
16718        }
16719
16720        for file in files {
16721            let meta = if show_link {
16722                std::fs::symlink_metadata(file)
16723            } else {
16724                std::fs::metadata(file)
16725            };
16726
16727            let meta = match meta {
16728                Ok(m) => m,
16729                Err(e) => {
16730                    eprintln!("zstat: {}: {}", file, e);
16731                    return 1;
16732                }
16733            };
16734
16735            let output_element = |name: &str, value: &str| {
16736                if _as_array {
16737                    // Would need mutable self to store in array
16738                    println!("{}={}", name, value);
16739                } else if show_all || elements.contains(&name.to_string()) {
16740                    println!("{}: {}", name, value);
16741                }
16742            };
16743
16744            output_element("device", &meta.dev().to_string());
16745            output_element("inode", &meta.ino().to_string());
16746
16747            if symbolic_mode {
16748                let mode = meta.permissions().mode();
16749                let mode_str = format!(
16750                    "{}{}{}{}{}{}{}{}{}{}",
16751                    match mode & 0o170000 {
16752                        0o040000 => 'd',
16753                        0o120000 => 'l',
16754                        0o100000 => '-',
16755                        0o060000 => 'b',
16756                        0o020000 => 'c',
16757                        0o010000 => 'p',
16758                        0o140000 => 's',
16759                        _ => '?',
16760                    },
16761                    if mode & 0o400 != 0 { 'r' } else { '-' },
16762                    if mode & 0o200 != 0 { 'w' } else { '-' },
16763                    if mode & 0o4000 != 0 {
16764                        's'
16765                    } else if mode & 0o100 != 0 {
16766                        'x'
16767                    } else {
16768                        '-'
16769                    },
16770                    if mode & 0o040 != 0 { 'r' } else { '-' },
16771                    if mode & 0o020 != 0 { 'w' } else { '-' },
16772                    if mode & 0o2000 != 0 {
16773                        's'
16774                    } else if mode & 0o010 != 0 {
16775                        'x'
16776                    } else {
16777                        '-'
16778                    },
16779                    if mode & 0o004 != 0 { 'r' } else { '-' },
16780                    if mode & 0o002 != 0 { 'w' } else { '-' },
16781                    if mode & 0o1000 != 0 {
16782                        't'
16783                    } else if mode & 0o001 != 0 {
16784                        'x'
16785                    } else {
16786                        '-'
16787                    },
16788                );
16789                output_element("mode", &mode_str);
16790            } else {
16791                output_element("mode", &format!("{:o}", meta.permissions().mode()));
16792            }
16793
16794            output_element("nlink", &meta.nlink().to_string());
16795            output_element("uid", &meta.uid().to_string());
16796            output_element("gid", &meta.gid().to_string());
16797            output_element("rdev", &meta.rdev().to_string());
16798            output_element("size", &meta.len().to_string());
16799
16800            let format_timestamp = |secs: i64| -> String {
16801                if format_time.is_empty() {
16802                    secs.to_string()
16803                } else {
16804                    chrono::DateTime::from_timestamp(secs, 0)
16805                        .map(|dt| dt.format(&format_time).to_string())
16806                        .unwrap_or_else(|| secs.to_string())
16807                }
16808            };
16809
16810            output_element("atime", &format_timestamp(meta.atime()));
16811            output_element("mtime", &format_timestamp(meta.mtime()));
16812            output_element("ctime", &format_timestamp(meta.ctime()));
16813            output_element("blksize", &meta.blksize().to_string());
16814            output_element("blocks", &meta.blocks().to_string());
16815
16816            if show_link && meta.file_type().is_symlink() {
16817                if let Ok(target) = std::fs::read_link(file) {
16818                    output_element("link", &target.to_string_lossy());
16819                }
16820            }
16821        }
16822
16823        0
16824    }
16825
16826    /// strftime - format date/time (zsh/datetime module)
16827    fn builtin_strftime(&self, args: &[String]) -> i32 {
16828        let mut format = "%c".to_string();
16829        let mut timestamp: Option<i64> = None;
16830        let mut to_var = false;
16831        let mut var_name = String::new();
16832
16833        let mut iter = args.iter();
16834        while let Some(arg) = iter.next() {
16835            match arg.as_str() {
16836                "-s" => {
16837                    to_var = true;
16838                    if let Some(name) = iter.next() {
16839                        var_name = name.clone();
16840                    }
16841                }
16842                "-r" => {
16843                    // Reference time from a variable
16844                    if let Some(ts_str) = iter.next() {
16845                        timestamp = ts_str.parse().ok();
16846                    }
16847                }
16848                s if !s.starts_with('-') => {
16849                    if format == "%c" {
16850                        format = s.to_string();
16851                    } else if timestamp.is_none() {
16852                        timestamp = s.parse().ok();
16853                    }
16854                }
16855                _ => {}
16856            }
16857        }
16858
16859        let ts = timestamp.unwrap_or_else(|| chrono::Local::now().timestamp());
16860
16861        let result = chrono::DateTime::from_timestamp(ts, 0)
16862            .map(|dt: chrono::DateTime<chrono::Utc>| {
16863                dt.with_timezone(&chrono::Local).format(&format).to_string()
16864            })
16865            .unwrap_or_else(|| "invalid timestamp".to_string());
16866
16867        if to_var && !var_name.is_empty() {
16868            // Would need mutable self
16869            println!("{}={}", var_name, result);
16870        } else {
16871            println!("{}", result);
16872        }
16873
16874        0
16875    }
16876
16877    /// zsleep - sleep with fractional seconds
16878    fn builtin_zsleep(&self, args: &[String]) -> i32 {
16879        if args.is_empty() {
16880            eprintln!("zsleep: missing argument");
16881            return 1;
16882        }
16883
16884        let secs: f64 = match args[0].parse() {
16885            Ok(s) => s,
16886            Err(_) => {
16887                eprintln!("zsleep: invalid number: {}", args[0]);
16888                return 1;
16889            }
16890        };
16891
16892        std::thread::sleep(std::time::Duration::from_secs_f64(secs));
16893        0
16894    }
16895
16896    /// zsystem - system interface (zsh/system module)
16897    /// Ported from zsh/Src/Modules/system.c bin_zsystem() lines 805-816
16898    fn builtin_zsystem(&mut self, args: &[String]) -> i32 {
16899        if args.is_empty() {
16900            eprintln!("zsystem: subcommand expected");
16901            return 1;
16902        }
16903        match args[0].as_str() {
16904            "flock" => self.builtin_zsystem_flock(&args[1..]),
16905            "supports" => self.builtin_zsystem_supports(&args[1..]),
16906            _ => {
16907                eprintln!("zsystem: unknown subcommand: {}", args[0]);
16908                1
16909            }
16910        }
16911    }
16912
16913    /// zsystem supports - ported from system.c bin_zsystem_supports() lines 780-801
16914    fn builtin_zsystem_supports(&self, args: &[String]) -> i32 {
16915        if args.is_empty() {
16916            eprintln!("zsystem: supports: not enough arguments");
16917            return 255;
16918        }
16919        if args.len() > 1 {
16920            eprintln!("zsystem: supports: too many arguments");
16921            return 255;
16922        }
16923        match args[0].as_str() {
16924            "supports" | "flock" => 0,
16925            _ => 1,
16926        }
16927    }
16928
16929    /// zsystem flock - ported from system.c bin_zsystem_flock() lines 546-774
16930    fn builtin_zsystem_flock(&mut self, args: &[String]) -> i32 {
16931        #[cfg(unix)]
16932        {
16933            use std::os::unix::io::AsRawFd;
16934
16935            let mut cloexec = true;
16936            let mut readlock = false;
16937            let mut timeout: Option<f64> = None;
16938            let mut fdvar: Option<String> = None;
16939            let mut file: Option<&str> = None;
16940
16941            let mut i = 0;
16942            while i < args.len() {
16943                let arg = &args[i];
16944                if arg == "--" {
16945                    i += 1;
16946                    if i < args.len() {
16947                        file = Some(&args[i]);
16948                    }
16949                    break;
16950                }
16951                if !arg.starts_with('-') {
16952                    file = Some(arg);
16953                    break;
16954                }
16955                let mut chars = arg[1..].chars().peekable();
16956                while let Some(c) = chars.next() {
16957                    match c {
16958                        'e' => cloexec = false,
16959                        'r' => readlock = true,
16960                        'u' => return 0,
16961                        'f' => {
16962                            let rest: String = chars.collect();
16963                            if !rest.is_empty() {
16964                                fdvar = Some(rest);
16965                            } else {
16966                                i += 1;
16967                                if i < args.len() {
16968                                    fdvar = Some(args[i].clone());
16969                                } else {
16970                                    eprintln!("zsystem: flock: option f requires a variable name");
16971                                    return 1;
16972                                }
16973                            }
16974                            break;
16975                        }
16976                        't' => {
16977                            let rest: String = chars.collect();
16978                            let val = if !rest.is_empty() {
16979                                rest
16980                            } else {
16981                                i += 1;
16982                                if i < args.len() {
16983                                    args[i].clone()
16984                                } else {
16985                                    eprintln!(
16986                                        "zsystem: flock: option t requires a numeric timeout"
16987                                    );
16988                                    return 1;
16989                                }
16990                            };
16991                            match val.parse::<f64>() {
16992                                Ok(t) => timeout = Some(t),
16993                                Err(_) => {
16994                                    eprintln!("zsystem: flock: invalid timeout value: '{}'", val);
16995                                    return 1;
16996                                }
16997                            }
16998                            break;
16999                        }
17000                        'i' => {
17001                            let rest: String = chars.collect();
17002                            if rest.is_empty() {
17003                                i += 1;
17004                                if i >= args.len() {
17005                                    eprintln!("zsystem: flock: option i requires a numeric retry interval");
17006                                    return 1;
17007                                }
17008                            }
17009                            break;
17010                        }
17011                        _ => {
17012                            eprintln!("zsystem: flock: unknown option: -{}", c);
17013                            return 1;
17014                        }
17015                    }
17016                }
17017                i += 1;
17018            }
17019
17020            let filepath = match file {
17021                Some(f) => f,
17022                None => {
17023                    eprintln!("zsystem: flock: not enough arguments");
17024                    return 1;
17025                }
17026            };
17027
17028            use std::fs::OpenOptions;
17029            let file_handle = match OpenOptions::new()
17030                .read(true)
17031                .write(!readlock)
17032                .create(true)
17033                .truncate(false)
17034                .open(filepath)
17035            {
17036                Ok(f) => f,
17037                Err(e) => {
17038                    eprintln!("zsystem: flock: {}: {}", filepath, e);
17039                    return 1;
17040                }
17041            };
17042
17043            let lock_type = if readlock {
17044                libc::F_RDLCK as i16
17045            } else {
17046                libc::F_WRLCK as i16
17047            };
17048
17049            let mut flock = libc::flock {
17050                l_type: lock_type,
17051                l_whence: libc::SEEK_SET as i16,
17052                l_start: 0,
17053                l_len: 0,
17054                l_pid: 0,
17055            };
17056
17057            let cmd = if timeout.is_some() {
17058                libc::F_SETLK
17059            } else {
17060                libc::F_SETLKW
17061            };
17062            let start = std::time::Instant::now();
17063            let timeout_duration = timeout.map(|t| std::time::Duration::from_secs_f64(t));
17064
17065            loop {
17066                let ret = unsafe { libc::fcntl(file_handle.as_raw_fd(), cmd, &mut flock) };
17067                if ret == 0 {
17068                    if let Some(ref var) = fdvar {
17069                        let fd = file_handle.as_raw_fd();
17070                        std::mem::forget(file_handle);
17071                        self.variables.insert(var.clone(), fd.to_string());
17072                    } else {
17073                        std::mem::forget(file_handle);
17074                    }
17075                    let _ = cloexec;
17076                    return 0;
17077                }
17078                let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
17079                if errno != libc::EACCES && errno != libc::EAGAIN {
17080                    eprintln!(
17081                        "zsystem: flock: {}: {}",
17082                        filepath,
17083                        std::io::Error::last_os_error()
17084                    );
17085                    return 1;
17086                }
17087                if let Some(td) = timeout_duration {
17088                    if start.elapsed() >= td {
17089                        return 2;
17090                    }
17091                    std::thread::sleep(std::time::Duration::from_millis(100));
17092                } else {
17093                    eprintln!(
17094                        "zsystem: flock: {}: {}",
17095                        filepath,
17096                        std::io::Error::last_os_error()
17097                    );
17098                    return 1;
17099                }
17100            }
17101        }
17102        #[cfg(not(unix))]
17103        {
17104            eprintln!("zsystem: flock: not supported on this platform");
17105            1
17106        }
17107    }
17108
17109    /// sync - flush filesystem buffers
17110    /// Port from zsh/Src/Modules/files.c bin_sync() lines 52-57
17111    fn builtin_sync(&self, _args: &[String]) -> i32 {
17112        #[cfg(unix)]
17113        unsafe {
17114            libc::sync();
17115        }
17116        0
17117    }
17118
17119    /// mkdir - create directories
17120    /// Port from zsh/Src/Modules/files.c bin_mkdir() lines 62-111
17121    fn builtin_mkdir(&self, args: &[String]) -> i32 {
17122        let mut mode: u32 = 0o777;
17123        let mut parents = false;
17124        let mut dirs: Vec<&str> = Vec::new();
17125
17126        let mut i = 0;
17127        while i < args.len() {
17128            let arg = &args[i];
17129            if arg == "-p" {
17130                parents = true;
17131            } else if arg == "-m" && i + 1 < args.len() {
17132                i += 1;
17133                mode = u32::from_str_radix(&args[i], 8).unwrap_or(0o777);
17134            } else if arg.starts_with("-m") {
17135                mode = u32::from_str_radix(&arg[2..], 8).unwrap_or(0o777);
17136            } else if !arg.starts_with('-') || arg == "-" || arg == "--" {
17137                if arg == "--" {
17138                    dirs.extend(args[i + 1..].iter().map(|s| s.as_str()));
17139                    break;
17140                }
17141                dirs.push(arg);
17142            }
17143            i += 1;
17144        }
17145
17146        let mut err = 0;
17147        for dir in dirs {
17148            let path = std::path::Path::new(dir);
17149            let result = if parents {
17150                std::fs::create_dir_all(path)
17151            } else {
17152                std::fs::create_dir(path)
17153            };
17154            if let Err(e) = result {
17155                eprintln!("mkdir: cannot create directory '{}': {}", dir, e);
17156                err = 1;
17157            } else {
17158                #[cfg(unix)]
17159                {
17160                    use std::os::unix::fs::PermissionsExt;
17161                    let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode));
17162                }
17163            }
17164        }
17165        err
17166    }
17167
17168    /// rmdir - remove directories
17169    /// Port from zsh/Src/Modules/files.c bin_rmdir() lines 149-166
17170    fn builtin_rmdir(&self, args: &[String]) -> i32 {
17171        let mut err = 0;
17172        for arg in args {
17173            if arg.starts_with('-') {
17174                continue;
17175            }
17176            if let Err(e) = std::fs::remove_dir(arg) {
17177                eprintln!("rmdir: cannot remove '{}': {}", arg, e);
17178                err = 1;
17179            }
17180        }
17181        err
17182    }
17183
17184    /// ln - create links
17185    /// Port from zsh/Src/Modules/files.c bin_ln() lines 200-294
17186    fn builtin_ln(&self, args: &[String]) -> i32 {
17187        let mut symbolic = false;
17188        let mut force = false;
17189        let mut no_deref = false;
17190        let mut files: Vec<&str> = Vec::new();
17191
17192        for arg in args {
17193            match arg.as_str() {
17194                "-s" => symbolic = true,
17195                "-f" => force = true,
17196                "-n" | "-h" => no_deref = true,
17197                s if !s.starts_with('-') => files.push(s),
17198                _ => {}
17199            }
17200        }
17201
17202        if files.len() < 2 {
17203            if files.len() == 1 {
17204                let src = files[0];
17205                let target = std::path::Path::new(src)
17206                    .file_name()
17207                    .map(|n| n.to_string_lossy().to_string())
17208                    .unwrap_or_else(|| src.to_string());
17209                files.push(Box::leak(target.into_boxed_str()));
17210            } else {
17211                eprintln!("ln: missing file operand");
17212                return 1;
17213            }
17214        }
17215
17216        let target = files.pop().unwrap();
17217        let target_path = std::path::Path::new(target);
17218        let is_dir = !no_deref && target_path.is_dir();
17219
17220        for src in files {
17221            let dest = if is_dir {
17222                format!(
17223                    "{}/{}",
17224                    target,
17225                    std::path::Path::new(src)
17226                        .file_name()
17227                        .map(|n| n.to_string_lossy().to_string())
17228                        .unwrap_or_else(|| src.to_string())
17229                )
17230            } else {
17231                target.to_string()
17232            };
17233
17234            let dest_path = std::path::Path::new(&dest);
17235            if force && dest_path.exists() {
17236                let _ = std::fs::remove_file(&dest);
17237            }
17238
17239            let result = if symbolic {
17240                #[cfg(unix)]
17241                {
17242                    std::os::unix::fs::symlink(src, &dest)
17243                }
17244                #[cfg(not(unix))]
17245                {
17246                    Err(std::io::Error::new(
17247                        std::io::ErrorKind::Unsupported,
17248                        "symlinks not supported",
17249                    ))
17250                }
17251            } else {
17252                std::fs::hard_link(src, &dest)
17253            };
17254
17255            if let Err(e) = result {
17256                eprintln!("ln: cannot create link '{}' -> '{}': {}", dest, src, e);
17257                return 1;
17258            }
17259        }
17260        0
17261    }
17262
17263    /// mv - move/rename files
17264    /// Port from zsh/Src/Modules/files.c bin_ln()/domove() for mv mode
17265    fn builtin_mv(&self, args: &[String]) -> i32 {
17266        let mut force = false;
17267        let mut interactive = false;
17268        let mut verbose = false;
17269        let mut files: Vec<&str> = Vec::new();
17270
17271        for arg in args {
17272            match arg.as_str() {
17273                "-f" => force = true,
17274                "-i" => interactive = true,
17275                "-v" => verbose = true,
17276                s if !s.starts_with('-') => files.push(s),
17277                _ => {}
17278            }
17279        }
17280
17281        if files.len() < 2 {
17282            eprintln!("mv: missing file operand");
17283            return 1;
17284        }
17285
17286        let target = files.pop().unwrap();
17287        let target_path = std::path::Path::new(target);
17288        let is_dir = target_path.is_dir();
17289
17290        for src in files {
17291            let dest = if is_dir {
17292                format!(
17293                    "{}/{}",
17294                    target,
17295                    std::path::Path::new(src)
17296                        .file_name()
17297                        .map(|n| n.to_string_lossy().to_string())
17298                        .unwrap_or_else(|| src.to_string())
17299                )
17300            } else {
17301                target.to_string()
17302            };
17303
17304            let dest_path = std::path::Path::new(&dest);
17305            if dest_path.exists() && !force {
17306                if interactive {
17307                    eprint!("mv: overwrite '{}'? ", dest);
17308                    let mut response = String::new();
17309                    if std::io::stdin().read_line(&mut response).is_err()
17310                        || !response.trim().eq_ignore_ascii_case("y")
17311                    {
17312                        continue;
17313                    }
17314                } else {
17315                    eprintln!("mv: cannot overwrite '{}': File exists", dest);
17316                    return 1;
17317                }
17318            }
17319
17320            if let Err(e) = std::fs::rename(src, &dest) {
17321                eprintln!("mv: cannot move '{}' to '{}': {}", src, dest, e);
17322                return 1;
17323            }
17324
17325            if verbose {
17326                println!("'{}' -> '{}'", src, dest);
17327            }
17328        }
17329        0
17330    }
17331
17332    /// cp - copy files
17333    /// Port from zsh/Src/Modules/files.c recursive copy functionality
17334    fn builtin_cp(&self, args: &[String]) -> i32 {
17335        let mut recursive = false;
17336        let mut force = false;
17337        let mut interactive = false;
17338        let mut preserve = false;
17339        let mut verbose = false;
17340        let mut files: Vec<&str> = Vec::new();
17341
17342        for arg in args {
17343            match arg.as_str() {
17344                "-r" | "-R" => recursive = true,
17345                "-f" => force = true,
17346                "-i" => interactive = true,
17347                "-p" => preserve = true,
17348                "-v" => verbose = true,
17349                s if !s.starts_with('-') => files.push(s),
17350                _ => {}
17351            }
17352        }
17353
17354        let _ = preserve; // unused for now
17355
17356        if files.len() < 2 {
17357            eprintln!("cp: missing file operand");
17358            return 1;
17359        }
17360
17361        let target = files.pop().unwrap();
17362        let target_path = std::path::Path::new(target);
17363        let is_dir = target_path.is_dir();
17364
17365        for src in files {
17366            let src_path = std::path::Path::new(src);
17367            let dest = if is_dir {
17368                format!(
17369                    "{}/{}",
17370                    target,
17371                    src_path
17372                        .file_name()
17373                        .map(|n| n.to_string_lossy().to_string())
17374                        .unwrap_or_else(|| src.to_string())
17375                )
17376            } else {
17377                target.to_string()
17378            };
17379
17380            let dest_path = std::path::Path::new(&dest);
17381            if dest_path.exists() && !force {
17382                if interactive {
17383                    eprint!("cp: overwrite '{}'? ", dest);
17384                    let mut response = String::new();
17385                    if std::io::stdin().read_line(&mut response).is_err()
17386                        || !response.trim().eq_ignore_ascii_case("y")
17387                    {
17388                        continue;
17389                    }
17390                }
17391            }
17392
17393            let result = if src_path.is_dir() {
17394                if recursive {
17395                    Self::copy_dir_recursive(src_path, dest_path)
17396                } else {
17397                    eprintln!("cp: -r not specified; omitting directory '{}'", src);
17398                    continue;
17399                }
17400            } else {
17401                std::fs::copy(src, &dest).map(|_| ())
17402            };
17403
17404            if let Err(e) = result {
17405                eprintln!("cp: cannot copy '{}' to '{}': {}", src, dest, e);
17406                return 1;
17407            }
17408
17409            if verbose {
17410                println!("'{}' -> '{}'", src, dest);
17411            }
17412        }
17413        0
17414    }
17415
17416    fn copy_dir_recursive(src: &std::path::Path, dest: &std::path::Path) -> std::io::Result<()> {
17417        if !dest.exists() {
17418            std::fs::create_dir_all(dest)?;
17419        }
17420        for entry in std::fs::read_dir(src)? {
17421            let entry = entry?;
17422            let file_type = entry.file_type()?;
17423            let src_path = entry.path();
17424            let dest_path = dest.join(entry.file_name());
17425
17426            if file_type.is_dir() {
17427                Self::copy_dir_recursive(&src_path, &dest_path)?;
17428            } else {
17429                std::fs::copy(&src_path, &dest_path)?;
17430            }
17431        }
17432        Ok(())
17433    }
17434
17435    /// rm - remove files
17436    fn builtin_rm(&self, args: &[String]) -> i32 {
17437        let mut recursive = false;
17438        let mut force = false;
17439        let mut interactive = false;
17440        let mut verbose = false;
17441        let mut files: Vec<&str> = Vec::new();
17442
17443        for arg in args {
17444            match arg.as_str() {
17445                "-r" | "-R" => recursive = true,
17446                "-f" => force = true,
17447                "-i" => interactive = true,
17448                "-v" => verbose = true,
17449                "-rf" | "-fr" => {
17450                    recursive = true;
17451                    force = true;
17452                }
17453                s if !s.starts_with('-') => files.push(s),
17454                _ => {}
17455            }
17456        }
17457
17458        for file in files {
17459            let path = std::path::Path::new(file);
17460
17461            if !path.exists() {
17462                if !force {
17463                    eprintln!("rm: cannot remove '{}': No such file or directory", file);
17464                    return 1;
17465                }
17466                continue;
17467            }
17468
17469            if interactive {
17470                let file_type = if path.is_dir() { "directory" } else { "file" };
17471                eprint!("rm: remove {} '{}'? ", file_type, file);
17472                let mut response = String::new();
17473                if std::io::stdin().read_line(&mut response).is_err()
17474                    || !response.trim().eq_ignore_ascii_case("y")
17475                {
17476                    continue;
17477                }
17478            }
17479
17480            let result = if path.is_dir() {
17481                if recursive {
17482                    std::fs::remove_dir_all(path)
17483                } else {
17484                    eprintln!("rm: cannot remove '{}': Is a directory", file);
17485                    return 1;
17486                }
17487            } else {
17488                std::fs::remove_file(path)
17489            };
17490
17491            if let Err(e) = result {
17492                if !force {
17493                    eprintln!("rm: cannot remove '{}': {}", file, e);
17494                    return 1;
17495                }
17496            } else if verbose {
17497                println!("removed '{}'", file);
17498            }
17499        }
17500        0
17501    }
17502
17503    /// chown - change file owner (Unix only)
17504    #[cfg(unix)]
17505    fn builtin_chown(&self, args: &[String]) -> i32 {
17506        use std::os::unix::fs::MetadataExt;
17507
17508        let mut recursive = false;
17509        let mut positional: Vec<&str> = Vec::new();
17510
17511        for arg in args {
17512            match arg.as_str() {
17513                "-R" => recursive = true,
17514                "-h" => {} // don't deference symlinks (default on most systems)
17515                s if !s.starts_with('-') => positional.push(s),
17516                _ => {}
17517            }
17518        }
17519
17520        if positional.len() < 2 {
17521            eprintln!("chown: missing operand");
17522            return 1;
17523        }
17524
17525        let owner_spec = positional[0];
17526        let files = &positional[1..];
17527
17528        // Parse owner[:group]
17529        let (user, group) = if let Some(colon_pos) = owner_spec.find(':') {
17530            (&owner_spec[..colon_pos], Some(&owner_spec[colon_pos + 1..]))
17531        } else {
17532            (owner_spec, None)
17533        };
17534
17535        let uid: u32 = if user.is_empty() {
17536            u32::MAX
17537        } else if let Ok(id) = user.parse() {
17538            id
17539        } else {
17540            // Look up user name
17541            unsafe {
17542                let c_user = std::ffi::CString::new(user).unwrap();
17543                let pw = libc::getpwnam(c_user.as_ptr());
17544                if pw.is_null() {
17545                    eprintln!("chown: invalid user: '{}'", user);
17546                    return 1;
17547                }
17548                (*pw).pw_uid
17549            }
17550        };
17551
17552        let gid: u32 = match group {
17553            Some(g) if !g.is_empty() => {
17554                if let Ok(id) = g.parse() {
17555                    id
17556                } else {
17557                    unsafe {
17558                        let c_group = std::ffi::CString::new(g).unwrap();
17559                        let gr = libc::getgrnam(c_group.as_ptr());
17560                        if gr.is_null() {
17561                            eprintln!("chown: invalid group: '{}'", g);
17562                            return 1;
17563                        }
17564                        (*gr).gr_gid
17565                    }
17566                }
17567            }
17568            _ => u32::MAX,
17569        };
17570
17571        fn do_chown(path: &std::path::Path, uid: u32, gid: u32, recursive: bool) -> i32 {
17572            let c_path = match std::ffi::CString::new(path.to_string_lossy().as_bytes()) {
17573                Ok(p) => p,
17574                Err(_) => return 1,
17575            };
17576
17577            let ret = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
17578            if ret != 0 {
17579                eprintln!(
17580                    "chown: changing ownership of '{}': {}",
17581                    path.display(),
17582                    std::io::Error::last_os_error()
17583                );
17584                return 1;
17585            }
17586
17587            if recursive && path.is_dir() {
17588                if let Ok(entries) = std::fs::read_dir(path) {
17589                    for entry in entries.flatten() {
17590                        if do_chown(&entry.path(), uid, gid, true) != 0 {
17591                            return 1;
17592                        }
17593                    }
17594                }
17595            }
17596            0
17597        }
17598
17599        for file in files {
17600            if do_chown(std::path::Path::new(file), uid, gid, recursive) != 0 {
17601                return 1;
17602            }
17603        }
17604        0
17605    }
17606
17607    #[cfg(not(unix))]
17608    fn builtin_chown(&self, _args: &[String]) -> i32 {
17609        eprintln!("chown: not supported on this platform");
17610        1
17611    }
17612
17613    /// chmod - change file permissions
17614    fn builtin_chmod(&self, args: &[String]) -> i32 {
17615        let mut recursive = false;
17616        let mut positional: Vec<&str> = Vec::new();
17617
17618        for arg in args {
17619            match arg.as_str() {
17620                "-R" => recursive = true,
17621                s if !s.starts_with('-') => positional.push(s),
17622                _ => {}
17623            }
17624        }
17625
17626        if positional.len() < 2 {
17627            eprintln!("chmod: missing operand");
17628            return 1;
17629        }
17630
17631        let mode_spec = positional[0];
17632        let files = &positional[1..];
17633
17634        // Parse mode (octal or symbolic)
17635        let mode: Option<u32> = u32::from_str_radix(mode_spec, 8).ok();
17636
17637        if mode.is_none() {
17638            // Symbolic mode not fully implemented
17639            eprintln!("chmod: symbolic mode not implemented, use octal");
17640            return 1;
17641        }
17642
17643        let mode = mode.unwrap();
17644
17645        fn do_chmod(path: &std::path::Path, mode: u32, recursive: bool) -> i32 {
17646            #[cfg(unix)]
17647            {
17648                use std::os::unix::fs::PermissionsExt;
17649                if let Err(e) =
17650                    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
17651                {
17652                    eprintln!("chmod: changing permissions of '{}': {}", path.display(), e);
17653                    return 1;
17654                }
17655
17656                if recursive && path.is_dir() {
17657                    if let Ok(entries) = std::fs::read_dir(path) {
17658                        for entry in entries.flatten() {
17659                            if do_chmod(&entry.path(), mode, true) != 0 {
17660                                return 1;
17661                            }
17662                        }
17663                    }
17664                }
17665            }
17666            #[cfg(not(unix))]
17667            {
17668                let _ = (path, mode, recursive);
17669            }
17670            0
17671        }
17672
17673        for file in files {
17674            if do_chmod(std::path::Path::new(file), mode, recursive) != 0 {
17675                return 1;
17676            }
17677        }
17678        0
17679    }
17680
17681    /// zln/zmv/zcp - file operations (zsh/files module)
17682    fn builtin_zfiles(&self, cmd: &str, args: &[String]) -> i32 {
17683        let mut force = false;
17684        let mut verbose = false;
17685        let mut files: Vec<&str> = Vec::new();
17686
17687        for arg in args {
17688            match arg.as_str() {
17689                "-f" => force = true,
17690                "-v" => verbose = true,
17691                "-i" => {} // interactive - ignored
17692                s if !s.starts_with('-') => files.push(s),
17693                _ => {}
17694            }
17695        }
17696
17697        if files.len() < 2 {
17698            eprintln!("{}: missing operand", cmd);
17699            return 1;
17700        }
17701
17702        let target = files.pop().unwrap();
17703        let target_is_dir = std::path::Path::new(target).is_dir();
17704
17705        for src in files {
17706            let dest = if target_is_dir {
17707                format!(
17708                    "{}/{}",
17709                    target,
17710                    std::path::Path::new(src)
17711                        .file_name()
17712                        .map(|n| n.to_string_lossy().to_string())
17713                        .unwrap_or_else(|| src.to_string())
17714                )
17715            } else {
17716                target.to_string()
17717            };
17718
17719            if !force && std::path::Path::new(&dest).exists() {
17720                eprintln!("{}: '{}' already exists", cmd, dest);
17721                continue;
17722            }
17723
17724            let result = match cmd {
17725                "zln" => {
17726                    #[cfg(unix)]
17727                    {
17728                        std::os::unix::fs::symlink(src, &dest)
17729                    }
17730                    #[cfg(not(unix))]
17731                    {
17732                        Err(std::io::Error::new(
17733                            std::io::ErrorKind::Unsupported,
17734                            "symlinks not supported",
17735                        ))
17736                    }
17737                }
17738                "zcp" => std::fs::copy(src, &dest).map(|_| ()),
17739                "zmv" => std::fs::rename(src, &dest),
17740                _ => Ok(()),
17741            };
17742
17743            match result {
17744                Ok(()) => {
17745                    if verbose {
17746                        println!("{} -> {}", src, dest);
17747                    }
17748                }
17749                Err(e) => {
17750                    eprintln!("{}: {}: {}", cmd, src, e);
17751                    return 1;
17752                }
17753            }
17754        }
17755
17756        0
17757    }
17758
17759    /// coproc - manage coprocesses
17760    fn builtin_coproc(&mut self, args: &[String]) -> i32 {
17761        // Basic coproc implementation
17762        if args.is_empty() {
17763            // List coprocesses
17764            println!("(no coprocesses)");
17765            return 0;
17766        }
17767
17768        // Start a coprocess
17769        let cmd = args.join(" ");
17770        match std::process::Command::new("sh")
17771            .arg("-c")
17772            .arg(&cmd)
17773            .stdin(std::process::Stdio::piped())
17774            .stdout(std::process::Stdio::piped())
17775            .spawn()
17776        {
17777            Ok(child) => {
17778                println!("[coproc] {}", child.id());
17779                0
17780            }
17781            Err(e) => {
17782                eprintln!("coproc: {}", e);
17783                1
17784            }
17785        }
17786    }
17787
17788    /// zparseopts - parse options from positional parameters
17789    fn builtin_zparseopts(&mut self, args: &[String]) -> i32 {
17790        let mut remove_parsed = false; // -D
17791        let mut keep_going = false; // -E
17792        let mut fail_on_error = false; // -F
17793        let mut keep_values = false; // -K
17794        let mut _map_names = false; // -M (TODO: implement)
17795        let mut array_name: Option<String> = None; // -a
17796        let mut assoc_name: Option<String> = None; // -A
17797        let mut specs: Vec<String> = Vec::new();
17798
17799        let mut iter = args.iter().peekable();
17800
17801        // Parse zparseopts options
17802        while let Some(arg) = iter.next() {
17803            match arg.as_str() {
17804                "-D" => remove_parsed = true,
17805                "-E" => keep_going = true,
17806                "-F" => fail_on_error = true,
17807                "-K" => keep_values = true,
17808                "-M" => _map_names = true,
17809                "-a" => {
17810                    if let Some(name) = iter.next() {
17811                        array_name = Some(name.clone());
17812                    }
17813                }
17814                "-A" => {
17815                    if let Some(name) = iter.next() {
17816                        assoc_name = Some(name.clone());
17817                    }
17818                }
17819                "-" | "--" => break,
17820                s if !s.starts_with('-') || s.contains('=') || s.contains(':') => {
17821                    specs.push(s.to_string());
17822                }
17823                _ => specs.push(arg.clone()),
17824            }
17825        }
17826
17827        // Collect remaining specs
17828        for arg in iter {
17829            specs.push(arg.clone());
17830        }
17831
17832        // Parse the specs to understand what options we're looking for
17833        #[derive(Clone)]
17834        struct OptSpec {
17835            name: String,
17836            takes_arg: bool,
17837            optional_arg: bool,
17838            #[allow(dead_code)]
17839            append: bool,
17840            target_array: Option<String>,
17841        }
17842
17843        let mut opt_specs: Vec<OptSpec> = Vec::new();
17844        for spec in &specs {
17845            let mut s = spec.as_str();
17846            let mut target = None;
17847
17848            // Check for =array at end
17849            if let Some(eq_pos) = s.rfind('=') {
17850                if !s[eq_pos + 1..].contains(':') {
17851                    target = Some(s[eq_pos + 1..].to_string());
17852                    s = &s[..eq_pos];
17853                }
17854            }
17855
17856            let append = s.ends_with('+') || s.contains("+:");
17857            let s = s.trim_end_matches('+');
17858
17859            let (name, takes_arg, optional_arg) = if s.ends_with("::") {
17860                (s.trim_end_matches(':').trim_end_matches(':'), true, true)
17861            } else if s.ends_with(':') {
17862                (s.trim_end_matches(':'), true, false)
17863            } else {
17864                (s, false, false)
17865            };
17866
17867            opt_specs.push(OptSpec {
17868                name: name.to_string(),
17869                takes_arg,
17870                optional_arg,
17871                append,
17872                target_array: target,
17873            });
17874        }
17875
17876        // Get positional parameters to parse
17877        let positionals: Vec<String> = (1..=99)
17878            .map(|i| self.get_variable(&i.to_string()))
17879            .take_while(|v| !v.is_empty())
17880            .collect();
17881
17882        // Results
17883        let mut results: Vec<(String, Option<String>)> = Vec::new();
17884        let mut i = 0;
17885        let mut parsed_count = 0;
17886
17887        while i < positionals.len() {
17888            let arg = &positionals[i];
17889
17890            if arg == "-" || arg == "--" {
17891                parsed_count = i + 1;
17892                break;
17893            }
17894
17895            if !arg.starts_with('-') {
17896                if !keep_going {
17897                    break;
17898                }
17899                i += 1;
17900                continue;
17901            }
17902
17903            // Try to match against specs
17904            let opt_name = arg.trim_start_matches('-');
17905            let mut matched = false;
17906
17907            for spec in &opt_specs {
17908                if opt_name == spec.name || opt_name.starts_with(&format!("{}=", spec.name)) {
17909                    matched = true;
17910
17911                    if spec.takes_arg {
17912                        let arg_value = if opt_name.contains('=') {
17913                            Some(opt_name.splitn(2, '=').nth(1).unwrap_or("").to_string())
17914                        } else if i + 1 < positionals.len()
17915                            && (!positionals[i + 1].starts_with('-') || spec.optional_arg)
17916                        {
17917                            i += 1;
17918                            Some(positionals[i].clone())
17919                        } else if spec.optional_arg {
17920                            None
17921                        } else if fail_on_error {
17922                            eprintln!("zparseopts: missing argument for option: {}", spec.name);
17923                            return 1;
17924                        } else {
17925                            None
17926                        };
17927                        results.push((format!("-{}", spec.name), arg_value));
17928                    } else {
17929                        results.push((format!("-{}", spec.name), None));
17930                    }
17931                    break;
17932                }
17933            }
17934
17935            if !matched && !keep_going {
17936                break;
17937            }
17938
17939            i += 1;
17940            parsed_count = i;
17941        }
17942
17943        // Store results in array
17944        if let Some(arr_name) = &array_name {
17945            let mut arr_values: Vec<String> = Vec::new();
17946            for (opt, val) in &results {
17947                arr_values.push(opt.clone());
17948                if let Some(v) = val {
17949                    arr_values.push(v.clone());
17950                }
17951            }
17952            self.arrays.insert(arr_name.clone(), arr_values);
17953        }
17954
17955        // Store in associative array
17956        if let Some(assoc) = &assoc_name {
17957            let mut map: HashMap<String, String> = HashMap::new();
17958            for (opt, val) in &results {
17959                map.insert(opt.clone(), val.clone().unwrap_or_default());
17960            }
17961            self.assoc_arrays.insert(assoc.clone(), map);
17962        }
17963
17964        // Store in per-option arrays
17965        for spec in &opt_specs {
17966            if let Some(target) = &spec.target_array {
17967                let values: Vec<String> = results
17968                    .iter()
17969                    .filter(|(opt, _)| opt.trim_start_matches('-') == spec.name)
17970                    .flat_map(|(opt, val)| {
17971                        let mut v = vec![opt.clone()];
17972                        if let Some(arg) = val {
17973                            v.push(arg.clone());
17974                        }
17975                        v
17976                    })
17977                    .collect();
17978                if !values.is_empty() || !keep_values {
17979                    self.arrays.insert(target.clone(), values);
17980                }
17981            }
17982        }
17983
17984        // Remove parsed arguments if -D
17985        if remove_parsed && parsed_count > 0 {
17986            for i in 1..=parsed_count {
17987                self.variables.remove(&i.to_string());
17988                std::env::remove_var(i.to_string());
17989            }
17990            // Shift remaining
17991            let remaining: Vec<String> = ((parsed_count + 1)..=99)
17992                .map(|i| self.get_variable(&i.to_string()))
17993                .take_while(|v| !v.is_empty())
17994                .collect();
17995            for (i, val) in remaining.iter().enumerate() {
17996                self.variables.insert((i + 1).to_string(), val.clone());
17997            }
17998        }
17999
18000        0
18001    }
18002
18003    /// readonly - mark variables as read-only
18004    fn builtin_readonly(&mut self, args: &[String]) -> i32 {
18005        if args.is_empty() {
18006            // List readonly variables
18007            for name in &self.readonly_vars {
18008                if let Some(val) = self.variables.get(name) {
18009                    println!("readonly {}={}", name, val);
18010                }
18011            }
18012            return 0;
18013        }
18014
18015        for arg in args {
18016            if arg == "-p" {
18017                for name in &self.readonly_vars {
18018                    if let Some(val) = self.variables.get(name) {
18019                        println!("declare -r {}=\"{}\"", name, val);
18020                    }
18021                }
18022            } else if let Some(eq_pos) = arg.find('=') {
18023                let name = &arg[..eq_pos];
18024                let value = &arg[eq_pos + 1..];
18025                self.variables.insert(name.to_string(), value.to_string());
18026                self.readonly_vars.insert(name.to_string());
18027            } else {
18028                self.readonly_vars.insert(arg.clone());
18029            }
18030        }
18031        0
18032    }
18033
18034    /// unfunction - remove function definitions
18035    fn builtin_unfunction(&mut self, args: &[String]) -> i32 {
18036        for name in args {
18037            if self.functions.remove(name).is_none() {
18038                eprintln!("unfunction: no such function: {}", name);
18039            }
18040        }
18041        0
18042    }
18043
18044    /// getln - read line from buffer
18045    fn builtin_getln(&mut self, args: &[String]) -> i32 {
18046        if args.is_empty() {
18047            eprintln!("getln: missing variable name");
18048            return 1;
18049        }
18050        // Read from line buffer (simplified - just reads from stdin)
18051        let mut line = String::new();
18052        if std::io::stdin().read_line(&mut line).is_ok() {
18053            let line = line.trim_end_matches('\n');
18054            self.variables.insert(args[0].clone(), line.to_string());
18055            0
18056        } else {
18057            1
18058        }
18059    }
18060
18061    /// pushln - push line to buffer
18062    fn builtin_pushln(&mut self, args: &[String]) -> i32 {
18063        for arg in args {
18064            println!("{}", arg);
18065        }
18066        0
18067    }
18068
18069    /// bindkey - key binding management
18070    fn builtin_bindkey(&mut self, args: &[String]) -> i32 {
18071        use crate::zle::{zle, KeymapName};
18072
18073        if args.is_empty() {
18074            // List all bindings in main keymap
18075            let zle = zle();
18076            for (keys, widget) in zle
18077                .keymaps
18078                .get(&KeymapName::Main)
18079                .map(|km| km.list_bindings().collect::<Vec<_>>())
18080                .unwrap_or_default()
18081            {
18082                println!("\"{}\" {}", keys, widget);
18083            }
18084            return 0;
18085        }
18086
18087        let mut iter = args.iter().peekable();
18088        let mut keymap = KeymapName::Main;
18089        let mut list_mode = false;
18090        let mut list_all = false;
18091        let mut remove = false;
18092
18093        while let Some(arg) = iter.next() {
18094            match arg.as_str() {
18095                "-l" => {
18096                    list_mode = true;
18097                }
18098                "-L" => {
18099                    list_mode = true;
18100                    list_all = true;
18101                }
18102                "-la" | "-lL" => {
18103                    list_mode = true;
18104                    list_all = true;
18105                }
18106                "-M" => {
18107                    if let Some(name) = iter.next() {
18108                        if let Some(km) = KeymapName::from_str(name) {
18109                            keymap = km;
18110                        }
18111                    }
18112                }
18113                "-r" => {
18114                    remove = true;
18115                }
18116                "-A" => {
18117                    // Link keymaps - stub
18118                    return 0;
18119                }
18120                "-N" => {
18121                    // Create new keymap - stub
18122                    return 0;
18123                }
18124                "-e" => {
18125                    keymap = KeymapName::Emacs;
18126                }
18127                "-v" => {
18128                    keymap = KeymapName::ViInsert;
18129                }
18130                "-a" => {
18131                    keymap = KeymapName::ViCommand;
18132                }
18133                key if !key.starts_with('-') => {
18134                    // Key sequence - next arg is widget
18135                    if let Some(widget) = iter.next() {
18136                        let mut zle = zle();
18137                        if remove {
18138                            zle.unbind_key(keymap, key);
18139                        } else {
18140                            zle.bind_key(keymap, key, widget);
18141                        }
18142                    }
18143                    return 0;
18144                }
18145                _ => {}
18146            }
18147        }
18148
18149        if list_mode {
18150            let zle = zle();
18151            if list_all {
18152                for km_name in &[
18153                    KeymapName::Emacs,
18154                    KeymapName::ViInsert,
18155                    KeymapName::ViCommand,
18156                ] {
18157                    println!("{}", km_name.as_str());
18158                }
18159            } else {
18160                if let Some(km) = zle.keymaps.get(&keymap) {
18161                    for (keys, widget) in km.list_bindings() {
18162                        println!("bindkey \"{}\" {}", keys, widget);
18163                    }
18164                }
18165            }
18166        }
18167
18168        0
18169    }
18170
18171    /// zle - line editor control
18172    fn builtin_zle(&mut self, args: &[String]) -> i32 {
18173        use crate::zle::zle;
18174
18175        if args.is_empty() {
18176            return 0;
18177        }
18178
18179        let mut iter = args.iter().peekable();
18180
18181        while let Some(arg) = iter.next() {
18182            match arg.as_str() {
18183                "-l" => {
18184                    // List widgets
18185                    let zle = zle();
18186                    let mut widgets: Vec<&str> = zle.list_widgets();
18187                    widgets.sort();
18188                    for w in widgets {
18189                        println!("{}", w);
18190                    }
18191                    return 0;
18192                }
18193                "-la" | "-lL" => {
18194                    // List all widgets with details
18195                    let zle = zle();
18196                    let mut widgets: Vec<&str> = zle.list_widgets();
18197                    widgets.sort();
18198                    for w in widgets {
18199                        println!("{}", w);
18200                    }
18201                    return 0;
18202                }
18203                "-N" => {
18204                    // Define new widget: zle -N widget-name [function]
18205                    if let Some(widget_name) = iter.next() {
18206                        let func_name = iter
18207                            .next()
18208                            .map(|s| s.as_str())
18209                            .unwrap_or(widget_name.as_str());
18210                        let mut zle = zle();
18211                        zle.define_widget(widget_name, func_name);
18212                    }
18213                    return 0;
18214                }
18215                "-D" => {
18216                    // Delete widget - stub
18217                    return 0;
18218                }
18219                "-A" => {
18220                    // Define widget alias - stub
18221                    return 0;
18222                }
18223                "-R" => {
18224                    // Redisplay
18225                    return 0;
18226                }
18227                "-U" => {
18228                    // Unget characters - stub
18229                    return 0;
18230                }
18231                "-K" => {
18232                    // Select keymap - stub
18233                    return 0;
18234                }
18235                "-F" => {
18236                    // Install file descriptor handler - stub
18237                    return 0;
18238                }
18239                "-M" => {
18240                    // Display message - stub
18241                    return 0;
18242                }
18243                "-I" => {
18244                    // Invalidate completion - stub
18245                    return 0;
18246                }
18247                "-f" => {
18248                    // Check widget exists
18249                    if let Some(name) = iter.next() {
18250                        let zle = zle();
18251                        return if zle.get_widget(name).is_some() { 0 } else { 1 };
18252                    }
18253                    return 1;
18254                }
18255                widget_name if !widget_name.starts_with('-') => {
18256                    // Call widget
18257                    let mut zle = zle();
18258                    match zle.execute_widget(widget_name, None) {
18259                        crate::zle::WidgetResult::Ok => return 0,
18260                        crate::zle::WidgetResult::Error(e) => {
18261                            eprintln!("zle: {}", e);
18262                            return 1;
18263                        }
18264                        crate::zle::WidgetResult::CallFunction(func) => {
18265                            // Would need to call shell function
18266                            drop(zle);
18267                            if let Some(f) = self.functions.get(&func).cloned() {
18268                                return self.call_function(&f, &[]).unwrap_or(1);
18269                            }
18270                            return 1;
18271                        }
18272                        _ => return 0,
18273                    }
18274                }
18275                _ => {}
18276            }
18277        }
18278
18279        0
18280    }
18281
18282    /// sched - scheduled command execution (stub)
18283    fn builtin_sched(&mut self, args: &[String]) -> i32 {
18284        use std::time::{Duration, SystemTime};
18285
18286        if args.is_empty() {
18287            // List scheduled commands
18288            if self.scheduled_commands.is_empty() {
18289                return 0;
18290            }
18291            let now = SystemTime::now();
18292            for cmd in &self.scheduled_commands {
18293                let remaining = cmd.run_at.duration_since(now).unwrap_or(Duration::ZERO);
18294                println!("{:3}  +{:5}  {}", cmd.id, remaining.as_secs(), cmd.command);
18295            }
18296            return 0;
18297        }
18298
18299        let mut i = 0;
18300        while i < args.len() {
18301            match args[i].as_str() {
18302                "-" => {
18303                    // Remove scheduled item
18304                    i += 1;
18305                    if i >= args.len() {
18306                        eprintln!("sched: -: need item number");
18307                        return 1;
18308                    }
18309                    if let Ok(id) = args[i].parse::<u32>() {
18310                        self.scheduled_commands.retain(|c| c.id != id);
18311                        return 0;
18312                    } else {
18313                        eprintln!("sched: invalid item number");
18314                        return 1;
18315                    }
18316                }
18317                "+" => {
18318                    // Schedule relative time
18319                    i += 1;
18320                    if i >= args.len() {
18321                        eprintln!("sched: +: need time");
18322                        return 1;
18323                    }
18324                    let secs: u64 = args[i].parse().unwrap_or(0);
18325                    i += 1;
18326                    let command = args[i..].join(" ");
18327
18328                    let id = self.scheduled_commands.len() as u32 + 1;
18329                    self.scheduled_commands.push(ScheduledCommand {
18330                        id,
18331                        run_at: SystemTime::now() + Duration::from_secs(secs),
18332                        command,
18333                    });
18334                    return 0;
18335                }
18336                time_str => {
18337                    // Parse HH:MM or HH:MM:SS
18338                    let parts: Vec<&str> = time_str.split(':').collect();
18339                    if parts.len() >= 2 {
18340                        let hour: u32 = parts[0].parse().unwrap_or(0);
18341                        let min: u32 = parts[1].parse().unwrap_or(0);
18342                        let sec: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
18343
18344                        // Calculate duration until that time today/tomorrow
18345                        let now = SystemTime::now();
18346                        let target_secs = (hour * 3600 + min * 60 + sec) as u64;
18347                        let _day_secs = 86400u64;
18348
18349                        // Simplified: just add as seconds from now
18350                        let run_at = now + Duration::from_secs(target_secs);
18351
18352                        i += 1;
18353                        let command = args[i..].join(" ");
18354
18355                        let id = self.scheduled_commands.len() as u32 + 1;
18356                        self.scheduled_commands.push(ScheduledCommand {
18357                            id,
18358                            run_at,
18359                            command,
18360                        });
18361                        return 0;
18362                    } else {
18363                        eprintln!("sched: invalid time format");
18364                        return 1;
18365                    }
18366                }
18367            }
18368        }
18369        0
18370    }
18371
18372    /// zcompile - compile shell scripts to ZWC format
18373    fn builtin_zcompile(&mut self, args: &[String]) -> i32 {
18374        use crate::zwc::{ZwcBuilder, ZwcFile};
18375
18376        let mut list_mode = false; // -t: list functions in zwc
18377        let mut compile_current = false; // -c: compile current functions
18378        let mut compile_auto = false; // -a: compile autoload functions
18379        let mut files: Vec<String> = Vec::new();
18380
18381        let mut i = 0;
18382        while i < args.len() {
18383            let arg = &args[i];
18384            if arg.starts_with('-') && arg.len() > 1 {
18385                for c in arg[1..].chars() {
18386                    match c {
18387                        't' => list_mode = true,
18388                        'c' => compile_current = true,
18389                        'a' => compile_auto = true,
18390                        'U' | 'M' | 'R' | 'm' | 'z' | 'k' => {} // ignored for now
18391                        _ => {
18392                            eprintln!("zcompile: unknown option: -{}", c);
18393                            return 1;
18394                        }
18395                    }
18396                }
18397            } else {
18398                files.push(arg.clone());
18399            }
18400            i += 1;
18401        }
18402
18403        if files.is_empty() {
18404            eprintln!("zcompile: not enough arguments");
18405            return 1;
18406        }
18407
18408        // -t mode: list functions in ZWC file
18409        if list_mode {
18410            let zwc_path = if files[0].ends_with(".zwc") {
18411                files[0].clone()
18412            } else {
18413                format!("{}.zwc", files[0])
18414            };
18415
18416            match ZwcFile::load(&zwc_path) {
18417                Ok(zwc) => {
18418                    println!("zwc file for zshrs-{}", env!("CARGO_PKG_VERSION"));
18419                    if files.len() > 1 {
18420                        // Check specific functions
18421                        for name in &files[1..] {
18422                            if zwc.get_function(name).is_some() {
18423                                println!("{}", name);
18424                            } else {
18425                                eprintln!("zcompile: function not found: {}", name);
18426                                return 1;
18427                            }
18428                        }
18429                    } else {
18430                        // List all functions
18431                        for name in zwc.list_functions() {
18432                            println!("{}", name);
18433                        }
18434                    }
18435                    return 0;
18436                }
18437                Err(e) => {
18438                    eprintln!("zcompile: can't read zwc file: {}: {}", zwc_path, e);
18439                    return 1;
18440                }
18441            }
18442        }
18443
18444        // -c or -a mode: compile current/autoload functions
18445        if compile_current || compile_auto {
18446            let zwc_path = if files[0].ends_with(".zwc") {
18447                files[0].clone()
18448            } else {
18449                format!("{}.zwc", files[0])
18450            };
18451
18452            let mut builder = ZwcBuilder::new();
18453
18454            if files.len() > 1 {
18455                // Compile specific functions
18456                for name in &files[1..] {
18457                    if let Some(func) = self.functions.get(name) {
18458                        // Serialize the function (simplified - just store as comment for now)
18459                        let source = format!("# Compiled function: {}\n# Body: {:?}", name, func);
18460                        builder.add_source(name, &source);
18461                    } else if compile_auto && self.autoload_pending.contains_key(name) {
18462                        // Try to load autoload function source
18463                        if let Some(path) = self.find_function_file(name) {
18464                            if let Err(e) = builder.add_file(&path) {
18465                                eprintln!("zcompile: can't read {}: {}", name, e);
18466                                return 1;
18467                            }
18468                        }
18469                    } else {
18470                        eprintln!("zcompile: no such function: {}", name);
18471                        return 1;
18472                    }
18473                }
18474            } else {
18475                // Compile all functions
18476                for (name, func) in &self.functions {
18477                    let source = format!("# Compiled function: {}\n# Body: {:?}", name, func);
18478                    builder.add_source(name, &source);
18479                }
18480            }
18481
18482            if let Err(e) = builder.write(&zwc_path) {
18483                eprintln!("zcompile: can't write {}: {}", zwc_path, e);
18484                return 1;
18485            }
18486            return 0;
18487        }
18488
18489        // Default: compile files to ZWC
18490        let zwc_path = if files[0].ends_with(".zwc") {
18491            files[0].clone()
18492        } else {
18493            format!("{}.zwc", files[0])
18494        };
18495
18496        let mut builder = ZwcBuilder::new();
18497
18498        // If only one file given, it's both the source and output base
18499        let source_files = if files.len() == 1 {
18500            // Check if it's a directory
18501            let path = std::path::Path::new(&files[0]);
18502            if path.is_dir() {
18503                // Compile all files in directory
18504                match std::fs::read_dir(path) {
18505                    Ok(entries) => {
18506                        for entry in entries.flatten() {
18507                            let p = entry.path();
18508                            if p.is_file() && !p.extension().map_or(false, |e| e == "zwc") {
18509                                if let Err(e) = builder.add_file(&p) {
18510                                    eprintln!("zcompile: can't read {:?}: {}", p, e);
18511                                }
18512                            }
18513                        }
18514                    }
18515                    Err(e) => {
18516                        eprintln!("zcompile: can't read directory: {}", e);
18517                        return 1;
18518                    }
18519                }
18520                vec![]
18521            } else {
18522                vec![files[0].clone()]
18523            }
18524        } else {
18525            files[1..].to_vec()
18526        };
18527
18528        for file in &source_files {
18529            let path = std::path::Path::new(file);
18530            if let Err(e) = builder.add_file(path) {
18531                eprintln!("zcompile: can't read {}: {}", file, e);
18532                return 1;
18533            }
18534        }
18535
18536        if let Err(e) = builder.write(&zwc_path) {
18537            eprintln!("zcompile: can't write {}: {}", zwc_path, e);
18538            return 1;
18539        }
18540
18541        0
18542    }
18543
18544    /// zformat - format strings
18545    fn builtin_zformat(&self, args: &[String]) -> i32 {
18546        if args.len() < 2 {
18547            eprintln!("zformat: not enough arguments");
18548            return 1;
18549        }
18550
18551        match args[0].as_str() {
18552            "-f" => {
18553                // Format string: zformat -f var format specs...
18554                if args.len() < 3 {
18555                    return 1;
18556                }
18557                let _var_name = &args[1];
18558                let format = &args[2];
18559                let specs: HashMap<char, &str> = args[3..]
18560                    .iter()
18561                    .filter_map(|s| {
18562                        let mut chars = s.chars();
18563                        let key = chars.next()?;
18564                        if chars.next() == Some(':') {
18565                            Some((key, &s[2..]))
18566                        } else {
18567                            None
18568                        }
18569                    })
18570                    .collect();
18571
18572                let mut result = String::new();
18573                let mut chars = format.chars().peekable();
18574                while let Some(c) = chars.next() {
18575                    if c == '%' {
18576                        if let Some(&spec_char) = chars.peek() {
18577                            if let Some(replacement) = specs.get(&spec_char) {
18578                                result.push_str(replacement);
18579                                chars.next();
18580                                continue;
18581                            }
18582                        }
18583                    }
18584                    result.push(c);
18585                }
18586                println!("{}", result);
18587            }
18588            "-a" => {
18589                // Format into array elements: zformat -a array sep specs...
18590                // Each spec is "text:value" or "text:value:cond"
18591                if args.len() < 4 {
18592                    eprintln!("zformat -a: need array, separator, and specs");
18593                    return 1;
18594                }
18595                let _array_name = &args[1];
18596                let sep = &args[2];
18597
18598                let mut results = Vec::new();
18599                for spec in &args[3..] {
18600                    let parts: Vec<&str> = spec.splitn(3, ':').collect();
18601                    if parts.len() >= 2 {
18602                        let text = parts[0];
18603                        let value = parts[1];
18604                        let cond = parts.get(2).copied();
18605
18606                        // If condition exists and is empty/false, skip
18607                        if let Some(c) = cond {
18608                            if c.is_empty() || c == "0" {
18609                                continue;
18610                            }
18611                        }
18612
18613                        if !value.is_empty() {
18614                            results.push(format!("{}{}{}", text, sep, value));
18615                        }
18616                    }
18617                }
18618
18619                for r in results {
18620                    println!("{}", r);
18621                }
18622            }
18623            _ => {
18624                eprintln!("zformat: unknown option: {}", args[0]);
18625                return 1;
18626            }
18627        }
18628        0
18629    }
18630
18631    /// vared - visually edit a variable
18632    fn builtin_vared(&mut self, args: &[String]) -> i32 {
18633        if args.is_empty() {
18634            eprintln!("vared: not enough arguments");
18635            return 1;
18636        }
18637
18638        let mut var_name = String::new();
18639        let mut prompt = String::new();
18640        let mut rprompt = String::new();
18641        let mut _history = false; // TODO: implement history completion
18642        let mut i = 0;
18643
18644        while i < args.len() {
18645            match args[i].as_str() {
18646                "-p" if i + 1 < args.len() => {
18647                    i += 1;
18648                    prompt = args[i].clone();
18649                }
18650                "-r" if i + 1 < args.len() => {
18651                    i += 1;
18652                    rprompt = args[i].clone();
18653                }
18654                "-h" => _history = true,
18655                "-c" => {} // Use completion - ignored
18656                "-e" => {} // Use emacs mode - ignored
18657                "-M" | "-m" => {
18658                    i += 1;
18659                } // Main/alt keymap - skip arg
18660                "-a" | "-A" => {
18661                    i += 1;
18662                } // Array assignment - skip arg
18663                s if !s.starts_with('-') => {
18664                    var_name = s.to_string();
18665                }
18666                _ => {}
18667            }
18668            i += 1;
18669        }
18670
18671        if var_name.is_empty() {
18672            eprintln!("vared: not enough arguments");
18673            return 1;
18674        }
18675
18676        // Get current value
18677        let current = self.get_variable(&var_name);
18678
18679        // Simple line editing using stdin
18680        if !prompt.is_empty() {
18681            eprint!("{}", prompt);
18682        }
18683        print!("{}", current);
18684        if !rprompt.is_empty() {
18685            eprint!("{}", rprompt);
18686        }
18687
18688        let mut input = String::new();
18689        if std::io::stdin().read_line(&mut input).is_ok() {
18690            let value = input.trim_end_matches('\n').to_string();
18691            self.variables.insert(var_name, value);
18692            return 0;
18693        }
18694        1
18695    }
18696
18697    /// echotc - output termcap value
18698    fn builtin_echotc(&self, args: &[String]) -> i32 {
18699        if args.is_empty() {
18700            eprintln!("echotc: not enough arguments");
18701            return 1;
18702        }
18703
18704        // Common termcap capabilities
18705        match args[0].as_str() {
18706            "cl" => print!("\x1b[H\x1b[2J"), // clear screen
18707            "cd" => print!("\x1b[J"),        // clear to end of display
18708            "ce" => print!("\x1b[K"),        // clear to end of line
18709            "cm" => {
18710                // cursor motion - needs row, col args
18711                if args.len() >= 3 {
18712                    if let (Ok(row), Ok(col)) = (args[1].parse::<u32>(), args[2].parse::<u32>()) {
18713                        print!("\x1b[{};{}H", row + 1, col + 1);
18714                    }
18715                }
18716            }
18717            "up" => print!("\x1b[A"),    // cursor up
18718            "do" => print!("\x1b[B"),    // cursor down
18719            "le" => print!("\x1b[D"),    // cursor left
18720            "nd" => print!("\x1b[C"),    // cursor right
18721            "ho" => print!("\x1b[H"),    // home cursor
18722            "vi" => print!("\x1b[?25l"), // invisible cursor
18723            "ve" => print!("\x1b[?25h"), // visible cursor
18724            "so" => print!("\x1b[7m"),   // standout mode
18725            "se" => print!("\x1b[27m"),  // end standout
18726            "us" => print!("\x1b[4m"),   // underline
18727            "ue" => print!("\x1b[24m"),  // end underline
18728            "md" => print!("\x1b[1m"),   // bold
18729            "me" => print!("\x1b[0m"),   // end all attributes
18730            "mr" => print!("\x1b[7m"),   // reverse video
18731            "AF" | "setaf" => {
18732                // Set foreground color
18733                if args.len() >= 2 {
18734                    if let Ok(color) = args[1].parse::<u32>() {
18735                        print!("\x1b[38;5;{}m", color);
18736                    }
18737                }
18738            }
18739            "AB" | "setab" => {
18740                // Set background color
18741                if args.len() >= 2 {
18742                    if let Ok(color) = args[1].parse::<u32>() {
18743                        print!("\x1b[48;5;{}m", color);
18744                    }
18745                }
18746            }
18747            "Co" | "colors" => {
18748                // Number of colors - assume 256
18749                println!("256");
18750            }
18751            "co" | "cols" => {
18752                // Number of columns
18753                println!(
18754                    "{}",
18755                    std::env::var("COLUMNS")
18756                        .ok()
18757                        .and_then(|s| s.parse().ok())
18758                        .unwrap_or(80u16)
18759                );
18760            }
18761            "li" | "lines" => {
18762                // Number of lines
18763                println!(
18764                    "{}",
18765                    std::env::var("LINES")
18766                        .ok()
18767                        .and_then(|s| s.parse().ok())
18768                        .unwrap_or(24u16)
18769                );
18770            }
18771            cap => {
18772                eprintln!("echotc: unknown capability: {}", cap);
18773                return 1;
18774            }
18775        }
18776        use std::io::Write;
18777        let _ = std::io::stdout().flush();
18778        0
18779    }
18780
18781    /// echoti - output terminfo value
18782    fn builtin_echoti(&self, args: &[String]) -> i32 {
18783        // echoti is similar to echotc but uses terminfo names
18784        // For simplicity, we'll use the same implementation
18785        self.builtin_echotc(args)
18786    }
18787
18788    /// zpty - manage pseudo-terminals
18789    fn builtin_zpty(&mut self, args: &[String]) -> i32 {
18790        use std::io::{Read, Write};
18791        use std::process::{Command, Stdio};
18792
18793        if args.is_empty() {
18794            // List all ptys
18795            if self.zptys.is_empty() {
18796                return 0;
18797            }
18798            for (name, state) in &self.zptys {
18799                println!("{}: {} (pid {})", name, state.cmd, state.pid);
18800            }
18801            return 0;
18802        }
18803
18804        let mut i = 0;
18805        while i < args.len() {
18806            match args[i].as_str() {
18807                "-d" => {
18808                    // Delete pty
18809                    i += 1;
18810                    if i >= args.len() {
18811                        eprintln!("zpty: -d requires pty name");
18812                        return 1;
18813                    }
18814                    let name = &args[i];
18815                    if let Some(mut state) = self.zptys.remove(name) {
18816                        if let Some(ref mut child) = state.child {
18817                            let _ = child.kill();
18818                        }
18819                        return 0;
18820                    } else {
18821                        eprintln!("zpty: no such pty: {}", name);
18822                        return 1;
18823                    }
18824                }
18825                "-w" => {
18826                    // Write to pty: zpty -w name string...
18827                    i += 1;
18828                    if i >= args.len() {
18829                        eprintln!("zpty: -w requires pty name");
18830                        return 1;
18831                    }
18832                    let name = args[i].clone();
18833                    i += 1;
18834                    let data = args[i..].join(" ") + "\n";
18835
18836                    if let Some(state) = self.zptys.get_mut(&name) {
18837                        if let Some(ref mut stdin) = state.stdin {
18838                            if stdin.write_all(data.as_bytes()).is_ok() {
18839                                let _ = stdin.flush();
18840                                return 0;
18841                            }
18842                        }
18843                        eprintln!("zpty: write failed");
18844                        return 1;
18845                    } else {
18846                        eprintln!("zpty: no such pty: {}", name);
18847                        return 1;
18848                    }
18849                }
18850                "-r" => {
18851                    // Read from pty: zpty -r name [param]
18852                    i += 1;
18853                    if i >= args.len() {
18854                        eprintln!("zpty: -r requires pty name");
18855                        return 1;
18856                    }
18857                    let name = args[i].clone();
18858                    i += 1;
18859                    let var_name = if i < args.len() {
18860                        args[i].clone()
18861                    } else {
18862                        "REPLY".to_string()
18863                    };
18864
18865                    if let Some(state) = self.zptys.get_mut(&name) {
18866                        if let Some(ref mut stdout) = state.stdout {
18867                            let mut buf = vec![0u8; 4096];
18868                            match stdout.read(&mut buf) {
18869                                Ok(n) => {
18870                                    let data = String::from_utf8_lossy(&buf[..n]).to_string();
18871                                    self.variables.insert(var_name, data);
18872                                    return 0;
18873                                }
18874                                Err(_) => return 1,
18875                            }
18876                        }
18877                        return 1;
18878                    } else {
18879                        eprintln!("zpty: no such pty: {}", name);
18880                        return 1;
18881                    }
18882                }
18883                "-t" => {
18884                    // Test if data available
18885                    i += 1;
18886                    if i >= args.len() {
18887                        return 1;
18888                    }
18889                    let name = &args[i];
18890                    if self.zptys.contains_key(name) {
18891                        return 0; // Assume data available if pty exists
18892                    }
18893                    return 1;
18894                }
18895                "-L" => {
18896                    // List in script-friendly format
18897                    for (name, state) in &self.zptys {
18898                        println!("zpty {} {}", name, state.cmd);
18899                    }
18900                    return 0;
18901                }
18902                "-b" | "-e" => {
18903                    // Options: -b (blocking), -e (echo)
18904                    i += 1;
18905                    continue;
18906                }
18907                name if !name.starts_with('-') => {
18908                    // Create new pty: zpty name command [args...]
18909                    i += 1;
18910                    if i >= args.len() {
18911                        eprintln!("zpty: command required");
18912                        return 1;
18913                    }
18914                    let cmd_str = args[i..].join(" ");
18915
18916                    match Command::new("sh")
18917                        .arg("-c")
18918                        .arg(&cmd_str)
18919                        .stdin(Stdio::piped())
18920                        .stdout(Stdio::piped())
18921                        .stderr(Stdio::piped())
18922                        .spawn()
18923                    {
18924                        Ok(mut child) => {
18925                            let pid = child.id();
18926                            let stdin = child.stdin.take();
18927                            let stdout = child.stdout.take();
18928
18929                            self.zptys.insert(
18930                                name.to_string(),
18931                                ZptyState {
18932                                    pid,
18933                                    cmd: cmd_str,
18934                                    stdin,
18935                                    stdout,
18936                                    child: Some(child),
18937                                },
18938                            );
18939                            return 0;
18940                        }
18941                        Err(e) => {
18942                            eprintln!("zpty: failed to start: {}", e);
18943                            return 1;
18944                        }
18945                    }
18946                }
18947                _ => {
18948                    i += 1;
18949                }
18950            }
18951            i += 1;
18952        }
18953        0
18954    }
18955
18956    /// zprof - profiling support
18957    fn builtin_zprof(&mut self, args: &[String]) -> i32 {
18958        use crate::zprof::ZprofOptions;
18959
18960        let options = ZprofOptions {
18961            clear: args.iter().any(|a| a == "-c"),
18962        };
18963
18964        let (status, output) = crate::zprof::builtin_zprof(&mut self.profiler, &options);
18965        if !output.is_empty() {
18966            print!("{}", output);
18967        }
18968        status
18969    }
18970
18971    /// zsocket - create/manage sockets
18972    fn builtin_zsocket(&mut self, args: &[String]) -> i32 {
18973        use std::os::unix::net::{UnixListener, UnixStream};
18974
18975        if args.is_empty() {
18976            // List open sockets
18977            if self.unix_sockets.is_empty() {
18978                return 0;
18979            }
18980            for (fd, state) in &self.unix_sockets {
18981                let path = state
18982                    .path
18983                    .as_ref()
18984                    .map(|p| p.display().to_string())
18985                    .unwrap_or_default();
18986                let status = if state.listening {
18987                    "listening"
18988                } else {
18989                    "connected"
18990                };
18991                println!("{}: {} ({})", fd, path, status);
18992            }
18993            return 0;
18994        }
18995
18996        let mut i = 0;
18997        let mut verbose = false;
18998        let mut var_name = "REPLY".to_string();
18999
19000        while i < args.len() {
19001            match args[i].as_str() {
19002                "-v" => {
19003                    verbose = true;
19004                    i += 1;
19005                    if i < args.len() && !args[i].starts_with('-') {
19006                        var_name = args[i].clone();
19007                    }
19008                }
19009                "-l" => {
19010                    // Listen on Unix socket: zsocket -l path
19011                    i += 1;
19012                    if i >= args.len() {
19013                        eprintln!("zsocket: -l requires path");
19014                        return 1;
19015                    }
19016                    let path = PathBuf::from(&args[i]);
19017
19018                    // Remove existing socket file
19019                    let _ = std::fs::remove_file(&path);
19020
19021                    match UnixListener::bind(&path) {
19022                        Ok(listener) => {
19023                            let fd = self.next_fd;
19024                            self.next_fd += 1;
19025
19026                            self.unix_sockets.insert(
19027                                fd,
19028                                UnixSocketState {
19029                                    path: Some(path),
19030                                    listening: true,
19031                                    stream: None,
19032                                    listener: Some(listener),
19033                                },
19034                            );
19035
19036                            if verbose {
19037                                self.variables.insert(var_name.clone(), fd.to_string());
19038                            }
19039                            println!("{}", fd);
19040                            return 0;
19041                        }
19042                        Err(e) => {
19043                            eprintln!("zsocket: bind failed: {}", e);
19044                            return 1;
19045                        }
19046                    }
19047                }
19048                "-a" => {
19049                    // Accept connection: zsocket -a fd
19050                    i += 1;
19051                    if i >= args.len() {
19052                        eprintln!("zsocket: -a requires fd");
19053                        return 1;
19054                    }
19055                    let listen_fd: i32 = args[i].parse().unwrap_or(-1);
19056
19057                    if let Some(state) = self.unix_sockets.get(&listen_fd) {
19058                        if let Some(ref listener) = state.listener {
19059                            match listener.accept() {
19060                                Ok((stream, _addr)) => {
19061                                    let new_fd = self.next_fd;
19062                                    self.next_fd += 1;
19063
19064                                    self.unix_sockets.insert(
19065                                        new_fd,
19066                                        UnixSocketState {
19067                                            path: None,
19068                                            listening: false,
19069                                            stream: Some(stream),
19070                                            listener: None,
19071                                        },
19072                                    );
19073
19074                                    if verbose {
19075                                        self.variables.insert(var_name.clone(), new_fd.to_string());
19076                                    }
19077                                    println!("{}", new_fd);
19078                                    return 0;
19079                                }
19080                                Err(e) => {
19081                                    eprintln!("zsocket: accept failed: {}", e);
19082                                    return 1;
19083                                }
19084                            }
19085                        }
19086                    }
19087                    eprintln!("zsocket: invalid fd");
19088                    return 1;
19089                }
19090                "-d" => {
19091                    // Close socket: zsocket -d fd
19092                    i += 1;
19093                    if i >= args.len() {
19094                        eprintln!("zsocket: -d requires fd");
19095                        return 1;
19096                    }
19097                    let fd: i32 = args[i].parse().unwrap_or(-1);
19098
19099                    if let Some(state) = self.unix_sockets.remove(&fd) {
19100                        if let Some(path) = state.path {
19101                            let _ = std::fs::remove_file(path);
19102                        }
19103                        return 0;
19104                    }
19105                    eprintln!("zsocket: no such fd");
19106                    return 1;
19107                }
19108                path if !path.starts_with('-') => {
19109                    // Connect to Unix socket: zsocket path
19110                    match UnixStream::connect(path) {
19111                        Ok(stream) => {
19112                            let fd = self.next_fd;
19113                            self.next_fd += 1;
19114
19115                            self.unix_sockets.insert(
19116                                fd,
19117                                UnixSocketState {
19118                                    path: Some(PathBuf::from(path)),
19119                                    listening: false,
19120                                    stream: Some(stream),
19121                                    listener: None,
19122                                },
19123                            );
19124
19125                            if verbose {
19126                                self.variables.insert(var_name.clone(), fd.to_string());
19127                            }
19128                            println!("{}", fd);
19129                            return 0;
19130                        }
19131                        Err(e) => {
19132                            eprintln!("zsocket: connect failed: {}", e);
19133                            return 1;
19134                        }
19135                    }
19136                }
19137                _ => {}
19138            }
19139            i += 1;
19140        }
19141        0
19142    }
19143
19144    /// ztcp - TCP socket operations
19145    fn builtin_ztcp(&mut self, args: &[String]) -> i32 {
19146        // Similar to zsocket but TCP specific
19147        self.builtin_zsocket(args)
19148    }
19149
19150    /// zregexparse - parse with regex
19151    fn builtin_zregexparse(&mut self, args: &[String]) -> i32 {
19152        if args.len() < 2 {
19153            eprintln!("zregexparse: usage: zregexparse var pattern [string]");
19154            return 1;
19155        }
19156
19157        let var_name = &args[0];
19158        let pattern = &args[1];
19159        let string = if args.len() > 2 {
19160            args[2].clone()
19161        } else {
19162            self.variables.get("REPLY").cloned().unwrap_or_default()
19163        };
19164
19165        match regex::Regex::new(pattern) {
19166            Ok(re) => {
19167                if let Some(captures) = re.captures(&string) {
19168                    // Store full match in var
19169                    if let Some(m) = captures.get(0) {
19170                        self.variables
19171                            .insert(var_name.clone(), m.as_str().to_string());
19172                    }
19173
19174                    // Store capture groups in MATCH array
19175                    let mut match_array = Vec::new();
19176                    let mut mbegin_array = Vec::new();
19177                    let mut mend_array = Vec::new();
19178
19179                    for (i, cap) in captures.iter().enumerate() {
19180                        if let Some(c) = cap {
19181                            match_array.push(c.as_str().to_string());
19182                            mbegin_array.push((c.start() + 1).to_string());
19183                            mend_array.push(c.end().to_string());
19184                            self.variables
19185                                .insert(format!("match[{}]", i), c.as_str().to_string());
19186                        }
19187                    }
19188                    self.arrays.insert("match".to_string(), match_array);
19189                    self.arrays.insert("mbegin".to_string(), mbegin_array);
19190                    self.arrays.insert("mend".to_string(), mend_array);
19191
19192                    // Store match positions
19193                    if let Some(m) = captures.get(0) {
19194                        self.variables
19195                            .insert("MBEGIN".to_string(), (m.start() + 1).to_string());
19196                        self.variables
19197                            .insert("MEND".to_string(), m.end().to_string());
19198                    }
19199
19200                    0
19201                } else {
19202                    1
19203                }
19204            }
19205            Err(e) => {
19206                eprintln!("zregexparse: invalid regex: {}", e);
19207                2
19208            }
19209        }
19210    }
19211
19212    /// clone - create a subshell with forked state
19213    fn builtin_clone(&mut self, args: &[String]) -> i32 {
19214        use std::process::Command;
19215
19216        // clone creates a subshell that shares the parent's state
19217        // We simulate this by spawning a new zshrs process
19218        let mut cmd =
19219            Command::new(std::env::current_exe().unwrap_or_else(|_| PathBuf::from("zshrs")));
19220
19221        if !args.is_empty() {
19222            // If args provided, run them in the subshell
19223            cmd.arg("-c").arg(args.join(" "));
19224        }
19225
19226        // Export current variables to child
19227        for (k, v) in &self.variables {
19228            cmd.env(k, v);
19229        }
19230
19231        match cmd.spawn() {
19232            Ok(mut child) => match child.wait() {
19233                Ok(status) => status.code().unwrap_or(0),
19234                Err(_) => 1,
19235            },
19236            Err(e) => {
19237                eprintln!("clone: failed to spawn subshell: {}", e);
19238                1
19239            }
19240        }
19241    }
19242
19243    /// log - same as logout for login shells
19244    fn builtin_log(&mut self, args: &[String]) -> i32 {
19245        self.builtin_exit(args)
19246    }
19247
19248    // Completion system builtins (stubs for compsys)
19249
19250    /// comparguments - parse completion arguments
19251    fn builtin_comparguments(&mut self, _args: &[String]) -> i32 {
19252        // Used internally by _arguments
19253        0
19254    }
19255
19256    /// compcall - call completion function
19257    fn builtin_compcall(&mut self, _args: &[String]) -> i32 {
19258        // Calls the completion function
19259        0
19260    }
19261
19262    /// compctl - old-style completion (deprecated)
19263    fn builtin_compctl(&mut self, args: &[String]) -> i32 {
19264        if args.is_empty() {
19265            println!("compctl: old-style completion system");
19266            println!("Use the new completion system (compsys) instead");
19267            return 0;
19268        }
19269        // Parse compctl options for backwards compatibility
19270        0
19271    }
19272
19273    /// compdescribe - describe completions
19274    fn builtin_compdescribe(&mut self, _args: &[String]) -> i32 {
19275        0
19276    }
19277
19278    /// compfiles - complete files
19279    fn builtin_compfiles(&mut self, _args: &[String]) -> i32 {
19280        0
19281    }
19282
19283    /// compgroups - group completions
19284    fn builtin_compgroups(&mut self, _args: &[String]) -> i32 {
19285        0
19286    }
19287
19288    /// compquote - quote completion strings
19289    fn builtin_compquote(&mut self, _args: &[String]) -> i32 {
19290        0
19291    }
19292
19293    /// comptags - manage completion tags
19294    fn builtin_comptags(&mut self, args: &[String]) -> i32 {
19295        if args.is_empty() {
19296            return 1;
19297        }
19298        match args[0].as_str() {
19299            "-i" => {
19300                // Initialize tags
19301                0
19302            }
19303            "-S" => {
19304                // Set tags
19305                0
19306            }
19307            _ => 1,
19308        }
19309    }
19310
19311    /// comptry - try completion
19312    fn builtin_comptry(&mut self, _args: &[String]) -> i32 {
19313        1 // No match
19314    }
19315
19316    /// compvalues - complete values
19317    fn builtin_compvalues(&mut self, _args: &[String]) -> i32 {
19318        0
19319    }
19320
19321    /// cap/getcap/setcap - Linux capabilities (stub on macOS)
19322    fn builtin_cap(&self, args: &[String]) -> i32 {
19323        // Linux capabilities are not available on macOS
19324        // On Linux, these would interact with libcap
19325        if args.is_empty() {
19326            println!("cap: display/set capabilities");
19327            println!("  getcap file...  - display capabilities");
19328            println!("  setcap caps file - set capabilities");
19329            return 0;
19330        }
19331
19332        #[cfg(target_os = "linux")]
19333        {
19334            // On Linux, we could use libcap bindings
19335            // For now, just run the external commands
19336            let status = std::process::Command::new(&args[0])
19337                .args(&args[1..])
19338                .status();
19339            return status.map(|s| s.code().unwrap_or(1)).unwrap_or(1);
19340        }
19341
19342        #[cfg(not(target_os = "linux"))]
19343        {
19344            eprintln!("cap: capabilities not supported on this platform");
19345            1
19346        }
19347    }
19348
19349    /// zcurses - curses interface (stub)
19350    fn builtin_zcurses(&mut self, args: &[String]) -> i32 {
19351        if args.is_empty() {
19352            eprintln!("zcurses: requires subcommand");
19353            return 1;
19354        }
19355
19356        match args[0].as_str() {
19357            "init" => {
19358                // Initialize curses
19359                println!("zcurses: would initialize curses");
19360                0
19361            }
19362            "end" => {
19363                // End curses mode
19364                println!("zcurses: would end curses");
19365                0
19366            }
19367            "addwin" => {
19368                // Add a window
19369                0
19370            }
19371            "delwin" => {
19372                // Delete a window
19373                0
19374            }
19375            "refresh" => {
19376                // Refresh display
19377                0
19378            }
19379            "move" => {
19380                // Move cursor
19381                0
19382            }
19383            "clear" => {
19384                // Clear window
19385                0
19386            }
19387            "char" | "string" => {
19388                // Output character/string
19389                0
19390            }
19391            "border" => {
19392                // Draw border
19393                0
19394            }
19395            "attr" => {
19396                // Set attributes
19397                0
19398            }
19399            "color" => {
19400                // Set colors
19401                0
19402            }
19403            "scroll" => {
19404                // Scroll window
19405                0
19406            }
19407            "input" => {
19408                // Get input
19409                0
19410            }
19411            "mouse" => {
19412                // Mouse support
19413                0
19414            }
19415            "querychar" => {
19416                // Query character at position
19417                0
19418            }
19419            "resize" => {
19420                // Resize window
19421                0
19422            }
19423            cmd => {
19424                eprintln!("zcurses: unknown subcommand: {}", cmd);
19425                1
19426            }
19427        }
19428    }
19429
19430    /// sysread - low-level read (zsh/system module)
19431    fn builtin_sysread(&mut self, args: &[String]) -> i32 {
19432        use std::io::Read;
19433
19434        let mut fd = 0i32; // stdin
19435        let mut count: Option<usize> = None;
19436        let mut var_name = "REPLY".to_string();
19437        let mut i = 0;
19438
19439        while i < args.len() {
19440            match args[i].as_str() {
19441                "-c" if i + 1 < args.len() => {
19442                    i += 1;
19443                    count = args[i].parse().ok();
19444                }
19445                "-i" if i + 1 < args.len() => {
19446                    i += 1;
19447                    fd = args[i].parse().unwrap_or(0);
19448                }
19449                "-o" if i + 1 < args.len() => {
19450                    i += 1;
19451                    var_name = args[i].clone();
19452                }
19453                "-t" if i + 1 < args.len() => {
19454                    i += 1;
19455                    // Timeout - ignored for now
19456                }
19457                _ => {
19458                    var_name = args[i].clone();
19459                }
19460            }
19461            i += 1;
19462        }
19463
19464        let mut buffer = vec![0u8; count.unwrap_or(8192)];
19465
19466        // Only support stdin for now
19467        if fd == 0 {
19468            match std::io::stdin().read(&mut buffer) {
19469                Ok(n) => {
19470                    buffer.truncate(n);
19471                    let s = String::from_utf8_lossy(&buffer).to_string();
19472                    self.variables.insert(var_name, s);
19473                    0
19474                }
19475                Err(_) => 1,
19476            }
19477        } else {
19478            eprintln!("sysread: only fd 0 (stdin) supported");
19479            1
19480        }
19481    }
19482
19483    /// syswrite - low-level write (zsh/system module)
19484    fn builtin_syswrite(&mut self, args: &[String]) -> i32 {
19485        use std::io::Write;
19486
19487        let mut fd = 1i32; // stdout
19488        let mut data = String::new();
19489        let mut i = 0;
19490
19491        while i < args.len() {
19492            match args[i].as_str() {
19493                "-o" if i + 1 < args.len() => {
19494                    i += 1;
19495                    fd = args[i].parse().unwrap_or(1);
19496                }
19497                "-c" if i + 1 < args.len() => {
19498                    i += 1;
19499                    // Count - ignored
19500                }
19501                _ => {
19502                    data = args[i].clone();
19503                }
19504            }
19505            i += 1;
19506        }
19507
19508        match fd {
19509            1 => {
19510                let _ = std::io::stdout().write_all(data.as_bytes());
19511                let _ = std::io::stdout().flush();
19512                0
19513            }
19514            2 => {
19515                let _ = std::io::stderr().write_all(data.as_bytes());
19516                let _ = std::io::stderr().flush();
19517                0
19518            }
19519            _ => {
19520                eprintln!("syswrite: only fd 1 (stdout) and 2 (stderr) supported");
19521                1
19522            }
19523        }
19524    }
19525
19526    /// syserror - get error message (zsh/system module)
19527    fn builtin_syserror(&self, args: &[String]) -> i32 {
19528        let errno = if args.is_empty() {
19529            // Use last errno
19530            std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
19531        } else {
19532            args[0].parse().unwrap_or(0)
19533        };
19534
19535        let err = std::io::Error::from_raw_os_error(errno);
19536        println!("{}", err);
19537        0
19538    }
19539
19540    /// sysopen - open file descriptor (zsh/system module)
19541    fn builtin_sysopen(&mut self, args: &[String]) -> i32 {
19542        use std::fs::OpenOptions;
19543
19544        let mut filename = String::new();
19545        let mut var_name = "REPLY".to_string();
19546        let mut read = false;
19547        let mut write = false;
19548        let mut append = false;
19549        let mut create = false;
19550        let mut truncate = false;
19551
19552        let mut i = 0;
19553        while i < args.len() {
19554            match args[i].as_str() {
19555                "-r" => read = true,
19556                "-w" => write = true,
19557                "-a" => append = true,
19558                "-c" => create = true,
19559                "-t" => truncate = true,
19560                "-u" => {
19561                    i += 1;
19562                    if i < args.len() {
19563                        var_name = args[i].clone();
19564                    }
19565                }
19566                "-o" => {
19567                    i += 1;
19568                    // Mode flags like O_RDONLY etc - parse as needed
19569                }
19570                s if !s.starts_with('-') => {
19571                    filename = s.to_string();
19572                }
19573                _ => {}
19574            }
19575            i += 1;
19576        }
19577
19578        if filename.is_empty() {
19579            eprintln!("sysopen: need filename");
19580            return 1;
19581        }
19582
19583        // Default to read if nothing specified
19584        if !read && !write && !append {
19585            read = true;
19586        }
19587
19588        let file = OpenOptions::new()
19589            .read(read)
19590            .write(write || append || truncate)
19591            .append(append)
19592            .create(create || write)
19593            .truncate(truncate)
19594            .open(&filename);
19595
19596        match file {
19597            Ok(f) => {
19598                let fd = self.next_fd;
19599                self.next_fd += 1;
19600                self.open_fds.insert(fd, f);
19601                self.variables.insert(var_name, fd.to_string());
19602                0
19603            }
19604            Err(e) => {
19605                eprintln!("sysopen: {}: {}", filename, e);
19606                1
19607            }
19608        }
19609    }
19610
19611    /// sysseek - seek on file descriptor (zsh/system module)
19612    fn builtin_sysseek(&mut self, args: &[String]) -> i32 {
19613        use std::io::{Seek, SeekFrom};
19614
19615        let mut fd = -1i32;
19616        let mut offset = 0i64;
19617        let mut whence = SeekFrom::Start(0);
19618
19619        let mut i = 0;
19620        while i < args.len() {
19621            match args[i].as_str() {
19622                "-u" => {
19623                    i += 1;
19624                    if i < args.len() {
19625                        fd = args[i].parse().unwrap_or(-1);
19626                    }
19627                }
19628                "-w" => {
19629                    i += 1;
19630                    if i < args.len() {
19631                        whence = match args[i].as_str() {
19632                            "start" | "set" | "0" => SeekFrom::Start(offset as u64),
19633                            "current" | "cur" | "1" => SeekFrom::Current(offset),
19634                            "end" | "2" => SeekFrom::End(offset),
19635                            _ => SeekFrom::Start(offset as u64),
19636                        };
19637                    }
19638                }
19639                s if !s.starts_with('-') => {
19640                    offset = s.parse().unwrap_or(0);
19641                }
19642                _ => {}
19643            }
19644            i += 1;
19645        }
19646
19647        if fd < 0 {
19648            eprintln!("sysseek: need fd (-u)");
19649            return 1;
19650        }
19651
19652        // Update whence with actual offset
19653        whence = match whence {
19654            SeekFrom::Start(_) => SeekFrom::Start(offset as u64),
19655            SeekFrom::Current(_) => SeekFrom::Current(offset),
19656            SeekFrom::End(_) => SeekFrom::End(offset),
19657        };
19658
19659        if let Some(file) = self.open_fds.get_mut(&fd) {
19660            match file.seek(whence) {
19661                Ok(pos) => {
19662                    self.variables.insert("REPLY".to_string(), pos.to_string());
19663                    0
19664                }
19665                Err(e) => {
19666                    eprintln!("sysseek: {}", e);
19667                    1
19668                }
19669            }
19670        } else {
19671            eprintln!("sysseek: bad fd: {}", fd);
19672            1
19673        }
19674    }
19675
19676    /// private - declare private variables (zsh/param/private module)
19677    fn builtin_private(&mut self, args: &[String]) -> i32 {
19678        // Similar to local but with stricter scoping
19679        self.builtin_local(args)
19680    }
19681
19682    /// zgetattr/zsetattr/zdelattr/zlistattr - extended attributes (zsh/attr module)
19683    fn builtin_zattr(&self, cmd: &str, args: &[String]) -> i32 {
19684        match cmd {
19685            "zgetattr" => {
19686                if args.len() < 2 {
19687                    eprintln!("zgetattr: need file and attribute name");
19688                    return 1;
19689                }
19690                #[cfg(target_os = "macos")]
19691                {
19692                    // macOS uses xattr
19693                    let output = std::process::Command::new("xattr")
19694                        .arg("-p")
19695                        .arg(&args[1])
19696                        .arg(&args[0])
19697                        .output();
19698                    if let Ok(out) = output {
19699                        print!("{}", String::from_utf8_lossy(&out.stdout));
19700                        return if out.status.success() { 0 } else { 1 };
19701                    }
19702                }
19703                #[cfg(target_os = "linux")]
19704                {
19705                    let output = std::process::Command::new("getfattr")
19706                        .arg("-n")
19707                        .arg(&args[1])
19708                        .arg(&args[0])
19709                        .output();
19710                    if let Ok(out) = output {
19711                        print!("{}", String::from_utf8_lossy(&out.stdout));
19712                        return if out.status.success() { 0 } else { 1 };
19713                    }
19714                }
19715                1
19716            }
19717            "zsetattr" => {
19718                if args.len() < 3 {
19719                    eprintln!("zsetattr: need file, attribute name, and value");
19720                    return 1;
19721                }
19722                #[cfg(target_os = "macos")]
19723                {
19724                    let status = std::process::Command::new("xattr")
19725                        .arg("-w")
19726                        .arg(&args[1])
19727                        .arg(&args[2])
19728                        .arg(&args[0])
19729                        .status();
19730                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
19731                }
19732                #[cfg(target_os = "linux")]
19733                {
19734                    let status = std::process::Command::new("setfattr")
19735                        .arg("-n")
19736                        .arg(&args[1])
19737                        .arg("-v")
19738                        .arg(&args[2])
19739                        .arg(&args[0])
19740                        .status();
19741                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
19742                }
19743                #[allow(unreachable_code)]
19744                1
19745            }
19746            "zdelattr" => {
19747                if args.len() < 2 {
19748                    eprintln!("zdelattr: need file and attribute name");
19749                    return 1;
19750                }
19751                #[cfg(target_os = "macos")]
19752                {
19753                    let status = std::process::Command::new("xattr")
19754                        .arg("-d")
19755                        .arg(&args[1])
19756                        .arg(&args[0])
19757                        .status();
19758                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
19759                }
19760                #[cfg(target_os = "linux")]
19761                {
19762                    let status = std::process::Command::new("setfattr")
19763                        .arg("-x")
19764                        .arg(&args[1])
19765                        .arg(&args[0])
19766                        .status();
19767                    return status.map(|s| if s.success() { 0 } else { 1 }).unwrap_or(1);
19768                }
19769                #[allow(unreachable_code)]
19770                1
19771            }
19772            "zlistattr" => {
19773                if args.is_empty() {
19774                    eprintln!("zlistattr: need file");
19775                    return 1;
19776                }
19777                #[cfg(target_os = "macos")]
19778                {
19779                    let output = std::process::Command::new("xattr").arg(&args[0]).output();
19780                    if let Ok(out) = output {
19781                        print!("{}", String::from_utf8_lossy(&out.stdout));
19782                        return if out.status.success() { 0 } else { 1 };
19783                    }
19784                }
19785                #[cfg(target_os = "linux")]
19786                {
19787                    let output = std::process::Command::new("getfattr")
19788                        .arg("-d")
19789                        .arg(&args[0])
19790                        .output();
19791                    if let Ok(out) = output {
19792                        print!("{}", String::from_utf8_lossy(&out.stdout));
19793                        return if out.status.success() { 0 } else { 1 };
19794                    }
19795                }
19796                1
19797            }
19798            _ => 1,
19799        }
19800    }
19801
19802    /// zftp - FTP client builtin
19803    fn builtin_zftp(&mut self, args: &[String]) -> i32 {
19804        if args.is_empty() {
19805            println!("zftp: FTP client");
19806            println!("  zftp open host [port]");
19807            println!("  zftp login [user [password]]");
19808            println!("  zftp cd dir");
19809            println!("  zftp get file [localfile]");
19810            println!("  zftp put file [remotefile]");
19811            println!("  zftp ls [dir]");
19812            println!("  zftp close");
19813            return 0;
19814        }
19815
19816        match args[0].as_str() {
19817            "open" => {
19818                if args.len() < 2 {
19819                    eprintln!("zftp open: need hostname");
19820                    return 1;
19821                }
19822                // Would connect to FTP server
19823                println!("zftp: would connect to {}", args[1]);
19824                0
19825            }
19826            "login" => {
19827                // Would authenticate
19828                println!("zftp: would login");
19829                0
19830            }
19831            "cd" => {
19832                if args.len() < 2 {
19833                    eprintln!("zftp cd: need directory");
19834                    return 1;
19835                }
19836                println!("zftp: would cd to {}", args[1]);
19837                0
19838            }
19839            "get" => {
19840                if args.len() < 2 {
19841                    eprintln!("zftp get: need filename");
19842                    return 1;
19843                }
19844                println!("zftp: would download {}", args[1]);
19845                0
19846            }
19847            "put" => {
19848                if args.len() < 2 {
19849                    eprintln!("zftp put: need filename");
19850                    return 1;
19851                }
19852                println!("zftp: would upload {}", args[1]);
19853                0
19854            }
19855            "ls" => {
19856                println!("zftp: would list directory");
19857                0
19858            }
19859            "close" | "quit" => {
19860                println!("zftp: would close connection");
19861                0
19862            }
19863            "params" => {
19864                // Display/set FTP parameters
19865                println!("ZFTP_HOST=");
19866                println!("ZFTP_PORT=21");
19867                println!("ZFTP_USER=");
19868                println!("ZFTP_PWD=");
19869                println!("ZFTP_TYPE=A");
19870                0
19871            }
19872            cmd => {
19873                eprintln!("zftp: unknown command: {}", cmd);
19874                1
19875            }
19876        }
19877    }
19878
19879    /// promptinit - initialize prompt theme system
19880    fn builtin_promptinit(&mut self, _args: &[String]) -> i32 {
19881        self.arrays.insert(
19882            "prompt_themes".to_string(),
19883            vec![
19884                "adam1".to_string(),
19885                "adam2".to_string(),
19886                "bart".to_string(),
19887                "bigfade".to_string(),
19888                "clint".to_string(),
19889                "default".to_string(),
19890                "elite".to_string(),
19891                "elite2".to_string(),
19892                "fade".to_string(),
19893                "fire".to_string(),
19894                "minimal".to_string(),
19895                "off".to_string(),
19896                "oliver".to_string(),
19897                "pws".to_string(),
19898                "redhat".to_string(),
19899                "restore".to_string(),
19900                "suse".to_string(),
19901                "walters".to_string(),
19902                "zefram".to_string(),
19903            ],
19904        );
19905        self.variables
19906            .insert("prompt_theme".to_string(), "default".to_string());
19907        0
19908    }
19909
19910    /// prompt - set or list prompt themes
19911    fn builtin_prompt(&mut self, args: &[String]) -> i32 {
19912        if args.is_empty() {
19913            let theme = self
19914                .variables
19915                .get("prompt_theme")
19916                .cloned()
19917                .unwrap_or_else(|| "default".to_string());
19918            println!("Current prompt theme: {}", theme);
19919            return 0;
19920        }
19921        match args[0].as_str() {
19922            "-l" | "--list" => {
19923                println!("Available prompt themes:");
19924                if let Some(themes) = self.arrays.get("prompt_themes") {
19925                    for theme in themes {
19926                        println!("  {}", theme);
19927                    }
19928                }
19929            }
19930            "-p" | "--preview" => {
19931                let theme = args.get(1).map(|s| s.as_str()).unwrap_or("default");
19932                self.apply_prompt_theme(theme, true);
19933            }
19934            "-h" | "--help" => {
19935                println!("prompt [options] [theme]");
19936                println!("  -l, --list     List available themes");
19937                println!("  -p, --preview  Preview a theme");
19938                println!("  -s, --setup    Set up a theme");
19939            }
19940            _ => {
19941                let theme = if args[0].starts_with('-') {
19942                    args.get(1).map(|s| s.as_str()).unwrap_or("default")
19943                } else {
19944                    args[0].as_str()
19945                };
19946                self.apply_prompt_theme(theme, false);
19947            }
19948        }
19949        0
19950    }
19951
19952    fn apply_prompt_theme(&mut self, theme: &str, preview: bool) {
19953        let (ps1, rps1) = match theme {
19954            "minimal" => ("%# ", ""),
19955            "off" => ("$ ", ""),
19956            "adam1" => (
19957                "%B%F{cyan}%n@%m %F{blue}%~%f%b %# ",
19958                "%F{yellow}%D{%H:%M}%f",
19959            ),
19960            "redhat" => ("[%n@%m %~]$ ", ""),
19961            _ => ("%n@%m %~ %# ", ""),
19962        };
19963        if preview {
19964            println!("PS1={:?}", ps1);
19965            println!("RPS1={:?}", rps1);
19966        } else {
19967            self.variables.insert("PS1".to_string(), ps1.to_string());
19968            self.variables.insert("RPS1".to_string(), rps1.to_string());
19969            self.variables
19970                .insert("prompt_theme".to_string(), theme.to_string());
19971        }
19972    }
19973
19974    /// pcre_compile - compile a PCRE pattern
19975    fn builtin_pcre_compile(&mut self, args: &[String]) -> i32 {
19976        use crate::pcre::{pcre_compile, PcreCompileOptions};
19977
19978        let mut pattern = String::new();
19979        let mut options = PcreCompileOptions::default();
19980
19981        for arg in args {
19982            match arg.as_str() {
19983                "-a" => options.anchored = true,
19984                "-i" => options.caseless = true,
19985                "-m" => options.multiline = true,
19986                "-s" => options.dotall = true,
19987                "-x" => options.extended = true,
19988                s if !s.starts_with('-') => pattern = s.to_string(),
19989                _ => {}
19990            }
19991        }
19992
19993        if pattern.is_empty() {
19994            eprintln!("pcre_compile: no pattern specified");
19995            return 1;
19996        }
19997
19998        match pcre_compile(&pattern, &options, &mut self.pcre_state) {
19999            Ok(()) => 0,
20000            Err(e) => {
20001                eprintln!("pcre_compile: {}", e);
20002                1
20003            }
20004        }
20005    }
20006
20007    /// pcre_match - match string against compiled PCRE
20008    fn builtin_pcre_match(&mut self, args: &[String]) -> i32 {
20009        use crate::pcre::{pcre_match, PcreMatchOptions};
20010
20011        let mut var_name = "MATCH".to_string();
20012        let mut array_name = "match".to_string();
20013        let mut string = String::new();
20014        let mut i = 0;
20015
20016        while i < args.len() {
20017            match args[i].as_str() {
20018                "-v" => {
20019                    i += 1;
20020                    if i < args.len() {
20021                        var_name = args[i].clone();
20022                    }
20023                }
20024                "-a" => {
20025                    i += 1;
20026                    if i < args.len() {
20027                        array_name = args[i].clone();
20028                    }
20029                }
20030                s if !s.starts_with('-') => string = s.to_string(),
20031                _ => {}
20032            }
20033            i += 1;
20034        }
20035
20036        let options = PcreMatchOptions {
20037            match_var: Some(var_name.clone()),
20038            array_var: Some(array_name.clone()),
20039            ..Default::default()
20040        };
20041
20042        match pcre_match(&string, &options, &self.pcre_state) {
20043            Ok(result) => {
20044                if result.matched {
20045                    if let Some(m) = result.full_match {
20046                        self.variables.insert(var_name, m);
20047                    }
20048                    let matches: Vec<String> =
20049                        result.captures.into_iter().filter_map(|c| c).collect();
20050                    self.arrays.insert(array_name, matches);
20051                    0
20052                } else {
20053                    1
20054                }
20055            }
20056            Err(e) => {
20057                eprintln!("pcre_match: {}", e);
20058                1
20059            }
20060        }
20061    }
20062
20063    /// pcre_study - optimize compiled PCRE (no-op in Rust regex)
20064    fn builtin_pcre_study(&mut self, _args: &[String]) -> i32 {
20065        use crate::pcre::pcre_study;
20066
20067        match pcre_study(&self.pcre_state) {
20068            Ok(()) => 0,
20069            Err(e) => {
20070                eprintln!("pcre_study: {}", e);
20071                1
20072            }
20073        }
20074    }
20075
20076    // =========================================================================
20077    // Process control functions - Port from exec.c
20078    // =========================================================================
20079
20080    /// Fork a new process
20081    /// Port of zfork() from exec.c
20082    pub fn zfork(&mut self, flags: ForkFlags) -> std::io::Result<ForkResult> {
20083        // Check for job control
20084        let can_background = self.options.get("monitor").copied().unwrap_or(false);
20085
20086        unsafe {
20087            match libc::fork() {
20088                -1 => Err(std::io::Error::last_os_error()),
20089                0 => {
20090                    // Child process
20091                    if !flags.contains(ForkFlags::NOJOB) && can_background {
20092                        // Set up job control
20093                        let pid = libc::getpid();
20094                        if flags.contains(ForkFlags::NEWGRP) {
20095                            libc::setpgid(0, 0);
20096                        }
20097                        if flags.contains(ForkFlags::FGTTY) {
20098                            libc::tcsetpgrp(0, pid);
20099                        }
20100                    }
20101
20102                    // Reset signal handlers
20103                    if !flags.contains(ForkFlags::KEEPSIGS) {
20104                        self.reset_signals();
20105                    }
20106
20107                    Ok(ForkResult::Child)
20108                }
20109                pid => {
20110                    // Parent process
20111                    if !flags.contains(ForkFlags::NOJOB) {
20112                        // Add to job table
20113                        self.add_child_process(pid);
20114                    }
20115                    Ok(ForkResult::Parent(pid))
20116                }
20117            }
20118        }
20119    }
20120
20121    /// Add a child process to tracking
20122    fn add_child_process(&mut self, pid: i32) {
20123        // Would track in job table
20124        self.variables.insert("!".to_string(), pid.to_string());
20125    }
20126
20127    /// Reset signal handlers to defaults
20128    fn reset_signals(&self) {
20129        unsafe {
20130            libc::signal(libc::SIGINT, libc::SIG_DFL);
20131            libc::signal(libc::SIGQUIT, libc::SIG_DFL);
20132            libc::signal(libc::SIGTERM, libc::SIG_DFL);
20133            libc::signal(libc::SIGTSTP, libc::SIG_DFL);
20134            libc::signal(libc::SIGTTIN, libc::SIG_DFL);
20135            libc::signal(libc::SIGTTOU, libc::SIG_DFL);
20136            libc::signal(libc::SIGCHLD, libc::SIG_DFL);
20137        }
20138    }
20139
20140    /// Execute a command in the current process (exec family)
20141    /// Port of zexecve() from exec.c
20142    pub fn zexecve(&self, cmd: &str, args: &[String]) -> ! {
20143        use std::ffi::CString;
20144        use std::os::unix::ffi::OsStrExt;
20145
20146        let c_cmd = CString::new(cmd).expect("CString::new failed");
20147
20148        // Build argv
20149        let c_args: Vec<CString> = std::iter::once(c_cmd.clone())
20150            .chain(args.iter().map(|s| CString::new(s.as_str()).unwrap()))
20151            .collect();
20152
20153        let c_argv: Vec<*const libc::c_char> = c_args
20154            .iter()
20155            .map(|s| s.as_ptr())
20156            .chain(std::iter::once(std::ptr::null()))
20157            .collect();
20158
20159        // Build envp from current environment
20160        let env_vars: Vec<CString> = std::env::vars()
20161            .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
20162            .collect();
20163
20164        let c_envp: Vec<*const libc::c_char> = env_vars
20165            .iter()
20166            .map(|s| s.as_ptr())
20167            .chain(std::iter::once(std::ptr::null()))
20168            .collect();
20169
20170        unsafe {
20171            libc::execve(c_cmd.as_ptr(), c_argv.as_ptr(), c_envp.as_ptr());
20172            // If we get here, exec failed
20173            eprintln!(
20174                "zshrs: exec failed: {}: {}",
20175                cmd,
20176                std::io::Error::last_os_error()
20177            );
20178            std::process::exit(127);
20179        }
20180    }
20181
20182    /// Enter a subshell
20183    /// Port of entersubsh() from exec.c
20184    pub fn entersubsh(&mut self, flags: SubshellFlags) {
20185        // Increment subshell level
20186        let level = self
20187            .get_variable("ZSH_SUBSHELL")
20188            .parse::<i32>()
20189            .unwrap_or(0);
20190        self.variables
20191            .insert("ZSH_SUBSHELL".to_string(), (level + 1).to_string());
20192
20193        // Handle job control
20194        if flags.contains(SubshellFlags::NOMONITOR) {
20195            self.options.insert("monitor".to_string(), false);
20196        }
20197
20198        // Close unneeded fds
20199        if !flags.contains(SubshellFlags::KEEPFDS) {
20200            self.close_extra_fds();
20201        }
20202
20203        // Reset traps
20204        if !flags.contains(SubshellFlags::KEEPTRAPS) {
20205            self.reset_traps();
20206        }
20207    }
20208
20209    /// Close extra file descriptors
20210    fn close_extra_fds(&self) {
20211        // Close fds > 10 (common shell convention)
20212        for fd in 10..256 {
20213            unsafe {
20214                libc::close(fd);
20215            }
20216        }
20217    }
20218
20219    /// Reset all traps
20220    fn reset_traps(&mut self) {
20221        self.traps.clear();
20222    }
20223
20224    /// Execute a shell function
20225    /// Port of doshfunc() from exec.c
20226    pub fn doshfunc(
20227        &mut self,
20228        name: &str,
20229        func: &ShellCommand,
20230        args: &[String],
20231    ) -> Result<i32, String> {
20232        // Save current state
20233        let old_argv = self.positional_params.clone();
20234        let old_funcstack = self.arrays.get("funcstack").cloned();
20235        let old_funcsourcetrace = self.arrays.get("funcsourcetrace").cloned();
20236
20237        // Set positional parameters to function arguments
20238        self.positional_params = args.to_vec();
20239
20240        // Update funcstack
20241        let mut funcstack = old_funcstack.clone().unwrap_or_default();
20242        funcstack.insert(0, name.to_string());
20243        self.arrays.insert("funcstack".to_string(), funcstack);
20244
20245        // Execute function body
20246        let result = self.execute_command(func);
20247
20248        // Restore state
20249        self.positional_params = old_argv;
20250        if let Some(fs) = old_funcstack {
20251            self.arrays.insert("funcstack".to_string(), fs);
20252        } else {
20253            self.arrays.remove("funcstack");
20254        }
20255        if let Some(fst) = old_funcsourcetrace {
20256            self.arrays.insert("funcsourcetrace".to_string(), fst);
20257        }
20258
20259        result
20260    }
20261
20262    /// Execute arithmetic expression
20263    /// Port of execarith() from exec.c
20264    pub fn execarith(&mut self, expr: &str) -> i32 {
20265        let result = self.eval_arith_expr(expr);
20266        if result == 0 {
20267            1
20268        } else {
20269            0
20270        }
20271    }
20272
20273    /// Execute conditional expression
20274    /// Port of execcond() from exec.c
20275    pub fn execcond(&mut self, cond: &CondExpr) -> i32 {
20276        if self.eval_cond_expr(cond) {
20277            0
20278        } else {
20279            1
20280        }
20281    }
20282
20283    /// Execute command and capture time
20284    /// Port of exectime() from exec.c
20285    pub fn exectime(&mut self, cmd: &ShellCommand) -> Result<i32, String> {
20286        use std::time::Instant;
20287
20288        let start = Instant::now();
20289        let result = self.execute_command(cmd);
20290        let elapsed = start.elapsed();
20291
20292        // Print time in zsh format
20293        let user_time = elapsed.as_secs_f64() * 0.7; // Approximation
20294        let sys_time = elapsed.as_secs_f64() * 0.1;
20295        let real_time = elapsed.as_secs_f64();
20296
20297        eprintln!(
20298            "{:.2}s user {:.2}s system {:.0}% cpu {:.3} total",
20299            user_time,
20300            sys_time,
20301            ((user_time + sys_time) / real_time * 100.0).min(100.0),
20302            real_time
20303        );
20304
20305        result
20306    }
20307
20308    /// Find command in PATH
20309    /// Port of findcmd() from exec.c
20310    pub fn findcmd(&self, name: &str, do_hash: bool) -> Option<String> {
20311        // Check command hash table first
20312        if do_hash {
20313            if let Some(path) = self.command_hash.get(name) {
20314                if std::path::Path::new(path).exists() {
20315                    return Some(path.clone());
20316                }
20317            }
20318        }
20319
20320        // Search PATH
20321        if let Ok(path_var) = std::env::var("PATH") {
20322            for dir in path_var.split(':') {
20323                let full_path = format!("{}/{}", dir, name);
20324                if std::path::Path::new(&full_path).is_file() {
20325                    return Some(full_path);
20326                }
20327            }
20328        }
20329
20330        None
20331    }
20332
20333    /// Hash a command (add to command hash table)
20334    /// Port of hashcmd() from exec.c
20335    pub fn hashcmd(&mut self, name: &str, path: &str) {
20336        self.command_hash.insert(name.to_string(), path.to_string());
20337    }
20338
20339    /// Check if command exists and is executable
20340    /// Port of iscom() from exec.c
20341    pub fn iscom(&self, name: &str) -> bool {
20342        // Check if it's a builtin
20343        if self.is_builtin_cmd(name) {
20344            return true;
20345        }
20346
20347        // Check if it's a function
20348        if self.functions.contains_key(name) {
20349            return true;
20350        }
20351
20352        // Check if it's an alias
20353        if self.aliases.contains_key(name) {
20354            return true;
20355        }
20356
20357        // Check in PATH
20358        self.findcmd(name, true).is_some()
20359    }
20360
20361    /// Check if name is a builtin (process control version)
20362    fn is_builtin_cmd(&self, name: &str) -> bool {
20363        BUILTIN_SET.contains(name)
20364    }
20365
20366    /// Close all file descriptors except stdin/stdout/stderr
20367    /// Port of closem() from exec.c
20368    pub fn closem(&self, exceptions: &[i32]) {
20369        for fd in 3..256 {
20370            if !exceptions.contains(&fd) {
20371                unsafe {
20372                    libc::close(fd);
20373                }
20374            }
20375        }
20376    }
20377
20378    /// Create a pipe
20379    /// Port of mpipe() from exec.c
20380    pub fn mpipe(&self) -> std::io::Result<(i32, i32)> {
20381        let mut fds = [0i32; 2];
20382        let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
20383        if result == -1 {
20384            Err(std::io::Error::last_os_error())
20385        } else {
20386            Ok((fds[0], fds[1]))
20387        }
20388    }
20389
20390    /// Add a file descriptor for redirection
20391    /// Port of addfd() from exec.c
20392    pub fn addfd(&self, fd: i32, target_fd: i32, mode: RedirMode) -> std::io::Result<()> {
20393        match mode {
20394            RedirMode::Dup => {
20395                if fd != target_fd {
20396                    unsafe {
20397                        if libc::dup2(fd, target_fd) == -1 {
20398                            return Err(std::io::Error::last_os_error());
20399                        }
20400                    }
20401                }
20402            }
20403            RedirMode::Close => unsafe {
20404                libc::close(target_fd);
20405            },
20406        }
20407        Ok(())
20408    }
20409
20410    /// Get heredoc content
20411    /// Port of gethere() from exec.c
20412    pub fn gethere(&mut self, terminator: &str, strip_tabs: bool) -> String {
20413        let mut content = String::new();
20414
20415        // Would read until terminator is found
20416        // This is simplified - real impl reads from input
20417
20418        if strip_tabs {
20419            content = content
20420                .lines()
20421                .map(|line| line.trim_start_matches('\t'))
20422                .collect::<Vec<_>>()
20423                .join("\n");
20424        }
20425
20426        content
20427    }
20428
20429    /// Get herestring content
20430    /// Port of getherestr() from exec.c
20431    pub fn getherestr(&mut self, word: &str) -> String {
20432        let expanded = self.expand_string(word);
20433        format!("{}\n", expanded)
20434    }
20435
20436    /// Resolve a builtin command
20437    /// Port of resolvebuiltin() from exec.c
20438    pub fn resolvebuiltin(&self, name: &str) -> Option<BuiltinType> {
20439        if self.is_builtin_cmd(name) {
20440            Some(BuiltinType::Normal)
20441        } else {
20442            // Check disabled_builtins if we had that field
20443            None
20444        }
20445    }
20446
20447    /// Check if cd is possible
20448    /// Port of cancd() from exec.c
20449    pub fn cancd(&self, path_str: &str) -> bool {
20450        use std::os::unix::fs::PermissionsExt;
20451
20452        let path = std::path::Path::new(path_str);
20453        if !path.is_dir() {
20454            return false;
20455        }
20456
20457        if let Ok(meta) = path.metadata() {
20458            let mode = meta.permissions().mode();
20459            // Check execute permission (needed for cd)
20460            let uid = unsafe { libc::getuid() };
20461            let gid = unsafe { libc::getgid() };
20462            let file_uid = meta.uid();
20463            let file_gid = meta.gid();
20464
20465            if uid == file_uid {
20466                return (mode & 0o100) != 0;
20467            } else if gid == file_gid {
20468                return (mode & 0o010) != 0;
20469            } else {
20470                return (mode & 0o001) != 0;
20471            }
20472        }
20473
20474        false
20475    }
20476
20477    /// Command not found handler
20478    /// Port of commandnotfound() from exec.c
20479    pub fn commandnotfound(&mut self, name: &str, args: &[String]) -> i32 {
20480        // Check for command_not_found_handler function
20481        if self.functions.contains_key("command_not_found_handler") {
20482            let mut handler_args = vec![name.to_string()];
20483            handler_args.extend(args.iter().cloned());
20484
20485            if let Some(func) = self.functions.get("command_not_found_handler").cloned() {
20486                if let Ok(code) = self.doshfunc("command_not_found_handler", &func, &handler_args) {
20487                    return code;
20488                }
20489            }
20490        }
20491
20492        eprintln!("zshrs: command not found: {}", name);
20493        127
20494    }
20495}
20496
20497use std::os::unix::fs::MetadataExt;
20498
20499bitflags::bitflags! {
20500    /// Flags for zfork()
20501    #[derive(Debug, Clone, Copy, Default)]
20502    pub struct ForkFlags: u32 {
20503        const NOJOB = 1 << 0;    // Don't add to job table
20504        const NEWGRP = 1 << 1;   // Create new process group
20505        const FGTTY = 1 << 2;    // Take foreground terminal
20506        const KEEPSIGS = 1 << 3; // Keep signal handlers
20507    }
20508}
20509
20510bitflags::bitflags! {
20511    /// Flags for entersubsh()
20512    #[derive(Debug, Clone, Copy, Default)]
20513    pub struct SubshellFlags: u32 {
20514        const NOMONITOR = 1 << 0; // Disable job control
20515        const KEEPFDS = 1 << 1;   // Keep file descriptors
20516        const KEEPTRAPS = 1 << 2; // Keep trap handlers
20517    }
20518}
20519
20520/// Result of fork operation
20521#[derive(Debug)]
20522pub enum ForkResult {
20523    Parent(i32), // Contains child PID
20524    Child,
20525}
20526
20527/// Redirection mode
20528#[derive(Debug, Clone, Copy)]
20529pub enum RedirMode {
20530    Dup,
20531    Close,
20532}
20533
20534/// Builtin command type
20535#[derive(Debug, Clone, Copy)]
20536pub enum BuiltinType {
20537    Normal,
20538    Disabled,
20539}