1use std::collections::HashMap;
7use std::env;
8
9pub struct App {
15 name: String,
16 version: String,
17 about: String,
18 commands: Vec<Command>,
19 global_args: Vec<Arg>,
20}
21
22impl App {
23 pub fn new(name: impl Into<String>) -> Self {
24 Self {
25 name: name.into(),
26 version: "0.1.0".to_string(),
27 about: String::new(),
28 commands: Vec::new(),
29 global_args: Vec::new(),
30 }
31 }
32
33 pub fn version(mut self, version: impl Into<String>) -> Self {
34 self.version = version.into();
35 self
36 }
37
38 pub fn about(mut self, about: impl Into<String>) -> Self {
39 self.about = about.into();
40 self
41 }
42
43 pub fn command(mut self, cmd: Command) -> Self {
44 self.commands.push(cmd);
45 self
46 }
47
48 pub fn arg(mut self, arg: Arg) -> Self {
49 self.global_args.push(arg);
50 self
51 }
52
53 pub fn parse(self) -> Matches {
54 let args: Vec<String> = env::args().skip(1).collect();
55 self.parse_args(&args)
56 }
57
58 fn parse_args(self, args: &[String]) -> Matches {
59 let mut matches = Matches {
60 command: None,
61 args: HashMap::new(),
62 values: Vec::new(),
63 };
64
65 if args.is_empty() {
66 return matches;
67 }
68
69 if args[0] == "--help" || args[0] == "-h" {
71 self.print_help();
72 std::process::exit(0);
73 }
74 if args[0] == "--version" || args[0] == "-V" {
75 println!("{} {}", self.name, self.version);
76 std::process::exit(0);
77 }
78
79 if let Some(cmd) = self.commands.iter().find(|c| c.name == args[0]) {
81 matches.command = Some(args[0].clone());
82 matches.parse_command_args(cmd, &args[1..]);
83 } else {
84 matches.parse_args_list(&self.global_args, args);
85 }
86
87 matches
88 }
89
90 fn print_help(&self) {
91 println!("{}", self.name);
92 if !self.about.is_empty() {
93 println!("{}\n", self.about);
94 }
95 println!("Usage: {} [OPTIONS] [COMMAND]\n", self.name.to_lowercase());
96
97 if !self.commands.is_empty() {
98 println!("Commands:");
99 for cmd in &self.commands {
100 println!(" {:<12} {}", cmd.name, cmd.about);
101 }
102 println!();
103 }
104
105 println!("Options:");
106 println!(" -h, --help Print help");
107 println!(" -V, --version Print version");
108
109 for arg in &self.global_args {
110 let short = arg.short.as_ref().map(|s| format!("-{}, ", s)).unwrap_or_default();
111 println!(" {}{:<12} {}", short, format!("--{}", arg.long), arg.help);
112 }
113 }
114}
115
116pub struct Command {
121 name: String,
122 about: String,
123 args: Vec<Arg>,
124}
125
126impl Command {
127 pub fn new(name: impl Into<String>) -> Self {
128 Self {
129 name: name.into(),
130 about: String::new(),
131 args: Vec::new(),
132 }
133 }
134
135 pub fn about(mut self, about: impl Into<String>) -> Self {
136 self.about = about.into();
137 self
138 }
139
140 pub fn arg(mut self, arg: Arg) -> Self {
141 self.args.push(arg);
142 self
143 }
144}
145
146pub struct Arg {
151 name: String,
152 long: String,
153 short: Option<String>,
154 help: String,
155 takes_value: bool,
156 required: bool,
157}
158
159impl Arg {
160 pub fn new(name: impl Into<String>) -> Self {
161 let name = name.into();
162 Self {
163 long: name.clone(),
164 name,
165 short: None,
166 help: String::new(),
167 takes_value: false,
168 required: false,
169 }
170 }
171
172 pub fn long(mut self, long: impl Into<String>) -> Self {
173 self.long = long.into();
174 self
175 }
176
177 pub fn short(mut self, short: char) -> Self {
178 self.short = Some(short.to_string());
179 self
180 }
181
182 pub fn help(mut self, help: impl Into<String>) -> Self {
183 self.help = help.into();
184 self
185 }
186
187 pub fn takes_value(mut self, takes: bool) -> Self {
188 self.takes_value = takes;
189 self
190 }
191
192 pub fn required(mut self, req: bool) -> Self {
193 self.required = req;
194 self
195 }
196}
197
198pub struct Matches {
203 command: Option<String>,
204 args: HashMap<String, Option<String>>,
205 values: Vec<String>,
206}
207
208impl Matches {
209 pub fn subcommand(&self) -> Option<&str> {
210 self.command.as_deref()
211 }
212
213 pub fn is_present(&self, name: &str) -> bool {
214 self.args.contains_key(name)
215 }
216
217 pub fn value_of(&self, name: &str) -> Option<&str> {
218 self.args.get(name)?.as_deref()
219 }
220
221 pub fn values(&self) -> &[String] {
222 &self.values
223 }
224
225 fn parse_command_args(&mut self, cmd: &Command, args: &[String]) {
226 self.parse_args_list(&cmd.args, args);
227 }
228
229 fn parse_args_list(&mut self, arg_defs: &[Arg], args: &[String]) {
230 let mut i = 0;
231 while i < args.len() {
232 let arg = &args[i];
233
234 if arg.starts_with("--") {
235 let key = &arg[2..];
236 if let Some(arg_def) = arg_defs.iter().find(|a| a.long == key) {
237 if arg_def.takes_value && i + 1 < args.len() {
238 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
239 i += 2;
240 } else {
241 self.args.insert(arg_def.name.clone(), None);
242 i += 1;
243 }
244 } else {
245 i += 1;
246 }
247 } else if arg.starts_with('-') && arg.len() == 2 {
248 let short = &arg[1..];
249 if let Some(arg_def) = arg_defs.iter().find(|a| a.short.as_deref() == Some(short)) {
250 if arg_def.takes_value && i + 1 < args.len() {
251 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
252 i += 2;
253 } else {
254 self.args.insert(arg_def.name.clone(), None);
255 i += 1;
256 }
257 } else {
258 i += 1;
259 }
260 } else {
261 self.values.push(arg.clone());
262 i += 1;
263 }
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_arg_creation() {
274 let arg = Arg::new("test")
275 .long("test")
276 .short('t')
277 .help("Test argument")
278 .takes_value(true);
279
280 assert_eq!(arg.name, "test");
281 assert_eq!(arg.long, "test");
282 assert_eq!(arg.short, Some("t".to_string()));
283 }
284
285 #[test]
286 fn test_command_creation() {
287 let cmd = Command::new("test")
288 .about("Test command")
289 .arg(Arg::new("arg1"));
290
291 assert_eq!(cmd.name, "test");
292 assert_eq!(cmd.args.len(), 1);
293 }
294}