flag_rs/command.rs
1//! Command execution and management
2//!
3//! This module provides the core [`Command`] struct and [`CommandBuilder`] for creating
4//! CLI applications with subcommands, flags, and dynamic completions.
5
6use crate::completion::{CompletionFunc, CompletionResult};
7use crate::completion_format::CompletionFormat;
8use crate::context::Context;
9use crate::error::{Error, Result};
10use crate::flag::{Flag, FlagConstraint, FlagType, FlagValue};
11use crate::suggestion::{DEFAULT_SUGGESTION_DISTANCE, find_suggestions};
12use crate::terminal::{format_help_entry, get_terminal_width, wrap_text_to_terminal};
13use crate::validator::ArgValidator;
14use std::collections::{HashMap, HashSet};
15
16/// Type alias for the function that executes when a command runs
17pub type RunFunc = Box<dyn Fn(&mut Context) -> Result<()> + Send + Sync>;
18
19/// Type alias for lifecycle hook functions
20pub type HookFunc = Box<dyn Fn(&mut Context) -> Result<()> + Send + Sync>;
21
22/// Represents a command in the CLI application
23///
24/// Commands can have:
25/// - Subcommands for nested command structures
26/// - Flags that modify behavior
27/// - A run function that executes the command logic
28/// - Dynamic completion functions for arguments and flags
29/// - Help text and aliases
30///
31/// # Examples
32///
33/// ```rust
34/// use flag_rs::{Command, CommandBuilder, Context};
35///
36/// // Using the builder pattern (recommended)
37/// let cmd = CommandBuilder::new("serve")
38/// .short("Start the web server")
39/// .run(|ctx| {
40/// println!("Server starting...");
41/// Ok(())
42/// })
43/// .build();
44///
45/// // Direct construction
46/// let mut cmd = Command::new("serve");
47/// ```
48pub struct Command {
49 name: String,
50 aliases: Vec<String>,
51 short: String,
52 long: String,
53 examples: Vec<String>,
54 group_id: Option<String>,
55 subcommands: HashMap<String, Self>,
56 flags: HashMap<String, Flag>,
57 run: Option<RunFunc>,
58 parent: Option<*mut Self>,
59 arg_completions: Option<CompletionFunc>,
60 flag_completions: HashMap<String, CompletionFunc>,
61 arg_validator: Option<ArgValidator>,
62 suggestions_enabled: bool,
63 suggestion_distance: usize,
64 // Lifecycle hooks
65 persistent_pre_run: Option<HookFunc>,
66 pre_run: Option<HookFunc>,
67 post_run: Option<HookFunc>,
68 persistent_post_run: Option<HookFunc>,
69}
70
71unsafe impl Send for Command {}
72unsafe impl Sync for Command {}
73
74impl Command {
75 /// Creates a new command with the given name
76 ///
77 /// # Examples
78 ///
79 /// ```rust
80 /// use flag_rs::Command;
81 ///
82 /// let cmd = Command::new("myapp");
83 /// ```
84 pub fn new(name: impl Into<String>) -> Self {
85 Self {
86 name: name.into(),
87 aliases: Vec::new(),
88 short: String::new(),
89 long: String::new(),
90 examples: Vec::new(),
91 group_id: None,
92 subcommands: HashMap::new(),
93 flags: HashMap::new(),
94 run: None,
95 parent: None,
96 arg_completions: None,
97 flag_completions: HashMap::new(),
98 arg_validator: None,
99 suggestions_enabled: true,
100 suggestion_distance: DEFAULT_SUGGESTION_DISTANCE,
101 persistent_pre_run: None,
102 pre_run: None,
103 post_run: None,
104 persistent_post_run: None,
105 }
106 }
107
108 pub fn name(&self) -> &str {
109 &self.name
110 }
111
112 /// Returns the short description
113 pub fn short(&self) -> &str {
114 &self.short
115 }
116
117 /// Returns the long description
118 pub fn long(&self) -> &str {
119 &self.long
120 }
121
122 pub fn subcommands(&self) -> &HashMap<String, Self> {
123 &self.subcommands
124 }
125
126 pub fn flags(&self) -> &HashMap<String, Flag> {
127 &self.flags
128 }
129
130 /// Finds a subcommand by name or alias
131 ///
132 /// # Examples
133 ///
134 /// ```rust
135 /// # use flag_rs::{Command, CommandBuilder};
136 /// let mut root = Command::new("app");
137 /// let sub = CommandBuilder::new("server")
138 /// .aliases(vec!["serve", "s"])
139 /// .build();
140 /// root.add_command(sub);
141 ///
142 /// assert!(root.find_subcommand("server").is_some());
143 /// assert!(root.find_subcommand("serve").is_some());
144 /// assert!(root.find_subcommand("s").is_some());
145 /// ```
146 pub fn find_subcommand(&self, name: &str) -> Option<&Self> {
147 self.subcommands.get(name).or_else(|| {
148 self.subcommands
149 .values()
150 .find(|cmd| cmd.aliases.contains(&name.to_string()))
151 })
152 }
153
154 /// Finds a mutable reference to a subcommand by name or alias
155 pub fn find_subcommand_mut(&mut self, name: &str) -> Option<&mut Self> {
156 let name_string = name.to_string();
157 if self.subcommands.contains_key(name) {
158 self.subcommands.get_mut(name)
159 } else {
160 self.subcommands
161 .values_mut()
162 .find(|cmd| cmd.aliases.contains(&name_string))
163 }
164 }
165
166 /// Adds a subcommand to this command
167 ///
168 /// # Examples
169 ///
170 /// ```rust
171 /// use flag_rs::{Command, CommandBuilder};
172 ///
173 /// let mut root = Command::new("myapp");
174 /// let serve = CommandBuilder::new("serve")
175 /// .short("Start the server")
176 /// .build();
177 ///
178 /// root.add_command(serve);
179 /// ```
180 pub fn add_command(&mut self, mut cmd: Self) {
181 cmd.parent = Some(std::ptr::from_mut::<Self>(self));
182 self.subcommands.insert(cmd.name.clone(), cmd);
183 }
184
185 /// Executes the command with the given arguments
186 ///
187 /// This is the main entry point for running your CLI application.
188 /// It handles:
189 /// - Shell completion requests
190 /// - Flag parsing
191 /// - Subcommand routing
192 /// - Execution of the appropriate run function
193 ///
194 /// # Examples
195 ///
196 /// ```rust
197 /// use flag_rs::CommandBuilder;
198 ///
199 /// let app = CommandBuilder::new("myapp")
200 /// .run(|ctx| {
201 /// println!("Hello from myapp!");
202 /// Ok(())
203 /// })
204 /// .build();
205 ///
206 /// // In main():
207 /// // let args: Vec<String> = std::env::args().skip(1).collect();
208 /// // if let Err(e) = app.execute(args) {
209 /// // eprintln!("Error: {}", e);
210 /// // std::process::exit(1);
211 /// // }
212 /// ```
213 pub fn execute(&self, args: Vec<String>) -> Result<()> {
214 // Check if we're in completion mode
215 if let Ok(_shell) = std::env::var(format!("{}_COMPLETE", self.name.to_uppercase())) {
216 // Disable colors during completion to avoid terminal rendering issues
217 unsafe { std::env::set_var("NO_COLOR", "1") };
218
219 match self.handle_completion_request(&args) {
220 Ok(suggestions) => {
221 for suggestion in suggestions {
222 println!("{suggestion}");
223 }
224 return Ok(());
225 }
226 Err(e) => {
227 // Don't write to stderr during completion - it can mess up the terminal
228 return Err(e);
229 }
230 }
231 }
232
233 let mut ctx = Context::new(args);
234 self.execute_with_context(&mut ctx)
235 }
236
237 /// Executes the command with an existing context
238 ///
239 /// This method is useful when you need to provide pre-configured context
240 /// or when implementing custom command routing.
241 pub fn execute_with_context(&self, ctx: &mut Context) -> Result<()> {
242 // Call the internal method with an empty hook chain
243 self.execute_with_context_and_hooks(ctx, &mut Vec::new())
244 }
245
246 /// Internal method that executes the command while collecting parent hooks
247 fn execute_with_context_and_hooks<'a>(
248 &'a self,
249 ctx: &mut Context,
250 parent_hooks: &mut Vec<(&'a Option<HookFunc>, &'a Option<HookFunc>)>,
251 ) -> Result<()> {
252 let args = ctx.args().to_vec();
253
254 // Parse flags first, before checking for empty args
255 let (flags, remaining_args) = self.parse_flags(&args)?;
256
257 *ctx.args_mut() = remaining_args;
258
259 if let Some(subcommand_name) = ctx.args().first() {
260 if let Some(subcommand) = self.find_subcommand(subcommand_name) {
261 if flags.contains_key("help") {
262 subcommand.print_help();
263 return Ok(());
264 }
265
266 self.validate_flags(&flags)?;
267
268 for (name, value) in flags {
269 ctx.set_flag(name, value);
270 }
271
272 // Add our persistent hooks to the chain for subcommands
273 parent_hooks.push((&self.persistent_pre_run, &self.persistent_post_run));
274
275 ctx.args_mut().remove(0);
276 return subcommand.execute_with_context_and_hooks(ctx, parent_hooks);
277 }
278 }
279
280 if flags.contains_key("help") {
281 self.print_help();
282 return Ok(());
283 }
284
285 self.validate_flags(&flags)?;
286
287 for (name, value) in flags {
288 ctx.set_flag(name, value);
289 }
290
291 if let Some(ref run) = self.run {
292 // Validate arguments before running
293 if let Some(ref validator) = self.arg_validator {
294 validator.validate(ctx.args())?;
295 }
296 self.execute_with_parent_hooks(ctx, run, parent_hooks)
297 } else if ctx.args().is_empty() {
298 // No args and no run function - show help
299 Err(Error::SubcommandRequired(self.name.clone()))
300 } else {
301 let unknown_command = ctx.args().first().unwrap_or(&String::new()).clone();
302 let suggestions = if self.suggestions_enabled {
303 self.find_command_suggestions(&unknown_command)
304 } else {
305 Vec::new()
306 };
307
308 Err(Error::CommandNotFound {
309 command: unknown_command,
310 suggestions,
311 })
312 }
313 }
314
315 fn parse_flags(&self, args: &[String]) -> Result<(HashMap<String, String>, Vec<String>)> {
316 let mut flags = HashMap::new();
317 let mut remaining = Vec::new();
318 let mut i = 0;
319
320 while i < args.len() {
321 let arg = &args[i];
322
323 if arg == "--" {
324 remaining.extend_from_slice(&args[i + 1..]);
325 break;
326 } else if arg.starts_with("--") {
327 let flag_name = arg.trim_start_matches("--");
328
329 if flag_name == "help" {
330 flags.insert("help".to_string(), "true".to_string());
331 } else if let Some((name, value)) = flag_name.split_once('=') {
332 // Validate the flag value
333 if let Some(flag) = self.find_flag(name) {
334 flag.parse_value(value)?;
335 }
336 flags.insert(name.to_string(), value.to_string());
337 } else if let Some(flag) = self.find_flag(flag_name) {
338 if i + 1 < args.len() && !args[i + 1].starts_with('-') {
339 let value = &args[i + 1];
340 // Validate the flag value
341 flag.parse_value(value)?;
342 flags.insert(flag_name.to_string(), value.clone());
343 i += 1;
344 } else {
345 flags.insert(flag_name.to_string(), "true".to_string());
346 }
347 } else {
348 // Unknown flag - might belong to a subcommand
349 remaining.push(arg.clone());
350 }
351 } else if arg.starts_with('-') && arg.len() > 1 {
352 let short_flags = arg.trim_start_matches('-');
353 let chars: Vec<char> = short_flags.chars().collect();
354
355 for (idx, ch) in chars.iter().enumerate() {
356 if *ch == 'h' {
357 flags.insert("help".to_string(), "true".to_string());
358 } else if let Some(flag) = self.find_flag_by_short(*ch) {
359 // If this is the last char and the flag takes a value
360 if idx == chars.len() - 1
361 && i + 1 < args.len()
362 && !args[i + 1].starts_with('-')
363 {
364 let value = &args[i + 1];
365 // Validate the flag value
366 flag.parse_value(value)?;
367 flags.insert(flag.name.clone(), value.clone());
368 i += 1;
369 } else {
370 flags.insert(flag.name.clone(), "true".to_string());
371 }
372 } else {
373 // Unknown short flag - might belong to a subcommand
374 remaining.push(format!("-{}", chars[idx..].iter().collect::<String>()));
375 break;
376 }
377 }
378 } else {
379 remaining.push(arg.clone());
380 }
381
382 i += 1;
383 }
384
385 Ok((flags, remaining))
386 }
387
388 /// Sets the argument completion function for this command
389 ///
390 /// The completion function is called when the user presses TAB to complete
391 /// command arguments. It receives the current context and the prefix to complete.
392 ///
393 /// # Examples
394 ///
395 /// ```rust
396 /// use flag_rs::{Command, CompletionResult};
397 ///
398 /// let mut cmd = Command::new("get");
399 /// cmd.set_arg_completion(|ctx, prefix| {
400 /// let items = vec!["users", "posts", "comments"];
401 /// Ok(CompletionResult::new().extend(
402 /// items.into_iter()
403 /// .filter(|i| i.starts_with(prefix))
404 /// .map(String::from)
405 /// ))
406 /// });
407 /// ```
408 pub fn set_arg_completion<F>(&mut self, f: F)
409 where
410 F: Fn(&Context, &str) -> Result<CompletionResult> + Send + Sync + 'static,
411 {
412 self.arg_completions = Some(Box::new(f));
413 }
414
415 /// Sets the completion function for a specific flag
416 ///
417 /// This allows dynamic completion of flag values based on runtime state.
418 ///
419 /// # Examples
420 ///
421 /// ```rust
422 /// use flag_rs::{Command, CompletionResult};
423 ///
424 /// let mut cmd = Command::new("deploy");
425 /// cmd.set_flag_completion("environment", |ctx, prefix| {
426 /// let envs = vec!["dev", "staging", "production"];
427 /// Ok(CompletionResult::new().extend(
428 /// envs.into_iter()
429 /// .filter(|e| e.starts_with(prefix))
430 /// .map(String::from)
431 /// ))
432 /// });
433 /// ```
434 pub fn set_flag_completion<F>(&mut self, flag_name: impl Into<String>, f: F)
435 where
436 F: Fn(&Context, &str) -> Result<CompletionResult> + Send + Sync + 'static,
437 {
438 self.flag_completions.insert(flag_name.into(), Box::new(f));
439 }
440
441 /// Gets completion suggestions for the current context
442 ///
443 /// This method is primarily used internally by the shell completion system.
444 pub fn get_completions(
445 &self,
446 ctx: &Context,
447 to_complete: &str,
448 completing_flag: Option<&str>,
449 ) -> Result<CompletionResult> {
450 if let Some(flag_name) = completing_flag {
451 if let Some(completion_func) = self.flag_completions.get(flag_name) {
452 return completion_func(ctx, to_complete);
453 }
454 } else if let Some(ref completion_func) = self.arg_completions {
455 return completion_func(ctx, to_complete);
456 }
457
458 Ok(CompletionResult::new())
459 }
460
461 fn find_flag(&self, name: &str) -> Option<&Flag> {
462 self.flags.get(name).or_else(|| {
463 self.parent
464 .and_then(|parent| unsafe { (*parent).find_flag(name) })
465 })
466 }
467
468 fn find_flag_by_short(&self, short: char) -> Option<&Flag> {
469 self.flags
470 .values()
471 .find(|f| f.short == Some(short))
472 .or_else(|| {
473 self.parent
474 .and_then(|parent| unsafe { (*parent).find_flag_by_short(short) })
475 })
476 }
477
478 /// Validates all flags including required flags and constraints
479 fn validate_flags(&self, provided_flags: &HashMap<String, String>) -> Result<()> {
480 let provided_flag_names: HashSet<String> = provided_flags.keys().cloned().collect();
481
482 // Check required flags
483 for (flag_name, flag) in &self.flags {
484 if flag.required && !provided_flag_names.contains(flag_name) {
485 return Err(Error::flag_parsing_with_suggestions(
486 format!("Required flag '--{flag_name}' not provided"),
487 flag_name.clone(),
488 vec![format!("add --{flag_name} <value>")],
489 ));
490 }
491 }
492
493 // TODO: Fix unsafe parent flag validation
494 // Check parent flags if any
495 // if let Some(parent) = self.parent {
496 // unsafe {
497 // for (flag_name, flag) in &(*parent).flags {
498 // if flag.required && !provided_flag_names.contains(flag_name) {
499 // return Err(Error::flag_parsing_with_suggestions(
500 // format!("Required flag '--{flag_name}' not provided"),
501 // flag_name.to_string(),
502 // vec![format!("add --{flag_name} <value>")],
503 // ));
504 // }
505 // }
506 // }
507 // }
508
509 // Validate constraints for all flags
510 for (flag_name, flag) in &self.flags {
511 flag.validate_constraints(flag_name, &provided_flag_names)?;
512 }
513
514 // TODO: Fix unsafe parent flag constraint validation
515 // The current approach with raw pointers can lead to undefined behavior
516 // when the parent Command is moved or when accessing heap-allocated data
517 // through the pointer (like Vec<FlagConstraint>).
518 //
519 // Validate parent flag constraints
520 // if let Some(parent) = self.parent {
521 // unsafe {
522 // for (flag_name, flag) in &(*parent).flags {
523 // flag.validate_constraints(flag_name, &provided_flag_names)?;
524 // }
525 // }
526 // }
527
528 Ok(())
529 }
530
531 /// Executes the command with lifecycle hooks including parent hooks
532 fn execute_with_parent_hooks(
533 &self,
534 ctx: &mut Context,
535 run: &RunFunc,
536 parent_hooks: &[(&Option<HookFunc>, &Option<HookFunc>)],
537 ) -> Result<()> {
538 // Execute parent persistent pre-run hooks (from root to immediate parent)
539 for (pre_hook, _) in parent_hooks {
540 if let Some(hook) = pre_hook {
541 hook(ctx)?;
542 }
543 }
544
545 // Execute own persistent pre-run hook if present
546 if let Some(ref hook) = self.persistent_pre_run {
547 hook(ctx)?;
548 }
549
550 // Execute pre-run hook if present
551 if let Some(ref pre_run) = self.pre_run {
552 pre_run(ctx)?;
553 }
554
555 // Execute the main run function
556 let result = run(ctx);
557
558 // Execute post-run hook if present, but preserve the original error
559 let post_run_result = if let Some(ref post_run) = self.post_run {
560 match result {
561 Ok(()) => post_run(ctx),
562 Err(e) => {
563 // Try to run post-run even if main failed, but return original error
564 let _ = post_run(ctx);
565 Err(e)
566 }
567 }
568 } else {
569 result
570 };
571
572 // Execute own persistent post-run hook if present
573 let persistent_result = if let Some(ref hook) = self.persistent_post_run {
574 let result = hook(ctx);
575 match post_run_result {
576 Ok(()) => result,
577 Err(e) => {
578 // Try to run persistent post-run even if post-run failed
579 let _ = result;
580 Err(e)
581 }
582 }
583 } else {
584 post_run_result
585 };
586
587 // Execute parent persistent post-run hooks (from immediate parent to root)
588 let mut final_result = persistent_result;
589 for (_, post_hook) in parent_hooks.iter().rev() {
590 if let Some(hook) = post_hook {
591 match final_result {
592 Ok(()) => final_result = hook(ctx),
593 Err(e) => {
594 // Try to run parent post-run even if child failed
595 let _ = hook(ctx);
596 final_result = Err(e);
597 }
598 }
599 }
600 }
601
602 final_result
603 }
604
605 /// Prints the help message for this command
606 ///
607 /// The help message includes:
608 /// - Command description
609 /// - Usage information
610 /// - Available subcommands
611 /// - Local and global flags
612 ///
613 /// Help text is automatically colored when outputting to a TTY.
614 #[allow(clippy::cognitive_complexity)]
615 pub fn print_help(&self) {
616 use crate::color;
617
618 // Print description with text wrapping
619 if !self.long.is_empty() {
620 println!("{}", wrap_text_to_terminal(&self.long, None));
621 println!();
622 } else if !self.short.is_empty() {
623 println!("{}", wrap_text_to_terminal(&self.short, None));
624 println!();
625 }
626
627 // Print usage line
628 print!("{}:\n {}", color::bold("Usage"), self.name);
629 if !self.flags.is_empty() {
630 print!(" {}", color::yellow("[flags]"));
631 }
632 if !self.subcommands.is_empty() {
633 print!(" {}", color::yellow("[command]"));
634 }
635
636 // Show if command requires args
637 if let Some(validator) = &self.arg_validator {
638 match validator {
639 ArgValidator::MinimumArgs(n) if n > &0 => {
640 print!(" {}", color::yellow("<args>"));
641 }
642 ArgValidator::ExactArgs(n) if n > &0 => {
643 let arg_str = if n == &1 { "<arg>" } else { "<args>" };
644 print!(" {}", color::yellow(arg_str));
645 }
646 ArgValidator::RangeArgs(min, _) if min > &0 => {
647 print!(" {}", color::yellow("<args>"));
648 }
649 _ => {}
650 }
651 }
652 println!("\n");
653
654 // Print available commands
655 if !self.subcommands.is_empty() {
656 let mut commands: Vec<_> = self.subcommands.values().collect();
657 commands.sort_by_key(|cmd| &cmd.name);
658
659 // Group commands by their group_id
660 let mut grouped: std::collections::BTreeMap<Option<String>, Vec<&Self>> =
661 std::collections::BTreeMap::new();
662 for cmd in commands {
663 grouped.entry(cmd.group_id.clone()).or_default().push(cmd);
664 }
665
666 let terminal_width = get_terminal_width();
667 let left_column_width = 24;
668
669 // Print commands without groups first
670 if let Some(ungrouped) = grouped.get(&None) {
671 println!("{}:", color::bold("Available Commands"));
672 for &cmd in ungrouped {
673 Self::print_command_entry(cmd, left_column_width, terminal_width);
674 }
675 println!();
676 }
677
678 // Print grouped commands
679 for (group_id, cmds) in grouped {
680 if let Some(group) = group_id {
681 println!("{}:", color::bold(&group));
682 for cmd in cmds {
683 Self::print_command_entry(cmd, left_column_width, terminal_width);
684 }
685 println!();
686 }
687 }
688 }
689
690 // Print flags
691 if !self.flags.is_empty() || self.parent.is_some() {
692 // Separate required and optional flags
693 let mut required_flags: Vec<_> = self.flags.values().filter(|f| f.required).collect();
694 let mut optional_flags: Vec<_> = self.flags.values().filter(|f| !f.required).collect();
695
696 required_flags.sort_by_key(|f| &f.name);
697 optional_flags.sort_by_key(|f| &f.name);
698
699 // Print required flags first
700 if !required_flags.is_empty() {
701 println!("{} {}:", color::bold("Required Flags"), color::red("*"));
702 for flag in required_flags {
703 Self::print_flag(flag);
704 }
705 if !optional_flags.is_empty() {
706 println!();
707 }
708 }
709
710 // Print optional flags
711 if !optional_flags.is_empty() {
712 println!("{}:", color::bold("Flags"));
713 for flag in optional_flags {
714 Self::print_flag(flag);
715 }
716 }
717 }
718
719 // Print global flags from parent
720 if let Some(parent) = self.parent {
721 unsafe {
722 let parent_flags = &(*parent).flags;
723 if !parent_flags.is_empty() {
724 println!("\n{}:", color::bold("Global Flags"));
725 let mut global_flags: Vec<_> = parent_flags.values().collect();
726 global_flags.sort_by_key(|f| &f.name);
727
728 for flag in global_flags {
729 Self::print_flag(flag);
730 }
731 }
732 }
733 }
734
735 // Print examples if available
736 if !self.examples.is_empty() {
737 println!("{}:", color::bold("Examples"));
738 for example in &self.examples {
739 println!(" {}", color::dim(example));
740 }
741 println!();
742 }
743
744 // Print help about help
745 println!(
746 "Use \"{} {} --help\" for more information about a command.",
747 self.name,
748 color::yellow("[command]")
749 );
750 }
751
752 fn print_command_entry(cmd: &Self, left_column_width: usize, terminal_width: usize) {
753 use crate::color;
754
755 let mut name_with_aliases = color::green(&cmd.name);
756 if !cmd.aliases.is_empty() {
757 let aliases = cmd.aliases.join(", ");
758 name_with_aliases = format!(
759 "{} {}",
760 name_with_aliases,
761 color::dim(&format!("({aliases})"))
762 );
763 }
764
765 let formatted = format_help_entry(
766 &format!(" {name_with_aliases}"),
767 &cmd.short,
768 left_column_width + 2,
769 terminal_width,
770 );
771 println!("{formatted}");
772 }
773
774 fn print_flag(flag: &Flag) {
775 use crate::color;
776 use std::fmt::Write;
777
778 let short = flag
779 .short
780 .map_or_else(|| " ".to_string(), |s| format!("-{s}, "));
781
782 // Build constraint indicators
783 let mut constraint_info = String::new();
784 for constraint in &flag.constraints {
785 match constraint {
786 FlagConstraint::RequiredIf(other) => {
787 let _ = write!(
788 &mut constraint_info,
789 " {}",
790 color::yellow(&format!("[required if --{other}]"))
791 );
792 }
793 FlagConstraint::ConflictsWith(others) => {
794 let conflicts = others.join(", --");
795 let _ = write!(
796 &mut constraint_info,
797 " {}",
798 color::yellow(&format!("[conflicts with --{conflicts}]"))
799 );
800 }
801 FlagConstraint::Requires(others) => {
802 let requires = others.join(", --");
803 let _ = write!(
804 &mut constraint_info,
805 " {}",
806 color::yellow(&format!("[requires --{requires}]"))
807 );
808 }
809 }
810 }
811
812 // Handle special formatting for Choice and Range types
813 match &flag.value_type {
814 FlagType::Choice(choices) => {
815 let choices_str = choices.join("|");
816 let default = flag
817 .default
818 .as_ref()
819 .map(|d| match d {
820 FlagValue::String(s) => format!(" (default \"{s}\")"),
821 _ => String::new(),
822 })
823 .unwrap_or_default();
824 let flag_name_formatted = format!("{} {{{}}}", flag.name, choices_str);
825 Self::print_flag_line(
826 &flag_name_formatted,
827 &default,
828 &short,
829 &flag.usage,
830 &constraint_info,
831 );
832 return;
833 }
834 FlagType::Range(min, max) => {
835 let default = flag
836 .default
837 .as_ref()
838 .map(|d| match d {
839 FlagValue::Int(i) => format!(" (default {i})"),
840 _ => String::new(),
841 })
842 .unwrap_or_default();
843 let flag_name_formatted = format!("{} int[{}-{}]", flag.name, min, max);
844 Self::print_flag_line(
845 &flag_name_formatted,
846 &default,
847 &short,
848 &flag.usage,
849 &constraint_info,
850 );
851 return;
852 }
853 _ => {}
854 }
855
856 let flag_type = match &flag.value_type {
857 FlagType::String => " string",
858 FlagType::Int => " int",
859 FlagType::Float => " float",
860 FlagType::Bool => "",
861 FlagType::StringSlice | FlagType::StringArray => " strings",
862 FlagType::File => " file",
863 FlagType::Directory => " dir",
864 FlagType::Choice(_) | FlagType::Range(_, _) => unreachable!(),
865 };
866
867 let default = flag
868 .default
869 .as_ref()
870 .map(|d| match d {
871 FlagValue::String(s) => format!(" (default \"{s}\")"),
872 FlagValue::Bool(b) => format!(" (default {b})"),
873 FlagValue::Int(i) => format!(" (default {i})"),
874 FlagValue::Float(f) => format!(" (default {f})"),
875 FlagValue::StringSlice(v) => format!(" (default {v:?})"),
876 })
877 .unwrap_or_default();
878
879 let flag_name_formatted = format!("{}{flag_type}", flag.name);
880 Self::print_flag_line(
881 &flag_name_formatted,
882 &default,
883 &short,
884 &flag.usage,
885 &constraint_info,
886 );
887 }
888
889 fn print_flag_line(
890 flag_name_formatted: &str,
891 default: &str,
892 short: &str,
893 usage: &str,
894 constraint_info: &str,
895 ) {
896 use crate::color;
897
898 let left_part = format!(
899 " {}--{}",
900 color::cyan(short),
901 color::cyan(flag_name_formatted)
902 );
903 let description = format!("{}{}{}", usage, color::dim(default), constraint_info);
904 let terminal_width = get_terminal_width();
905 let left_column_width = 30;
906 let formatted =
907 format_help_entry(&left_part, &description, left_column_width, terminal_width);
908 println!("{formatted}");
909 }
910
911 /// Finds command suggestions based on similarity
912 fn find_command_suggestions(&self, input: &str) -> Vec<String> {
913 let candidates: Vec<String> = self.subcommands.keys().cloned().collect();
914 find_suggestions(input, &candidates, self.suggestion_distance)
915 }
916
917 /// Collects all available flags with their descriptions for completion.
918 /// Walks up the parent chain so global flags surface alongside local ones.
919 fn collect_all_flags_with_descriptions(&self, result: &mut CompletionResult, prefix: &str) {
920 for (flag_name, flag) in &self.flags {
921 if flag_name.starts_with(prefix) {
922 let formatted_flag = format!("--{flag_name}");
923 result.values.push(formatted_flag);
924 result.descriptions.push(flag.usage.clone());
925 }
926 }
927
928 if let Some(parent) = self.parent {
929 unsafe {
930 (*parent).collect_all_flags_with_descriptions(result, prefix);
931 }
932 }
933 }
934
935 /// Handles shell completion requests
936 ///
937 /// This method is called when the shell requests completions via the
938 /// environment variable (e.g., `MYAPP_COMPLETE=bash`).
939 pub fn handle_completion_request(&self, args: &[String]) -> Result<Vec<String>> {
940 // Detect shell type from environment variable
941 let shell_type = self.detect_completion_shell();
942
943 // args format: ["__complete", ...previous_args, current_word]
944 if args.is_empty() || args[0] != "__complete" {
945 return Err(Error::Completion("Invalid completion request".to_string()));
946 }
947
948 let args = &args[1..];
949 if args.is_empty() {
950 // Complete root level
951 return Ok(self.get_completion_suggestions("", None, shell_type.as_deref()));
952 }
953
954 let current_word = args.last().unwrap_or(&String::new()).clone();
955 let previous_args = &args[..args.len().saturating_sub(1)];
956
957 // Parse through the command hierarchy
958 let mut current_cmd = self;
959 let mut ctx = Context::new(vec![]);
960 let mut i = 0;
961
962 while i < previous_args.len() {
963 let arg = &previous_args[i];
964
965 if arg.starts_with("--") {
966 // Long flag
967 let flag_name = arg.trim_start_matches("--");
968 if let Some((name, _)) = flag_name.split_once('=') {
969 // Flag with value
970 ctx.set_flag(name.to_string(), String::new());
971 } else if let Some(_flag) = current_cmd.find_flag(flag_name) {
972 // Flag that might need a value
973 if i + 1 < previous_args.len() && !previous_args[i + 1].starts_with('-') {
974 ctx.set_flag(flag_name.to_string(), previous_args[i + 1].clone());
975 i += 1;
976 }
977 }
978 } else if arg.starts_with('-') && arg.len() > 1 {
979 // Short flags
980 for ch in arg.chars().skip(1) {
981 if let Some(flag) = current_cmd.find_flag_by_short(ch) {
982 ctx.set_flag(flag.name.clone(), String::new());
983 }
984 }
985 } else {
986 // Potential subcommand
987 if let Some(subcmd) = current_cmd.find_subcommand(arg) {
988 current_cmd = subcmd;
989 } else {
990 ctx.args_mut().push(arg.clone());
991 }
992 }
993 i += 1;
994 }
995
996 // Now determine what to complete
997 if current_word.starts_with("--") {
998 // Complete long flags only (when user explicitly started typing --)
999 let prefix = current_word.trim_start_matches("--");
1000 let mut flag_completions = CompletionResult::new();
1001
1002 // Collect flags with descriptions from current command and parents
1003 current_cmd.collect_all_flags_with_descriptions(&mut flag_completions, prefix);
1004
1005 let format = CompletionFormat::from_shell_type(shell_type.as_deref());
1006 Ok(format.format(&flag_completions, Some(&ctx)))
1007 } else if current_word.starts_with('-') && current_word.len() > 1 {
1008 // For short flags, we don't complete (too complex)
1009 Ok(vec![])
1010 } else {
1011 // Check if previous arg was a flag that needs a value
1012 if let Some(prev) = previous_args.last() {
1013 if prev.starts_with("--") {
1014 let flag_name = prev.trim_start_matches("--");
1015
1016 // First check if the flag itself has a completion function
1017 if let Some(flag) = current_cmd.flags.get(flag_name) {
1018 if let Some(ref completion_func) = flag.completion {
1019 return Self::run_flag_completion(
1020 completion_func,
1021 &ctx,
1022 ¤t_word,
1023 shell_type.as_deref(),
1024 );
1025 }
1026 }
1027
1028 // Fall back to flag_completions HashMap
1029 if let Some(completion_func) = current_cmd.flag_completions.get(flag_name) {
1030 return Self::run_flag_completion(
1031 completion_func,
1032 &ctx,
1033 ¤t_word,
1034 shell_type.as_deref(),
1035 );
1036 }
1037 } else if prev.starts_with('-') && prev.len() == 2 {
1038 // Handle short flag completions
1039 let Some(short_flag) = prev.chars().nth(1) else {
1040 // This should not happen given the length check, but handle gracefully
1041 return Ok(vec![]);
1042 };
1043 if let Some(flag) = current_cmd.find_flag_by_short(short_flag) {
1044 if let Some(ref completion_func) = flag.completion {
1045 return Self::run_flag_completion(
1046 completion_func,
1047 &ctx,
1048 ¤t_word,
1049 shell_type.as_deref(),
1050 );
1051 }
1052
1053 // Also check flag_completions HashMap by flag name
1054 if let Some(completion_func) = current_cmd.flag_completions.get(&flag.name)
1055 {
1056 return Self::run_flag_completion(
1057 completion_func,
1058 &ctx,
1059 ¤t_word,
1060 shell_type.as_deref(),
1061 );
1062 }
1063 }
1064 }
1065 }
1066
1067 // Complete subcommands, arguments AND flags together
1068 let mut combined_completions = CompletionResult::new();
1069
1070 // Get subcommand/argument completions
1071 let subcommand_suggestions = current_cmd.get_completion_suggestions(
1072 ¤t_word,
1073 Some(&ctx),
1074 shell_type.as_deref(),
1075 );
1076
1077 // Add flags that don't start with current_word (so user can discover them)
1078 // Only add flags if current_word is empty or doesn't look like it's trying to complete a specific subcommand
1079 if current_word.is_empty()
1080 || !current_cmd
1081 .subcommands
1082 .keys()
1083 .any(|name| name.starts_with(¤t_word))
1084 {
1085 current_cmd.collect_all_flags_with_descriptions(&mut combined_completions, "");
1086 }
1087
1088 // Convert subcommand suggestions to CompletionResult format and combine
1089 let format = CompletionFormat::from_shell_type(shell_type.as_deref());
1090 let mut final_suggestions = subcommand_suggestions;
1091 let flag_suggestions = format.format(&combined_completions, Some(&ctx));
1092 final_suggestions.extend(flag_suggestions);
1093
1094 Ok(final_suggestions)
1095 }
1096 }
1097
1098 /// Detects the shell type from the environment variable
1099 fn detect_completion_shell(&self) -> Option<String> {
1100 use std::env;
1101
1102 // Look for shell-specific completion environment variables
1103 let env_var = format!("{}_COMPLETE", self.name.to_uppercase());
1104 env::var(&env_var).ok()
1105 }
1106
1107 fn get_completion_suggestions(
1108 &self,
1109 prefix: &str,
1110 ctx: Option<&Context>,
1111 shell_type: Option<&str>,
1112 ) -> Vec<String> {
1113 let mut completion_result = CompletionResult::new();
1114 let mut has_suggestions = false;
1115
1116 // Add subcommands with their descriptions
1117 for (name, cmd) in &self.subcommands {
1118 if name.starts_with(prefix) {
1119 completion_result =
1120 completion_result.add_with_description(name.clone(), cmd.short.clone());
1121 has_suggestions = true;
1122 }
1123 // Also check aliases
1124 for alias in &cmd.aliases {
1125 if alias.starts_with(prefix) {
1126 completion_result = completion_result
1127 .add_with_description(alias.clone(), format!("Alias for {name}"));
1128 has_suggestions = true;
1129 }
1130 }
1131 }
1132
1133 // If we have arg completions and no subcommands match, try those
1134 if !has_suggestions {
1135 if let Some(ref completion_func) = self.arg_completions {
1136 let default_ctx = Context::new(vec![]);
1137 let ctx = ctx.unwrap_or(&default_ctx);
1138 if let Ok(result) = completion_func(ctx, prefix) {
1139 let format = CompletionFormat::from_shell_type(shell_type);
1140 return format.format(&result, Some(ctx));
1141 }
1142 }
1143 }
1144
1145 // Format the results
1146 let format = CompletionFormat::from_shell_type(shell_type);
1147 let default_ctx = Context::new(vec![]);
1148 let ctx_to_use = ctx.unwrap_or(&default_ctx);
1149 let mut suggestions = format.format(&completion_result, Some(ctx_to_use));
1150 suggestions.sort();
1151 suggestions.dedup();
1152 suggestions
1153 }
1154
1155 fn run_flag_completion(
1156 completion_func: &CompletionFunc,
1157 ctx: &Context,
1158 current_word: &str,
1159 shell_type: Option<&str>,
1160 ) -> Result<Vec<String>> {
1161 let result = completion_func(ctx, current_word)?;
1162 let format = CompletionFormat::from_shell_type(shell_type);
1163 Ok(format.format(&result, Some(ctx)))
1164 }
1165}
1166
1167/// Builder for creating commands with a fluent API
1168///
1169/// `CommandBuilder` provides a convenient way to construct commands
1170/// with method chaining. This is the recommended way to create commands.
1171///
1172/// # Examples
1173///
1174/// ```rust
1175/// use flag_rs::{CommandBuilder, Flag, FlagType, FlagValue};
1176///
1177/// let cmd = CommandBuilder::new("serve")
1178/// .short("Start the web server")
1179/// .long("Start the web server on the specified port with the given configuration")
1180/// .aliases(vec!["server", "s"])
1181/// .flag(
1182/// Flag::new("port")
1183/// .short('p')
1184/// .usage("Port to listen on")
1185/// .value_type(FlagType::Int)
1186/// .default(FlagValue::Int(8080))
1187/// )
1188/// .flag(
1189/// Flag::new("config")
1190/// .short('c')
1191/// .usage("Configuration file path")
1192/// .value_type(FlagType::String)
1193/// .required()
1194/// )
1195/// .run(|ctx| {
1196/// let port = ctx.flag("port")
1197/// .and_then(|s| s.parse::<i64>().ok())
1198/// .unwrap_or(8080);
1199/// let config = ctx.flag("config")
1200/// .map(|s| s.as_str())
1201/// .unwrap_or("config.toml");
1202///
1203/// println!("Starting server on port {} with config {}", port, config);
1204/// Ok(())
1205/// })
1206/// .build();
1207/// ```
1208pub struct CommandBuilder {
1209 command: Command,
1210}
1211
1212impl CommandBuilder {
1213 /// Creates a new command builder with the given name
1214 pub fn new(name: impl Into<String>) -> Self {
1215 Self {
1216 command: Command::new(name),
1217 }
1218 }
1219
1220 /// Adds a single alias for this command
1221 ///
1222 /// # Examples
1223 ///
1224 /// ```rust
1225 /// use flag_rs::CommandBuilder;
1226 ///
1227 /// let cmd = CommandBuilder::new("remove")
1228 /// .alias("rm")
1229 /// .alias("delete")
1230 /// .build();
1231 /// ```
1232 #[must_use]
1233 pub fn alias(mut self, alias: impl Into<String>) -> Self {
1234 self.command.aliases.push(alias.into());
1235 self
1236 }
1237
1238 /// Adds multiple aliases for this command
1239 ///
1240 /// # Examples
1241 ///
1242 /// ```rust
1243 /// use flag_rs::CommandBuilder;
1244 ///
1245 /// let cmd = CommandBuilder::new("remove")
1246 /// .aliases(vec!["rm", "delete", "del"])
1247 /// .build();
1248 /// ```
1249 #[must_use]
1250 pub fn aliases<I, S>(mut self, aliases: I) -> Self
1251 where
1252 I: IntoIterator<Item = S>,
1253 S: Into<String>,
1254 {
1255 self.command
1256 .aliases
1257 .extend(aliases.into_iter().map(Into::into));
1258 self
1259 }
1260
1261 /// Sets the short description for this command
1262 ///
1263 /// The short description is shown in the parent command's help output.
1264 #[must_use]
1265 pub fn short(mut self, short: impl Into<String>) -> Self {
1266 self.command.short = short.into();
1267 self
1268 }
1269
1270 /// Sets the long description for this command
1271 ///
1272 /// The long description is shown in this command's help output.
1273 #[must_use]
1274 pub fn long(mut self, long: impl Into<String>) -> Self {
1275 self.command.long = long.into();
1276 self
1277 }
1278
1279 /// Adds an example for this command
1280 ///
1281 /// Examples are shown in the help output to demonstrate command usage.
1282 ///
1283 /// # Examples
1284 ///
1285 /// ```rust
1286 /// use flag_rs::CommandBuilder;
1287 ///
1288 /// let cmd = CommandBuilder::new("deploy")
1289 /// .short("Deploy the application")
1290 /// .example("deploy --env production")
1291 /// .example("deploy --env staging --dry-run")
1292 /// .build();
1293 /// ```
1294 #[must_use]
1295 pub fn example(mut self, example: impl Into<String>) -> Self {
1296 self.command.examples.push(example.into());
1297 self
1298 }
1299
1300 /// Sets the group ID for this command
1301 ///
1302 /// Commands with the same group ID will be displayed together in help output.
1303 ///
1304 /// # Examples
1305 ///
1306 /// ```rust
1307 /// use flag_rs::CommandBuilder;
1308 ///
1309 /// let app = CommandBuilder::new("kubectl")
1310 /// .subcommand(
1311 /// CommandBuilder::new("get")
1312 /// .short("Display resources")
1313 /// .group_id("Basic Commands")
1314 /// .build()
1315 /// )
1316 /// .subcommand(
1317 /// CommandBuilder::new("create")
1318 /// .short("Create resources")
1319 /// .group_id("Basic Commands")
1320 /// .build()
1321 /// )
1322 /// .subcommand(
1323 /// CommandBuilder::new("config")
1324 /// .short("Modify kubeconfig files")
1325 /// .group_id("Settings Commands")
1326 /// .build()
1327 /// )
1328 /// .build();
1329 /// ```
1330 #[must_use]
1331 pub fn group_id(mut self, group_id: impl Into<String>) -> Self {
1332 self.command.group_id = Some(group_id.into());
1333 self
1334 }
1335
1336 /// Adds a subcommand to this command
1337 ///
1338 /// # Examples
1339 ///
1340 /// ```rust
1341 /// use flag_rs::CommandBuilder;
1342 ///
1343 /// let app = CommandBuilder::new("myapp")
1344 /// .subcommand(
1345 /// CommandBuilder::new("init")
1346 /// .short("Initialize a new project")
1347 /// .build()
1348 /// )
1349 /// .subcommand(
1350 /// CommandBuilder::new("build")
1351 /// .short("Build the project")
1352 /// .build()
1353 /// )
1354 /// .build();
1355 /// ```
1356 #[must_use]
1357 pub fn subcommand(mut self, cmd: Command) -> Self {
1358 self.command.add_command(cmd);
1359 self
1360 }
1361
1362 /// Adds multiple subcommands to this command at once
1363 ///
1364 /// # Examples
1365 ///
1366 /// ```rust
1367 /// use flag_rs::CommandBuilder;
1368 ///
1369 /// let cmd = CommandBuilder::new("git")
1370 /// .subcommands(vec![
1371 /// CommandBuilder::new("add")
1372 /// .short("Add file contents to the index")
1373 /// .build(),
1374 /// CommandBuilder::new("commit")
1375 /// .short("Record changes to the repository")
1376 /// .build(),
1377 /// CommandBuilder::new("push")
1378 /// .short("Update remote refs along with associated objects")
1379 /// .build(),
1380 /// ])
1381 /// .build();
1382 /// ```
1383 #[must_use]
1384 pub fn subcommands(mut self, cmds: Vec<Command>) -> Self {
1385 for cmd in cmds {
1386 self.command.add_command(cmd);
1387 }
1388 self
1389 }
1390
1391 /// Adds a flag to this command
1392 ///
1393 /// # Examples
1394 ///
1395 /// ```rust
1396 /// use flag_rs::{CommandBuilder, Flag, FlagType};
1397 ///
1398 /// let cmd = CommandBuilder::new("deploy")
1399 /// .flag(
1400 /// Flag::new("force")
1401 /// .short('f')
1402 /// .usage("Force deployment without confirmation")
1403 /// .value_type(FlagType::Bool)
1404 /// )
1405 /// .build();
1406 /// ```
1407 #[must_use]
1408 pub fn flag(mut self, flag: Flag) -> Self {
1409 self.command.flags.insert(flag.name.clone(), flag);
1410 self
1411 }
1412
1413 /// Adds multiple flags to this command at once
1414 ///
1415 /// # Examples
1416 ///
1417 /// ```rust
1418 /// use flag_rs::{CommandBuilder, Flag};
1419 ///
1420 /// let cmd = CommandBuilder::new("server")
1421 /// .flags(vec![
1422 /// Flag::bool("verbose").short('v').usage("Enable verbose output"),
1423 /// Flag::bool("quiet").short('q').usage("Suppress output"),
1424 /// Flag::int("port").short('p').usage("Port to listen on").default_int(8080),
1425 /// ])
1426 /// .build();
1427 /// ```
1428 #[must_use]
1429 pub fn flags(mut self, flags: Vec<Flag>) -> Self {
1430 for flag in flags {
1431 self.command.flags.insert(flag.name.clone(), flag);
1432 }
1433 self
1434 }
1435
1436 /// Sets the function to run when this command is executed
1437 ///
1438 /// The run function receives a mutable reference to the [`Context`]
1439 /// which provides access to parsed flags and arguments.
1440 ///
1441 /// # Examples
1442 ///
1443 /// ```rust
1444 /// use flag_rs::CommandBuilder;
1445 ///
1446 /// let cmd = CommandBuilder::new("greet")
1447 /// .run(|ctx| {
1448 /// let name = ctx.args().first()
1449 /// .map(|s| s.as_str())
1450 /// .unwrap_or("World");
1451 /// println!("Hello, {}!", name);
1452 /// Ok(())
1453 /// })
1454 /// .build();
1455 /// ```
1456 #[must_use]
1457 pub fn run<F>(mut self, f: F) -> Self
1458 where
1459 F: Fn(&mut Context) -> Result<()> + Send + Sync + 'static,
1460 {
1461 self.command.run = Some(Box::new(f));
1462 self
1463 }
1464
1465 /// Sets the argument validator for this command
1466 ///
1467 /// The validator will be called before the run function to ensure
1468 /// arguments meet the specified constraints.
1469 ///
1470 /// # Examples
1471 ///
1472 /// ```rust
1473 /// use flag_rs::{CommandBuilder, ArgValidator};
1474 ///
1475 /// let cmd = CommandBuilder::new("delete")
1476 /// .args(ArgValidator::MinimumArgs(1))
1477 /// .run(|ctx| {
1478 /// for file in ctx.args() {
1479 /// println!("Deleting: {}", file);
1480 /// }
1481 /// Ok(())
1482 /// })
1483 /// .build();
1484 /// ```
1485 #[must_use]
1486 pub fn args(mut self, validator: ArgValidator) -> Self {
1487 self.command.arg_validator = Some(validator);
1488 self
1489 }
1490
1491 /// Sets the persistent pre-run hook for this command
1492 ///
1493 /// This hook runs before the command and all its subcommands.
1494 /// It's inherited by all subcommands and runs in parent-to-child order.
1495 ///
1496 /// # Examples
1497 ///
1498 /// ```rust
1499 /// use flag_rs::CommandBuilder;
1500 ///
1501 /// let cmd = CommandBuilder::new("app")
1502 /// .persistent_pre_run(|ctx| {
1503 /// println!("Setting up logging...");
1504 /// Ok(())
1505 /// })
1506 /// .build();
1507 /// ```
1508 #[must_use]
1509 pub fn persistent_pre_run<F>(mut self, f: F) -> Self
1510 where
1511 F: Fn(&mut Context) -> Result<()> + Send + Sync + 'static,
1512 {
1513 self.command.persistent_pre_run = Some(Box::new(f));
1514 self
1515 }
1516
1517 /// Sets the pre-run hook for this command
1518 ///
1519 /// This hook runs only for this specific command, after any persistent
1520 /// pre-run hooks but before the main run function.
1521 ///
1522 /// # Examples
1523 ///
1524 /// ```rust
1525 /// use flag_rs::CommandBuilder;
1526 ///
1527 /// let cmd = CommandBuilder::new("deploy")
1528 /// .pre_run(|ctx| {
1529 /// println!("Validating deployment configuration...");
1530 /// Ok(())
1531 /// })
1532 /// .run(|ctx| {
1533 /// println!("Deploying application...");
1534 /// Ok(())
1535 /// })
1536 /// .build();
1537 /// ```
1538 #[must_use]
1539 pub fn pre_run<F>(mut self, f: F) -> Self
1540 where
1541 F: Fn(&mut Context) -> Result<()> + Send + Sync + 'static,
1542 {
1543 self.command.pre_run = Some(Box::new(f));
1544 self
1545 }
1546
1547 /// Sets the post-run hook for this command
1548 ///
1549 /// This hook runs only for this specific command, after the main run
1550 /// function but before any persistent post-run hooks.
1551 ///
1552 /// # Examples
1553 ///
1554 /// ```rust
1555 /// use flag_rs::CommandBuilder;
1556 ///
1557 /// let cmd = CommandBuilder::new("test")
1558 /// .run(|ctx| {
1559 /// println!("Running tests...");
1560 /// Ok(())
1561 /// })
1562 /// .post_run(|ctx| {
1563 /// println!("Generating test report...");
1564 /// Ok(())
1565 /// })
1566 /// .build();
1567 /// ```
1568 #[must_use]
1569 pub fn post_run<F>(mut self, f: F) -> Self
1570 where
1571 F: Fn(&mut Context) -> Result<()> + Send + Sync + 'static,
1572 {
1573 self.command.post_run = Some(Box::new(f));
1574 self
1575 }
1576
1577 /// Sets the persistent post-run hook for this command
1578 ///
1579 /// This hook runs after the command and all its subcommands.
1580 /// It's inherited by all subcommands and runs in child-to-parent order.
1581 ///
1582 /// # Examples
1583 ///
1584 /// ```rust
1585 /// use flag_rs::CommandBuilder;
1586 ///
1587 /// let cmd = CommandBuilder::new("app")
1588 /// .persistent_post_run(|ctx| {
1589 /// println!("Cleaning up resources...");
1590 /// Ok(())
1591 /// })
1592 /// .build();
1593 /// ```
1594 #[must_use]
1595 pub fn persistent_post_run<F>(mut self, f: F) -> Self
1596 where
1597 F: Fn(&mut Context) -> Result<()> + Send + Sync + 'static,
1598 {
1599 self.command.persistent_post_run = Some(Box::new(f));
1600 self
1601 }
1602
1603 /// Sets the argument completion function
1604 ///
1605 /// This function is called when the user presses TAB to complete arguments.
1606 /// It enables dynamic completions based on runtime state.
1607 ///
1608 /// # Examples
1609 ///
1610 /// ```rust
1611 /// use flag_rs::{CommandBuilder, CompletionResult};
1612 ///
1613 /// let cmd = CommandBuilder::new("edit")
1614 /// .arg_completion(|ctx, prefix| {
1615 /// // In a real app, list files from the filesystem
1616 /// let files = vec!["main.rs", "lib.rs", "Cargo.toml"];
1617 /// Ok(CompletionResult::new().extend(
1618 /// files.into_iter()
1619 /// .filter(|f| f.starts_with(prefix))
1620 /// .map(String::from)
1621 /// ))
1622 /// })
1623 /// .build();
1624 /// ```
1625 #[must_use]
1626 pub fn arg_completion<F>(mut self, f: F) -> Self
1627 where
1628 F: Fn(&Context, &str) -> Result<CompletionResult> + Send + Sync + 'static,
1629 {
1630 self.command.set_arg_completion(f);
1631 self
1632 }
1633
1634 /// Sets the completion function for a specific flag
1635 ///
1636 /// # Examples
1637 ///
1638 /// ```rust
1639 /// use flag_rs::{CommandBuilder, CompletionResult, Flag, FlagType};
1640 ///
1641 /// let cmd = CommandBuilder::new("connect")
1642 /// .flag(
1643 /// Flag::new("server")
1644 /// .usage("Server to connect to")
1645 /// .value_type(FlagType::String)
1646 /// )
1647 /// .flag_completion("server", |ctx, prefix| {
1648 /// // In a real app, discover available servers
1649 /// let servers = vec!["prod-1", "prod-2", "staging", "dev"];
1650 /// Ok(CompletionResult::new().extend(
1651 /// servers.into_iter()
1652 /// .filter(|s| s.starts_with(prefix))
1653 /// .map(String::from)
1654 /// ))
1655 /// })
1656 /// .build();
1657 /// ```
1658 #[must_use]
1659 pub fn flag_completion<F>(mut self, flag_name: impl Into<String>, f: F) -> Self
1660 where
1661 F: Fn(&Context, &str) -> Result<CompletionResult> + Send + Sync + 'static,
1662 {
1663 self.command.set_flag_completion(flag_name, f);
1664 self
1665 }
1666
1667 /// Enables or disables command suggestions
1668 ///
1669 /// When enabled, the framework will suggest similar commands when
1670 /// a user types an unknown command.
1671 ///
1672 /// # Examples
1673 ///
1674 /// ```rust
1675 /// use flag_rs::CommandBuilder;
1676 ///
1677 /// let cmd = CommandBuilder::new("myapp")
1678 /// .suggestions(true) // Enable suggestions (default)
1679 /// .build();
1680 /// ```
1681 #[must_use]
1682 pub fn suggestions(mut self, enabled: bool) -> Self {
1683 self.command.suggestions_enabled = enabled;
1684 self
1685 }
1686
1687 /// Sets the maximum Levenshtein distance for suggestions
1688 ///
1689 /// Commands within this distance will be suggested as alternatives.
1690 /// Default is 2.
1691 ///
1692 /// # Examples
1693 ///
1694 /// ```rust
1695 /// use flag_rs::CommandBuilder;
1696 ///
1697 /// let cmd = CommandBuilder::new("myapp")
1698 /// .suggestion_distance(3) // Allow more distant suggestions
1699 /// .build();
1700 /// ```
1701 #[must_use]
1702 pub fn suggestion_distance(mut self, distance: usize) -> Self {
1703 self.command.suggestion_distance = distance;
1704 self
1705 }
1706
1707 /// Builds and returns the completed [`Command`]
1708 #[must_use]
1709 pub fn build(self) -> Command {
1710 self.command
1711 }
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716 use super::*;
1717 use crate::flag::FlagType;
1718 use std::sync::{Arc, Mutex};
1719
1720 #[test]
1721 fn test_simple_command_execution() {
1722 let executed = Arc::new(Mutex::new(false));
1723 let executed_clone = executed.clone();
1724
1725 let cmd = CommandBuilder::new("test")
1726 .run(move |_ctx| {
1727 *executed_clone.lock().unwrap() = true;
1728 Ok(())
1729 })
1730 .build();
1731
1732 cmd.execute(vec![]).unwrap();
1733 assert!(*executed.lock().unwrap());
1734 }
1735
1736 #[test]
1737 fn test_command_with_args() {
1738 let received_args = Arc::new(Mutex::new(Vec::new()));
1739 let args_clone = received_args.clone();
1740
1741 let cmd = CommandBuilder::new("test")
1742 .run(move |ctx| {
1743 *args_clone.lock().unwrap() = ctx.args().to_vec();
1744 Ok(())
1745 })
1746 .build();
1747
1748 cmd.execute(vec!["arg1".to_string(), "arg2".to_string()])
1749 .unwrap();
1750 assert_eq!(*received_args.lock().unwrap(), vec!["arg1", "arg2"]);
1751 }
1752
1753 #[test]
1754 fn test_subcommand_execution() {
1755 let main_executed = Arc::new(Mutex::new(false));
1756 let sub_executed = Arc::new(Mutex::new(false));
1757 let sub_clone = sub_executed.clone();
1758
1759 let subcmd = CommandBuilder::new("sub")
1760 .run(move |_ctx| {
1761 *sub_clone.lock().unwrap() = true;
1762 Ok(())
1763 })
1764 .build();
1765
1766 let main_clone = main_executed.clone();
1767 let cmd = CommandBuilder::new("main")
1768 .run(move |_ctx| {
1769 *main_clone.lock().unwrap() = true;
1770 Ok(())
1771 })
1772 .subcommand(subcmd)
1773 .build();
1774
1775 // Execute subcommand
1776 cmd.execute(vec!["sub".to_string()]).unwrap();
1777 assert!(*sub_executed.lock().unwrap());
1778 assert!(!*main_executed.lock().unwrap());
1779 }
1780
1781 #[test]
1782 fn test_flag_parsing() {
1783 let cmd = CommandBuilder::new("test")
1784 .flag(Flag::new("verbose").short('v').value_type(FlagType::Bool))
1785 .flag(Flag::new("output").short('o').value_type(FlagType::String))
1786 .flag(Flag::new("count").value_type(FlagType::Int))
1787 .run(|ctx| {
1788 assert_eq!(ctx.flag("verbose"), Some(&"true".to_string()));
1789 assert_eq!(ctx.flag("output"), Some(&"file.txt".to_string()));
1790 assert_eq!(ctx.flag("count"), Some(&"42".to_string()));
1791 assert_eq!(ctx.args(), &["remaining"]);
1792 Ok(())
1793 })
1794 .build();
1795
1796 cmd.execute(vec![
1797 "-v".to_string(),
1798 "--output".to_string(),
1799 "file.txt".to_string(),
1800 "--count=42".to_string(),
1801 "remaining".to_string(),
1802 ])
1803 .unwrap();
1804 }
1805
1806 #[test]
1807 fn test_flag_inheritance() {
1808 let sub_executed = Arc::new(Mutex::new(false));
1809 let sub_clone = sub_executed.clone();
1810
1811 let subcmd = CommandBuilder::new("sub")
1812 .run(move |ctx| {
1813 assert_eq!(ctx.flag("global"), Some(&"value".to_string()));
1814 *sub_clone.lock().unwrap() = true;
1815 Ok(())
1816 })
1817 .build();
1818
1819 let cmd = CommandBuilder::new("main")
1820 .flag(Flag::new("global").value_type(FlagType::String))
1821 .subcommand(subcmd)
1822 .build();
1823
1824 cmd.execute(vec![
1825 "--global".to_string(),
1826 "value".to_string(),
1827 "sub".to_string(),
1828 ])
1829 .unwrap();
1830
1831 assert!(*sub_executed.lock().unwrap());
1832 }
1833
1834 #[test]
1835 fn test_command_aliases() {
1836 let executed = Arc::new(Mutex::new(String::new()));
1837 let exec_clone = executed.clone();
1838
1839 let subcmd = CommandBuilder::new("subcommand")
1840 .aliases(vec!["sub", "s"])
1841 .run(move |_ctx| {
1842 *exec_clone.lock().unwrap() = "subcommand".to_string();
1843 Ok(())
1844 })
1845 .build();
1846
1847 let cmd = CommandBuilder::new("main").subcommand(subcmd).build();
1848
1849 // Test main name
1850 cmd.execute(vec!["subcommand".to_string()]).unwrap();
1851 assert_eq!(*executed.lock().unwrap(), "subcommand");
1852
1853 // Test alias
1854 cmd.execute(vec!["sub".to_string()]).unwrap();
1855 assert_eq!(*executed.lock().unwrap(), "subcommand");
1856
1857 // Test short alias
1858 cmd.execute(vec!["s".to_string()]).unwrap();
1859 assert_eq!(*executed.lock().unwrap(), "subcommand");
1860 }
1861
1862 #[test]
1863 fn test_error_cases() {
1864 let cmd = CommandBuilder::new("main").build();
1865
1866 // No subcommand when required
1867 let result = cmd.execute(vec![]);
1868 assert!(result.is_err());
1869 assert!(matches!(result.unwrap_err(), Error::SubcommandRequired(_)));
1870
1871 // Unknown subcommand
1872 let result = cmd.execute(vec!["unknown".to_string()]);
1873 assert!(result.is_err());
1874 assert!(matches!(result.unwrap_err(), Error::CommandNotFound { .. }));
1875
1876 // Unknown flag (now treated as argument, so it becomes unknown command)
1877 let result = cmd.execute(vec!["--unknown".to_string()]);
1878 assert!(result.is_err());
1879 assert!(matches!(result.unwrap_err(), Error::CommandNotFound { .. }));
1880 }
1881
1882 #[test]
1883 fn test_completion() {
1884 let cmd = CommandBuilder::new("test")
1885 .arg_completion(|_ctx, prefix| {
1886 Ok(CompletionResult::new().extend(
1887 vec!["file1.txt", "file2.txt", "folder/"]
1888 .into_iter()
1889 .filter(|f| f.starts_with(prefix))
1890 .map(String::from),
1891 ))
1892 })
1893 .flag_completion("type", |_ctx, prefix| {
1894 Ok(CompletionResult::new().extend(
1895 vec!["json", "yaml", "xml"]
1896 .into_iter()
1897 .filter(|t| t.starts_with(prefix))
1898 .map(String::from),
1899 ))
1900 })
1901 .build();
1902
1903 let ctx = Context::new(vec![]);
1904
1905 // Test arg completion
1906 let result = cmd.get_completions(&ctx, "fi", None).unwrap();
1907 assert_eq!(result.values, vec!["file1.txt", "file2.txt"]);
1908
1909 // Test flag completion
1910 let result = cmd.get_completions(&ctx, "j", Some("type")).unwrap();
1911 assert_eq!(result.values, vec!["json"]);
1912 }
1913
1914 #[test]
1915 fn test_flag_with_equals() {
1916 let cmd = CommandBuilder::new("test")
1917 .flag(Flag::new("output").value_type(FlagType::String))
1918 .run(|ctx| {
1919 assert_eq!(
1920 ctx.flag("output"),
1921 Some(&"/path/with=equals.txt".to_string())
1922 );
1923 Ok(())
1924 })
1925 .build();
1926
1927 cmd.execute(vec!["--output=/path/with=equals.txt".to_string()])
1928 .unwrap();
1929 }
1930
1931 #[test]
1932 fn test_help_flag() {
1933 let cmd = CommandBuilder::new("test")
1934 .short("Test command")
1935 .long("This is a test command")
1936 .flag(
1937 Flag::new("verbose")
1938 .short('v')
1939 .usage("Enable verbose output"),
1940 )
1941 .build();
1942
1943 // Test --help
1944 let result = cmd.execute(vec!["--help".to_string()]);
1945 assert!(result.is_ok());
1946
1947 // Test -h
1948 let result = cmd.execute(vec!["-h".to_string()]);
1949 assert!(result.is_ok());
1950 }
1951
1952 #[test]
1953 fn test_subcommand_help() {
1954 let subcmd = CommandBuilder::new("sub")
1955 .short("Subcommand")
1956 .flag(Flag::new("subflag").usage("A flag for the subcommand"))
1957 .build();
1958
1959 let cmd = CommandBuilder::new("main")
1960 .flag(Flag::new("global").usage("A global flag"))
1961 .subcommand(subcmd)
1962 .build();
1963
1964 // Test help on subcommand
1965 let result = cmd.execute(vec!["sub".to_string(), "--help".to_string()]);
1966 assert!(result.is_ok());
1967 }
1968}