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 PosixlyCorrect,
131 Stdin,
133 Unset,
135 Verbose,
137 Vi,
139 XTrace,
141}
142
143pub use self::Option::*;
144
145impl Option {
146 #[must_use]
150 pub const fn is_modifiable(self) -> bool {
151 !matches!(self, CmdLine | Interactive | Stdin)
152 }
153
154 #[must_use]
162 pub const fn short_name(self) -> std::option::Option<(char, State)> {
163 match self {
164 AllExport => Some(('a', On)),
165 Clobber => Some(('C', Off)),
166 CmdLine => Some(('c', On)),
167 ErrExit => Some(('e', On)),
168 Exec => Some(('n', Off)),
169 Glob => Some(('f', Off)),
170 HashOnDefinition => Some(('h', On)),
171 IgnoreEof => None,
172 Interactive => Some(('i', On)),
173 Log => None,
174 Login => Some(('l', On)),
175 Monitor => Some(('m', On)),
176 Notify => Some(('b', On)),
177 PipeFail => None,
178 PosixlyCorrect => None,
179 Stdin => Some(('s', On)),
180 Unset => Some(('u', Off)),
181 Verbose => Some(('v', On)),
182 Vi => None,
183 XTrace => Some(('x', On)),
184 }
185 }
186
187 #[must_use]
192 pub const fn long_name(self) -> &'static str {
193 match self {
194 AllExport => "allexport",
195 Clobber => "clobber",
196 CmdLine => "cmdline",
197 ErrExit => "errexit",
198 Exec => "exec",
199 Glob => "glob",
200 HashOnDefinition => "hashondefinition",
201 IgnoreEof => "ignoreeof",
202 Interactive => "interactive",
203 Log => "log",
204 Login => "login",
205 Monitor => "monitor",
206 Notify => "notify",
207 PipeFail => "pipefail",
208 PosixlyCorrect => "posixlycorrect",
209 Stdin => "stdin",
210 Unset => "unset",
211 Verbose => "verbose",
212 Vi => "vi",
213 XTrace => "xtrace",
214 }
215 }
216}
217
218impl Display for Option {
220 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
221 self.long_name().fmt(f)
222 }
223}
224
225#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
227pub enum FromStrError {
228 #[error("no such option")]
230 NoSuchOption,
231
232 #[error("ambiguous option name")]
234 Ambiguous,
235}
236
237pub use FromStrError::*;
238
239impl FromStr for Option {
256 type Err = FromStrError;
257 fn from_str(name: &str) -> Result<Self, FromStrError> {
258 const OPTIONS: &[(&str, Option)] = &[
259 ("allexport", AllExport),
260 ("clobber", Clobber),
261 ("cmdline", CmdLine),
262 ("errexit", ErrExit),
263 ("exec", Exec),
264 ("glob", Glob),
265 ("hashondefinition", HashOnDefinition),
266 ("ignoreeof", IgnoreEof),
267 ("interactive", Interactive),
268 ("log", Log),
269 ("login", Login),
270 ("monitor", Monitor),
271 ("notify", Notify),
272 ("pipefail", PipeFail),
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}