1use std::io::Write;
2
3use super::parser::Parser;
4use crate::{application::AppContext, Command, Context, ContextInfo, Error, Result};
5
6pub struct App {
7 pub info: AppInfo,
8 pub root: Command,
9}
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct AppInfo {
13 pub name: String,
14 pub version: String,
15 pub title: String,
16 pub description: String,
17}
18
19impl App {
20 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
25 let name = name.into();
26 let root_name = name.clone();
27 Self {
28 info: AppInfo {
29 name,
30 version: version.into(),
31 title: String::new(),
32 description: String::new(),
33 },
34 root: Command::new(root_name),
35 }
36 }
37
38 pub fn command(mut self, cmd: Command) -> Self {
39 self.root.subcommands.push(cmd);
40 self
41 }
42
43 pub fn run(mut self, f: Box<crate::command::RunFn>) -> Self {
60 self.root.hooks.run_fn = Some(f);
61 self
62 }
63
64 pub fn pre_run(mut self, f: Box<crate::command::RunFn>) -> Self {
66 self.root.hooks.pre_run_fn = Some(f);
67 self
68 }
69
70 pub fn post_run(mut self, f: Box<crate::command::RunFn>) -> Self {
72 self.root.hooks.post_run_fn = Some(f);
73 self
74 }
75
76 pub fn arg(mut self, arg: crate::Arg) -> Self {
80 self.root.args.push(arg);
81 self
82 }
83
84 pub fn parse(&self, args: Vec<String>) -> Result<ContextInfo> {
85 let parsed = Parser::new(self, args).parse()?;
86
87 let cmd = self.find_command_by_path(parsed.commands());
89
90 Ok(ContextInfo {
91 app: self.info.clone(),
92 command: cmd
93 .map(|c| c.info.clone())
94 .unwrap_or_else(|| crate::CommandInfo {
95 name: self.info.name.clone(),
96 aliases: Vec::new(),
97 title: String::new(),
98 description: String::new(),
99 }),
100 args: parsed,
101 })
102 }
103
104 pub fn execute(&self) -> Result<()> {
117 let args: Vec<String> = std::env::args().collect();
118
119 let context_info = self.parse(args)?;
120
121 let mut ctx = AppContext::new(context_info);
123
124 self.execute_with_ctx(&mut ctx)
125 }
126
127 pub fn execute_with_ctx(&self, ctx: &mut dyn Context) -> Result<()> {
134 let is_version = ctx.info().args.is_version();
136 let is_help = ctx.info().args.is_help();
137 let commands = ctx.info().args.commands().to_vec();
138
139 let mut stdout = ctx.stdout();
140
141 if is_version {
143 self.write_version(&mut stdout)?;
144 return Ok(());
145 }
146
147 if is_help {
149 self.write_help_for_path(&mut stdout, &commands)?;
150 return Ok(());
151 }
152
153 if commands.len() == 1 {
155 if self.root.hooks.run_fn.is_some() {
156 drop(stdout);
158 return self.root.execute(ctx);
159 } else {
160 self.write_help_for_path(&mut stdout, &commands)?;
162 return Ok(());
163 }
164 }
165
166 drop(stdout);
168
169 let cmd = self.find_command_by_path(&commands).ok_or_else(|| {
171 let available = self.available_commands().join(", ");
172 Error::CommandNotFound(format!(
173 "'{}'. Available commands: {}",
174 commands.last().unwrap_or(&String::new()),
175 available
176 ))
177 })?;
178
179 cmd.execute(ctx)
180 }
181
182 pub fn find_command_by_path(&self, path: &[String]) -> Option<&Command> {
193 if path.len() < 2 {
195 return None;
196 }
197
198 if path[0] != self.info.name {
200 return None;
201 }
202
203 let mut current_cmd = self
205 .root
206 .subcommands
207 .iter()
208 .find(|cmd| cmd.info.name == path[1] || cmd.info.aliases.contains(&path[1]))?;
209
210 for name in path.iter().skip(2) {
212 current_cmd = current_cmd
213 .subcommands
214 .iter()
215 .find(|cmd| cmd.info.name == *name || cmd.info.aliases.contains(name))?;
216 }
217
218 Some(current_cmd)
219 }
220
221 pub fn available_commands(&self) -> Vec<&str> {
223 self.root
224 .subcommands
225 .iter()
226 .map(|c| c.info.name.as_str())
227 .collect()
228 }
229
230 pub fn check(&self) -> Result<()> {
237 let mut errors: Vec<String> = Vec::new();
238
239 for cmd in &self.root.subcommands {
240 check_command(cmd, &mut errors, vec![self.info.name.clone()]);
241 }
242
243 if errors.is_empty() {
244 Ok(())
245 } else {
246 Err(Error::custom(format!(
247 "Application configuration errors:\n{}",
248 errors.join("\n")
249 )))
250 }
251 }
252
253 pub fn print_version(&self) {
255 println!("{} {}", self.info.name, self.info.version);
256 }
257
258 pub fn write_version(&self, w: &mut dyn Write) -> Result<()> {
260 writeln!(w, "{} {}", self.info.name, self.info.version)?;
261 Ok(())
262 }
263
264 pub fn print_help(&self) {
266 self.print_help_for_path(std::slice::from_ref(&self.info.name));
267 }
268
269 pub fn print_help_for_path(&self, path: &[String]) {
271 let _ = self.write_help_for_path(&mut std::io::stdout(), path);
273 }
274
275 pub fn write_help_for_path(&self, w: &mut dyn Write, path: &[String]) -> Result<()> {
277 if path.is_empty() {
278 return Ok(());
279 }
280
281 if path.len() == 1 {
283 self.write_app_help(w)?;
284 return Ok(());
285 }
286
287 if let Some(cmd) = self.find_command_by_path(path) {
289 self.write_command_help(w, cmd, path)?;
290 } else {
291 self.write_app_help(w)?;
293 }
294 Ok(())
295 }
296
297 fn write_app_help(&self, w: &mut dyn Write) -> Result<()> {
299 if !self.info.title.is_empty() {
301 writeln!(w, "{}", self.info.title)?;
302 } else {
303 writeln!(w, "{} {}", self.info.name, self.info.version)?;
304 }
305
306 if !self.info.description.is_empty() {
307 writeln!(w)?;
308 writeln!(w, "{}", self.info.description)?;
309 }
310
311 writeln!(w)?;
313 writeln!(w, "Usage: {} <command> [options]", self.info.name)?;
314
315 if !self.root.subcommands.is_empty() {
317 writeln!(w)?;
318 writeln!(w, "Commands:")?;
319 for cmd in &self.root.subcommands {
320 let desc = if !cmd.info.title.is_empty() {
321 &cmd.info.title
322 } else {
323 &cmd.info.description
324 };
325 writeln!(w, " {:20} {}", cmd.info.name, desc)?;
326 }
327 }
328
329 writeln!(w)?;
331 writeln!(w, "Options:")?;
332 writeln!(w, " {:20} Show help information", "-h, --help")?;
333 writeln!(w, " {:20} Show version information", "-V, --version")?;
334 Ok(())
335 }
336
337 fn write_command_help(&self, w: &mut dyn Write, cmd: &Command, path: &[String]) -> Result<()> {
339 if !cmd.info.title.is_empty() {
341 writeln!(w, "{}", cmd.info.title)?;
342 } else {
343 writeln!(w, "{}", path.join(" "))?;
344 }
345
346 if !cmd.info.description.is_empty() {
347 writeln!(w)?;
348 writeln!(w, "{}", cmd.info.description)?;
349 }
350
351 writeln!(w)?;
353 let usage_path = path.join(" ");
354 if cmd.subcommands.is_empty() {
355 writeln!(w, "Usage: {} [options] [arguments]", usage_path)?;
356 } else {
357 writeln!(w, "Usage: {} <command> [options]", usage_path)?;
358 }
359
360 if !cmd.subcommands.is_empty() {
362 writeln!(w)?;
363 writeln!(w, "Commands:")?;
364 for subcmd in &cmd.subcommands {
365 let desc = if !subcmd.info.title.is_empty() {
366 &subcmd.info.title
367 } else {
368 &subcmd.info.description
369 };
370 writeln!(w, " {:20} {}", subcmd.info.name, desc)?;
371 }
372 }
373
374 let positional: Vec<_> = cmd
376 .args
377 .iter()
378 .filter(|a| matches!(a.info.kind, crate::ArgKind::Positional))
379 .collect();
380 let options: Vec<_> = cmd
381 .args
382 .iter()
383 .filter(|a| !matches!(a.info.kind, crate::ArgKind::Positional))
384 .collect();
385
386 if !positional.is_empty() {
387 writeln!(w)?;
388 writeln!(w, "Arguments:")?;
389 for arg in positional {
390 let required_marker = if arg.info.schema.required {
391 " (required)"
392 } else {
393 ""
394 };
395 writeln!(
396 w,
397 " {:20} {}{}",
398 arg.info.name, arg.info.description, required_marker
399 )?;
400 }
401 }
402
403 if !options.is_empty() {
404 writeln!(w)?;
405 writeln!(w, "Options:")?;
406 for arg in options {
407 let (short, long) = match &arg.info.kind {
408 crate::ArgKind::Flag { short, long, .. }
409 | crate::ArgKind::Option { short, long, .. } => {
410 let short_str = short.map(|c| format!("-{}, ", c)).unwrap_or_default();
411 let long_str = format!("--{}", long);
412 (short_str, long_str)
413 }
414 crate::ArgKind::Positional => (String::new(), String::new()),
415 };
416 let flag_str = format!("{}{}", short, long);
417 writeln!(w, " {:20} {}", flag_str, arg.info.description)?;
418 }
419 }
420
421 writeln!(w)?;
423 writeln!(w, " {:20} Show help information", "-h, --help")?;
424 Ok(())
425 }
426}
427
428fn check_command(cmd: &Command, errors: &mut Vec<String>, mut path: Vec<String>) {
430 path.push(cmd.info.name.clone());
431 let path_str = path.join(" ");
432
433 if cmd.is_leaf() && cmd.hooks.run_fn.is_none() {
434 errors.push(format!("Leaf command '{}' has no run function", path_str));
435 }
436
437 for subcmd in &cmd.subcommands {
438 check_command(subcmd, errors, path.clone());
439 }
440}