1use std::collections::HashMap;
43use std::sync::Arc;
44
45use crate::argument::Argument;
46use crate::context::{push_context, pop_context, Context, ContextBuilder};
47use crate::error::{ClickError, ErrorContext};
48use crate::option::ClickOption;
49use crate::parameter::{Nargs, Parameter};
50use crate::parser::{OptionAction, OptionParser, ParsedValue, NARGS_OPTIONAL};
51use crate::source::ParameterSource;
52use crate::termui;
53
54pub type CommandCallback = Box<dyn Fn(&Context) -> Result<(), ClickError> + Send + Sync>;
63
64pub struct Command {
85 pub name: Option<String>,
90
91 pub callback: Option<CommandCallback>,
95
96 pub options: Vec<ClickOption>,
98
99 pub arguments: Vec<Argument>,
101
102 pub help: Option<String>,
106
107 pub epilog: Option<String>,
109
110 pub short_help: Option<String>,
114
115 pub options_metavar: String,
117
118 pub add_help_option: bool,
120
121 pub no_args_is_help: bool,
123
124 pub hidden: bool,
126
127 pub deprecated: Option<String>,
132
133 pub allow_extra_args: bool,
135
136 pub allow_interspersed_args: bool,
138
139 pub ignore_unknown_options: bool,
141
142 help_option: Option<ClickOption>,
144}
145
146impl std::fmt::Debug for Command {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct("Command")
149 .field("name", &self.name)
150 .field("options", &format!("<{} options>", self.options.len()))
151 .field("arguments", &format!("<{} arguments>", self.arguments.len()))
152 .field("help", &self.help)
153 .field("epilog", &self.epilog)
154 .field("short_help", &self.short_help)
155 .field("options_metavar", &self.options_metavar)
156 .field("add_help_option", &self.add_help_option)
157 .field("no_args_is_help", &self.no_args_is_help)
158 .field("hidden", &self.hidden)
159 .field("deprecated", &self.deprecated)
160 .field("allow_extra_args", &self.allow_extra_args)
161 .field("allow_interspersed_args", &self.allow_interspersed_args)
162 .field("ignore_unknown_options", &self.ignore_unknown_options)
163 .finish()
164 }
165}
166
167impl Default for Command {
168 fn default() -> Self {
169 Self {
170 name: None,
171 callback: None,
172 options: Vec::new(),
173 arguments: Vec::new(),
174 help: None,
175 epilog: None,
176 short_help: None,
177 options_metavar: "[OPTIONS]".to_string(),
178 add_help_option: true,
179 no_args_is_help: false,
180 hidden: false,
181 deprecated: None,
182 allow_extra_args: false,
183 allow_interspersed_args: true,
184 ignore_unknown_options: false,
185 help_option: None,
186 }
187 }
188}
189
190impl Command {
191 const VERSION_METAVAR_PREFIX: &'static str = "__click_version__:";
192
193 #[allow(clippy::new_ret_no_self)]
205 pub fn new(name: &str) -> CommandBuilder {
206 CommandBuilder::new(name)
207 }
208
209 pub fn get_help_option(&self, ctx: &Context) -> Option<ClickOption> {
214 if !self.add_help_option {
215 return None;
216 }
217
218 let help_names = self.get_help_option_names(ctx);
219 if help_names.is_empty() {
220 return None;
221 }
222
223 if let Some(ref help_opt) = self.help_option {
225 return Some(help_opt.clone());
226 }
227
228 Some(make_help_option(&help_names))
230 }
231
232 fn get_help_option_names(&self, ctx: &Context) -> Vec<String> {
236 let mut all_names: std::collections::HashSet<String> =
237 ctx.help_option_names().iter().cloned().collect();
238
239 for opt in &self.options {
241 for name in &opt.long {
242 all_names.remove(name);
243 }
244 for name in &opt.short {
245 all_names.remove(name);
246 }
247 }
248
249 all_names.into_iter().collect()
250 }
251
252 pub fn param_count(&self) -> usize {
254 self.options.len() + self.arguments.len()
255 }
256
257 pub fn get_params(&self, _ctx: &Context) -> Vec<&dyn Parameter> {
262 let mut params: Vec<&dyn Parameter> = Vec::with_capacity(self.param_count());
263 for opt in &self.options {
264 params.push(opt);
265 }
266 for arg in &self.arguments {
267 params.push(arg);
268 }
269 params
270 }
271
272 pub fn make_context(
292 &self,
293 info_name: &str,
294 args: Vec<String>,
295 parent: Option<Arc<Context>>,
296 ) -> Result<Context, ClickError> {
297 let mut builder = ContextBuilder::new()
298 .info_name(info_name)
299 .allow_extra_args(self.allow_extra_args)
300 .allow_interspersed_args(self.allow_interspersed_args)
301 .ignore_unknown_options(self.ignore_unknown_options);
302
303 if let Some(parent) = parent {
304 builder = builder.parent(parent);
305 }
306
307 let mut ctx = builder.build();
308
309 self.parse_args(&mut ctx, args)?;
311
312 Ok(ctx)
313 }
314
315 pub fn parse_args(&self, ctx: &mut Context, args: Vec<String>) -> Result<(), ClickError> {
320 if args.is_empty() && self.no_args_is_help && !ctx.resilient_parsing() {
322 return Err(ClickError::Exit { code: 0 });
324 }
325
326 let mut parser = OptionParser::new()
328 .allow_interspersed_args(ctx.allow_interspersed_args())
329 .ignore_unknown_options(ctx.ignore_unknown_options());
330
331 let help_opt = self.get_help_option(ctx);
333
334 for opt in &self.options {
336 self.add_option_to_parser(&mut parser, opt);
337 }
338 if let Some(ref help) = help_opt {
339 self.add_option_to_parser(&mut parser, help);
340 }
341
342 for arg in &self.arguments {
344 self.add_argument_to_parser(&mut parser, arg);
345 }
346
347 let (opts, remaining, _order) = parser.parse_args(args)?;
349
350 if let Some(ref help) = help_opt {
353 if let Some(ParsedValue::Single(val)) = opts.get(help.name()) {
354 if val == "true" {
355 ctx.params_mut().insert(
358 help.name().to_string(),
359 Arc::new(true) as Arc<dyn std::any::Any + Send + Sync>,
360 );
361 return Err(ClickError::Exit { code: 0 });
364 }
365 }
366 }
367
368 for opt in &self.options {
370 if opt.is_eager() {
371 if let Some(ParsedValue::Single(val)) = opts.get(opt.name()) {
372 if val == "true" || !val.is_empty() {
373 ctx.params_mut().insert(
376 opt.name().to_string(),
377 Arc::new(val.clone()) as Arc<dyn std::any::Any + Send + Sync>,
378 );
379
380 if opt
382 .config
383 .metavar
384 .as_deref()
385 .is_some_and(|m| m.starts_with(Self::VERSION_METAVAR_PREFIX))
386 {
387 return Err(ClickError::Exit { code: 0 });
388 }
389 }
390 }
391 }
392 }
393
394 for opt in &self.options {
396 self.process_option_value(ctx, opt, &opts)?;
397 }
398 if let Some(ref help) = help_opt {
399 self.process_option_value(ctx, help, &opts)?;
400 }
401
402 for arg in &self.arguments {
404 self.process_argument_value(ctx, arg, &opts)?;
405 }
406
407 if !remaining.is_empty() && !ctx.allow_extra_args() && !ctx.resilient_parsing() {
409 let extra_args = remaining.join(" ");
410 return Err(ClickError::usage(if remaining.len() == 1 {
411 format!("Got unexpected extra argument ({})", extra_args)
412 } else {
413 format!("Got unexpected extra arguments ({})", extra_args)
414 }));
415 }
416
417 ctx.args_mut().extend(remaining);
419
420 Ok(())
421 }
422
423 fn add_option_to_parser(&self, parser: &mut OptionParser, opt: &ClickOption) {
425 let mut opts: Vec<&str> = Vec::new();
426 for s in &opt.short {
427 opts.push(s.as_str());
428 }
429 for l in &opt.long {
430 opts.push(l.as_str());
431 }
432
433 let (action, nargs, const_value, flag_needs_value) = if opt.count {
434 (OptionAction::Count, 0, None, false)
435 } else if opt.is_flag {
436 let const_val = opt.flag_value.as_deref().unwrap_or("true");
437 (OptionAction::StoreConst, 0, Some(const_val), false)
438 } else if opt.multiple() {
439 let (nargs_val, fnv) = match opt.nargs() {
440 Nargs::Count(n) => (n as i32, false),
441 Nargs::Variadic => (-1, false),
442 Nargs::Optional => (1, true), };
444 (OptionAction::Append, nargs_val, None, fnv)
445 } else {
446 let (nargs_val, fnv) = match opt.nargs() {
447 Nargs::Count(n) => (n as i32, false),
448 Nargs::Variadic => (-1, false),
449 Nargs::Optional => (1, true), };
451 (OptionAction::Store, nargs_val, None, fnv)
452 };
453
454 parser.add_option_ex(&opts, opt.name(), action, nargs, const_value, flag_needs_value);
455 }
456
457 fn add_argument_to_parser(&self, parser: &mut OptionParser, arg: &Argument) {
459 let nargs_val = match arg.nargs() {
460 Nargs::Count(n) => n as i32,
461 Nargs::Variadic => -1,
462 Nargs::Optional => NARGS_OPTIONAL, };
464
465 parser.add_argument(arg.name(), nargs_val);
466 }
467
468 fn resolve_envvar_value(&self, ctx: &Context, param: &dyn Parameter) -> Option<String> {
470 let mut candidates: Vec<String> = Vec::new();
471
472 if let Some(envvars) = param.envvar() {
473 candidates.extend(envvars.iter().cloned());
474 } else if let Some(prefix) = ctx.auto_envvar_prefix() {
475 let normalized = param.name().to_uppercase().replace('-', "_");
476 candidates.push(format!("{}_{}", prefix, normalized));
477 }
478
479 for name in candidates {
480 if let Ok(value) = std::env::var(&name) {
481 if !value.is_empty() {
482 return Some(value);
483 }
484 }
485 }
486
487 None
488 }
489
490 fn process_option_value(
492 &self,
493 ctx: &mut Context,
494 opt: &ClickOption,
495 opts: &HashMap<String, ParsedValue>,
496 ) -> Result<(), ClickError> {
497 let name = opt.name();
498 let parsed_value = opts.get(name);
499
500 let flag_needs_value_key = format!("__click_internal_flag_needs_value_{}", name);
503 let had_flag_needs_value = opts.get(&flag_needs_value_key).is_some();
504
505 let make_error_ctx = || {
506 ErrorContext::new()
507 .with_command_path(ctx.command_path())
508 .with_usage(self.get_usage(ctx))
509 .with_help_options(ctx.help_option_names().to_vec())
510 };
511
512 let convert_single = |value: &str| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
513 opt.convert_any(value)
514 .map(Arc::from)
515 .map_err(|msg| {
516 ClickError::bad_parameter_named(msg, opt.human_readable_name())
517 .with_context(make_error_ctx())
518 })
519 };
520
521 let convert_multi = |values: &[String]| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
522 opt.convert_multi(values)
523 .map(Arc::from)
524 .map_err(|msg| {
525 ClickError::bad_parameter_named(msg, opt.human_readable_name())
526 .with_context(make_error_ctx())
527 })
528 };
529
530 let envvar_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
532 self.resolve_envvar_value(ctx, opt)
533 } else {
534 None
535 };
536
537 let default_map_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
538 ctx.lookup_default_value(name)
539 } else {
540 None
541 };
542
543 let mut source: Option<ParameterSource> = None;
544 let mut value: Option<Arc<dyn std::any::Any + Send + Sync>> = match parsed_value {
545 Some(ParsedValue::Count(n)) => {
546 source = Some(ParameterSource::CommandLine);
547 Some(Arc::new(*n))
548 }
549 Some(ParsedValue::Flag(b)) => {
550 source = Some(ParameterSource::CommandLine);
551 Some(Arc::new(*b))
552 }
553 Some(ParsedValue::Single(s)) => {
554 source = Some(ParameterSource::CommandLine);
555 Some(convert_single(s)?)
556 }
557 Some(ParsedValue::Multiple(v)) => {
558 let mut values = v.clone();
559 if had_flag_needs_value {
560 if values.is_empty() {
561 if let Some(ref flag_val) = opt.flag_value {
562 values.push(flag_val.clone());
563 } else if let Some(ref default) = opt.default {
564 values.push(default.clone());
565 } else {
566 values.push(String::new());
567 }
568 } else if let Some(ref flag_val) = opt.flag_value {
569 values.push(flag_val.clone());
570 } else {
571 values.push(String::new());
572 }
573 }
574 source = Some(ParameterSource::CommandLine);
575 Some(convert_multi(&values)?)
576 }
577 Some(ParsedValue::FlagNeedsValue) => {
578 let fallback = opt
579 .flag_value
580 .as_ref()
581 .or(opt.default.as_ref())
582 .cloned()
583 .unwrap_or_else(String::new);
584 source = Some(ParameterSource::CommandLine);
585 Some(convert_single(&fallback)?)
586 }
587 Some(ParsedValue::Unset) | None => {
588 if let Some(envval) = envvar_value {
589 source = Some(ParameterSource::Environment);
590 if opt.count {
591 let parsed = envval.parse::<usize>().map_err(|_| {
592 ClickError::bad_parameter_named(
593 format!("'{}' is not a valid integer.", envval),
594 opt.human_readable_name(),
595 )
596 .with_context(make_error_ctx())
597 })?;
598 Some(Arc::new(parsed))
599 } else if opt.nargs().is_multi() || opt.multiple() {
600 let values = opt.type_converter().split_envvar_value(&envval);
601 if values.is_empty() {
602 None
603 } else {
604 Some(convert_multi(&values)?)
605 }
606 } else {
607 Some(convert_single(&envval)?)
608 }
609 } else if let Some(default_map) = default_map_value {
610 source = Some(ParameterSource::DefaultMap);
611 Some(default_map)
612 } else if let Some(ref prompt_text) = opt.prompt {
613 if ctx.resilient_parsing() || opt.is_flag || opt.count {
614 None
615 } else {
616 let default_value = opt.default.clone();
617 let prompted = termui::prompt(
618 prompt_text,
619 default_value,
620 opt.hide_input,
621 opt.confirmation_prompt,
622 |input| {
623 match opt.convert_any(input) {
624 Ok(any_val) => {
625 if let Ok(val) = any_val.downcast::<String>() {
626 Ok(*val)
627 } else {
628 Ok(input.to_string())
629 }
630 }
631 Err(msg) => Err(msg),
632 }
633 },
634 )?;
635
636 source = Some(ParameterSource::Prompt);
637 if opt.nargs().is_multi() || opt.multiple() {
638 Some(convert_multi(&vec![prompted])?)
639 } else {
640 Some(convert_single(&prompted)?)
641 }
642 }
643 } else if opt.count {
644 source = Some(ParameterSource::Default);
645 Some(Arc::new(0usize))
646 } else if let Some(ref default) = opt.default {
647 source = Some(ParameterSource::Default);
648 if opt.nargs().is_multi() || opt.multiple() {
649 Some(convert_multi(&vec![default.clone()])?)
650 } else {
651 Some(convert_single(default)?)
652 }
653 } else {
654 None
655 }
656 }
657 };
658
659 if value.is_none() && opt.required() && !ctx.resilient_parsing() {
661 let error_ctx = ErrorContext::new()
662 .with_command_path(ctx.command_path())
663 .with_usage(self.get_usage(ctx))
664 .with_help_options(ctx.help_option_names().to_vec());
665 return Err(ClickError::missing_option(opt.human_readable_name()).with_context(error_ctx));
666 }
667
668 if let Some(ref callback) = opt.config.callback {
669 if let Some(current) = value.take() {
670 value = Some(callback(ctx, opt, current)?);
671 }
672 }
673
674 if let Some(v) = value {
676 if let Some(source) = source {
677 ctx.set_parameter_source(name, source);
678 }
679 if opt.expose_value() {
680 ctx.params_mut().insert(name.to_string(), v);
681 }
682 }
683
684 Ok(())
685 }
686
687 fn process_argument_value(
689 &self,
690 ctx: &mut Context,
691 arg: &Argument,
692 opts: &HashMap<String, ParsedValue>,
693 ) -> Result<(), ClickError> {
694 let name = arg.name();
695 let parsed_value = opts.get(name);
696
697 let envvar_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
698 self.resolve_envvar_value(ctx, arg)
699 } else {
700 None
701 };
702
703 let make_error_ctx = || {
704 ErrorContext::new()
705 .with_command_path(ctx.command_path())
706 .with_usage(self.get_usage(ctx))
707 .with_help_options(ctx.help_option_names().to_vec())
708 };
709
710 let convert_single = |value: &str| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
711 arg.convert_any(value)
712 .map(Arc::from)
713 .map_err(|msg| {
714 ClickError::bad_parameter_named(msg, arg.human_readable_name())
715 .with_context(make_error_ctx())
716 })
717 };
718
719 let convert_multi = |values: &[String]| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
720 arg.type_converter()
721 .convert_multi(values)
722 .map(Arc::from)
723 .map_err(|msg| {
724 ClickError::bad_parameter_named(msg, arg.human_readable_name())
725 .with_context(make_error_ctx())
726 })
727 };
728
729 let default_map_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
731 ctx.lookup_default_value(name)
732 } else {
733 None
734 };
735
736 let mut source: Option<ParameterSource> = None;
737 let mut value: Option<Arc<dyn std::any::Any + Send + Sync>> = match parsed_value {
738 Some(ParsedValue::Single(s)) => {
739 source = Some(ParameterSource::CommandLine);
740 Some(convert_single(s)?)
741 }
742 Some(ParsedValue::Multiple(v)) => {
743 source = Some(ParameterSource::CommandLine);
744 Some(convert_multi(v)?)
745 }
746 Some(ParsedValue::Count(n)) => {
747 source = Some(ParameterSource::CommandLine);
748 Some(Arc::new(*n))
749 }
750 Some(ParsedValue::Flag(b)) => {
751 source = Some(ParameterSource::CommandLine);
752 Some(Arc::new(*b))
753 }
754 Some(ParsedValue::FlagNeedsValue) | Some(ParsedValue::Unset) | None => {
755 if let Some(envval) = envvar_value {
756 source = Some(ParameterSource::Environment);
757 if arg.nargs().is_multi() || arg.multiple() {
758 let values = arg.type_converter().split_envvar_value(&envval);
759 if values.is_empty() {
760 None
761 } else {
762 Some(convert_multi(&values)?)
763 }
764 } else {
765 Some(convert_single(&envval)?)
766 }
767 } else if let Some(default_map) = default_map_value {
768 source = Some(ParameterSource::DefaultMap);
769 Some(default_map)
770 } else {
771 arg.default_value()
772 .map(|d| {
773 source = Some(ParameterSource::Default);
774 if arg.nargs().is_multi() || arg.multiple() {
775 convert_multi(&vec![d.to_string()]).map_err(|e| e)
776 } else {
777 convert_single(d).map_err(|e| e)
778 }
779 })
780 .transpose()?
781 }
782 }
783 };
784
785 if value.is_none() && arg.required() && !ctx.resilient_parsing() {
787 let error_ctx = ErrorContext::new()
788 .with_command_path(ctx.command_path())
789 .with_usage(self.get_usage(ctx))
790 .with_help_options(ctx.help_option_names().to_vec());
791 return Err(ClickError::missing_argument(arg.human_readable_name()).with_context(error_ctx));
792 }
793
794 if let Some(ref callback) = arg.config.callback {
795 if let Some(current) = value.take() {
796 value = Some(callback(ctx, arg, current)?);
797 }
798 }
799
800 if let Some(v) = value {
802 if let Some(source) = source {
803 ctx.set_parameter_source(name, source);
804 }
805 if arg.expose_value() {
806 ctx.params_mut().insert(name.to_string(), v);
807 }
808 }
809
810 Ok(())
811 }
812
813 pub fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
817 if let Some(ref deprecated) = self.deprecated {
819 let extra = if deprecated.is_empty() {
820 String::new()
821 } else {
822 format!(" {}", deprecated)
823 };
824 eprintln!(
825 "DeprecationWarning: The command '{}' is deprecated.{}",
826 self.name.as_deref().unwrap_or(""),
827 extra
828 );
829 }
830
831 if let Some(ref callback) = self.callback {
833 callback(ctx)
834 } else {
835 Ok(())
836 }
837 }
838
839 #[allow(clippy::arc_with_non_send_sync)]
866 pub fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
867 let prog_name = self.name.clone().unwrap_or_else(|| {
868 std::env::args()
869 .next()
870 .unwrap_or_else(|| "program".to_string())
871 });
872
873 let args_for_eager = args.clone();
874
875 let ctx_result = self.make_context(&prog_name, args, None);
877
878 match ctx_result {
879 Ok(ctx) => {
880 let ctx = Arc::new(ctx);
883
884 push_context(Arc::clone(&ctx));
886
887 let result = self.invoke(&ctx);
889
890 pop_context();
892
893 ctx.close();
895
896 result
897 }
898 Err(ClickError::Exit { code: 0 }) => {
899 if let Some(version_output) = self.get_version_output_from_args(&args_for_eager) {
904 println!("{}", version_output);
905 return Ok(());
906 }
907
908 let ctx = ContextBuilder::new().info_name(&prog_name).build();
911 println!("{}", self.get_help(&ctx));
912 Ok(())
913 }
914 Err(e) => Err(e),
915 }
916 }
917
918 fn arg_matches_opt(arg: &str, opt: &str) -> bool {
919 if arg == opt {
920 return true;
921 }
922 if opt.starts_with("--") && arg.starts_with(opt) && arg.get(opt.len()..opt.len() + 1) == Some("=") {
923 return true;
924 }
925 if opt.starts_with('-') && opt.len() == 2 && !opt.starts_with("--") {
926 let needle = opt.chars().nth(1).unwrap_or('\0');
927 if arg.starts_with('-') && !arg.starts_with("--") {
928 return arg.chars().skip(1).any(|c| c == needle);
929 }
930 }
931 false
932 }
933
934 pub fn get_version_output_from_args(&self, args: &[String]) -> Option<String> {
939 for opt in &self.options {
940 let meta = opt.config.metavar.as_deref()?;
941 let output = meta.strip_prefix(Self::VERSION_METAVAR_PREFIX)?;
942
943 let mut names = opt.long.iter().chain(opt.short.iter());
944 if names.any(|n| args.iter().any(|a| Self::arg_matches_opt(a, n))) {
945 return Some(output.to_string());
946 }
947 }
948 None
949 }
950
951 pub fn get_usage(&self, ctx: &Context) -> String {
955 let mut pieces = Vec::new();
956
957 if !self.options_metavar.is_empty() {
959 pieces.push(self.options_metavar.clone());
960 }
961
962 for arg in &self.arguments {
964 if !arg.hidden() {
965 pieces.push(arg.make_metavar());
966 }
967 }
968
969 format!("Usage: {} {}", ctx.command_path(), pieces.join(" "))
970 }
971
972 pub fn get_help(&self, ctx: &Context) -> String {
977 let mut parts = Vec::new();
978
979 parts.push(self.get_usage(ctx));
981
982 if let Some(ref help) = self.help {
984 let text = help.lines().next().unwrap_or("");
985 if !text.is_empty() {
986 parts.push(String::new()); let help_text = if let Some(ref dep) = self.deprecated {
988 if dep.is_empty() {
989 format!("{} (DEPRECATED)", text)
990 } else {
991 format!("{} (DEPRECATED: {})", text, dep)
992 }
993 } else {
994 text.to_string()
995 };
996 parts.push(format!(" {}", help_text));
997 }
998 } else if let Some(ref dep) = self.deprecated {
999 parts.push(String::new());
1000 let dep_msg = if dep.is_empty() {
1001 "(DEPRECATED)".to_string()
1002 } else {
1003 format!("(DEPRECATED: {})", dep)
1004 };
1005 parts.push(format!(" {}", dep_msg));
1006 }
1007
1008 let opt_records: Vec<(String, String)> = self
1010 .options
1011 .iter()
1012 .filter_map(|opt| opt.get_help_record())
1013 .collect();
1014
1015 let help_opt = self.get_help_option(ctx);
1017 let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
1018
1019 if !opt_records.is_empty() || help_record.is_some() {
1020 parts.push(String::new());
1021 parts.push("Options:".to_string());
1022
1023 for (opt_str, help) in &opt_records {
1024 parts.push(format!(" {} {}", opt_str, help));
1025 }
1026 if let Some((opt_str, help)) = help_record {
1027 parts.push(format!(" {} {}", opt_str, help));
1028 }
1029 }
1030
1031 let arg_records: Vec<(String, String)> = self
1033 .arguments
1034 .iter()
1035 .filter_map(|arg| arg.get_help_record())
1036 .filter(|(_, help)| !help.is_empty())
1037 .collect();
1038
1039 if !arg_records.is_empty() {
1040 parts.push(String::new());
1041 parts.push("Arguments:".to_string());
1042 for (metavar, help) in &arg_records {
1043 parts.push(format!(" {} {}", metavar, help));
1044 }
1045 }
1046
1047 if let Some(ref epilog) = self.epilog {
1049 parts.push(String::new());
1050 parts.push(epilog.clone());
1051 }
1052
1053 parts.join("\n")
1054 }
1055
1056 pub fn get_short_help(&self) -> String {
1061 if let Some(ref short_help) = self.short_help {
1062 let text = short_help.clone();
1063 if let Some(ref dep) = self.deprecated {
1064 if dep.is_empty() {
1065 format!("{} (DEPRECATED)", text)
1066 } else {
1067 format!("{} (DEPRECATED: {})", text, dep)
1068 }
1069 } else {
1070 text
1071 }
1072 } else if let Some(ref help) = self.help {
1073 let text = help
1075 .lines()
1076 .next()
1077 .unwrap_or("")
1078 .split('.')
1079 .next()
1080 .unwrap_or("")
1081 .trim();
1082 if let Some(ref dep) = self.deprecated {
1083 if dep.is_empty() {
1084 format!("{} (DEPRECATED)", text)
1085 } else {
1086 format!("{} (DEPRECATED: {})", text, dep)
1087 }
1088 } else {
1089 text.to_string()
1090 }
1091 } else if let Some(ref dep) = self.deprecated {
1092 if dep.is_empty() {
1093 "(DEPRECATED)".to_string()
1094 } else {
1095 format!("(DEPRECATED: {})", dep)
1096 }
1097 } else {
1098 String::new()
1099 }
1100 }
1101}
1102
1103pub struct CommandBuilder {
1113 name: String,
1114 callback: Option<CommandCallback>,
1115 options: Vec<ClickOption>,
1116 arguments: Vec<Argument>,
1117 help: Option<String>,
1118 epilog: Option<String>,
1119 short_help: Option<String>,
1120 options_metavar: String,
1121 add_help_option: bool,
1122 help_option: Option<ClickOption>,
1123 no_args_is_help: bool,
1124 hidden: bool,
1125 deprecated: Option<String>,
1126 allow_extra_args: bool,
1127 allow_interspersed_args: bool,
1128 ignore_unknown_options: bool,
1129}
1130
1131impl CommandBuilder {
1132 pub fn new(name: &str) -> Self {
1134 Self {
1135 name: name.to_string(),
1136 callback: None,
1137 options: Vec::new(),
1138 arguments: Vec::new(),
1139 help: None,
1140 epilog: None,
1141 short_help: None,
1142 options_metavar: "[OPTIONS]".to_string(),
1143 add_help_option: true,
1144 help_option: None,
1145 no_args_is_help: false,
1146 hidden: false,
1147 deprecated: None,
1148 allow_extra_args: false,
1149 allow_interspersed_args: true,
1150 ignore_unknown_options: false,
1151 }
1152 }
1153
1154 pub fn callback<F>(mut self, f: F) -> Self
1159 where
1160 F: Fn(&Context) -> Result<(), ClickError> + Send + Sync + 'static,
1161 {
1162 self.callback = Some(Box::new(f));
1163 self
1164 }
1165
1166 pub fn option(mut self, opt: ClickOption) -> Self {
1168 self.options.push(opt);
1169 self
1170 }
1171
1172 pub fn argument(mut self, arg: Argument) -> Self {
1174 self.arguments.push(arg);
1175 self
1176 }
1177
1178 pub fn help(mut self, help: &str) -> Self {
1180 self.help = Some(help.to_string());
1181 self
1182 }
1183
1184 pub fn epilog(mut self, epilog: &str) -> Self {
1186 self.epilog = Some(epilog.to_string());
1187 self
1188 }
1189
1190 pub fn short_help(mut self, short_help: &str) -> Self {
1192 self.short_help = Some(short_help.to_string());
1193 self
1194 }
1195
1196 pub fn options_metavar(mut self, metavar: &str) -> Self {
1198 self.options_metavar = metavar.to_string();
1199 self
1200 }
1201
1202 pub fn add_help_option(mut self, add: bool) -> Self {
1204 self.add_help_option = add;
1205 self
1206 }
1207
1208 pub fn help_option(mut self, opt: ClickOption) -> Self {
1215 self.add_help_option = true;
1216 self.help_option = Some(opt);
1217 self
1218 }
1219
1220 pub fn no_args_is_help(mut self, value: bool) -> Self {
1222 self.no_args_is_help = value;
1223 self
1224 }
1225
1226 pub fn hidden(mut self) -> Self {
1228 self.hidden = true;
1229 self
1230 }
1231
1232 pub fn deprecated(mut self, message: &str) -> Self {
1237 self.deprecated = Some(message.to_string());
1238 self
1239 }
1240
1241 pub fn allow_extra_args(mut self, allow: bool) -> Self {
1243 self.allow_extra_args = allow;
1244 self
1245 }
1246
1247 pub fn allow_interspersed_args(mut self, allow: bool) -> Self {
1249 self.allow_interspersed_args = allow;
1250 self
1251 }
1252
1253 pub fn ignore_unknown_options(mut self, ignore: bool) -> Self {
1255 self.ignore_unknown_options = ignore;
1256 self
1257 }
1258
1259 pub fn build(self) -> Command {
1261 Command {
1262 name: Some(self.name),
1263 callback: self.callback,
1264 options: self.options,
1265 arguments: self.arguments,
1266 help: self.help,
1267 epilog: self.epilog,
1268 short_help: self.short_help,
1269 options_metavar: self.options_metavar,
1270 add_help_option: self.add_help_option,
1271 no_args_is_help: self.no_args_is_help,
1272 hidden: self.hidden,
1273 deprecated: self.deprecated,
1274 allow_extra_args: self.allow_extra_args,
1275 allow_interspersed_args: self.allow_interspersed_args,
1276 ignore_unknown_options: self.ignore_unknown_options,
1277 help_option: self.help_option,
1278 }
1279 }
1280}
1281
1282fn make_help_option(names: &[String]) -> ClickOption {
1288 let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
1290
1291 ClickOption::new(&name_refs)
1292 .flag("true")
1293 .eager()
1294 .help("Show this message and exit.")
1295 .build()
1296}
1297
1298#[cfg(test)]
1303mod tests {
1304 use super::*;
1305 use crate::source::ParameterSource;
1306 use crate::types::INT;
1307 use std::collections::HashMap;
1308 use std::sync::Arc;
1309
1310 #[test]
1311 fn test_command_creation_defaults() {
1312 let cmd = Command::new("test").build();
1313
1314 assert_eq!(cmd.name, Some("test".to_string()));
1315 assert!(cmd.callback.is_none());
1316 assert!(cmd.options.is_empty());
1317 assert!(cmd.arguments.is_empty());
1318 assert!(cmd.help.is_none());
1319 assert!(cmd.epilog.is_none());
1320 assert!(cmd.short_help.is_none());
1321 assert_eq!(cmd.options_metavar, "[OPTIONS]");
1322 assert!(cmd.add_help_option);
1323 assert!(!cmd.no_args_is_help);
1324 assert!(!cmd.hidden);
1325 assert!(cmd.deprecated.is_none());
1326 assert!(!cmd.allow_extra_args);
1327 assert!(cmd.allow_interspersed_args);
1328 assert!(!cmd.ignore_unknown_options);
1329 }
1330
1331 #[test]
1332 fn test_command_builder_chain() {
1333 let cmd = Command::new("hello")
1334 .help("Say hello to someone")
1335 .epilog("Example: hello --name World")
1336 .short_help("Say hello")
1337 .options_metavar("[OPTS]")
1338 .add_help_option(false)
1339 .no_args_is_help(true)
1340 .hidden()
1341 .deprecated("Use 'greet' instead")
1342 .allow_extra_args(true)
1343 .allow_interspersed_args(false)
1344 .ignore_unknown_options(true)
1345 .build();
1346
1347 assert_eq!(cmd.name, Some("hello".to_string()));
1348 assert_eq!(cmd.help, Some("Say hello to someone".to_string()));
1349 assert_eq!(cmd.epilog, Some("Example: hello --name World".to_string()));
1350 assert_eq!(cmd.short_help, Some("Say hello".to_string()));
1351 assert_eq!(cmd.options_metavar, "[OPTS]");
1352 assert!(!cmd.add_help_option);
1353 assert!(cmd.no_args_is_help);
1354 assert!(cmd.hidden);
1355 assert_eq!(cmd.deprecated, Some("Use 'greet' instead".to_string()));
1356 assert!(cmd.allow_extra_args);
1357 assert!(!cmd.allow_interspersed_args);
1358 assert!(cmd.ignore_unknown_options);
1359 }
1360
1361 #[test]
1362 fn test_command_with_callback() {
1363 use std::sync::atomic::{AtomicBool, Ordering};
1364
1365 let called = Arc::new(AtomicBool::new(false));
1366 let called_clone = Arc::clone(&called);
1367
1368 let cmd = Command::new("test")
1369 .callback(move |_ctx| {
1370 called_clone.store(true, Ordering::SeqCst);
1371 Ok(())
1372 })
1373 .build();
1374
1375 assert!(cmd.callback.is_some());
1376
1377 let ctx = ContextBuilder::new().info_name("test").build();
1379 let result = cmd.invoke(&ctx);
1380 assert!(result.is_ok());
1381 assert!(called.load(Ordering::SeqCst));
1382 }
1383
1384 #[test]
1385 fn test_command_with_option() {
1386 let cmd = Command::new("greet")
1387 .option(
1388 ClickOption::new(&["--name", "-n"])
1389 .help("Name to greet")
1390 .default("World")
1391 .build(),
1392 )
1393 .build();
1394
1395 assert_eq!(cmd.options.len(), 1);
1396 assert_eq!(cmd.options[0].name(), "name");
1397 }
1398
1399 #[test]
1400 fn test_command_with_argument() {
1401 let cmd = Command::new("cat")
1402 .argument(Argument::new("file").help("File to read").build())
1403 .build();
1404
1405 assert_eq!(cmd.arguments.len(), 1);
1406 assert_eq!(cmd.arguments[0].name(), "file");
1407 }
1408
1409 #[test]
1410 fn test_command_with_multiple_params() {
1411 let cmd = Command::new("copy")
1412 .option(
1413 ClickOption::new(&["--recursive", "-r"])
1414 .flag("true")
1415 .help("Copy recursively")
1416 .build(),
1417 )
1418 .argument(Argument::new("src").help("Source path").build())
1419 .argument(Argument::new("dst").help("Destination path").build())
1420 .build();
1421
1422 assert_eq!(cmd.options.len(), 1);
1423 assert_eq!(cmd.arguments.len(), 2);
1424 }
1425
1426 #[test]
1427 fn test_make_context_basic() {
1428 let cmd = Command::new("hello").build();
1429 let ctx = cmd.make_context("hello", vec![], None);
1430
1431 assert!(ctx.is_ok());
1432 let ctx = ctx.unwrap();
1433 assert_eq!(ctx.info_name(), Some("hello"));
1434 }
1435
1436 #[test]
1437 fn test_parse_args_with_option() {
1438 let cmd = Command::new("greet")
1439 .option(
1440 ClickOption::new(&["--name", "-n"])
1441 .default("World")
1442 .build(),
1443 )
1444 .build();
1445
1446 let ctx = cmd.make_context("greet", vec!["--name".to_string(), "Alice".to_string()], None);
1447 assert!(ctx.is_ok());
1448
1449 let ctx = ctx.unwrap();
1450 let name = ctx.get_param::<String>("name");
1451 assert_eq!(name, Some(&"Alice".to_string()));
1452 }
1453
1454 #[test]
1455 fn test_parse_args_with_argument() {
1456 let cmd = Command::new("cat")
1457 .argument(Argument::new("file").build())
1458 .build();
1459
1460 let ctx = cmd.make_context("cat", vec!["test.txt".to_string()], None);
1461 assert!(ctx.is_ok());
1462
1463 let ctx = ctx.unwrap();
1464 let file = ctx.get_param::<String>("file");
1465 assert_eq!(file, Some(&"test.txt".to_string()));
1466 }
1467
1468 #[test]
1469 fn test_parse_args_missing_required() {
1470 let cmd = Command::new("cat")
1471 .argument(Argument::new("file").build())
1472 .build();
1473
1474 let ctx = cmd.make_context("cat", vec![], None);
1475 assert!(ctx.is_err());
1476
1477 let err = ctx.unwrap_err();
1478 assert!(matches!(err, ClickError::MissingParameter { .. }));
1479 }
1480
1481 #[test]
1482 fn test_parse_args_extra_args_error() {
1483 let cmd = Command::new("hello").build();
1484
1485 let ctx = cmd.make_context("hello", vec!["extra".to_string()], None);
1486 assert!(ctx.is_err());
1487
1488 let err = ctx.unwrap_err();
1489 assert!(matches!(err, ClickError::UsageError { .. }));
1490 }
1491
1492 #[test]
1493 fn test_parse_args_extra_args_allowed() {
1494 let cmd = Command::new("hello").allow_extra_args(true).build();
1495
1496 let ctx = cmd.make_context("hello", vec!["extra".to_string()], None);
1497 assert!(ctx.is_ok());
1498
1499 let ctx = ctx.unwrap();
1500 assert_eq!(ctx.args(), &["extra".to_string()]);
1501 }
1502
1503 #[test]
1504 fn test_get_usage() {
1505 let cmd = Command::new("copy")
1506 .argument(Argument::new("src").build())
1507 .argument(Argument::new("dst").build())
1508 .build();
1509
1510 let ctx = ContextBuilder::new().info_name("copy").build();
1511 let usage = cmd.get_usage(&ctx);
1512
1513 assert!(usage.contains("Usage:"));
1514 assert!(usage.contains("copy"));
1515 assert!(usage.contains("[OPTIONS]"));
1516 assert!(usage.contains("SRC"));
1517 assert!(usage.contains("DST"));
1518 }
1519
1520 #[test]
1521 fn test_get_help() {
1522 let cmd = Command::new("greet")
1523 .help("Greet someone")
1524 .option(
1525 ClickOption::new(&["--name", "-n"])
1526 .help("Name to greet")
1527 .build(),
1528 )
1529 .epilog("Example: greet --name World")
1530 .build();
1531
1532 let ctx = ContextBuilder::new().info_name("greet").build();
1533 let help = cmd.get_help(&ctx);
1534
1535 assert!(help.contains("Usage:"));
1536 assert!(help.contains("Greet someone"));
1537 assert!(help.contains("Options:"));
1538 assert!(help.contains("--name"));
1539 assert!(help.contains("Name to greet"));
1540 assert!(help.contains("Example:"));
1541 }
1542
1543 #[test]
1544 fn test_get_short_help() {
1545 let cmd = Command::new("test").short_help("Test command").build();
1547 assert_eq!(cmd.get_short_help(), "Test command");
1548
1549 let cmd = Command::new("test")
1551 .help("This is a test. It does things.")
1552 .build();
1553 assert_eq!(cmd.get_short_help(), "This is a test");
1554
1555 let cmd = Command::new("test")
1557 .short_help("Test command")
1558 .deprecated("Use 'new-test' instead")
1559 .build();
1560 assert!(cmd.get_short_help().contains("DEPRECATED"));
1561 assert!(cmd.get_short_help().contains("Use 'new-test' instead"));
1562 }
1563
1564 #[test]
1565 fn test_make_help_option() {
1566 let help_opt = make_help_option(&["--help".to_string(), "-h".to_string()]);
1567
1568 assert!(help_opt.is_flag);
1569 assert!(help_opt.is_eager());
1570 assert_eq!(help_opt.help(), Some("Show this message and exit."));
1571 }
1572
1573 #[test]
1574 fn test_command_debug() {
1575 let cmd = Command::new("test").build();
1576 let debug_str = format!("{:?}", cmd);
1577
1578 assert!(debug_str.contains("Command"));
1579 assert!(debug_str.contains("test"));
1580 }
1581
1582 #[test]
1583 fn test_invoke_with_deprecation() {
1584 let cmd = Command::new("old")
1585 .deprecated("Use 'new' instead")
1586 .callback(|_| Ok(()))
1587 .build();
1588
1589 let ctx = ContextBuilder::new().info_name("old").build();
1590 let result = cmd.invoke(&ctx);
1592 assert!(result.is_ok());
1593 }
1594
1595 #[test]
1596 fn test_parse_flag_option() {
1597 let cmd = Command::new("test")
1598 .option(
1599 ClickOption::new(&["--verbose", "-v"])
1600 .flag("true")
1601 .build(),
1602 )
1603 .build();
1604
1605 let ctx = cmd.make_context("test", vec!["--verbose".to_string()], None);
1606 assert!(ctx.is_ok());
1607
1608 let ctx = ctx.unwrap();
1609 let verbose = ctx.get_param::<String>("verbose");
1610 assert_eq!(verbose, Some(&"true".to_string()));
1611 }
1612
1613 #[test]
1614 fn test_parse_count_option() {
1615 let cmd = Command::new("test")
1616 .option(ClickOption::new(&["--verbose", "-v"]).count().build())
1617 .build();
1618
1619 let ctx = cmd.make_context(
1620 "test",
1621 vec!["-v".to_string(), "-v".to_string(), "-v".to_string()],
1622 None,
1623 );
1624 assert!(ctx.is_ok());
1625
1626 let ctx = ctx.unwrap();
1627 let verbose = ctx.get_param::<usize>("verbose");
1628 assert_eq!(verbose, Some(&3));
1629 }
1630
1631 #[test]
1632 fn test_option_type_conversion() {
1633 let cmd = Command::new("test")
1634 .option(ClickOption::new(&["--count"]).type_any(INT).build())
1635 .build();
1636
1637 let ctx = cmd.make_context("test", vec!["--count".to_string(), "42".to_string()], None);
1638 assert!(ctx.is_ok());
1639
1640 let ctx = ctx.unwrap();
1641 let count = ctx.get_param::<i64>("count");
1642 assert_eq!(count, Some(&42));
1643 }
1644
1645 #[test]
1646 fn test_option_callback_applied() {
1647 let cmd = Command::new("test")
1648 .option(
1649 ClickOption::new(&["--count"])
1650 .type_any(INT)
1651 .callback(|_ctx, _param, value| {
1652 let count = *value
1653 .as_ref()
1654 .downcast_ref::<i64>()
1655 .ok_or_else(|| ClickError::bad_parameter("bad callback value"))?;
1656 Ok(Arc::new(count + 1))
1657 })
1658 .build(),
1659 )
1660 .build();
1661
1662 let ctx = cmd.make_context("test", vec!["--count".to_string(), "2".to_string()], None);
1663 assert!(ctx.is_ok());
1664
1665 let ctx = ctx.unwrap();
1666 let count = ctx.get_param::<i64>("count");
1667 assert_eq!(count, Some(&3));
1668 }
1669
1670 #[test]
1671 fn test_option_with_default() {
1672 let cmd = Command::new("greet")
1673 .option(
1674 ClickOption::new(&["--name"])
1675 .default("World")
1676 .build(),
1677 )
1678 .build();
1679
1680 let ctx = cmd.make_context("greet", vec![], None);
1682 assert!(ctx.is_ok());
1683
1684 let ctx = ctx.unwrap();
1685 let name = ctx.get_param::<String>("name");
1686 assert_eq!(name, Some(&"World".to_string()));
1687 assert_eq!(ctx.get_parameter_source("name"), Some(ParameterSource::Default));
1688 }
1689
1690 #[test]
1691 fn test_option_default_map_value() {
1692 let cmd = Command::new("greet")
1693 .option(ClickOption::new(&["--name"]).build())
1694 .build();
1695
1696 let mut defaults: HashMap<String, Arc<dyn std::any::Any + Send + Sync>> = HashMap::new();
1697 defaults.insert("name".to_string(), Arc::new("Bob".to_string()));
1698
1699 let mut ctx = ContextBuilder::new()
1700 .info_name("greet")
1701 .default_map(defaults)
1702 .build();
1703
1704 let result = cmd.parse_args(&mut ctx, vec![]);
1705 assert!(result.is_ok());
1706
1707 let name = ctx.get_param::<String>("name");
1708 assert_eq!(name, Some(&"Bob".to_string()));
1709 assert_eq!(
1710 ctx.get_parameter_source("name"),
1711 Some(ParameterSource::DefaultMap)
1712 );
1713 }
1714
1715 #[test]
1716 fn test_option_envvar_value() {
1717 std::env::set_var("CLICK_TEST_COUNT", "9");
1718 let cmd = Command::new("test")
1719 .option(
1720 ClickOption::new(&["--count"])
1721 .envvar("CLICK_TEST_COUNT")
1722 .type_any(INT)
1723 .build(),
1724 )
1725 .build();
1726
1727 let ctx = cmd.make_context("test", vec![], None);
1728 std::env::remove_var("CLICK_TEST_COUNT");
1729
1730 assert!(ctx.is_ok());
1731 let ctx = ctx.unwrap();
1732 let count = ctx.get_param::<i64>("count");
1733 assert_eq!(count, Some(&9));
1734 assert_eq!(
1735 ctx.get_parameter_source("count"),
1736 Some(ParameterSource::Environment)
1737 );
1738 }
1739
1740 #[test]
1741 fn test_argument_type_conversion() {
1742 let cmd = Command::new("greet")
1743 .argument(Argument::new("count").type_(INT).build())
1744 .build();
1745
1746 let ctx = cmd.make_context("greet", vec!["7".to_string()], None);
1747 assert!(ctx.is_ok());
1748
1749 let ctx = ctx.unwrap();
1750 let count = ctx.get_param::<i64>("count");
1751 assert_eq!(count, Some(&7));
1752 assert_eq!(
1753 ctx.get_parameter_source("count"),
1754 Some(ParameterSource::CommandLine)
1755 );
1756 }
1757
1758 #[test]
1759 fn test_argument_callback_applied() {
1760 let cmd = Command::new("greet")
1761 .argument(
1762 Argument::new("count")
1763 .type_(INT)
1764 .callback(|_ctx, _param, value| {
1765 let count = *value
1766 .as_ref()
1767 .downcast_ref::<i64>()
1768 .ok_or_else(|| ClickError::bad_parameter("bad callback value"))?;
1769 Ok(Arc::new(count * 2))
1770 })
1771 .build(),
1772 )
1773 .build();
1774
1775 let ctx = cmd.make_context("greet", vec!["3".to_string()], None);
1776 assert!(ctx.is_ok());
1777
1778 let ctx = ctx.unwrap();
1779 let count = ctx.get_param::<i64>("count");
1780 assert_eq!(count, Some(&6));
1781 }
1782
1783 #[test]
1784 fn test_argument_type_conversion_error() {
1785 let cmd = Command::new("greet")
1786 .argument(Argument::new("count").type_(INT).build())
1787 .build();
1788
1789 let ctx = cmd.make_context("greet", vec!["nope".to_string()], None);
1790 assert!(matches!(ctx, Err(ClickError::BadParameter { .. })));
1791 }
1792
1793 #[test]
1794 fn test_argument_with_default() {
1795 let cmd = Command::new("greet")
1796 .argument(Argument::new("name").default("World").build())
1797 .build();
1798
1799 let ctx = cmd.make_context("greet", vec![], None);
1801 assert!(ctx.is_ok());
1802
1803 let ctx = ctx.unwrap();
1804 let name = ctx.get_param::<String>("name");
1805 assert_eq!(name, Some(&"World".to_string()));
1806 }
1807
1808 #[test]
1809 fn test_argument_default_map_value() {
1810 let cmd = Command::new("greet")
1811 .argument(Argument::new("name").build())
1812 .build();
1813
1814 let mut defaults: HashMap<String, Arc<dyn std::any::Any + Send + Sync>> = HashMap::new();
1815 defaults.insert("name".to_string(), Arc::new("Alice".to_string()));
1816
1817 let mut ctx = ContextBuilder::new()
1818 .info_name("greet")
1819 .default_map(defaults)
1820 .build();
1821
1822 let result = cmd.parse_args(&mut ctx, vec![]);
1823 assert!(result.is_ok());
1824
1825 let name = ctx.get_param::<String>("name");
1826 assert_eq!(name, Some(&"Alice".to_string()));
1827 }
1828
1829 #[test]
1830 fn test_argument_auto_envvar_prefix() {
1831 std::env::set_var("MYAPP_NAME", "Alice");
1832 let cmd = Command::new("greet")
1833 .argument(Argument::new("name").build())
1834 .build();
1835
1836 let mut ctx = ContextBuilder::new()
1837 .info_name("greet")
1838 .auto_envvar_prefix("MYAPP")
1839 .build();
1840
1841 let result = cmd.parse_args(&mut ctx, vec![]);
1842 std::env::remove_var("MYAPP_NAME");
1843
1844 assert!(result.is_ok());
1845 let name = ctx.get_param::<String>("name");
1846 assert_eq!(name, Some(&"Alice".to_string()));
1847 }
1848
1849 #[test]
1854 fn test_optional_argument_with_value() {
1855 use crate::parameter::Nargs;
1856
1857 let cmd = Command::new("test")
1858 .argument(
1859 Argument::new("file")
1860 .nargs(Nargs::Optional)
1861 .default("default.txt")
1862 .build(),
1863 )
1864 .build();
1865
1866 let ctx = cmd.make_context("test", vec!["input.txt".to_string()], None);
1868 assert!(ctx.is_ok());
1869
1870 let ctx = ctx.unwrap();
1871 let file = ctx.get_param::<String>("file");
1872 assert_eq!(file, Some(&"input.txt".to_string()));
1873 }
1874
1875 #[test]
1876 fn test_optional_argument_without_value() {
1877 use crate::parameter::Nargs;
1878
1879 let cmd = Command::new("test")
1880 .argument(
1881 Argument::new("file")
1882 .nargs(Nargs::Optional)
1883 .default("default.txt")
1884 .build(),
1885 )
1886 .build();
1887
1888 let ctx = cmd.make_context("test", vec![], None);
1890 assert!(ctx.is_ok());
1891
1892 let ctx = ctx.unwrap();
1893 let file = ctx.get_param::<String>("file");
1894 assert_eq!(file, Some(&"default.txt".to_string()));
1895 }
1896
1897 #[test]
1898 fn test_required_followed_by_optional_argument() {
1899 use crate::parameter::Nargs;
1900
1901 let cmd = Command::new("copy")
1902 .argument(Argument::new("src").build()) .argument(
1904 Argument::new("dst")
1905 .nargs(Nargs::Optional)
1906 .default(".")
1907 .build(),
1908 )
1909 .build();
1910
1911 let ctx = cmd.make_context("copy", vec!["source.txt".to_string()], None);
1913 assert!(ctx.is_ok());
1914
1915 let ctx = ctx.unwrap();
1916 let src = ctx.get_param::<String>("src");
1917 let dst = ctx.get_param::<String>("dst");
1918 assert_eq!(src, Some(&"source.txt".to_string()));
1919 assert_eq!(dst, Some(&".".to_string()));
1920 }
1921
1922 #[test]
1923 fn test_required_followed_by_optional_with_both_values() {
1924 use crate::parameter::Nargs;
1925
1926 let cmd = Command::new("copy")
1927 .argument(Argument::new("src").build()) .argument(
1929 Argument::new("dst")
1930 .nargs(Nargs::Optional)
1931 .default(".")
1932 .build(),
1933 )
1934 .build();
1935
1936 let ctx = cmd.make_context(
1938 "copy",
1939 vec!["source.txt".to_string(), "dest.txt".to_string()],
1940 None,
1941 );
1942 assert!(ctx.is_ok());
1943
1944 let ctx = ctx.unwrap();
1945 let src = ctx.get_param::<String>("src");
1946 let dst = ctx.get_param::<String>("dst");
1947 assert_eq!(src, Some(&"source.txt".to_string()));
1948 assert_eq!(dst, Some(&"dest.txt".to_string()));
1949 }
1950
1951 #[test]
1956 fn test_multi_value_argument_incomplete_error() {
1957 use crate::parameter::Nargs;
1958
1959 let cmd = Command::new("point")
1960 .argument(Argument::new("coords").nargs(Nargs::Count(3)).build())
1961 .build();
1962
1963 let ctx = cmd.make_context(
1965 "point",
1966 vec!["1".to_string(), "2".to_string()],
1967 None,
1968 );
1969
1970 assert!(ctx.is_err());
1972 let err = ctx.unwrap_err();
1973 assert!(err.to_string().contains("takes 3 values"));
1974 }
1975
1976 #[test]
1981 fn test_optional_option_value_with_flag_value() {
1982 use crate::parameter::Nargs;
1983
1984 let cmd = Command::new("test")
1985 .option(
1986 ClickOption::new(&["--opt"])
1987 .nargs(Nargs::Optional)
1988 .flag("flagval") .default("default")
1990 .build(),
1991 )
1992 .build();
1993
1994 let ctx = cmd.make_context("test", vec!["--opt".to_string()], None);
1996 assert!(ctx.is_ok());
1997
1998 let ctx = ctx.unwrap();
1999 let opt = ctx.get_param::<String>("opt");
2000 assert_eq!(opt, Some(&"flagval".to_string()));
2002 }
2003
2004 #[test]
2005 fn test_flag_value_group_shared_destination() {
2006 let cmd = Command::new("mine")
2007 .option(
2008 ClickOption::new(&["--moored", "-m"])
2009 .dest("ty")
2010 .flag("moored")
2011 .default("moored")
2012 .build(),
2013 )
2014 .option(
2015 ClickOption::new(&["--drifting", "-d"])
2016 .dest("ty")
2017 .flag("drifting")
2018 .build(),
2019 )
2020 .build();
2021
2022 let ctx_default = cmd.make_context("mine", vec![], None).unwrap();
2023 assert_eq!(ctx_default.get_param::<String>("ty"), Some(&"moored".to_string()));
2024
2025 let ctx_drifting = cmd
2026 .make_context("mine", vec!["--drifting".to_string()], None)
2027 .unwrap();
2028 assert_eq!(
2029 ctx_drifting.get_param::<String>("ty"),
2030 Some(&"drifting".to_string())
2031 );
2032
2033 let ctx_moored = cmd
2034 .make_context("mine", vec!["--moored".to_string()], None)
2035 .unwrap();
2036 assert_eq!(ctx_moored.get_param::<String>("ty"), Some(&"moored".to_string()));
2037 }
2038
2039 #[test]
2040 fn test_flag_value_group_last_option_wins() {
2041 let cmd = Command::new("mine")
2042 .option(
2043 ClickOption::new(&["--moored"])
2044 .dest("ty")
2045 .flag("moored")
2046 .default("moored")
2047 .build(),
2048 )
2049 .option(
2050 ClickOption::new(&["--drifting"])
2051 .dest("ty")
2052 .flag("drifting")
2053 .build(),
2054 )
2055 .build();
2056
2057 let ctx = cmd
2058 .make_context(
2059 "mine",
2060 vec!["--moored".to_string(), "--drifting".to_string()],
2061 None,
2062 )
2063 .unwrap();
2064 assert_eq!(ctx.get_param::<String>("ty"), Some(&"drifting".to_string()));
2065 }
2066
2067 #[test]
2072 fn test_help_with_missing_required_arg() {
2073 let cmd = Command::new("test")
2075 .argument(Argument::new("required_file").build())
2076 .build();
2077
2078 let ctx = cmd.make_context("test", vec![], None);
2080 assert!(ctx.is_err());
2081
2082 let ctx = cmd.make_context("test", vec!["--help".to_string()], None);
2084 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2085 }
2086
2087 #[test]
2088 fn test_help_with_missing_required_option() {
2089 let cmd = Command::new("test")
2091 .option(
2092 ClickOption::new(&["--name", "-n"])
2093 .required()
2094 .build(),
2095 )
2096 .build();
2097
2098 let ctx = cmd.make_context("test", vec![], None);
2100 assert!(ctx.is_err());
2101
2102 let ctx = cmd.make_context("test", vec!["--help".to_string()], None);
2104 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2105 }
2106}