1use std::ffi::{OsStr, OsString};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ArgType {
11 Flag,
13 Option,
15 Positional,
17}
18
19#[derive(Debug, Clone)]
21pub struct Arg {
22 pub arg_type: ArgType,
24 pub short: Option<&'static str>,
26 pub long: Option<&'static str>,
28 pub required: bool,
30 pub value: Option<OsString>,
32}
33
34impl Arg {
35 pub fn flag(short: &'static str, long: &'static str) -> Self {
37 Self {
38 arg_type: ArgType::Flag,
39 short: Some(short),
40 long: Some(long),
41 required: true,
42 value: None,
43 }
44 }
45
46 pub fn optional_flag(short: &'static str, long: &'static str) -> Self {
48 Self {
49 arg_type: ArgType::Flag,
50 short: Some(short),
51 long: Some(long),
52 required: false,
53 value: None,
54 }
55 }
56
57 pub fn option(short: &'static str, long: &'static str) -> Self {
59 Self {
60 arg_type: ArgType::Option,
61 short: Some(short),
62 long: Some(long),
63 required: true,
64 value: None,
65 }
66 }
67
68 pub fn optional_option(short: &'static str, long: &'static str) -> Self {
70 Self {
71 arg_type: ArgType::Option,
72 short: Some(short),
73 long: Some(long),
74 required: false,
75 value: None,
76 }
77 }
78
79 pub fn positional() -> Self {
81 Self {
82 arg_type: ArgType::Positional,
83 short: None,
84 long: None,
85 required: true,
86 value: None,
87 }
88 }
89
90 pub fn optional_positional() -> Self {
92 Self {
93 arg_type: ArgType::Positional,
94 short: None,
95 long: None,
96 required: false,
97 value: None,
98 }
99 }
100
101 #[must_use]
103 pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Self {
104 self.value = Some(value.as_ref().to_os_string());
105 self
106 }
107
108 pub fn to_strings(&self) -> Vec<OsString> {
110 let mut result = Vec::new();
111
112 match self.arg_type {
113 ArgType::Flag => {
114 if let Some(short) = self.short {
115 result.push(OsString::from(short));
116 }
117 }
118 ArgType::Option => {
119 if let Some(short) = self.short {
120 if let Some(ref value) = self.value {
121 result.push(OsString::from(short));
122 result.push(value.clone());
123 }
124 }
125 if let Some(long) = self.long {
126 if let Some(ref value) = self.value {
127 let mut long_arg = OsString::from(long);
128 long_arg.push("=");
129 long_arg.push(value);
130 result.push(long_arg);
131 }
132 }
133 }
134 ArgType::Positional => {
135 if let Some(ref value) = self.value {
136 result.push(value.clone());
137 }
138 }
139 }
140
141 result
142 }
143}
144
145#[derive(Debug, Default)]
147pub struct Args {
148 args: Vec<Arg>,
149 positional: Vec<Arg>,
150}
151
152impl Args {
153 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn flag(mut self, short: &'static str, long: &'static str) -> Self {
160 self.args.push(Arg::flag(short, long));
161 self
162 }
163
164 pub fn optional_flag(mut self, short: &'static str, long: &'static str) -> Self {
166 self.args.push(Arg::optional_flag(short, long));
167 self
168 }
169
170 pub fn option(mut self, short: &'static str, long: &'static str) -> Self {
172 self.args.push(Arg::option(short, long));
173 self
174 }
175
176 pub fn optional_option(mut self, short: &'static str, long: &'static str) -> Self {
178 self.args.push(Arg::optional_option(short, long));
179 self
180 }
181
182 pub fn positional(mut self) -> Self {
184 self.positional.push(Arg::positional());
185 self
186 }
187
188 pub fn optional_positional(mut self) -> Self {
190 self.positional.push(Arg::optional_positional());
191 self
192 }
193
194 pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Result<Self, String> {
202 for arg in self.args.iter_mut().rev() {
204 if arg.arg_type == ArgType::Option && arg.value.is_none() {
205 arg.value = Some(value.as_ref().to_os_string());
206 return Ok(self);
207 }
208 }
209
210 for arg in self.positional.iter_mut().rev() {
212 if arg.value.is_none() {
213 arg.value = Some(value.as_ref().to_os_string());
214 return Ok(self);
215 }
216 }
217
218 Err("No argument expects a value".to_string())
219 }
220
221 pub fn build(self) -> Vec<OsString> {
225 let mut result = Vec::new();
226
227 for arg in &self.args {
229 result.extend(arg.to_strings());
230 }
231
232 for arg in &self.positional {
234 result.extend(arg.to_strings());
235 }
236
237 result
238 }
239
240 pub fn validate(&self) -> Result<(), String> {
246 for arg in &self.args {
247 if arg.required && arg.value.is_none() && arg.arg_type != ArgType::Flag {
249 let arg_name = arg.long.unwrap_or(arg.short.unwrap_or("unknown"));
250 return Err(format!("Missing required argument: {arg_name}"));
251 }
252 }
253
254 for arg in &self.positional {
255 if arg.required && arg.value.is_none() {
256 return Err("Missing required positional argument".to_string());
257 }
258 }
259
260 Ok(())
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_flag() {
270 let args = Args::new().flag("-v", "--verbose").build();
271
272 assert!(args.contains(&OsString::from("-v")));
273 }
274
275 #[test]
276 fn test_option() {
277 let args = Args::new()
278 .option("-o", "--output")
279 .value("file.txt")
280 .unwrap()
281 .build();
282
283 assert!(args.contains(&OsString::from("-o")));
284 assert!(args.contains(&OsString::from("file.txt")));
285 }
286
287 #[test]
288 fn test_positional() {
289 let args = Args::new().positional().value("file.txt").unwrap().build();
290
291 assert!(args.contains(&OsString::from("file.txt")));
292 }
293
294 #[test]
295 fn test_validate_success() {
296 let args = Args::new()
297 .flag("-v", "--verbose")
298 .option("-o", "--output")
299 .value("file.txt")
300 .unwrap();
301
302 assert!(args.validate().is_ok());
303 }
304
305 #[test]
306 fn test_validate_failure() {
307 let args = Args::new()
308 .option("-o", "--output")
309 .value("file.txt")
310 .unwrap()
311 .positional();
312
313 assert!(args.validate().is_err());
314 }
315
316 #[test]
317 fn test_value_no_target() {
318 let result = Args::new().value("test");
319 assert!(result.is_err());
320 }
321}