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 #[must_use]
68 fn not(self) -> Self {
69 match self {
70 On => Off,
71 Off => On,
72 }
73 }
74}
75
76impl From<bool> for State {
78 fn from(is_on: bool) -> Self {
79 if is_on {
80 On
81 } else {
82 Off
83 }
84 }
85}
86
87impl From<State> for bool {
89 fn from(state: State) -> Self {
90 match state {
91 On => true,
92 Off => false,
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, EnumSetType, Eq, Hash, PartialEq)]
99#[enumset(no_super_impls)]
100#[non_exhaustive]
101pub enum Option {
102 AllExport,
104 Clobber,
107 CmdLine,
109 ErrExit,
111 Exec,
113 Glob,
115 HashOnDefinition,
118 IgnoreEof,
121 Interactive,
123 Log,
126 Login,
128 Monitor,
130 Notify,
132 PosixlyCorrect,
134 Stdin,
136 Unset,
138 Verbose,
140 Vi,
142 XTrace,
144}
145
146pub use self::Option::*;
147
148impl Option {
149 #[must_use]
153 pub const fn is_modifiable(self) -> bool {
154 !matches!(self, CmdLine | Interactive | Stdin)
155 }
156
157 #[must_use]
165 pub const fn short_name(self) -> std::option::Option<(char, State)> {
166 match self {
167 AllExport => Some(('a', On)),
168 Clobber => Some(('C', Off)),
169 CmdLine => Some(('c', On)),
170 ErrExit => Some(('e', On)),
171 Exec => Some(('n', Off)),
172 Glob => Some(('f', Off)),
173 HashOnDefinition => Some(('h', On)),
174 IgnoreEof => None,
175 Interactive => Some(('i', On)),
176 Log => None,
177 Login => Some(('l', On)),
178 Monitor => Some(('m', On)),
179 Notify => Some(('b', On)),
180 PosixlyCorrect => None,
181 Stdin => Some(('s', On)),
182 Unset => Some(('u', Off)),
183 Verbose => Some(('v', On)),
184 Vi => None,
185 XTrace => Some(('x', On)),
186 }
187 }
188
189 #[must_use]
194 pub const fn long_name(self) -> &'static str {
195 match self {
196 AllExport => "allexport",
197 Clobber => "clobber",
198 CmdLine => "cmdline",
199 ErrExit => "errexit",
200 Exec => "exec",
201 Glob => "glob",
202 HashOnDefinition => "hashondefinition",
203 IgnoreEof => "ignoreeof",
204 Interactive => "interactive",
205 Log => "log",
206 Login => "login",
207 Monitor => "monitor",
208 Notify => "notify",
209 PosixlyCorrect => "posixlycorrect",
210 Stdin => "stdin",
211 Unset => "unset",
212 Verbose => "verbose",
213 Vi => "vi",
214 XTrace => "xtrace",
215 }
216 }
217}
218
219impl Display for Option {
221 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
222 self.long_name().fmt(f)
223 }
224}
225
226#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
228pub enum FromStrError {
229 #[error("no such option")]
231 NoSuchOption,
232
233 #[error("ambiguous option name")]
235 Ambiguous,
236}
237
238pub use FromStrError::*;
239
240impl FromStr for Option {
257 type Err = FromStrError;
258 fn from_str(name: &str) -> Result<Self, FromStrError> {
259 const OPTIONS: &[(&str, Option)] = &[
260 ("allexport", AllExport),
261 ("clobber", Clobber),
262 ("cmdline", CmdLine),
263 ("errexit", ErrExit),
264 ("exec", Exec),
265 ("glob", Glob),
266 ("hashondefinition", HashOnDefinition),
267 ("ignoreeof", IgnoreEof),
268 ("interactive", Interactive),
269 ("log", Log),
270 ("login", Login),
271 ("monitor", Monitor),
272 ("notify", Notify),
273 ("posixlycorrect", PosixlyCorrect),
274 ("stdin", Stdin),
275 ("unset", Unset),
276 ("verbose", Verbose),
277 ("vi", Vi),
278 ("xtrace", XTrace),
279 ];
280
281 match OPTIONS.binary_search_by_key(&name, |&(full_name, _option)| full_name) {
282 Ok(index) => Ok(OPTIONS[index].1),
283 Err(index) => {
284 let mut options = OPTIONS[index..]
285 .iter()
286 .filter(|&(full_name, _option)| full_name.starts_with(name));
287 match options.next() {
288 Some(first) => match options.next() {
289 Some(_second) => Err(Ambiguous),
290 None => Ok(first.1),
291 },
292 None => Err(NoSuchOption),
293 }
294 }
295 }
296 }
297}
298
299#[must_use]
328pub const fn parse_short(name: char) -> std::option::Option<(self::Option, State)> {
329 match name {
330 'a' => Some((AllExport, On)),
331 'b' => Some((Notify, On)),
332 'C' => Some((Clobber, Off)),
333 'c' => Some((CmdLine, On)),
334 'e' => Some((ErrExit, On)),
335 'f' => Some((Glob, Off)),
336 'h' => Some((HashOnDefinition, On)),
337 'i' => Some((Interactive, On)),
338 'l' => Some((Login, On)),
339 'm' => Some((Monitor, On)),
340 'n' => Some((Exec, Off)),
341 's' => Some((Stdin, On)),
342 'u' => Some((Unset, Off)),
343 'v' => Some((Verbose, On)),
344 'x' => Some((XTrace, On)),
345 _ => None,
346 }
347}
348
349#[derive(Clone, Debug)]
355pub struct Iter {
356 inner: EnumSetIter<Option>,
357}
358
359impl Iterator for Iter {
360 type Item = Option;
361 fn next(&mut self) -> std::option::Option<self::Option> {
362 self.inner.next()
363 }
364 fn size_hint(&self) -> (usize, std::option::Option<usize>) {
365 self.inner.size_hint()
366 }
367}
368
369impl DoubleEndedIterator for Iter {
370 fn next_back(&mut self) -> std::option::Option<self::Option> {
371 self.inner.next_back()
372 }
373}
374
375impl ExactSizeIterator for Iter {}
376
377impl Option {
378 pub fn iter() -> Iter {
381 Iter {
382 inner: EnumSet::<Option>::all().iter(),
383 }
384 }
385}
386
387pub fn parse_long(name: &str) -> Result<(Option, State), FromStrError> {
406 if "no".starts_with(name) {
407 return Err(Ambiguous);
408 }
409
410 let intact = Option::from_str(name);
411 let without_no = name
412 .strip_prefix("no")
413 .ok_or(NoSuchOption)
414 .and_then(Option::from_str);
415
416 match (intact, without_no) {
417 (Ok(option), Err(NoSuchOption)) => Ok((option, On)),
418 (Err(NoSuchOption), Ok(option)) => Ok((option, Off)),
419 (Err(Ambiguous), _) | (_, Err(Ambiguous)) => Err(Ambiguous),
420 _ => Err(NoSuchOption),
421 }
422}
423
424pub fn canonicalize(name: &str) -> Cow<'_, str> {
431 if name
432 .chars()
433 .all(|c| c.is_alphanumeric() && !c.is_ascii_uppercase())
434 {
435 Cow::Borrowed(name)
436 } else {
437 Cow::Owned(
438 name.chars()
439 .filter(|c| c.is_alphanumeric())
440 .map(|c| c.to_ascii_lowercase())
441 .collect(),
442 )
443 }
444}
445
446#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
448pub struct OptionSet {
449 enabled_options: EnumSet<Option>,
450}
451
452impl Default for OptionSet {
457 fn default() -> Self {
458 let enabled_options = Clobber | Exec | Glob | Log | Unset;
459 OptionSet { enabled_options }
460 }
461}
462
463impl OptionSet {
464 pub fn empty() -> Self {
466 OptionSet {
467 enabled_options: EnumSet::empty(),
468 }
469 }
470
471 pub fn get(&self, option: Option) -> State {
476 if self.enabled_options.contains(option) {
477 On
478 } else {
479 Off
480 }
481 }
482
483 pub fn set(&mut self, option: Option, state: State) {
490 match state {
491 On => self.enabled_options.insert(option),
492 Off => self.enabled_options.remove(option),
493 };
494 }
495}
496
497impl Extend<Option> for OptionSet {
498 fn extend<T: IntoIterator<Item = Option>>(&mut self, iter: T) {
499 self.enabled_options.extend(iter);
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn short_name_round_trip() {
509 for option in EnumSet::<Option>::all() {
510 if let Some((name, state)) = option.short_name() {
511 assert_eq!(parse_short(name), Some((option, state)));
512 }
513 }
514 for name in 'A'..='z' {
515 if let Some((option, state)) = parse_short(name) {
516 assert_eq!(option.short_name(), Some((name, state)));
517 }
518 }
519 }
520
521 #[test]
522 fn display_and_from_str_round_trip() {
523 for option in EnumSet::<Option>::all() {
524 let name = option.to_string();
525 assert_eq!(Option::from_str(&name), Ok(option));
526 }
527 }
528
529 #[test]
530 fn from_str_unambiguous_abbreviation() {
531 assert_eq!(Option::from_str("allexpor"), Ok(AllExport));
532 assert_eq!(Option::from_str("a"), Ok(AllExport));
533 assert_eq!(Option::from_str("n"), Ok(Notify));
534 }
535
536 #[test]
537 fn from_str_ambiguous_abbreviation() {
538 assert_eq!(Option::from_str(""), Err(Ambiguous));
539 assert_eq!(Option::from_str("c"), Err(Ambiguous));
540 assert_eq!(Option::from_str("lo"), Err(Ambiguous));
541 }
542
543 #[test]
544 fn from_str_no_match() {
545 assert_eq!(Option::from_str("vim"), Err(NoSuchOption));
546 assert_eq!(Option::from_str("0"), Err(NoSuchOption));
547 assert_eq!(Option::from_str("LOG"), Err(NoSuchOption));
548 }
549
550 #[test]
551 fn display_and_parse_round_trip() {
552 for option in EnumSet::<Option>::all() {
553 let name = option.to_string();
554 assert_eq!(parse_long(&name), Ok((option, On)));
555 }
556 }
557
558 #[test]
559 fn display_and_parse_negated_round_trip() {
560 for option in EnumSet::<Option>::all() {
561 let name = format!("no{option}");
562 assert_eq!(parse_long(&name), Ok((option, Off)));
563 }
564 }
565
566 #[test]
567 fn parse_unambiguous_abbreviation() {
568 assert_eq!(parse_long("allexpor"), Ok((AllExport, On)));
569 assert_eq!(parse_long("not"), Ok((Notify, On)));
570 assert_eq!(parse_long("non"), Ok((Notify, Off)));
571 assert_eq!(parse_long("un"), Ok((Unset, On)));
572 assert_eq!(parse_long("noun"), Ok((Unset, Off)));
573 }
574
575 #[test]
576 fn parse_ambiguous_abbreviation() {
577 assert_eq!(parse_long(""), Err(Ambiguous));
578 assert_eq!(parse_long("n"), Err(Ambiguous));
579 assert_eq!(parse_long("no"), Err(Ambiguous));
580 assert_eq!(parse_long("noe"), Err(Ambiguous));
581 assert_eq!(parse_long("e"), Err(Ambiguous));
582 assert_eq!(parse_long("nolo"), Err(Ambiguous));
583 }
584
585 #[test]
586 fn parse_no_match() {
587 assert_eq!(parse_long("vim"), Err(NoSuchOption));
588 assert_eq!(parse_long("0"), Err(NoSuchOption));
589 assert_eq!(parse_long("novim"), Err(NoSuchOption));
590 assert_eq!(parse_long("no0"), Err(NoSuchOption));
591 assert_eq!(parse_long("LOG"), Err(NoSuchOption));
592 }
593
594 #[test]
595 fn test_canonicalize() {
596 assert_eq!(canonicalize(""), "");
597 assert_eq!(canonicalize("POSIXlyCorrect"), "posixlycorrect");
598 assert_eq!(canonicalize(" log "), "log");
599 assert_eq!(canonicalize("gLoB"), "glob");
600 assert_eq!(canonicalize("no-notify"), "nonotify");
601 assert_eq!(canonicalize(" no such_Option "), "nosuchoption");
602 assert_eq!(canonicalize("Abc"), "Abc");
603 }
604}