1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
8pub enum OptionValue {
9 On,
11 Off,
13 #[default]
15 Unknown,
16}
17
18impl OptionValue {
19 pub const fn is_definitely_on(self) -> bool {
21 matches!(self, Self::On)
22 }
23
24 pub const fn is_definitely_off(self) -> bool {
26 matches!(self, Self::Off)
27 }
28
29 pub const fn merge(self, other: Self) -> Self {
35 match (self, other) {
36 (Self::On, Self::On) => Self::On,
37 (Self::Off, Self::Off) => Self::Off,
38 _ => Self::Unknown,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum ZshEmulationMode {
49 Zsh,
51 Sh,
53 Ksh,
55 Csh,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub struct ZshOptionState {
68 pub sh_word_split: OptionValue,
71 pub glob_subst: OptionValue,
73 pub rc_expand_param: OptionValue,
75 pub glob: OptionValue,
77 pub nomatch: OptionValue,
79 pub null_glob: OptionValue,
81 pub csh_null_glob: OptionValue,
83 pub extended_glob: OptionValue,
85 pub ksh_glob: OptionValue,
87 pub sh_glob: OptionValue,
89 pub bare_glob_qual: OptionValue,
91 pub glob_dots: OptionValue,
93 pub equals: OptionValue,
95 pub magic_equal_subst: OptionValue,
98 pub sh_file_expansion: OptionValue,
100 pub glob_assign: OptionValue,
102 pub ignore_braces: OptionValue,
105 pub ignore_close_braces: OptionValue,
107 pub brace_ccl: OptionValue,
109 pub ksh_arrays: OptionValue,
111 pub ksh_zero_subscript: OptionValue,
113 pub short_loops: OptionValue,
115 pub short_repeat: OptionValue,
117 pub rc_quotes: OptionValue,
119 pub interactive_comments: OptionValue,
121 pub c_bases: OptionValue,
123 pub octal_zeroes: OptionValue,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128enum ZshOptionField {
129 ShWordSplit,
130 GlobSubst,
131 RcExpandParam,
132 Glob,
133 Nomatch,
134 NullGlob,
135 CshNullGlob,
136 ExtendedGlob,
137 KshGlob,
138 ShGlob,
139 BareGlobQual,
140 GlobDots,
141 Equals,
142 MagicEqualSubst,
143 ShFileExpansion,
144 GlobAssign,
145 IgnoreBraces,
146 IgnoreCloseBraces,
147 BraceCcl,
148 KshArrays,
149 KshZeroSubscript,
150 ShortLoops,
151 ShortRepeat,
152 RcQuotes,
153 InteractiveComments,
154 CBases,
155 OctalZeroes,
156}
157
158impl ZshOptionState {
159 pub const fn zsh_default() -> Self {
164 Self {
165 sh_word_split: OptionValue::Off,
166 glob_subst: OptionValue::Off,
167 rc_expand_param: OptionValue::Off,
168 glob: OptionValue::On,
169 nomatch: OptionValue::On,
170 null_glob: OptionValue::Off,
171 csh_null_glob: OptionValue::Off,
172 extended_glob: OptionValue::Off,
173 ksh_glob: OptionValue::Off,
174 sh_glob: OptionValue::Off,
175 bare_glob_qual: OptionValue::On,
176 glob_dots: OptionValue::Off,
177 equals: OptionValue::On,
178 magic_equal_subst: OptionValue::Off,
179 sh_file_expansion: OptionValue::Off,
180 glob_assign: OptionValue::Off,
181 ignore_braces: OptionValue::Off,
182 ignore_close_braces: OptionValue::Off,
183 brace_ccl: OptionValue::Off,
184 ksh_arrays: OptionValue::Off,
185 ksh_zero_subscript: OptionValue::Off,
186 short_loops: OptionValue::On,
187 short_repeat: OptionValue::On,
188 rc_quotes: OptionValue::Off,
189 interactive_comments: OptionValue::On,
190 c_bases: OptionValue::Off,
191 octal_zeroes: OptionValue::Off,
192 }
193 }
194
195 pub fn for_emulate(mode: ZshEmulationMode) -> Self {
201 let mut state = Self::zsh_default();
202 match mode {
203 ZshEmulationMode::Zsh => {}
204 ZshEmulationMode::Sh => {
205 state.sh_word_split = OptionValue::On;
206 state.glob_subst = OptionValue::On;
207 state.sh_glob = OptionValue::On;
208 state.sh_file_expansion = OptionValue::On;
209 state.bare_glob_qual = OptionValue::Off;
210 state.ksh_arrays = OptionValue::Off;
211 }
212 ZshEmulationMode::Ksh => {
213 state.sh_word_split = OptionValue::On;
214 state.glob_subst = OptionValue::On;
215 state.ksh_glob = OptionValue::On;
216 state.ksh_arrays = OptionValue::On;
217 state.sh_glob = OptionValue::On;
218 state.bare_glob_qual = OptionValue::Off;
219 }
220 ZshEmulationMode::Csh => {
221 state.csh_null_glob = OptionValue::On;
222 state.sh_word_split = OptionValue::Off;
223 state.glob_subst = OptionValue::Off;
224 }
225 }
226 state
227 }
228
229 pub fn apply_setopt(&mut self, name: &str) -> bool {
235 self.apply_named_option(name, true)
236 }
237
238 pub fn apply_unsetopt(&mut self, name: &str) -> bool {
244 self.apply_named_option(name, false)
245 }
246
247 fn set_field(&mut self, field: ZshOptionField, value: OptionValue) {
248 match field {
249 ZshOptionField::ShWordSplit => self.sh_word_split = value,
250 ZshOptionField::GlobSubst => self.glob_subst = value,
251 ZshOptionField::RcExpandParam => self.rc_expand_param = value,
252 ZshOptionField::Glob => self.glob = value,
253 ZshOptionField::Nomatch => self.nomatch = value,
254 ZshOptionField::NullGlob => self.null_glob = value,
255 ZshOptionField::CshNullGlob => self.csh_null_glob = value,
256 ZshOptionField::ExtendedGlob => self.extended_glob = value,
257 ZshOptionField::KshGlob => self.ksh_glob = value,
258 ZshOptionField::ShGlob => self.sh_glob = value,
259 ZshOptionField::BareGlobQual => self.bare_glob_qual = value,
260 ZshOptionField::GlobDots => self.glob_dots = value,
261 ZshOptionField::Equals => self.equals = value,
262 ZshOptionField::MagicEqualSubst => self.magic_equal_subst = value,
263 ZshOptionField::ShFileExpansion => self.sh_file_expansion = value,
264 ZshOptionField::GlobAssign => self.glob_assign = value,
265 ZshOptionField::IgnoreBraces => self.ignore_braces = value,
266 ZshOptionField::IgnoreCloseBraces => self.ignore_close_braces = value,
267 ZshOptionField::BraceCcl => self.brace_ccl = value,
268 ZshOptionField::KshArrays => self.ksh_arrays = value,
269 ZshOptionField::KshZeroSubscript => self.ksh_zero_subscript = value,
270 ZshOptionField::ShortLoops => self.short_loops = value,
271 ZshOptionField::ShortRepeat => self.short_repeat = value,
272 ZshOptionField::RcQuotes => self.rc_quotes = value,
273 ZshOptionField::InteractiveComments => self.interactive_comments = value,
274 ZshOptionField::CBases => self.c_bases = value,
275 ZshOptionField::OctalZeroes => self.octal_zeroes = value,
276 }
277 }
278
279 fn field(&self, field: ZshOptionField) -> OptionValue {
280 match field {
281 ZshOptionField::ShWordSplit => self.sh_word_split,
282 ZshOptionField::GlobSubst => self.glob_subst,
283 ZshOptionField::RcExpandParam => self.rc_expand_param,
284 ZshOptionField::Glob => self.glob,
285 ZshOptionField::Nomatch => self.nomatch,
286 ZshOptionField::NullGlob => self.null_glob,
287 ZshOptionField::CshNullGlob => self.csh_null_glob,
288 ZshOptionField::ExtendedGlob => self.extended_glob,
289 ZshOptionField::KshGlob => self.ksh_glob,
290 ZshOptionField::ShGlob => self.sh_glob,
291 ZshOptionField::BareGlobQual => self.bare_glob_qual,
292 ZshOptionField::GlobDots => self.glob_dots,
293 ZshOptionField::Equals => self.equals,
294 ZshOptionField::MagicEqualSubst => self.magic_equal_subst,
295 ZshOptionField::ShFileExpansion => self.sh_file_expansion,
296 ZshOptionField::GlobAssign => self.glob_assign,
297 ZshOptionField::IgnoreBraces => self.ignore_braces,
298 ZshOptionField::IgnoreCloseBraces => self.ignore_close_braces,
299 ZshOptionField::BraceCcl => self.brace_ccl,
300 ZshOptionField::KshArrays => self.ksh_arrays,
301 ZshOptionField::KshZeroSubscript => self.ksh_zero_subscript,
302 ZshOptionField::ShortLoops => self.short_loops,
303 ZshOptionField::ShortRepeat => self.short_repeat,
304 ZshOptionField::RcQuotes => self.rc_quotes,
305 ZshOptionField::InteractiveComments => self.interactive_comments,
306 ZshOptionField::CBases => self.c_bases,
307 ZshOptionField::OctalZeroes => self.octal_zeroes,
308 }
309 }
310
311 pub fn merge(&self, other: &Self) -> Self {
316 let mut merged = Self::zsh_default();
317 for field in ZshOptionField::ALL {
318 merged.set_field(field, self.field(field).merge(other.field(field)));
319 }
320 merged
321 }
322
323 fn apply_named_option(&mut self, name: &str, enable: bool) -> bool {
324 let Some((field, value)) = parse_zsh_option_assignment(name, enable) else {
325 return false;
326 };
327 self.set_field(
328 field,
329 if value {
330 OptionValue::On
331 } else {
332 OptionValue::Off
333 },
334 );
335 true
336 }
337}
338
339impl ZshOptionField {
340 const ALL: [Self; 27] = [
341 Self::ShWordSplit,
342 Self::GlobSubst,
343 Self::RcExpandParam,
344 Self::Glob,
345 Self::Nomatch,
346 Self::NullGlob,
347 Self::CshNullGlob,
348 Self::ExtendedGlob,
349 Self::KshGlob,
350 Self::ShGlob,
351 Self::BareGlobQual,
352 Self::GlobDots,
353 Self::Equals,
354 Self::MagicEqualSubst,
355 Self::ShFileExpansion,
356 Self::GlobAssign,
357 Self::IgnoreBraces,
358 Self::IgnoreCloseBraces,
359 Self::BraceCcl,
360 Self::KshArrays,
361 Self::KshZeroSubscript,
362 Self::ShortLoops,
363 Self::ShortRepeat,
364 Self::RcQuotes,
365 Self::InteractiveComments,
366 Self::CBases,
367 Self::OctalZeroes,
368 ];
369}
370
371fn parse_zsh_option_assignment(name: &str, enable: bool) -> Option<(ZshOptionField, bool)> {
372 let mut normalized = String::with_capacity(name.len());
373 for ch in name.chars() {
374 if matches!(ch, '_' | '-') {
375 continue;
376 }
377 normalized.push(ch.to_ascii_lowercase());
378 }
379
380 let (normalized, invert) = if let Some(rest) = normalized.strip_prefix("no") {
381 (rest, true)
382 } else {
383 (normalized.as_str(), false)
384 };
385
386 let field = match normalized {
387 "shwordsplit" => ZshOptionField::ShWordSplit,
388 "globsubst" => ZshOptionField::GlobSubst,
389 "rcexpandparam" => ZshOptionField::RcExpandParam,
390 "glob" | "noglob" => ZshOptionField::Glob,
391 "nomatch" => ZshOptionField::Nomatch,
392 "nullglob" => ZshOptionField::NullGlob,
393 "cshnullglob" => ZshOptionField::CshNullGlob,
394 "extendedglob" => ZshOptionField::ExtendedGlob,
395 "kshglob" => ZshOptionField::KshGlob,
396 "shglob" => ZshOptionField::ShGlob,
397 "bareglobqual" => ZshOptionField::BareGlobQual,
398 "globdots" => ZshOptionField::GlobDots,
399 "equals" => ZshOptionField::Equals,
400 "magicequalsubst" => ZshOptionField::MagicEqualSubst,
401 "shfileexpansion" => ZshOptionField::ShFileExpansion,
402 "globassign" => ZshOptionField::GlobAssign,
403 "ignorebraces" => ZshOptionField::IgnoreBraces,
404 "ignoreclosebraces" => ZshOptionField::IgnoreCloseBraces,
405 "braceccl" => ZshOptionField::BraceCcl,
406 "ksharrays" => ZshOptionField::KshArrays,
407 "kshzerosubscript" => ZshOptionField::KshZeroSubscript,
408 "shortloops" => ZshOptionField::ShortLoops,
409 "shortrepeat" => ZshOptionField::ShortRepeat,
410 "rcquotes" => ZshOptionField::RcQuotes,
411 "interactivecomments" => ZshOptionField::InteractiveComments,
412 "cbases" => ZshOptionField::CBases,
413 "octalzeroes" => ZshOptionField::OctalZeroes,
414 _ => return None,
415 };
416
417 Some((field, if invert { !enable } else { enable }))
418}