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]
195 pub const fn long_name(self) -> &'static str {
196 match self {
197 AllExport => "allexport",
198 Clobber => "clobber",
199 CmdLine => "cmdline",
200 ErrExit => "errexit",
201 Exec => "exec",
202 Glob => "glob",
203 HashOnDefinition => "hashondefinition",
204 IgnoreEof => "ignoreeof",
205 Interactive => "interactive",
206 Log => "log",
207 Login => "login",
208 Monitor => "monitor",
209 Notify => "notify",
210 PipeFail => "pipefail",
211 Portable => "portable",
212 PosixlyCorrect => "posixlycorrect",
213 Stdin => "stdin",
214 Unset => "unset",
215 Verbose => "verbose",
216 Vi => "vi",
217 XTrace => "xtrace",
218 }
219 }
220}
221
222impl Display for Option {
224 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
225 self.long_name().fmt(f)
226 }
227}
228
229#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
231pub enum FromStrError {
232 #[error("no such option")]
234 NoSuchOption,
235
236 #[error("ambiguous option name")]
238 Ambiguous,
239}
240
241pub use FromStrError::*;
242
243impl FromStr for Option {
260 type Err = FromStrError;
261 fn from_str(name: &str) -> Result<Self, FromStrError> {
262 const OPTIONS: &[(&str, Option)] = &[
263 ("allexport", AllExport),
264 ("clobber", Clobber),
265 ("cmdline", CmdLine),
266 ("errexit", ErrExit),
267 ("exec", Exec),
268 ("glob", Glob),
269 ("hashondefinition", HashOnDefinition),
270 ("ignoreeof", IgnoreEof),
271 ("interactive", Interactive),
272 ("log", Log),
273 ("login", Login),
274 ("monitor", Monitor),
275 ("notify", Notify),
276 ("pipefail", PipeFail),
277 ("portable", Portable),
278 ("posixlycorrect", PosixlyCorrect),
279 ("stdin", Stdin),
280 ("unset", Unset),
281 ("verbose", Verbose),
282 ("vi", Vi),
283 ("xtrace", XTrace),
284 ];
285
286 match OPTIONS.binary_search_by_key(&name, |&(full_name, _option)| full_name) {
287 Ok(index) => Ok(OPTIONS[index].1),
288 Err(index) => {
289 let mut options = OPTIONS[index..]
290 .iter()
291 .filter(|&(full_name, _option)| full_name.starts_with(name));
292 match options.next() {
293 Some(first) => match options.next() {
294 Some(_second) => Err(Ambiguous),
295 None => Ok(first.1),
296 },
297 None => Err(NoSuchOption),
298 }
299 }
300 }
301 }
302}
303
304#[must_use]
333pub const fn parse_short(name: char) -> std::option::Option<(self::Option, State)> {
334 match name {
335 'a' => Some((AllExport, On)),
336 'b' => Some((Notify, On)),
337 'C' => Some((Clobber, Off)),
338 'c' => Some((CmdLine, On)),
339 'e' => Some((ErrExit, On)),
340 'f' => Some((Glob, Off)),
341 'h' => Some((HashOnDefinition, On)),
342 'i' => Some((Interactive, On)),
343 'l' => Some((Login, On)),
344 'm' => Some((Monitor, On)),
345 'n' => Some((Exec, Off)),
346 's' => Some((Stdin, On)),
347 'u' => Some((Unset, Off)),
348 'v' => Some((Verbose, On)),
349 'x' => Some((XTrace, On)),
350 _ => None,
351 }
352}
353
354#[derive(Clone, Debug)]
360pub struct Iter {
361 inner: EnumSetIter<Option>,
362}
363
364impl Iterator for Iter {
365 type Item = Option;
366 fn next(&mut self) -> std::option::Option<self::Option> {
367 self.inner.next()
368 }
369 fn size_hint(&self) -> (usize, std::option::Option<usize>) {
370 self.inner.size_hint()
371 }
372}
373
374impl DoubleEndedIterator for Iter {
375 fn next_back(&mut self) -> std::option::Option<self::Option> {
376 self.inner.next_back()
377 }
378}
379
380impl ExactSizeIterator for Iter {}
381
382impl Option {
383 pub fn iter() -> Iter {
386 Iter {
387 inner: EnumSet::<Option>::all().iter(),
388 }
389 }
390}
391
392pub fn parse_long(name: &str) -> Result<(Option, State), FromStrError> {
411 if "no".starts_with(name) {
412 return Err(Ambiguous);
413 }
414
415 let intact = Option::from_str(name);
416 let without_no = name
417 .strip_prefix("no")
418 .ok_or(NoSuchOption)
419 .and_then(Option::from_str);
420
421 match (intact, without_no) {
422 (Ok(option), Err(NoSuchOption)) => Ok((option, On)),
423 (Err(NoSuchOption), Ok(option)) => Ok((option, Off)),
424 (Err(Ambiguous), _) | (_, Err(Ambiguous)) => Err(Ambiguous),
425 _ => Err(NoSuchOption),
426 }
427}
428
429pub fn canonicalize(name: &str) -> Cow<'_, str> {
436 if name
437 .chars()
438 .all(|c| c.is_alphanumeric() && !c.is_ascii_uppercase())
439 {
440 Cow::Borrowed(name)
441 } else {
442 Cow::Owned(
443 name.chars()
444 .filter(|c| c.is_alphanumeric())
445 .map(|c| c.to_ascii_lowercase())
446 .collect(),
447 )
448 }
449}
450
451#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
453pub struct OptionSet {
454 enabled_options: EnumSet<Option>,
455}
456
457impl Default for OptionSet {
462 fn default() -> Self {
463 let enabled_options = Clobber | Exec | Glob | Log | Unset;
464 OptionSet { enabled_options }
465 }
466}
467
468impl OptionSet {
469 pub fn empty() -> Self {
471 OptionSet {
472 enabled_options: EnumSet::empty(),
473 }
474 }
475
476 pub fn get(&self, option: Option) -> State {
481 if self.enabled_options.contains(option) {
482 On
483 } else {
484 Off
485 }
486 }
487
488 pub fn set(&mut self, option: Option, state: State) {
495 match state {
496 On => self.enabled_options.insert(option),
497 Off => self.enabled_options.remove(option),
498 };
499 }
500}
501
502impl Extend<Option> for OptionSet {
503 fn extend<T: IntoIterator<Item = Option>>(&mut self, iter: T) {
504 self.enabled_options.extend(iter);
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn short_name_round_trip() {
514 for option in EnumSet::<Option>::all() {
515 if let Some((name, state)) = option.short_name() {
516 assert_eq!(parse_short(name), Some((option, state)));
517 }
518 }
519 for name in 'A'..='z' {
520 if let Some((option, state)) = parse_short(name) {
521 assert_eq!(option.short_name(), Some((name, state)));
522 }
523 }
524 }
525
526 #[test]
527 fn display_and_from_str_round_trip() {
528 for option in EnumSet::<Option>::all() {
529 let name = option.to_string();
530 assert_eq!(Option::from_str(&name), Ok(option));
531 }
532 }
533
534 #[test]
535 fn from_str_unambiguous_abbreviation() {
536 assert_eq!(Option::from_str("allexpor"), Ok(AllExport));
537 assert_eq!(Option::from_str("a"), Ok(AllExport));
538 assert_eq!(Option::from_str("n"), Ok(Notify));
539 }
540
541 #[test]
542 fn from_str_ambiguous_abbreviation() {
543 assert_eq!(Option::from_str(""), Err(Ambiguous));
544 assert_eq!(Option::from_str("c"), Err(Ambiguous));
545 assert_eq!(Option::from_str("lo"), Err(Ambiguous));
546 }
547
548 #[test]
549 fn from_str_no_match() {
550 assert_eq!(Option::from_str("vim"), Err(NoSuchOption));
551 assert_eq!(Option::from_str("0"), Err(NoSuchOption));
552 assert_eq!(Option::from_str("LOG"), Err(NoSuchOption));
553 }
554
555 #[test]
556 fn display_and_parse_round_trip() {
557 for option in EnumSet::<Option>::all() {
558 let name = option.to_string();
559 assert_eq!(parse_long(&name), Ok((option, On)));
560 }
561 }
562
563 #[test]
564 fn display_and_parse_negated_round_trip() {
565 for option in EnumSet::<Option>::all() {
566 let name = format!("no{option}");
567 assert_eq!(parse_long(&name), Ok((option, Off)));
568 }
569 }
570
571 #[test]
572 fn parse_unambiguous_abbreviation() {
573 assert_eq!(parse_long("allexpor"), Ok((AllExport, On)));
574 assert_eq!(parse_long("not"), Ok((Notify, On)));
575 assert_eq!(parse_long("non"), Ok((Notify, Off)));
576 assert_eq!(parse_long("un"), Ok((Unset, On)));
577 assert_eq!(parse_long("noun"), Ok((Unset, Off)));
578 }
579
580 #[test]
581 fn parse_ambiguous_abbreviation() {
582 assert_eq!(parse_long(""), Err(Ambiguous));
583 assert_eq!(parse_long("n"), Err(Ambiguous));
584 assert_eq!(parse_long("no"), Err(Ambiguous));
585 assert_eq!(parse_long("noe"), Err(Ambiguous));
586 assert_eq!(parse_long("e"), Err(Ambiguous));
587 assert_eq!(parse_long("nolo"), Err(Ambiguous));
588 }
589
590 #[test]
591 fn parse_no_match() {
592 assert_eq!(parse_long("vim"), Err(NoSuchOption));
593 assert_eq!(parse_long("0"), Err(NoSuchOption));
594 assert_eq!(parse_long("novim"), Err(NoSuchOption));
595 assert_eq!(parse_long("no0"), Err(NoSuchOption));
596 assert_eq!(parse_long("LOG"), Err(NoSuchOption));
597 }
598
599 #[test]
600 fn test_canonicalize() {
601 assert_eq!(canonicalize(""), "");
602 assert_eq!(canonicalize("POSIXlyCorrect"), "posixlycorrect");
603 assert_eq!(canonicalize(" log "), "log");
604 assert_eq!(canonicalize("gLoB"), "glob");
605 assert_eq!(canonicalize("no-notify"), "nonotify");
606 assert_eq!(canonicalize(" no such_Option "), "nosuchoption");
607 assert_eq!(canonicalize("Abc"), "Abc");
608 }
609}