use enumset::EnumSet;
use enumset::EnumSetIter;
use enumset::EnumSetType;
use std::borrow::Cow;
use std::fmt::Display;
use std::fmt::Formatter;
use std::ops::Not;
use std::str::FromStr;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum State {
On,
Off,
}
pub use State::*;
impl State {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
On => "on",
Off => "off",
}
}
}
impl Display for State {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
impl Not for State {
type Output = Self;
fn not(self) -> Self {
match self {
On => Off,
Off => On,
}
}
}
impl From<bool> for State {
fn from(is_on: bool) -> Self {
if is_on { On } else { Off }
}
}
impl From<State> for bool {
fn from(state: State) -> Self {
match state {
On => true,
Off => false,
}
}
}
#[derive(Clone, Copy, Debug, EnumSetType, Eq, Hash, PartialEq)]
#[enumset(no_super_impls)]
#[non_exhaustive]
pub enum Option {
AllExport,
Clobber,
CmdLine,
ErrExit,
Exec,
Glob,
HashOnDefinition,
IgnoreEof,
Interactive,
Log,
Login,
Monitor,
Notify,
PipeFail,
PosixlyCorrect,
Stdin,
Unset,
Verbose,
Vi,
XTrace,
}
pub use self::Option::*;
impl Option {
#[must_use]
pub const fn is_modifiable(self) -> bool {
!matches!(self, CmdLine | Interactive | Stdin)
}
#[must_use]
pub const fn short_name(self) -> std::option::Option<(char, State)> {
match self {
AllExport => Some(('a', On)),
Clobber => Some(('C', Off)),
CmdLine => Some(('c', On)),
ErrExit => Some(('e', On)),
Exec => Some(('n', Off)),
Glob => Some(('f', Off)),
HashOnDefinition => Some(('h', On)),
IgnoreEof => None,
Interactive => Some(('i', On)),
Log => None,
Login => Some(('l', On)),
Monitor => Some(('m', On)),
Notify => Some(('b', On)),
PipeFail => None,
PosixlyCorrect => None,
Stdin => Some(('s', On)),
Unset => Some(('u', Off)),
Verbose => Some(('v', On)),
Vi => None,
XTrace => Some(('x', On)),
}
}
#[must_use]
pub const fn long_name(self) -> &'static str {
match self {
AllExport => "allexport",
Clobber => "clobber",
CmdLine => "cmdline",
ErrExit => "errexit",
Exec => "exec",
Glob => "glob",
HashOnDefinition => "hashondefinition",
IgnoreEof => "ignoreeof",
Interactive => "interactive",
Log => "log",
Login => "login",
Monitor => "monitor",
Notify => "notify",
PipeFail => "pipefail",
PosixlyCorrect => "posixlycorrect",
Stdin => "stdin",
Unset => "unset",
Verbose => "verbose",
Vi => "vi",
XTrace => "xtrace",
}
}
}
impl Display for Option {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.long_name().fmt(f)
}
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
pub enum FromStrError {
#[error("no such option")]
NoSuchOption,
#[error("ambiguous option name")]
Ambiguous,
}
pub use FromStrError::*;
impl FromStr for Option {
type Err = FromStrError;
fn from_str(name: &str) -> Result<Self, FromStrError> {
const OPTIONS: &[(&str, Option)] = &[
("allexport", AllExport),
("clobber", Clobber),
("cmdline", CmdLine),
("errexit", ErrExit),
("exec", Exec),
("glob", Glob),
("hashondefinition", HashOnDefinition),
("ignoreeof", IgnoreEof),
("interactive", Interactive),
("log", Log),
("login", Login),
("monitor", Monitor),
("notify", Notify),
("pipefail", PipeFail),
("posixlycorrect", PosixlyCorrect),
("stdin", Stdin),
("unset", Unset),
("verbose", Verbose),
("vi", Vi),
("xtrace", XTrace),
];
match OPTIONS.binary_search_by_key(&name, |&(full_name, _option)| full_name) {
Ok(index) => Ok(OPTIONS[index].1),
Err(index) => {
let mut options = OPTIONS[index..]
.iter()
.filter(|&(full_name, _option)| full_name.starts_with(name));
match options.next() {
Some(first) => match options.next() {
Some(_second) => Err(Ambiguous),
None => Ok(first.1),
},
None => Err(NoSuchOption),
}
}
}
}
}
#[must_use]
pub const fn parse_short(name: char) -> std::option::Option<(self::Option, State)> {
match name {
'a' => Some((AllExport, On)),
'b' => Some((Notify, On)),
'C' => Some((Clobber, Off)),
'c' => Some((CmdLine, On)),
'e' => Some((ErrExit, On)),
'f' => Some((Glob, Off)),
'h' => Some((HashOnDefinition, On)),
'i' => Some((Interactive, On)),
'l' => Some((Login, On)),
'm' => Some((Monitor, On)),
'n' => Some((Exec, Off)),
's' => Some((Stdin, On)),
'u' => Some((Unset, Off)),
'v' => Some((Verbose, On)),
'x' => Some((XTrace, On)),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct Iter {
inner: EnumSetIter<Option>,
}
impl Iterator for Iter {
type Item = Option;
fn next(&mut self) -> std::option::Option<self::Option> {
self.inner.next()
}
fn size_hint(&self) -> (usize, std::option::Option<usize>) {
self.inner.size_hint()
}
}
impl DoubleEndedIterator for Iter {
fn next_back(&mut self) -> std::option::Option<self::Option> {
self.inner.next_back()
}
}
impl ExactSizeIterator for Iter {}
impl Option {
pub fn iter() -> Iter {
Iter {
inner: EnumSet::<Option>::all().iter(),
}
}
}
pub fn parse_long(name: &str) -> Result<(Option, State), FromStrError> {
if "no".starts_with(name) {
return Err(Ambiguous);
}
let intact = Option::from_str(name);
let without_no = name
.strip_prefix("no")
.ok_or(NoSuchOption)
.and_then(Option::from_str);
match (intact, without_no) {
(Ok(option), Err(NoSuchOption)) => Ok((option, On)),
(Err(NoSuchOption), Ok(option)) => Ok((option, Off)),
(Err(Ambiguous), _) | (_, Err(Ambiguous)) => Err(Ambiguous),
_ => Err(NoSuchOption),
}
}
pub fn canonicalize(name: &str) -> Cow<'_, str> {
if name
.chars()
.all(|c| c.is_alphanumeric() && !c.is_ascii_uppercase())
{
Cow::Borrowed(name)
} else {
Cow::Owned(
name.chars()
.filter(|c| c.is_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect(),
)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct OptionSet {
enabled_options: EnumSet<Option>,
}
impl Default for OptionSet {
fn default() -> Self {
let enabled_options = Clobber | Exec | Glob | Log | Unset;
OptionSet { enabled_options }
}
}
impl OptionSet {
pub fn empty() -> Self {
OptionSet {
enabled_options: EnumSet::empty(),
}
}
pub fn get(&self, option: Option) -> State {
if self.enabled_options.contains(option) {
On
} else {
Off
}
}
pub fn set(&mut self, option: Option, state: State) {
match state {
On => self.enabled_options.insert(option),
Off => self.enabled_options.remove(option),
};
}
}
impl Extend<Option> for OptionSet {
fn extend<T: IntoIterator<Item = Option>>(&mut self, iter: T) {
self.enabled_options.extend(iter);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_name_round_trip() {
for option in EnumSet::<Option>::all() {
if let Some((name, state)) = option.short_name() {
assert_eq!(parse_short(name), Some((option, state)));
}
}
for name in 'A'..='z' {
if let Some((option, state)) = parse_short(name) {
assert_eq!(option.short_name(), Some((name, state)));
}
}
}
#[test]
fn display_and_from_str_round_trip() {
for option in EnumSet::<Option>::all() {
let name = option.to_string();
assert_eq!(Option::from_str(&name), Ok(option));
}
}
#[test]
fn from_str_unambiguous_abbreviation() {
assert_eq!(Option::from_str("allexpor"), Ok(AllExport));
assert_eq!(Option::from_str("a"), Ok(AllExport));
assert_eq!(Option::from_str("n"), Ok(Notify));
}
#[test]
fn from_str_ambiguous_abbreviation() {
assert_eq!(Option::from_str(""), Err(Ambiguous));
assert_eq!(Option::from_str("c"), Err(Ambiguous));
assert_eq!(Option::from_str("lo"), Err(Ambiguous));
}
#[test]
fn from_str_no_match() {
assert_eq!(Option::from_str("vim"), Err(NoSuchOption));
assert_eq!(Option::from_str("0"), Err(NoSuchOption));
assert_eq!(Option::from_str("LOG"), Err(NoSuchOption));
}
#[test]
fn display_and_parse_round_trip() {
for option in EnumSet::<Option>::all() {
let name = option.to_string();
assert_eq!(parse_long(&name), Ok((option, On)));
}
}
#[test]
fn display_and_parse_negated_round_trip() {
for option in EnumSet::<Option>::all() {
let name = format!("no{option}");
assert_eq!(parse_long(&name), Ok((option, Off)));
}
}
#[test]
fn parse_unambiguous_abbreviation() {
assert_eq!(parse_long("allexpor"), Ok((AllExport, On)));
assert_eq!(parse_long("not"), Ok((Notify, On)));
assert_eq!(parse_long("non"), Ok((Notify, Off)));
assert_eq!(parse_long("un"), Ok((Unset, On)));
assert_eq!(parse_long("noun"), Ok((Unset, Off)));
}
#[test]
fn parse_ambiguous_abbreviation() {
assert_eq!(parse_long(""), Err(Ambiguous));
assert_eq!(parse_long("n"), Err(Ambiguous));
assert_eq!(parse_long("no"), Err(Ambiguous));
assert_eq!(parse_long("noe"), Err(Ambiguous));
assert_eq!(parse_long("e"), Err(Ambiguous));
assert_eq!(parse_long("nolo"), Err(Ambiguous));
}
#[test]
fn parse_no_match() {
assert_eq!(parse_long("vim"), Err(NoSuchOption));
assert_eq!(parse_long("0"), Err(NoSuchOption));
assert_eq!(parse_long("novim"), Err(NoSuchOption));
assert_eq!(parse_long("no0"), Err(NoSuchOption));
assert_eq!(parse_long("LOG"), Err(NoSuchOption));
}
#[test]
fn test_canonicalize() {
assert_eq!(canonicalize(""), "");
assert_eq!(canonicalize("POSIXlyCorrect"), "posixlycorrect");
assert_eq!(canonicalize(" log "), "log");
assert_eq!(canonicalize("gLoB"), "glob");
assert_eq!(canonicalize("no-notify"), "nonotify");
assert_eq!(canonicalize(" no such_Option "), "nosuchoption");
assert_eq!(canonicalize("Abc"), "Abc");
}
}