1use alloc::string::{String, ToString};
28use core::str::FromStr;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum CliError {
33 UnknownFlag(String),
35 MissingValue(String),
37 InvalidValue {
39 flag: String,
41 value: String,
43 },
44 Unpaired {
46 given: String,
48 needs: String,
50 },
51}
52
53impl core::fmt::Display for CliError {
54 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55 match self {
56 Self::UnknownFlag(n) => write!(f, "unknown flag: {n}"),
57 Self::MissingValue(n) => write!(f, "flag {n} needs a value"),
58 Self::InvalidValue { flag, value } => write!(f, "bad value for {flag}: {value}"),
59 Self::Unpaired { given, needs } => write!(f, "{given} needs {needs}"),
60 }
61 }
62}
63
64impl std::error::Error for CliError {}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Flag {
69 name: String,
70 inline: Option<String>,
71}
72
73impl Flag {
74 #[must_use]
76 pub fn name(&self) -> &str {
77 &self.name
78 }
79}
80
81#[derive(Debug)]
83pub struct Flags<'a> {
84 args: &'a [String],
85 next: usize,
86}
87
88impl<'a> Flags<'a> {
89 #[must_use]
91 pub fn new(args: &'a [String]) -> Self {
92 Self { args, next: 0 }
93 }
94
95 pub fn next_flag(&mut self) -> Option<Flag> {
97 let raw = self.args.get(self.next)?;
98 self.next += 1;
99 Some(match raw.split_once('=') {
100 Some((name, value)) => Flag {
101 name: name.to_string(),
102 inline: Some(value.to_string()),
103 },
104 None => Flag {
105 name: raw.clone(),
106 inline: None,
107 },
108 })
109 }
110
111 pub fn value(&mut self, flag: &Flag) -> Result<String, CliError> {
117 if let Some(v) = &flag.inline {
118 return Ok(v.clone());
119 }
120 let v = self
121 .args
122 .get(self.next)
123 .cloned()
124 .ok_or_else(|| CliError::MissingValue(flag.name.clone()))?;
125 self.next += 1;
126 Ok(v)
127 }
128
129 pub fn parsed<T: FromStr>(&mut self, flag: &Flag) -> Result<T, CliError> {
135 let v = self.value(flag)?;
136 v.parse().map_err(|_| CliError::InvalidValue {
137 flag: flag.name.clone(),
138 value: v,
139 })
140 }
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used)]
145mod tests {
146 use super::*;
147 use alloc::vec;
148 use alloc::vec::Vec;
149
150 fn args(parts: &[&str]) -> Vec<String> {
151 parts.iter().map(|s| (*s).to_string()).collect()
152 }
153
154 #[test]
155 fn a_value_comes_inline_or_from_the_next_argument() {
156 let a = args(&["--a=1", "--b", "2", "--switch"]);
157 let mut flags = Flags::new(&a);
158 let f = flags.next_flag().unwrap();
159 assert_eq!((f.name(), flags.value(&f).unwrap().as_str()), ("--a", "1"));
160 let f = flags.next_flag().unwrap();
161 assert_eq!(flags.parsed::<u8>(&f).unwrap(), 2);
162 assert_eq!(flags.next_flag().unwrap().name(), "--switch");
163 assert!(flags.next_flag().is_none());
164 }
165
166 #[test]
167 fn a_switch_given_a_value_inline_keeps_its_name() {
168 let a = args(&["--help=yes"]);
169 let mut flags = Flags::new(&a);
170 assert_eq!(flags.next_flag().unwrap().name(), "--help");
171 assert!(flags.next_flag().is_none());
172 }
173
174 #[test]
175 fn a_missing_and_an_unparseable_value_name_their_flag() {
176 let a = args(&["--n"]);
177 let mut flags = Flags::new(&a);
178 let f = flags.next_flag().unwrap();
179 assert_eq!(
180 flags.value(&f).unwrap_err(),
181 CliError::MissingValue("--n".into())
182 );
183 let a = args(&["--n", "x"]);
184 let mut flags = Flags::new(&a);
185 let f = flags.next_flag().unwrap();
186 assert_eq!(
187 flags.parsed::<i32>(&f).unwrap_err(),
188 CliError::InvalidValue {
189 flag: "--n".into(),
190 value: "x".into()
191 }
192 );
193 }
194
195 #[test]
196 fn the_messages_are_the_daemons_messages() {
197 let cases = vec![
198 (CliError::UnknownFlag("--x".into()), "unknown flag: --x"),
199 (
200 CliError::MissingValue("--x".into()),
201 "flag --x needs a value",
202 ),
203 (
204 CliError::InvalidValue {
205 flag: "--x".into(),
206 value: "v".into(),
207 },
208 "bad value for --x: v",
209 ),
210 (
211 CliError::Unpaired {
212 given: "--a".into(),
213 needs: "--b".into(),
214 },
215 "--a needs --b",
216 ),
217 ];
218 for (e, text) in cases {
219 assert_eq!(e.to_string(), text);
220 }
221 }
222}