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