1use enumset::EnumSet;
27use enumset::EnumSetIter;
28use enumset::EnumSetType;
29use std::borrow::Cow;
30use std::fmt::Display;
31use std::fmt::Formatter;
32use std::ops::Not;
33use std::str::FromStr;
34use thiserror::Error;
35
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
38pub enum State {
39 On,
41 Off,
43}
44
45pub use State::*;
46
47impl State {
48 #[must_use]
50 pub const fn as_str(self) -> &'static str {
51 match self {
52 On => "on",
53 Off => "off",
54 }
55 }
56}
57
58impl Display for State {
60 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
61 self.as_str().fmt(f)
62 }
63}
64
65impl Not for State {
66 type Output = Self;
67 fn not(self) -> Self {
68 match self {
69 On => Off,
70 Off => On,
71 }
72 }
73}
74
75impl From<bool> for State {
77 fn from(is_on: bool) -> Self {
78 if is_on { On } else { Off }
79 }
80}
81
82impl From<State> for bool {
84 fn from(state: State) -> Self {
85 match state {
86 On => true,
87 Off => false,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, EnumSetType, Eq, Hash, PartialEq)]
94#[enumset(no_super_impls)]
95#[non_exhaustive]
96pub enum Option {
97 AllExport,
99 Clobber,
102 CmdLine,
104 ErrExit,
106 Exec,
108 Glob,
110 HashOnDefinition,
113 IgnoreEof,
116 Interactive,
118 Log,
121 Login,
123 Monitor,
125 Notify,
127 PipeFail,
129 Portable,
131 PosixlyCorrect,
133 Stdin,
135 Unset,
137 Verbose,
139 Vi,
141 XTrace,
143}
144
145pub use self::Option::*;
146
147impl Option {
148 #[must_use]
152 pub const fn is_modifiable(self) -> bool {
153 !matches!(self, CmdLine | Interactive | Stdin)
154 }
155
156 #[must_use]
164 pub const fn short_name(self) -> std::option::Option<(char, State)> {
165 match self {
166 AllExport => Some(('a', On)),
167 Clobber => Some(('C', Off)),
168 CmdLine => Some(('c', On)),
169 ErrExit => Some(('e', On)),
170 Exec => Some(('n', Off)),
171 Glob => Some(('f', Off)),
172 HashOnDefinition => Some(('h', On)),
173 IgnoreEof => None,
174 Interactive => Some(('i', On)),
175 Log => None,
176 Login => Some(('l', On)),
177 Monitor => Some(('m', On)),
178 Notify => Some(('b', On)),
179 PipeFail => None,
180 Portable => None,
181 PosixlyCorrect => None,
182 Stdin => Some(('s', On)),
183 Unset => Some(('u', Off)),
184 Verbose => Some(('v', On)),
185 Vi => None,
186 XTrace => Some(('x', On)),
187 }
188 }
189
190 #[must_use]
208 pub const fn portable_short_name(self) -> std::option::Option<(char, State)> {
209 match self {
210 AllExport => Some(('a', On)),
211 Clobber => Some(('C', Off)),
212 CmdLine => Some(('c', On)),
213 ErrExit => Some(('e', On)),
214 Exec => Some(('n', Off)),
215 Glob => Some(('f', Off)),
216 HashOnDefinition => Some(('h', On)),
217 Interactive => Some(('i', On)),
218 Monitor => Some(('m', On)),
219 Notify => Some(('b', On)),
220 Stdin => Some(('s', On)),
221 Unset => Some(('u', Off)),
222 Verbose => Some(('v', On)),
223 XTrace => Some(('x', On)),
224 IgnoreEof | Log | Login | PipeFail | Portable | PosixlyCorrect | Vi => None,
225 }
226 }
227
228 #[must_use]
233 pub const fn long_name(self) -> &'static str {
234 match self {
235 AllExport => "allexport",
236 Clobber => "clobber",
237 CmdLine => "cmdline",
238 ErrExit => "errexit",
239 Exec => "exec",
240 Glob => "glob",
241 HashOnDefinition => "hashondefinition",
242 IgnoreEof => "ignoreeof",
243 Interactive => "interactive",
244 Log => "log",
245 Login => "login",
246 Monitor => "monitor",
247 Notify => "notify",
248 PipeFail => "pipefail",
249 Portable => "portable",
250 PosixlyCorrect => "posixlycorrect",
251 Stdin => "stdin",
252 Unset => "unset",
253 Verbose => "verbose",
254 Vi => "vi",
255 XTrace => "xtrace",
256 }
257 }
258
259 #[must_use]
278 pub const fn portable_long_name(self) -> std::option::Option<(&'static str, State)> {
279 match self {
280 AllExport => Some(("allexport", On)),
281 Clobber => Some(("noclobber", Off)),
282 ErrExit => Some(("errexit", On)),
283 Exec => Some(("noexec", Off)),
284 Glob => Some(("noglob", Off)),
285 IgnoreEof => Some(("ignoreeof", On)),
286 Log => Some(("nolog", Off)),
287 Monitor => Some(("monitor", On)),
288 Notify => Some(("notify", On)),
289 PipeFail => Some(("pipefail", On)),
290 Unset => Some(("nounset", Off)),
291 Verbose => Some(("verbose", On)),
292 Vi => Some(("vi", On)),
293 XTrace => Some(("xtrace", On)),
294 CmdLine | HashOnDefinition | Interactive | Login | Portable | PosixlyCorrect
295 | Stdin => None,
296 }
297 }
298}
299
300impl Display for Option {
302 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
303 self.long_name().fmt(f)
304 }
305}
306
307#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
309pub enum FromStrError {
310 #[error("no such option")]
312 NoSuchOption,
313
314 #[error("ambiguous option name")]
316 Ambiguous,
317}
318
319pub use FromStrError::*;
320
321impl FromStr for Option {
338 type Err = FromStrError;
339 fn from_str(name: &str) -> Result<Self, FromStrError> {
340 const OPTIONS: &[(&str, Option)] = &[
341 ("allexport", AllExport),
342 ("clobber", Clobber),
343 ("cmdline", CmdLine),
344 ("errexit", ErrExit),
345 ("exec", Exec),
346 ("glob", Glob),
347 ("hashondefinition", HashOnDefinition),
348 ("ignoreeof", IgnoreEof),
349 ("interactive", Interactive),
350 ("log", Log),
351 ("login", Login),
352 ("monitor", Monitor),
353 ("notify", Notify),
354 ("pipefail", PipeFail),
355 ("portable", Portable),
356 ("posixlycorrect", PosixlyCorrect),
357 ("stdin", Stdin),
358 ("unset", Unset),
359 ("verbose", Verbose),
360 ("vi", Vi),
361 ("xtrace", XTrace),
362 ];
363
364 match OPTIONS.binary_search_by_key(&name, |&(full_name, _option)| full_name) {
365 Ok(index) => Ok(OPTIONS[index].1),
366 Err(index) => {
367 let mut options = OPTIONS[index..]
368 .iter()
369 .filter(|&(full_name, _option)| full_name.starts_with(name));
370 match options.next() {
371 Some(first) => match options.next() {
372 Some(_second) => Err(Ambiguous),
373 None => Ok(first.1),
374 },
375 None => Err(NoSuchOption),
376 }
377 }
378 }
379 }
380}
381
382#[must_use]
411pub const fn parse_short(name: char) -> std::option::Option<(self::Option, State)> {
412 match name {
413 'a' => Some((AllExport, On)),
414 'b' => Some((Notify, On)),
415 'C' => Some((Clobber, Off)),
416 'c' => Some((CmdLine, On)),
417 'e' => Some((ErrExit, On)),
418 'f' => Some((Glob, Off)),
419 'h' => Some((HashOnDefinition, On)),
420 'i' => Some((Interactive, On)),
421 'l' => Some((Login, On)),
422 'm' => Some((Monitor, On)),
423 'n' => Some((Exec, Off)),
424 's' => Some((Stdin, On)),
425 'u' => Some((Unset, Off)),
426 'v' => Some((Verbose, On)),
427 'x' => Some((XTrace, On)),
428 _ => None,
429 }
430}
431
432#[derive(Clone, Debug)]
438pub struct Iter {
439 inner: EnumSetIter<Option>,
440}
441
442impl Iterator for Iter {
443 type Item = Option;
444 fn next(&mut self) -> std::option::Option<self::Option> {
445 self.inner.next()
446 }
447 fn size_hint(&self) -> (usize, std::option::Option<usize>) {
448 self.inner.size_hint()
449 }
450}
451
452impl DoubleEndedIterator for Iter {
453 fn next_back(&mut self) -> std::option::Option<self::Option> {
454 self.inner.next_back()
455 }
456}
457
458impl ExactSizeIterator for Iter {}
459
460impl Option {
461 pub fn iter() -> Iter {
464 Iter {
465 inner: EnumSet::<Option>::all().iter(),
466 }
467 }
468}
469
470pub fn parse_long(name: &str) -> Result<(Option, State), FromStrError> {
489 if "no".starts_with(name) {
490 return Err(Ambiguous);
491 }
492
493 let intact = Option::from_str(name);
494 let without_no = name
495 .strip_prefix("no")
496 .ok_or(NoSuchOption)
497 .and_then(Option::from_str);
498
499 match (intact, without_no) {
500 (Ok(option), Err(NoSuchOption)) => Ok((option, On)),
501 (Err(NoSuchOption), Ok(option)) => Ok((option, Off)),
502 (Err(Ambiguous), _) | (_, Err(Ambiguous)) => Err(Ambiguous),
503 _ => Err(NoSuchOption),
504 }
505}
506
507pub fn canonicalize(name: &str) -> Cow<'_, str> {
514 if name
515 .chars()
516 .all(|c| c.is_alphanumeric() && !c.is_ascii_uppercase())
517 {
518 Cow::Borrowed(name)
519 } else {
520 Cow::Owned(
521 name.chars()
522 .filter(|c| c.is_alphanumeric())
523 .map(|c| c.to_ascii_lowercase())
524 .collect(),
525 )
526 }
527}
528
529#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
531pub struct OptionSet {
532 enabled_options: EnumSet<Option>,
533}
534
535impl Default for OptionSet {
540 fn default() -> Self {
541 let enabled_options = Clobber | Exec | Glob | Log | Unset;
542 OptionSet { enabled_options }
543 }
544}
545
546impl OptionSet {
547 pub fn empty() -> Self {
549 OptionSet {
550 enabled_options: EnumSet::empty(),
551 }
552 }
553
554 pub fn get(&self, option: Option) -> State {
559 if self.enabled_options.contains(option) {
560 On
561 } else {
562 Off
563 }
564 }
565
566 pub fn set(&mut self, option: Option, state: State) {
573 match state {
574 On => self.enabled_options.insert(option),
575 Off => self.enabled_options.remove(option),
576 };
577 }
578}
579
580impl Extend<Option> for OptionSet {
581 fn extend<T: IntoIterator<Item = Option>>(&mut self, iter: T) {
582 self.enabled_options.extend(iter);
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
591 fn short_name_round_trip() {
592 for option in EnumSet::<Option>::all() {
593 if let Some((name, state)) = option.short_name() {
594 assert_eq!(parse_short(name), Some((option, state)));
595 }
596 }
597 for name in 'A'..='z' {
598 if let Some((option, state)) = parse_short(name) {
599 assert_eq!(option.short_name(), Some((name, state)));
600 }
601 }
602 }
603
604 #[test]
605 fn portable_short_names() {
606 assert_eq!(AllExport.portable_short_name(), Some(('a', On)));
607 assert_eq!(Clobber.portable_short_name(), Some(('C', Off)));
608 assert_eq!(CmdLine.portable_short_name(), Some(('c', On)));
609 assert_eq!(ErrExit.portable_short_name(), Some(('e', On)));
610 assert_eq!(Exec.portable_short_name(), Some(('n', Off)));
611 assert_eq!(Glob.portable_short_name(), Some(('f', Off)));
612 assert_eq!(HashOnDefinition.portable_short_name(), Some(('h', On)));
613 assert_eq!(IgnoreEof.portable_short_name(), None);
614 assert_eq!(Interactive.portable_short_name(), Some(('i', On)));
615 assert_eq!(Log.portable_short_name(), None);
616 assert_eq!(Login.portable_short_name(), None);
617 assert_eq!(Monitor.portable_short_name(), Some(('m', On)));
618 assert_eq!(Notify.portable_short_name(), Some(('b', On)));
619 assert_eq!(PipeFail.portable_short_name(), None);
620 assert_eq!(Portable.portable_short_name(), None);
621 assert_eq!(PosixlyCorrect.portable_short_name(), None);
622 assert_eq!(Stdin.portable_short_name(), Some(('s', On)));
623 assert_eq!(Unset.portable_short_name(), Some(('u', Off)));
624 assert_eq!(Verbose.portable_short_name(), Some(('v', On)));
625 assert_eq!(Vi.portable_short_name(), None);
626 assert_eq!(XTrace.portable_short_name(), Some(('x', On)));
627 }
628
629 #[test]
630 fn portable_short_name_agrees_with_short_name() {
631 for option in EnumSet::<Option>::all() {
632 if let Some(name) = option.portable_short_name() {
633 assert_eq!(option.short_name(), Some(name), "{option}");
634 }
635 }
636 }
637
638 #[test]
639 fn portable_long_names() {
640 assert_eq!(AllExport.portable_long_name(), Some(("allexport", On)));
641 assert_eq!(Clobber.portable_long_name(), Some(("noclobber", Off)));
642 assert_eq!(CmdLine.portable_long_name(), None);
643 assert_eq!(ErrExit.portable_long_name(), Some(("errexit", On)));
644 assert_eq!(Exec.portable_long_name(), Some(("noexec", Off)));
645 assert_eq!(Glob.portable_long_name(), Some(("noglob", Off)));
646 assert_eq!(HashOnDefinition.portable_long_name(), None);
647 assert_eq!(IgnoreEof.portable_long_name(), Some(("ignoreeof", On)));
648 assert_eq!(Interactive.portable_long_name(), None);
649 assert_eq!(Log.portable_long_name(), Some(("nolog", Off)));
650 assert_eq!(Login.portable_long_name(), None);
651 assert_eq!(Monitor.portable_long_name(), Some(("monitor", On)));
652 assert_eq!(Notify.portable_long_name(), Some(("notify", On)));
653 assert_eq!(PipeFail.portable_long_name(), Some(("pipefail", On)));
654 assert_eq!(Portable.portable_long_name(), None);
655 assert_eq!(PosixlyCorrect.portable_long_name(), None);
656 assert_eq!(Stdin.portable_long_name(), None);
657 assert_eq!(Unset.portable_long_name(), Some(("nounset", Off)));
658 assert_eq!(Verbose.portable_long_name(), Some(("verbose", On)));
659 assert_eq!(Vi.portable_long_name(), Some(("vi", On)));
660 assert_eq!(XTrace.portable_long_name(), Some(("xtrace", On)));
661 }
662
663 #[test]
664 fn portable_long_name_parses_back_to_the_option() {
665 for option in EnumSet::<Option>::all() {
666 if let Some((name, state)) = option.portable_long_name() {
667 assert_eq!(parse_long(name), Ok((option, state)), "{option}");
668 }
669 }
670 }
671
672 #[test]
673 fn display_and_from_str_round_trip() {
674 for option in EnumSet::<Option>::all() {
675 let name = option.to_string();
676 assert_eq!(Option::from_str(&name), Ok(option));
677 }
678 }
679
680 #[test]
681 fn from_str_unambiguous_abbreviation() {
682 assert_eq!(Option::from_str("allexpor"), Ok(AllExport));
683 assert_eq!(Option::from_str("a"), Ok(AllExport));
684 assert_eq!(Option::from_str("n"), Ok(Notify));
685 }
686
687 #[test]
688 fn from_str_ambiguous_abbreviation() {
689 assert_eq!(Option::from_str(""), Err(Ambiguous));
690 assert_eq!(Option::from_str("c"), Err(Ambiguous));
691 assert_eq!(Option::from_str("lo"), Err(Ambiguous));
692 }
693
694 #[test]
695 fn from_str_no_match() {
696 assert_eq!(Option::from_str("vim"), Err(NoSuchOption));
697 assert_eq!(Option::from_str("0"), Err(NoSuchOption));
698 assert_eq!(Option::from_str("LOG"), Err(NoSuchOption));
699 }
700
701 #[test]
702 fn display_and_parse_round_trip() {
703 for option in EnumSet::<Option>::all() {
704 let name = option.to_string();
705 assert_eq!(parse_long(&name), Ok((option, On)));
706 }
707 }
708
709 #[test]
710 fn display_and_parse_negated_round_trip() {
711 for option in EnumSet::<Option>::all() {
712 let name = format!("no{option}");
713 assert_eq!(parse_long(&name), Ok((option, Off)));
714 }
715 }
716
717 #[test]
718 fn parse_unambiguous_abbreviation() {
719 assert_eq!(parse_long("allexpor"), Ok((AllExport, On)));
720 assert_eq!(parse_long("not"), Ok((Notify, On)));
721 assert_eq!(parse_long("non"), Ok((Notify, Off)));
722 assert_eq!(parse_long("un"), Ok((Unset, On)));
723 assert_eq!(parse_long("noun"), Ok((Unset, Off)));
724 }
725
726 #[test]
727 fn parse_ambiguous_abbreviation() {
728 assert_eq!(parse_long(""), Err(Ambiguous));
729 assert_eq!(parse_long("n"), Err(Ambiguous));
730 assert_eq!(parse_long("no"), Err(Ambiguous));
731 assert_eq!(parse_long("noe"), Err(Ambiguous));
732 assert_eq!(parse_long("e"), Err(Ambiguous));
733 assert_eq!(parse_long("nolo"), Err(Ambiguous));
734 }
735
736 #[test]
737 fn parse_no_match() {
738 assert_eq!(parse_long("vim"), Err(NoSuchOption));
739 assert_eq!(parse_long("0"), Err(NoSuchOption));
740 assert_eq!(parse_long("novim"), Err(NoSuchOption));
741 assert_eq!(parse_long("no0"), Err(NoSuchOption));
742 assert_eq!(parse_long("LOG"), Err(NoSuchOption));
743 }
744
745 #[test]
746 fn test_canonicalize() {
747 assert_eq!(canonicalize(""), "");
748 assert_eq!(canonicalize("POSIXlyCorrect"), "posixlycorrect");
749 assert_eq!(canonicalize(" log "), "log");
750 assert_eq!(canonicalize("gLoB"), "glob");
751 assert_eq!(canonicalize("no-notify"), "nonotify");
752 assert_eq!(canonicalize(" no such_Option "), "nosuchoption");
753 assert_eq!(canonicalize("Abc"), "Abc");
754 }
755}