1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
3pub enum OptionValue {
4 On,
6 Off,
8 #[default]
10 Unknown,
11}
12
13impl OptionValue {
14 pub const fn is_definitely_on(self) -> bool {
16 matches!(self, Self::On)
17 }
18
19 pub const fn is_definitely_off(self) -> bool {
21 matches!(self, Self::Off)
22 }
23
24 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum ZshEmulationMode {
37 Zsh,
39 Sh,
41 Ksh,
43 Csh,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct ZshOptionState {
50 pub sh_word_split: OptionValue,
52 pub glob_subst: OptionValue,
54 pub rc_expand_param: OptionValue,
56 pub glob: OptionValue,
58 pub nomatch: OptionValue,
60 pub null_glob: OptionValue,
62 pub csh_null_glob: OptionValue,
64 pub extended_glob: OptionValue,
66 pub ksh_glob: OptionValue,
68 pub sh_glob: OptionValue,
70 pub bare_glob_qual: OptionValue,
72 pub glob_dots: OptionValue,
74 pub equals: OptionValue,
76 pub magic_equal_subst: OptionValue,
78 pub sh_file_expansion: OptionValue,
80 pub glob_assign: OptionValue,
82 pub ignore_braces: OptionValue,
84 pub ignore_close_braces: OptionValue,
86 pub brace_ccl: OptionValue,
88 pub ksh_arrays: OptionValue,
90 pub ksh_zero_subscript: OptionValue,
92 pub short_loops: OptionValue,
94 pub short_repeat: OptionValue,
96 pub rc_quotes: OptionValue,
98 pub interactive_comments: OptionValue,
100 pub c_bases: OptionValue,
102 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 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 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 pub fn apply_setopt(&mut self, name: &str) -> bool {
205 self.apply_named_option(name, true)
206 }
207
208 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 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}