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