sarge/
tag.rs

1//! Everything surrounding [tags](`Full`).
2
3use std::{fmt::Display, hash::Hash};
4
5/// Create a tag with just a short variant.
6#[inline]
7pub fn short<S: Into<char>>(s: S) -> Full {
8    Full::from(Cli::Short(s.into()))
9}
10
11/// Create a tag with just a long variant.
12#[inline]
13#[allow(clippy::needless_pass_by_value)]
14pub fn long<L: Into<String>>(l: L) -> Full {
15    Full::from(Cli::Long(l.into()))
16}
17
18/// Create a tag with both short and long variants.
19#[inline]
20#[allow(clippy::needless_pass_by_value)]
21pub fn both<S: Into<char>, L: Into<String>>(s: S, l: L) -> Full {
22    Full::from(Cli::Both(s.into(), l.into()))
23}
24
25/// Create an environment variable argument.
26#[inline]
27#[allow(clippy::needless_pass_by_value)]
28pub fn env<E: Into<String>>(e: E) -> Full {
29    Full {
30        cli: None,
31        env: Some(e.into()),
32
33        #[cfg(feature = "help")]
34        doc: None,
35    }
36}
37
38/// An argument name that may have either a CLI component,
39/// environment variable component, or both.
40///
41/// Create with [`short`], [`long`], [`both`], and [`env`](env()).
42#[derive(Debug, Clone)]
43pub struct Full {
44    pub(crate) cli: Option<Cli>,
45    pub(crate) env: Option<String>,
46
47    /// The documentation for this argument.
48    #[cfg(feature = "help")]
49    pub doc: Option<String>,
50}
51
52impl Full {
53    /// Add a CLI component.
54    #[must_use]
55    pub fn cli(mut self, tag: Cli) -> Self {
56        self.cli = Some(tag);
57        self
58    }
59
60    /// Add an environment variable component.
61    #[must_use]
62    #[allow(clippy::needless_pass_by_value)]
63    pub fn env<S: Into<String>>(mut self, name: S) -> Self {
64        self.env = Some(name.into());
65        self
66    }
67
68    /// Add documentation to the argument. If `doc.is_empty()`, instead
69    /// removes any documentation.
70    ///
71    /// Only available on feature `help`.
72    #[must_use]
73    #[cfg(feature = "help")]
74    pub fn doc<S: Into<String>>(mut self, doc: S) -> Self {
75        let doc = doc.into();
76        if doc.is_empty() {
77            self.doc = None;
78            self
79        } else {
80            self.doc = Some(doc);
81            self
82        }
83    }
84
85    /// Returns whether or not this tag has a CLI component.
86    pub fn has_cli(&self) -> bool {
87        self.cli.is_some()
88    }
89
90    /// Returns whether or not this tag has an environment variable component.
91    pub fn has_env(&self) -> bool {
92        self.env.is_some()
93    }
94
95    /// Returns whether or not the CLI component matches the given tag.
96    /// Automatically determines whether it's a short or long tag.
97    pub fn matches_cli(&self, tag: &str) -> bool {
98        self.cli.as_ref().map_or(false, |t| t.matches(tag))
99    }
100
101    /// Returns whether or not the CLI component matches the given long-form
102    /// tag; assumes that the leading `--` has been stripped.
103    pub fn matches_long(&self, long: &str) -> bool {
104        self.cli
105            .as_ref()
106            .map_or(false, |tag| tag.matches_long(long))
107    }
108
109    /// Returns whether or not the CLI component matches the given short-form
110    /// tag; assumes that the leading `-` has been stripped.
111    pub fn matches_short(&self, short: char) -> bool {
112        self.cli
113            .as_ref()
114            .map_or(false, |tag| tag.matches_short(short))
115    }
116
117    /// Returns whether or not the environment variable component matches the
118    /// given name.
119    pub fn matches_env(&self, env: &str) -> bool {
120        self.env.as_ref().map_or(false, |arg| arg == env)
121    }
122}
123
124impl From<Cli> for Full {
125    fn from(tag: Cli) -> Self {
126        Self {
127            cli: Some(tag),
128            env: None,
129
130            #[cfg(feature = "help")]
131            doc: None,
132        }
133    }
134}
135
136impl Hash for Full {
137    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
138        if let Some(tag) = &self.cli {
139            core::mem::discriminant(tag).hash(state);
140        }
141
142        if let Some(arg) = &self.env {
143            arg.hash(state);
144        }
145    }
146}
147
148/// A CLI argument tag, or name. Easiest to create via
149/// [`short`], [`long`], and [`both`].
150///
151/// `Short` means one dash and one character, e.g. `-h`.
152/// `Long` means two dashes and any number of characters,
153/// e.g. `--help`. `Both` means all of the above, e.g.
154/// `-h` AND `--help`.
155#[derive(Debug, Clone)]
156pub enum Cli {
157    /// A short-form tag, e.g. `-h`.
158    Short(char),
159    /// A long-form tag, e.g. `--help`.
160    Long(String),
161    /// Both a long- and short-form tag, e.g. `-h` AND `--help`.
162    Both(char, String),
163}
164
165impl Cli {
166    /// Create a [`Full`] from a [`Cli`].
167    pub fn env(self, env: String) -> Full {
168        Full {
169            cli: Some(self),
170            env: Some(env),
171
172            #[cfg(feature = "help")]
173            doc: None,
174        }
175    }
176
177    /// Returns whether or not the given tag matches. Automatically determines
178    /// if it's a short or long tag.
179    pub fn matches(&self, tag: &str) -> bool {
180        if let Some(tag) = tag.strip_prefix("--") {
181            self.matches_long(tag)
182        } else if let Some(tag) = tag.strip_prefix('-') {
183            if let Some(ch) = tag.chars().next() {
184                self.matches_short(ch)
185            } else {
186                false
187            }
188        } else {
189            false
190        }
191    }
192
193    /// Returns whether or not the given long-form tag matches. Assumes that
194    /// the leading `--` has been stripped.
195    pub fn matches_long(&self, long: &str) -> bool {
196        match self {
197            Cli::Short(_) => false,
198            Cli::Long(l) | Cli::Both(_, l) => l == long,
199        }
200    }
201
202    /// Returns whether or not the given short-form tag matches. Assumes that
203    /// the leading `-` has been stripped.
204    pub fn matches_short(&self, short: char) -> bool {
205        match self {
206            Cli::Long(_) => false,
207            Cli::Short(s) | Cli::Both(s, _) => *s == short,
208        }
209    }
210}
211
212impl PartialEq for Cli {
213    fn eq(&self, other: &Self) -> bool {
214        match self {
215            Self::Short(s) => match other {
216                Self::Short(o) | Self::Both(o, _) => s == o,
217                Self::Long(_) => false,
218            },
219            Self::Long(s) => match other {
220                Self::Long(o) | Self::Both(_, o) => s == o,
221                Self::Short(_) => false,
222            },
223            Self::Both(s1, s2) => match other {
224                Self::Short(o) => s1 == o,
225                Self::Long(o) => s2 == o,
226                Self::Both(o1, o2) => (s1 == o1) || (s2 == o2),
227            },
228        }
229    }
230}
231
232impl Display for Cli {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            Self::Short(ch) => write!(f, "-{ch}"),
236            Self::Long(s) => write!(f, "--{s}"),
237            Self::Both(ch, s) => write!(f, "-{ch} / --{s}"),
238        }
239    }
240}