Skip to main content

shuck_parser/parser/
zsh_options.rs

1/// Tri-state option value used when modeling zsh option state.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
3pub enum OptionValue {
4    /// The option is enabled.
5    On,
6    /// The option is disabled.
7    Off,
8    /// The option value is unknown or differs across merged states.
9    #[default]
10    Unknown,
11}
12
13impl OptionValue {
14    /// Returns `true` when the option is known to be enabled.
15    pub const fn is_definitely_on(self) -> bool {
16        matches!(self, Self::On)
17    }
18
19    /// Returns `true` when the option is known to be disabled.
20    pub const fn is_definitely_off(self) -> bool {
21        matches!(self, Self::Off)
22    }
23
24    /// Merge two option values, preserving certainty only when they agree.
25    pub const fn merge(self, other: Self) -> Self {
26        match (self, other) {
27            (Self::On, Self::On) => Self::On,
28            (Self::Off, Self::Off) => Self::Off,
29            _ => Self::Unknown,
30        }
31    }
32}
33
34/// Target emulation mode for zsh's `emulate` behavior.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum ZshEmulationMode {
37    /// Native zsh behavior.
38    Zsh,
39    /// `sh` compatibility mode.
40    Sh,
41    /// `ksh` compatibility mode.
42    Ksh,
43    /// `csh` compatibility mode.
44    Csh,
45}
46
47/// Snapshot of zsh option state used by the parser and lexer.
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct ZshOptionState {
50    /// State of the `sh_word_split` option.
51    pub sh_word_split: OptionValue,
52    /// State of the `glob_subst` option.
53    pub glob_subst: OptionValue,
54    /// State of the `rc_expand_param` option.
55    pub rc_expand_param: OptionValue,
56    /// State of the `glob` option.
57    pub glob: OptionValue,
58    /// State of the `nomatch` option.
59    pub nomatch: OptionValue,
60    /// State of the `null_glob` option.
61    pub null_glob: OptionValue,
62    /// State of the `csh_null_glob` option.
63    pub csh_null_glob: OptionValue,
64    /// State of the `extended_glob` option.
65    pub extended_glob: OptionValue,
66    /// State of the `ksh_glob` option.
67    pub ksh_glob: OptionValue,
68    /// State of the `sh_glob` option.
69    pub sh_glob: OptionValue,
70    /// State of the `bare_glob_qual` option.
71    pub bare_glob_qual: OptionValue,
72    /// State of the `glob_dots` option.
73    pub glob_dots: OptionValue,
74    /// State of the `equals` option.
75    pub equals: OptionValue,
76    /// State of the `magic_equal_subst` option.
77    pub magic_equal_subst: OptionValue,
78    /// State of the `sh_file_expansion` option.
79    pub sh_file_expansion: OptionValue,
80    /// State of the `glob_assign` option.
81    pub glob_assign: OptionValue,
82    /// State of the `ignore_braces` option.
83    pub ignore_braces: OptionValue,
84    /// State of the `ignore_close_braces` option.
85    pub ignore_close_braces: OptionValue,
86    /// State of the `brace_ccl` option.
87    pub brace_ccl: OptionValue,
88    /// State of the `ksh_arrays` option.
89    pub ksh_arrays: OptionValue,
90    /// State of the `ksh_zero_subscript` option.
91    pub ksh_zero_subscript: OptionValue,
92    /// State of the `short_loops` option.
93    pub short_loops: OptionValue,
94    /// State of the `short_repeat` option.
95    pub short_repeat: OptionValue,
96    /// State of the `rc_quotes` option.
97    pub rc_quotes: OptionValue,
98    /// State of the `interactive_comments` option.
99    pub interactive_comments: OptionValue,
100    /// State of the `c_bases` option.
101    pub c_bases: OptionValue,
102    /// State of the `octal_zeroes` option.
103    pub octal_zeroes: OptionValue,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107enum ZshOptionField {
108    ShWordSplit,
109    GlobSubst,
110    RcExpandParam,
111    Glob,
112    Nomatch,
113    NullGlob,
114    CshNullGlob,
115    ExtendedGlob,
116    KshGlob,
117    ShGlob,
118    BareGlobQual,
119    GlobDots,
120    Equals,
121    MagicEqualSubst,
122    ShFileExpansion,
123    GlobAssign,
124    IgnoreBraces,
125    IgnoreCloseBraces,
126    BraceCcl,
127    KshArrays,
128    KshZeroSubscript,
129    ShortLoops,
130    ShortRepeat,
131    RcQuotes,
132    InteractiveComments,
133    CBases,
134    OctalZeroes,
135}
136
137impl ZshOptionState {
138    /// Default zsh option state used for native zsh parsing.
139    pub const fn zsh_default() -> Self {
140        Self {
141            sh_word_split: OptionValue::Off,
142            glob_subst: OptionValue::Off,
143            rc_expand_param: OptionValue::Off,
144            glob: OptionValue::On,
145            nomatch: OptionValue::On,
146            null_glob: OptionValue::Off,
147            csh_null_glob: OptionValue::Off,
148            extended_glob: OptionValue::Off,
149            ksh_glob: OptionValue::Off,
150            sh_glob: OptionValue::Off,
151            bare_glob_qual: OptionValue::On,
152            glob_dots: OptionValue::Off,
153            equals: OptionValue::On,
154            magic_equal_subst: OptionValue::Off,
155            sh_file_expansion: OptionValue::Off,
156            glob_assign: OptionValue::Off,
157            ignore_braces: OptionValue::Off,
158            ignore_close_braces: OptionValue::Off,
159            brace_ccl: OptionValue::Off,
160            ksh_arrays: OptionValue::Off,
161            ksh_zero_subscript: OptionValue::Off,
162            short_loops: OptionValue::On,
163            short_repeat: OptionValue::On,
164            rc_quotes: OptionValue::Off,
165            interactive_comments: OptionValue::On,
166            c_bases: OptionValue::Off,
167            octal_zeroes: OptionValue::Off,
168        }
169    }
170
171    /// Option state implied by `emulate <mode>`.
172    pub fn for_emulate(mode: ZshEmulationMode) -> Self {
173        let mut state = Self::zsh_default();
174        match mode {
175            ZshEmulationMode::Zsh => {}
176            ZshEmulationMode::Sh => {
177                state.sh_word_split = OptionValue::On;
178                state.glob_subst = OptionValue::On;
179                state.sh_glob = OptionValue::On;
180                state.sh_file_expansion = OptionValue::On;
181                state.bare_glob_qual = OptionValue::Off;
182                state.ksh_arrays = OptionValue::Off;
183            }
184            ZshEmulationMode::Ksh => {
185                state.sh_word_split = OptionValue::On;
186                state.glob_subst = OptionValue::On;
187                state.ksh_glob = OptionValue::On;
188                state.ksh_arrays = OptionValue::On;
189                state.sh_glob = OptionValue::On;
190                state.bare_glob_qual = OptionValue::Off;
191            }
192            ZshEmulationMode::Csh => {
193                state.csh_null_glob = OptionValue::On;
194                state.sh_word_split = OptionValue::Off;
195                state.glob_subst = OptionValue::Off;
196            }
197        }
198        state
199    }
200
201    /// Apply a zsh `setopt`-style option name.
202    ///
203    /// Returns `true` when the option name was recognized.
204    pub fn apply_setopt(&mut self, name: &str) -> bool {
205        self.apply_named_option(name, true)
206    }
207
208    /// Apply a zsh `unsetopt`-style option name.
209    ///
210    /// Returns `true` when the option name was recognized.
211    pub fn apply_unsetopt(&mut self, name: &str) -> bool {
212        self.apply_named_option(name, false)
213    }
214
215    fn set_field(&mut self, field: ZshOptionField, value: OptionValue) {
216        match field {
217            ZshOptionField::ShWordSplit => self.sh_word_split = value,
218            ZshOptionField::GlobSubst => self.glob_subst = value,
219            ZshOptionField::RcExpandParam => self.rc_expand_param = value,
220            ZshOptionField::Glob => self.glob = value,
221            ZshOptionField::Nomatch => self.nomatch = value,
222            ZshOptionField::NullGlob => self.null_glob = value,
223            ZshOptionField::CshNullGlob => self.csh_null_glob = value,
224            ZshOptionField::ExtendedGlob => self.extended_glob = value,
225            ZshOptionField::KshGlob => self.ksh_glob = value,
226            ZshOptionField::ShGlob => self.sh_glob = value,
227            ZshOptionField::BareGlobQual => self.bare_glob_qual = value,
228            ZshOptionField::GlobDots => self.glob_dots = value,
229            ZshOptionField::Equals => self.equals = value,
230            ZshOptionField::MagicEqualSubst => self.magic_equal_subst = value,
231            ZshOptionField::ShFileExpansion => self.sh_file_expansion = value,
232            ZshOptionField::GlobAssign => self.glob_assign = value,
233            ZshOptionField::IgnoreBraces => self.ignore_braces = value,
234            ZshOptionField::IgnoreCloseBraces => self.ignore_close_braces = value,
235            ZshOptionField::BraceCcl => self.brace_ccl = value,
236            ZshOptionField::KshArrays => self.ksh_arrays = value,
237            ZshOptionField::KshZeroSubscript => self.ksh_zero_subscript = value,
238            ZshOptionField::ShortLoops => self.short_loops = value,
239            ZshOptionField::ShortRepeat => self.short_repeat = value,
240            ZshOptionField::RcQuotes => self.rc_quotes = value,
241            ZshOptionField::InteractiveComments => self.interactive_comments = value,
242            ZshOptionField::CBases => self.c_bases = value,
243            ZshOptionField::OctalZeroes => self.octal_zeroes = value,
244        }
245    }
246
247    fn field(&self, field: ZshOptionField) -> OptionValue {
248        match field {
249            ZshOptionField::ShWordSplit => self.sh_word_split,
250            ZshOptionField::GlobSubst => self.glob_subst,
251            ZshOptionField::RcExpandParam => self.rc_expand_param,
252            ZshOptionField::Glob => self.glob,
253            ZshOptionField::Nomatch => self.nomatch,
254            ZshOptionField::NullGlob => self.null_glob,
255            ZshOptionField::CshNullGlob => self.csh_null_glob,
256            ZshOptionField::ExtendedGlob => self.extended_glob,
257            ZshOptionField::KshGlob => self.ksh_glob,
258            ZshOptionField::ShGlob => self.sh_glob,
259            ZshOptionField::BareGlobQual => self.bare_glob_qual,
260            ZshOptionField::GlobDots => self.glob_dots,
261            ZshOptionField::Equals => self.equals,
262            ZshOptionField::MagicEqualSubst => self.magic_equal_subst,
263            ZshOptionField::ShFileExpansion => self.sh_file_expansion,
264            ZshOptionField::GlobAssign => self.glob_assign,
265            ZshOptionField::IgnoreBraces => self.ignore_braces,
266            ZshOptionField::IgnoreCloseBraces => self.ignore_close_braces,
267            ZshOptionField::BraceCcl => self.brace_ccl,
268            ZshOptionField::KshArrays => self.ksh_arrays,
269            ZshOptionField::KshZeroSubscript => self.ksh_zero_subscript,
270            ZshOptionField::ShortLoops => self.short_loops,
271            ZshOptionField::ShortRepeat => self.short_repeat,
272            ZshOptionField::RcQuotes => self.rc_quotes,
273            ZshOptionField::InteractiveComments => self.interactive_comments,
274            ZshOptionField::CBases => self.c_bases,
275            ZshOptionField::OctalZeroes => self.octal_zeroes,
276        }
277    }
278
279    /// Merge two option snapshots field by field.
280    pub fn merge(&self, other: &Self) -> Self {
281        let mut merged = Self::zsh_default();
282        for field in ZshOptionField::ALL {
283            merged.set_field(field, self.field(field).merge(other.field(field)));
284        }
285        merged
286    }
287
288    fn apply_named_option(&mut self, name: &str, enable: bool) -> bool {
289        let Some((field, value)) = parse_zsh_option_assignment(name, enable) else {
290            return false;
291        };
292        self.set_field(
293            field,
294            if value {
295                OptionValue::On
296            } else {
297                OptionValue::Off
298            },
299        );
300        true
301    }
302}
303
304impl ZshOptionField {
305    const ALL: [Self; 27] = [
306        Self::ShWordSplit,
307        Self::GlobSubst,
308        Self::RcExpandParam,
309        Self::Glob,
310        Self::Nomatch,
311        Self::NullGlob,
312        Self::CshNullGlob,
313        Self::ExtendedGlob,
314        Self::KshGlob,
315        Self::ShGlob,
316        Self::BareGlobQual,
317        Self::GlobDots,
318        Self::Equals,
319        Self::MagicEqualSubst,
320        Self::ShFileExpansion,
321        Self::GlobAssign,
322        Self::IgnoreBraces,
323        Self::IgnoreCloseBraces,
324        Self::BraceCcl,
325        Self::KshArrays,
326        Self::KshZeroSubscript,
327        Self::ShortLoops,
328        Self::ShortRepeat,
329        Self::RcQuotes,
330        Self::InteractiveComments,
331        Self::CBases,
332        Self::OctalZeroes,
333    ];
334}
335
336fn parse_zsh_option_assignment(name: &str, enable: bool) -> Option<(ZshOptionField, bool)> {
337    let mut normalized = String::with_capacity(name.len());
338    for ch in name.chars() {
339        if matches!(ch, '_' | '-') {
340            continue;
341        }
342        normalized.push(ch.to_ascii_lowercase());
343    }
344
345    let (normalized, invert) = if let Some(rest) = normalized.strip_prefix("no") {
346        (rest, true)
347    } else {
348        (normalized.as_str(), false)
349    };
350
351    let field = match normalized {
352        "shwordsplit" => ZshOptionField::ShWordSplit,
353        "globsubst" => ZshOptionField::GlobSubst,
354        "rcexpandparam" => ZshOptionField::RcExpandParam,
355        "glob" | "noglob" => ZshOptionField::Glob,
356        "nomatch" => ZshOptionField::Nomatch,
357        "nullglob" => ZshOptionField::NullGlob,
358        "cshnullglob" => ZshOptionField::CshNullGlob,
359        "extendedglob" => ZshOptionField::ExtendedGlob,
360        "kshglob" => ZshOptionField::KshGlob,
361        "shglob" => ZshOptionField::ShGlob,
362        "bareglobqual" => ZshOptionField::BareGlobQual,
363        "globdots" => ZshOptionField::GlobDots,
364        "equals" => ZshOptionField::Equals,
365        "magicequalsubst" => ZshOptionField::MagicEqualSubst,
366        "shfileexpansion" => ZshOptionField::ShFileExpansion,
367        "globassign" => ZshOptionField::GlobAssign,
368        "ignorebraces" => ZshOptionField::IgnoreBraces,
369        "ignoreclosebraces" => ZshOptionField::IgnoreCloseBraces,
370        "braceccl" => ZshOptionField::BraceCcl,
371        "ksharrays" => ZshOptionField::KshArrays,
372        "kshzerosubscript" => ZshOptionField::KshZeroSubscript,
373        "shortloops" => ZshOptionField::ShortLoops,
374        "shortrepeat" => ZshOptionField::ShortRepeat,
375        "rcquotes" => ZshOptionField::RcQuotes,
376        "interactivecomments" => ZshOptionField::InteractiveComments,
377        "cbases" => ZshOptionField::CBases,
378        "octalzeroes" => ZshOptionField::OctalZeroes,
379        _ => return None,
380    };
381
382    Some((field, if invert { !enable } else { enable }))
383}