Skip to main content

zsh/
options.rs

1//! Shell options for zshrs
2//!
3//! Direct port from zsh/Src/options.c
4//!
5//! Manages all shell options including:
6//! - Option lookup by name and single-letter
7//! - Emulation modes (zsh, ksh, sh, csh)
8//! - Option aliases (bash/ksh compatibility)
9//! - setopt/unsetopt builtins
10
11use std::collections::HashMap;
12
13/// Emulation modes
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Emulation {
16    Zsh = 1,
17    Csh = 2,
18    Ksh = 4,
19    Sh = 8,
20}
21
22/// Emulation flags for option defaults
23const OPT_CSH: u8 = 1;
24const OPT_KSH: u8 = 2;
25const OPT_SH: u8 = 4;
26const OPT_ZSH: u8 = 8;
27const OPT_ALL: u8 = OPT_CSH | OPT_KSH | OPT_SH | OPT_ZSH;
28const OPT_BOURNE: u8 = OPT_KSH | OPT_SH;
29const OPT_BSHELL: u8 = OPT_KSH | OPT_SH | OPT_ZSH;
30const OPT_NONBOURNE: u8 = OPT_ALL & !OPT_BOURNE;
31const OPT_NONZSH: u8 = OPT_ALL & !OPT_ZSH;
32
33/// Option flags
34const OPT_EMULATE: u16 = 0x100; // Relevant to emulation
35const OPT_SPECIAL: u16 = 0x200; // Never set by emulate()
36const OPT_ALIAS: u16 = 0x400; // Alias to another option
37
38/// All shell option names
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[repr(u16)]
41pub enum ShellOption {
42    // A
43    Aliases = 1,
44    AliasFuncDef,
45    AllExport,
46    AlwaysLastPrompt,
47    AlwaysToEnd,
48    AppendCreate,
49    AppendHistory,
50    AutoCd,
51    AutoContinue,
52    AutoList,
53    AutoMenu,
54    AutoNamedDirs,
55    AutoParamKeys,
56    AutoParamSlash,
57    AutoPushd,
58    AutoRemoveSlash,
59    AutoResume,
60    // B
61    BadPattern,
62    BangHist,
63    BareGlobQual,
64    BashAutoList,
65    BashRematch,
66    Beep,
67    BgNice,
68    BraceCcl,
69    BsdEcho,
70    // C
71    CaseGlob,
72    CaseMatch,
73    CasePaths,
74    CBases,
75    CPrecedences,
76    CdAbleVars,
77    CdSilent,
78    ChaseDots,
79    ChaseLinks,
80    CheckJobs,
81    CheckRunningJobs,
82    Clobber,
83    ClobberEmpty,
84    CombiningChars,
85    CompleteAliases,
86    CompleteInWord,
87    ContinueOnError,
88    Correct,
89    CorrectAll,
90    CshJunkieHistory,
91    CshJunkieLoops,
92    CshJunkieQuotes,
93    CshNullCmd,
94    CshNullGlob,
95    // D
96    DebugBeforeCmd,
97    // E
98    Emacs,
99    Equals,
100    ErrExit,
101    ErrReturn,
102    Exec,
103    ExtendedGlob,
104    ExtendedHistory,
105    EvalLineno,
106    // F
107    FlowControl,
108    ForceFloat,
109    FunctionArgZero,
110    // G
111    Glob,
112    GlobalExport,
113    GlobalRcs,
114    GlobAssign,
115    GlobComplete,
116    GlobDots,
117    GlobStarShort,
118    GlobSubst,
119    // H
120    HashCmds,
121    HashDirs,
122    HashExecutablesOnly,
123    HashListAll,
124    HistAllowClobber,
125    HistBeep,
126    HistExpireDupsFirst,
127    HistFcntlLock,
128    HistFindNoDups,
129    HistIgnoreAllDups,
130    HistIgnoreDups,
131    HistIgnoreSpace,
132    HistLexWords,
133    HistNoFunctions,
134    HistNoStore,
135    HistSubstPattern,
136    HistReduceBlanks,
137    HistSaveByCopy,
138    HistSaveNoDups,
139    HistVerify,
140    Hup,
141    // I
142    IgnoreBraces,
143    IgnoreCloseBraces,
144    IgnoreEof,
145    IncAppendHistory,
146    IncAppendHistoryTime,
147    Interactive,
148    InteractiveComments,
149    // K
150    KshArrays,
151    KshAutoload,
152    KshGlob,
153    KshOptionPrint,
154    KshTypeset,
155    KshZeroSubscript,
156    // L
157    ListAmbiguous,
158    ListBeep,
159    ListPacked,
160    ListRowsFirst,
161    ListTypes,
162    LocalOptions,
163    LocalLoops,
164    LocalPatterns,
165    LocalTraps,
166    Login,
167    LongListJobs,
168    // M
169    MagicEqualSubst,
170    MailWarning,
171    MarkDirs,
172    MenuComplete,
173    Monitor,
174    MultiByte,
175    MultiFuncDef,
176    MultiOs,
177    // N
178    NoMatch,
179    Notify,
180    NullGlob,
181    NumericGlobSort,
182    // O
183    OctalZeroes,
184    OverStrike,
185    // P
186    PathDirs,
187    PathScript,
188    PipeFail,
189    PosixAliases,
190    PosixArgZero,
191    PosixBuiltins,
192    PosixCd,
193    PosixIdentifiers,
194    PosixJobs,
195    PosixStrings,
196    PosixTraps,
197    PrintEightBit,
198    PrintExitValue,
199    Privileged,
200    PromptBang,
201    PromptCr,
202    PromptPercent,
203    PromptSp,
204    PromptSubst,
205    PushdIgnoreDups,
206    PushdMinus,
207    PushdSilent,
208    PushdToHome,
209    // R
210    RcExpandParam,
211    RcQuotes,
212    Rcs,
213    RecExact,
214    RematchPcre,
215    RmStarSilent,
216    RmStarWait,
217    // S
218    ShareHistory,
219    ShFileExpansion,
220    ShGlob,
221    ShInstdin,
222    ShNullCmd,
223    ShOptionLetters,
224    ShortLoops,
225    ShortRepeat,
226    ShWordSplit,
227    SingleCommand,
228    SingleLineZle,
229    SourceTrace,
230    SunKeyboardHack,
231    // T
232    TransientRprompt,
233    TrapsAsync,
234    TypesetSilent,
235    TypesetToUnset,
236    // U
237    Unset,
238    // V
239    Verbose,
240    Vi,
241    // W
242    WarnCreateGlobal,
243    WarnNestedVar,
244    // X
245    Xtrace,
246    // Z
247    Zle,
248    Dvorak,
249}
250
251impl ShellOption {
252    /// Get the canonical name of this option
253    pub fn name(self) -> &'static str {
254        match self {
255            Self::Aliases => "aliases",
256            Self::AliasFuncDef => "aliasfuncdef",
257            Self::AllExport => "allexport",
258            Self::AlwaysLastPrompt => "alwayslastprompt",
259            Self::AlwaysToEnd => "alwaystoend",
260            Self::AppendCreate => "appendcreate",
261            Self::AppendHistory => "appendhistory",
262            Self::AutoCd => "autocd",
263            Self::AutoContinue => "autocontinue",
264            Self::AutoList => "autolist",
265            Self::AutoMenu => "automenu",
266            Self::AutoNamedDirs => "autonamedirs",
267            Self::AutoParamKeys => "autoparamkeys",
268            Self::AutoParamSlash => "autoparamslash",
269            Self::AutoPushd => "autopushd",
270            Self::AutoRemoveSlash => "autoremoveslash",
271            Self::AutoResume => "autoresume",
272            Self::BadPattern => "badpattern",
273            Self::BangHist => "banghist",
274            Self::BareGlobQual => "bareglobqual",
275            Self::BashAutoList => "bashautolist",
276            Self::BashRematch => "bashrematch",
277            Self::Beep => "beep",
278            Self::BgNice => "bgnice",
279            Self::BraceCcl => "braceccl",
280            Self::BsdEcho => "bsdecho",
281            Self::CaseGlob => "caseglob",
282            Self::CaseMatch => "casematch",
283            Self::CasePaths => "casepaths",
284            Self::CBases => "cbases",
285            Self::CPrecedences => "cprecedences",
286            Self::CdAbleVars => "cdablevars",
287            Self::CdSilent => "cdsilent",
288            Self::ChaseDots => "chasedots",
289            Self::ChaseLinks => "chaselinks",
290            Self::CheckJobs => "checkjobs",
291            Self::CheckRunningJobs => "checkrunningjobs",
292            Self::Clobber => "clobber",
293            Self::ClobberEmpty => "clobberempty",
294            Self::CombiningChars => "combiningchars",
295            Self::CompleteAliases => "completealiases",
296            Self::CompleteInWord => "completeinword",
297            Self::ContinueOnError => "continueonerror",
298            Self::Correct => "correct",
299            Self::CorrectAll => "correctall",
300            Self::CshJunkieHistory => "cshjunkiehistory",
301            Self::CshJunkieLoops => "cshjunkieloops",
302            Self::CshJunkieQuotes => "cshjunkiequotes",
303            Self::CshNullCmd => "cshnullcmd",
304            Self::CshNullGlob => "cshnullglob",
305            Self::DebugBeforeCmd => "debugbeforecmd",
306            Self::Emacs => "emacs",
307            Self::Equals => "equals",
308            Self::ErrExit => "errexit",
309            Self::ErrReturn => "errreturn",
310            Self::Exec => "exec",
311            Self::ExtendedGlob => "extendedglob",
312            Self::ExtendedHistory => "extendedhistory",
313            Self::EvalLineno => "evallineno",
314            Self::FlowControl => "flowcontrol",
315            Self::ForceFloat => "forcefloat",
316            Self::FunctionArgZero => "functionargzero",
317            Self::Glob => "glob",
318            Self::GlobalExport => "globalexport",
319            Self::GlobalRcs => "globalrcs",
320            Self::GlobAssign => "globassign",
321            Self::GlobComplete => "globcomplete",
322            Self::GlobDots => "globdots",
323            Self::GlobStarShort => "globstarshort",
324            Self::GlobSubst => "globsubst",
325            Self::HashCmds => "hashcmds",
326            Self::HashDirs => "hashdirs",
327            Self::HashExecutablesOnly => "hashexecutablesonly",
328            Self::HashListAll => "hashlistall",
329            Self::HistAllowClobber => "histallowclobber",
330            Self::HistBeep => "histbeep",
331            Self::HistExpireDupsFirst => "histexpiredupsfirst",
332            Self::HistFcntlLock => "histfcntllock",
333            Self::HistFindNoDups => "histfindnodups",
334            Self::HistIgnoreAllDups => "histignorealldups",
335            Self::HistIgnoreDups => "histignoredups",
336            Self::HistIgnoreSpace => "histignorespace",
337            Self::HistLexWords => "histlexwords",
338            Self::HistNoFunctions => "histnofunctions",
339            Self::HistNoStore => "histnostore",
340            Self::HistSubstPattern => "histsubstpattern",
341            Self::HistReduceBlanks => "histreduceblanks",
342            Self::HistSaveByCopy => "histsavebycopy",
343            Self::HistSaveNoDups => "histsavenodups",
344            Self::HistVerify => "histverify",
345            Self::Hup => "hup",
346            Self::IgnoreBraces => "ignorebraces",
347            Self::IgnoreCloseBraces => "ignoreclosebraces",
348            Self::IgnoreEof => "ignoreeof",
349            Self::IncAppendHistory => "incappendhistory",
350            Self::IncAppendHistoryTime => "incappendhistorytime",
351            Self::Interactive => "interactive",
352            Self::InteractiveComments => "interactivecomments",
353            Self::KshArrays => "ksharrays",
354            Self::KshAutoload => "kshautoload",
355            Self::KshGlob => "kshglob",
356            Self::KshOptionPrint => "kshoptionprint",
357            Self::KshTypeset => "kshtypeset",
358            Self::KshZeroSubscript => "kshzerosubscript",
359            Self::ListAmbiguous => "listambiguous",
360            Self::ListBeep => "listbeep",
361            Self::ListPacked => "listpacked",
362            Self::ListRowsFirst => "listrowsfirst",
363            Self::ListTypes => "listtypes",
364            Self::LocalOptions => "localoptions",
365            Self::LocalLoops => "localloops",
366            Self::LocalPatterns => "localpatterns",
367            Self::LocalTraps => "localtraps",
368            Self::Login => "login",
369            Self::LongListJobs => "longlistjobs",
370            Self::MagicEqualSubst => "magicequalsubst",
371            Self::MailWarning => "mailwarning",
372            Self::MarkDirs => "markdirs",
373            Self::MenuComplete => "menucomplete",
374            Self::Monitor => "monitor",
375            Self::MultiByte => "multibyte",
376            Self::MultiFuncDef => "multifuncdef",
377            Self::MultiOs => "multios",
378            Self::NoMatch => "nomatch",
379            Self::Notify => "notify",
380            Self::NullGlob => "nullglob",
381            Self::NumericGlobSort => "numericglobsort",
382            Self::OctalZeroes => "octalzeroes",
383            Self::OverStrike => "overstrike",
384            Self::PathDirs => "pathdirs",
385            Self::PathScript => "pathscript",
386            Self::PipeFail => "pipefail",
387            Self::PosixAliases => "posixaliases",
388            Self::PosixArgZero => "posixargzero",
389            Self::PosixBuiltins => "posixbuiltins",
390            Self::PosixCd => "posixcd",
391            Self::PosixIdentifiers => "posixidentifiers",
392            Self::PosixJobs => "posixjobs",
393            Self::PosixStrings => "posixstrings",
394            Self::PosixTraps => "posixtraps",
395            Self::PrintEightBit => "printeightbit",
396            Self::PrintExitValue => "printexitvalue",
397            Self::Privileged => "privileged",
398            Self::PromptBang => "promptbang",
399            Self::PromptCr => "promptcr",
400            Self::PromptPercent => "promptpercent",
401            Self::PromptSp => "promptsp",
402            Self::PromptSubst => "promptsubst",
403            Self::PushdIgnoreDups => "pushdignoredups",
404            Self::PushdMinus => "pushdminus",
405            Self::PushdSilent => "pushdsilent",
406            Self::PushdToHome => "pushdtohome",
407            Self::RcExpandParam => "rcexpandparam",
408            Self::RcQuotes => "rcquotes",
409            Self::Rcs => "rcs",
410            Self::RecExact => "recexact",
411            Self::RematchPcre => "rematchpcre",
412            Self::RmStarSilent => "rmstarsilent",
413            Self::RmStarWait => "rmstarwait",
414            Self::ShareHistory => "sharehistory",
415            Self::ShFileExpansion => "shfileexpansion",
416            Self::ShGlob => "shglob",
417            Self::ShInstdin => "shinstdin",
418            Self::ShNullCmd => "shnullcmd",
419            Self::ShOptionLetters => "shoptionletters",
420            Self::ShortLoops => "shortloops",
421            Self::ShortRepeat => "shortrepeat",
422            Self::ShWordSplit => "shwordsplit",
423            Self::SingleCommand => "singlecommand",
424            Self::SingleLineZle => "singlelinezle",
425            Self::SourceTrace => "sourcetrace",
426            Self::SunKeyboardHack => "sunkeyboardhack",
427            Self::TransientRprompt => "transientrprompt",
428            Self::TrapsAsync => "trapsasync",
429            Self::TypesetSilent => "typesetsilent",
430            Self::TypesetToUnset => "typesettounset",
431            Self::Unset => "unset",
432            Self::Verbose => "verbose",
433            Self::Vi => "vi",
434            Self::WarnCreateGlobal => "warncreateglobal",
435            Self::WarnNestedVar => "warnnestedvar",
436            Self::Xtrace => "xtrace",
437            Self::Zle => "zle",
438            Self::Dvorak => "dvorak",
439        }
440    }
441}
442
443/// Option aliases for bash/ksh compatibility
444pub static OPTION_ALIASES: &[(&str, &str, bool)] = &[
445    ("braceexpand", "ignorebraces", true),  // ksh/bash, negated
446    ("dotglob", "globdots", false),         // bash
447    ("hashall", "hashcmds", false),         // bash
448    ("histappend", "appendhistory", false), // bash
449    ("histexpand", "banghist", false),      // bash
450    ("log", "histnofunctions", true),       // ksh, negated
451    ("mailwarn", "mailwarning", false),     // bash
452    ("onecmd", "singlecommand", false),     // bash
453    ("physical", "chaselinks", false),      // ksh/bash
454    ("promptvars", "promptsubst", false),   // bash
455    ("stdin", "shinstdin", false),          // ksh
456    ("trackall", "hashcmds", false),        // ksh
457];
458
459/// Zsh single-letter options (zshletters in C)
460pub static ZSH_LETTERS: &[(char, &str, bool)] = &[
461    ('0', "correct", false),
462    ('1', "printexitvalue", false),
463    ('2', "badpattern", true),
464    ('3', "nomatch", true),
465    ('4', "globdots", false),
466    ('5', "notify", false),
467    ('6', "bgnice", false),
468    ('7', "ignoreeof", false),
469    ('8', "markdirs", false),
470    ('9', "autolist", false),
471    ('B', "beep", true),
472    ('C', "clobber", true),
473    ('D', "pushdtohome", false),
474    ('E', "pushdsilent", false),
475    ('F', "glob", true),
476    ('G', "nullglob", false),
477    ('H', "rmstarsilent", false),
478    ('I', "ignorebraces", false),
479    ('J', "autocd", false),
480    ('K', "banghist", true),
481    ('L', "sunkeyboardhack", false),
482    ('M', "singlelinezle", false),
483    ('N', "autopushd", false),
484    ('O', "correctall", false),
485    ('P', "rcexpandparam", false),
486    ('Q', "pathdirs", false),
487    ('R', "longlistjobs", false),
488    ('S', "recexact", false),
489    ('T', "cdablevars", false),
490    ('U', "mailwarning", false),
491    ('V', "promptcr", true),
492    ('W', "autoresume", false),
493    ('X', "listtypes", false),
494    ('Y', "menucomplete", false),
495    ('Z', "zle", false),
496    ('a', "allexport", false),
497    ('d', "globalrcs", true),
498    ('e', "errexit", false),
499    ('f', "rcs", true),
500    ('g', "histignorespace", false),
501    ('h', "histignoredups", false),
502    ('i', "interactive", false),
503    ('k', "interactivecomments", false),
504    ('l', "login", false),
505    ('m', "monitor", false),
506    ('n', "exec", true),
507    ('p', "privileged", false),
508    ('s', "shinstdin", false),
509    ('t', "singlecommand", false),
510    ('u', "unset", true),
511    ('v', "verbose", false),
512    ('w', "chaselinks", false),
513    ('x', "xtrace", false),
514    ('y', "shwordsplit", false),
515];
516
517/// Ksh single-letter options
518pub static KSH_LETTERS: &[(char, &str, bool)] = &[
519    ('C', "clobber", true),
520    ('T', "trapsasync", false),
521    ('X', "markdirs", false),
522    ('a', "allexport", false),
523    ('b', "notify", false),
524    ('e', "errexit", false),
525    ('f', "glob", true),
526    ('i', "interactive", false),
527    ('l', "login", false),
528    ('m', "monitor", false),
529    ('n', "exec", true),
530    ('p', "privileged", false),
531    ('s', "shinstdin", false),
532    ('t', "singlecommand", false),
533    ('u', "unset", true),
534    ('v', "verbose", false),
535    ('x', "xtrace", false),
536];
537
538/// Shell options manager
539#[derive(Debug, Clone)]
540pub struct ShellOptions {
541    /// Current option values (true = set)
542    options: HashMap<String, bool>,
543    /// Current emulation mode
544    pub emulation: Emulation,
545    /// Is fully emulating (vs just setting some options)
546    pub fully_emulating: bool,
547}
548
549impl Default for ShellOptions {
550    fn default() -> Self {
551        Self::new()
552    }
553}
554
555impl ShellOptions {
556    /// Create a new options manager with zsh defaults
557    pub fn new() -> Self {
558        let mut opts = ShellOptions {
559            options: HashMap::new(),
560            emulation: Emulation::Zsh,
561            fully_emulating: false,
562        };
563        opts.set_zsh_defaults();
564        opts
565    }
566
567    /// Set zsh default options
568    pub fn set_zsh_defaults(&mut self) {
569        // Options that default to ON in zsh
570        let default_on = [
571            "aliases",
572            "alwayslastprompt",
573            "appendhistory",
574            "autolist",
575            "automenu",
576            "autoparamkeys",
577            "autoparamslash",
578            "autoremoveslash",
579            "bareglobqual",
580            "beep",
581            "bgnice",
582            "caseglob",
583            "casematch",
584            "checkjobs",
585            "checkrunningjobs",
586            "clobber",
587            "debugbeforecmd",
588            "equals",
589            "evallineno",
590            "exec",
591            "flowcontrol",
592            "functionargzero",
593            "glob",
594            "globalexport",
595            "globalrcs",
596            "hashcmds",
597            "hashdirs",
598            "hashlistall",
599            "histbeep",
600            "histsavebycopy",
601            "hup",
602            "interactive",
603            "listambiguous",
604            "listbeep",
605            "listtypes",
606            "multifuncdef",
607            "multios",
608            "nomatch",
609            "notify",
610            "promptcr",
611            "promptpercent",
612            "promptsp",
613            "rcs",
614            "shortloops",
615            "unset",
616            "zle",
617        ];
618
619        for opt in default_on {
620            self.options.insert(opt.to_string(), true);
621        }
622    }
623
624    /// Look up an option by name (case insensitive, underscores ignored)
625    pub fn lookup(&self, name: &str) -> Option<bool> {
626        let normalized = normalize_option_name(name);
627
628        // Check for "no" prefix
629        if let Some(stripped) = normalized.strip_prefix("no") {
630            self.options.get(stripped).map(|v| !v)
631        } else {
632            self.options.get(&normalized).copied()
633        }
634    }
635
636    /// Check if an option is set
637    pub fn is_set(&self, name: &str) -> bool {
638        self.lookup(name).unwrap_or(false)
639    }
640
641    /// Set an option value
642    pub fn set(&mut self, name: &str, value: bool) -> Result<(), String> {
643        let normalized = normalize_option_name(name);
644
645        // Handle "no" prefix
646        let (actual_name, actual_value) = if let Some(stripped) = normalized.strip_prefix("no") {
647            (stripped.to_string(), !value)
648        } else {
649            (normalized, value)
650        };
651
652        // Check for aliases
653        for (alias, target, negated) in OPTION_ALIASES {
654            if actual_name == *alias {
655                let target_value = if *negated {
656                    !actual_value
657                } else {
658                    actual_value
659                };
660                self.options.insert(target.to_string(), target_value);
661                return Ok(());
662            }
663        }
664
665        // Special options that can't be changed
666        let special = ["interactive", "login", "shinstdin", "singlecommand"];
667        if special.contains(&actual_name.as_str()) {
668            if self.options.get(&actual_name) == Some(&actual_value) {
669                return Ok(());
670            }
671            return Err(format!("can't change option: {}", actual_name));
672        }
673
674        self.options.insert(actual_name, actual_value);
675        Ok(())
676    }
677
678    /// Unset an option (same as set(name, false))
679    pub fn unset(&mut self, name: &str) -> Result<(), String> {
680        self.set(name, false)
681    }
682
683    /// Look up option by single letter
684    pub fn lookup_letter(&self, c: char) -> Option<(&'static str, bool)> {
685        let letters = if self.is_set("shoptionletters") {
686            KSH_LETTERS
687        } else {
688            ZSH_LETTERS
689        };
690
691        for (ch, name, negated) in letters {
692            if *ch == c {
693                return Some((name, *negated));
694            }
695        }
696        None
697    }
698
699    /// Set option by single letter
700    pub fn set_by_letter(&mut self, c: char, value: bool) -> Result<(), String> {
701        if let Some((name, negated)) = self.lookup_letter(c) {
702            let actual_value = if negated { !value } else { value };
703            self.set(name, actual_value)
704        } else {
705            Err(format!("bad option: -{}", c))
706        }
707    }
708
709    /// Set emulation mode
710    pub fn emulate(&mut self, mode: &str, fully: bool) {
711        let ch = mode.chars().next().unwrap_or('z');
712        let ch = if ch == 'r' {
713            mode.chars().nth(1).unwrap_or('z')
714        } else {
715            ch
716        };
717
718        self.emulation = match ch {
719            'c' => Emulation::Csh,
720            'k' => Emulation::Ksh,
721            's' | 'b' => Emulation::Sh,
722            _ => Emulation::Zsh,
723        };
724        self.fully_emulating = fully;
725
726        // Reset options to emulation defaults
727        self.install_emulation_defaults();
728    }
729
730    /// Install default options for current emulation
731    fn install_emulation_defaults(&mut self) {
732        // This would set all the emulation-specific defaults
733        // For now, just set some key differences
734        match self.emulation {
735            Emulation::Sh | Emulation::Ksh => {
736                self.options.insert("shwordsplit".to_string(), true);
737                self.options.insert("globsubst".to_string(), true);
738                self.options.insert("ksharrays".to_string(), true);
739                self.options.insert("posixbuiltins".to_string(), true);
740                self.options.insert("promptpercent".to_string(), false);
741                self.options.insert("banghist".to_string(), false);
742            }
743            Emulation::Csh => {
744                self.options.insert("cshjunkiehistory".to_string(), true);
745                self.options.insert("cshjunkieloops".to_string(), true);
746                self.options.insert("cshnullcmd".to_string(), true);
747            }
748            Emulation::Zsh => {
749                self.set_zsh_defaults();
750            }
751        }
752    }
753
754    /// Get the $- parameter value (active single-letter options)
755    pub fn dash_string(&self) -> String {
756        let mut result = String::new();
757        let letters = if self.is_set("shoptionletters") {
758            KSH_LETTERS
759        } else {
760            ZSH_LETTERS
761        };
762
763        for (c, name, negated) in letters {
764            let is_set = self.is_set(name);
765            if (*negated && !is_set) || (!*negated && is_set) {
766                result.push(*c);
767            }
768        }
769        result
770    }
771
772    /// List all options and their current state
773    pub fn list(&self) -> Vec<(String, bool)> {
774        let mut result: Vec<_> = self.options.iter().map(|(k, v)| (k.clone(), *v)).collect();
775        result.sort_by(|a, b| a.0.cmp(&b.0));
776        result
777    }
778
779    /// Get all option names
780    pub fn all_names(&self) -> Vec<&str> {
781        // Return all known option names
782        let mut names: Vec<_> = self.options.keys().map(|s| s.as_str()).collect();
783        names.sort();
784        names
785    }
786}
787
788/// Normalize an option name: lowercase, remove underscores
789pub fn normalize_option_name(name: &str) -> String {
790    name.chars()
791        .filter(|&c| c != '_')
792        .flat_map(|c| c.to_lowercase())
793        .collect()
794}
795
796/// Parse option arguments from setopt/unsetopt
797pub fn parse_option_args(
798    opts: &mut ShellOptions,
799    args: &[&str],
800    is_unset: bool,
801) -> Result<(), Vec<String>> {
802    let mut errors = Vec::new();
803
804    for arg in args {
805        let (name, value) = if let Some(stripped) = arg.strip_prefix("no") {
806            (stripped, is_unset) // "nofoo" with unsetopt means set foo
807        } else {
808            (*arg, !is_unset)
809        };
810
811        if let Err(e) = opts.set(name, value) {
812            errors.push(e);
813        }
814    }
815
816    if errors.is_empty() {
817        Ok(())
818    } else {
819        Err(errors)
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826
827    #[test]
828    fn test_default_options() {
829        let opts = ShellOptions::new();
830        assert!(opts.is_set("glob"));
831        assert!(opts.is_set("exec"));
832        assert!(opts.is_set("zle"));
833        assert!(!opts.is_set("xtrace"));
834    }
835
836    #[test]
837    fn test_set_option() {
838        let mut opts = ShellOptions::new();
839        opts.set("xtrace", true).unwrap();
840        assert!(opts.is_set("xtrace"));
841        opts.set("xtrace", false).unwrap();
842        assert!(!opts.is_set("xtrace"));
843    }
844
845    #[test]
846    fn test_no_prefix() {
847        let mut opts = ShellOptions::new();
848        opts.set("noglob", true).unwrap();
849        assert!(!opts.is_set("glob"));
850
851        assert!(opts.lookup("noglob") == Some(true));
852    }
853
854    #[test]
855    fn test_case_insensitive() {
856        let opts = ShellOptions::new();
857        assert_eq!(opts.lookup("GLOB"), opts.lookup("glob"));
858        assert_eq!(opts.lookup("GlOb"), opts.lookup("glob"));
859    }
860
861    #[test]
862    fn test_underscore_ignored() {
863        let opts = ShellOptions::new();
864        assert_eq!(opts.lookup("auto_list"), opts.lookup("autolist"));
865        assert_eq!(opts.lookup("AUTO_LIST"), opts.lookup("autolist"));
866    }
867
868    #[test]
869    fn test_option_alias() {
870        let mut opts = ShellOptions::new();
871
872        // braceexpand is alias for noignorebraces
873        opts.set("braceexpand", true).unwrap();
874        assert!(!opts.is_set("ignorebraces"));
875    }
876
877    #[test]
878    fn test_single_letter() {
879        let mut opts = ShellOptions::new();
880
881        // -x is xtrace
882        opts.set_by_letter('x', true).unwrap();
883        assert!(opts.is_set("xtrace"));
884
885        // -n is noexec (negated)
886        opts.set_by_letter('n', true).unwrap();
887        assert!(!opts.is_set("exec"));
888    }
889
890    #[test]
891    fn test_emulation() {
892        let mut opts = ShellOptions::new();
893
894        opts.emulate("sh", true);
895        assert_eq!(opts.emulation, Emulation::Sh);
896        assert!(opts.is_set("shwordsplit"));
897
898        opts.emulate("zsh", true);
899        assert_eq!(opts.emulation, Emulation::Zsh);
900    }
901
902    #[test]
903    fn test_dash_string() {
904        let mut opts = ShellOptions::new();
905        opts.set("interactive", true).unwrap();
906        opts.set("monitor", true).unwrap();
907
908        let dash = opts.dash_string();
909        assert!(dash.contains('i'));
910        assert!(dash.contains('m'));
911    }
912
913    #[test]
914    fn test_normalize_name() {
915        assert_eq!(normalize_option_name("AUTO_LIST"), "autolist");
916        assert_eq!(normalize_option_name("AutoList"), "autolist");
917        assert_eq!(normalize_option_name("auto__list"), "autolist");
918    }
919}