Skip to main content

bare_script/
args.rs

1//! Type-safe argument building for commands.
2//!
3//! This module provides a type-safe way to build command arguments,
4//! following the "Parse, don't validate" philosophy.
5
6use std::ffi::{OsStr, OsString};
7
8/// Represents the type of an argument.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ArgType {
11    /// A flag (e.g., `-v`, `--verbose`)
12    Flag,
13    /// An option with a value (e.g., `-o file`, `--output=file`)
14    Option,
15    /// A positional argument
16    Positional,
17}
18
19/// Represents a type-safe argument specification.
20#[derive(Debug, Clone)]
21pub struct Arg {
22    /// The argument type
23    pub arg_type: ArgType,
24    /// Short form (e.g., `-v`)
25    pub short: Option<&'static str>,
26    /// Long form (e.g., `--verbose`)
27    pub long: Option<&'static str>,
28    /// Whether this argument is required
29    pub required: bool,
30    /// The argument value (if any)
31    pub value: Option<OsString>,
32}
33
34impl Arg {
35    /// Creates a new required flag argument.
36    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    /// Creates a new optional flag argument.
47    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    /// Creates a new required option argument.
58    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    /// Creates a new optional option argument.
69    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    /// Creates a new positional argument.
80    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    /// Creates a new optional positional argument.
91    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    /// Sets the value for this argument.
102    #[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    /// Converts this argument to command-line strings.
109    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/// A builder for type-safe command arguments.
146#[derive(Debug, Default)]
147pub struct Args {
148    args: Vec<Arg>,
149    positional: Vec<Arg>,
150}
151
152impl Args {
153    /// Creates a new empty args builder.
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Adds a flag argument.
159    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    /// Adds an optional flag argument.
165    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    /// Adds an option argument (requires value).
171    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    /// Adds an optional option argument.
177    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    /// Adds a positional argument.
183    pub fn positional(mut self) -> Self {
184        self.positional.push(Arg::positional());
185        self
186    }
187
188    /// Adds an optional positional argument.
189    pub fn optional_positional(mut self) -> Self {
190        self.positional.push(Arg::optional_positional());
191        self
192    }
193
194    /// Sets the value for the last added argument that requires a value.
195    ///
196    /// Sets the value for the last added argument that requires a value.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if no argument expects a value.
201    pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Result<Self, String> {
202        // Find the last argument that can accept a value
203        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        // Check positional arguments
211        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    /// Builds the argument list as a vector of OsString.
222    ///
223    /// Note: This consumes self. Use `to_vec()` for non-consuming access.
224    pub fn build(self) -> Vec<OsString> {
225        let mut result = Vec::new();
226
227        // Add all args
228        for arg in &self.args {
229            result.extend(arg.to_strings());
230        }
231
232        // Add positional arguments
233        for arg in &self.positional {
234            result.extend(arg.to_strings());
235        }
236
237        result
238    }
239
240    /// Validates that all required arguments are provided.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if any required argument is missing.
245    pub fn validate(&self) -> Result<(), String> {
246        for arg in &self.args {
247            // Flags don't require values - they're satisfied if present
248            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}