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