1use std::any::Any;
12use std::fmt;
13use std::sync::Arc;
14
15use crate::context::Context;
16use crate::error::ClickError;
17use crate::parameter::{Nargs, Parameter, ParameterCallback, ParameterConfig};
18use crate::argument::AnyTypeConverter;
19use crate::types::{CompletionItem, StringType, TypeConverter, STRING};
20
21pub type ShellCompleteCallback = Arc<dyn Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync>;
23
24pub fn parse_option_name(name: &str) -> Result<(String, bool), String> {
37 if let Some(stripped) = name.strip_prefix("--") {
38 if stripped.is_empty() {
39 return Err("Long option name cannot be empty: '--'".to_string());
40 }
41 if stripped.starts_with('-') {
42 return Err(format!(
43 "Long option name cannot start with dash: '{}'",
44 name
45 ));
46 }
47 Ok((stripped.to_string(), true))
48 } else if let Some(stripped) = name.strip_prefix('-') {
49 if stripped.is_empty() {
50 return Err("Short option name cannot be empty: '-'".to_string());
51 }
52 if stripped.len() != 1 {
53 return Err(format!(
54 "Short option must be a single character: '{}' (use '--{}' for long options)",
55 name, stripped
56 ));
57 }
58 if stripped.starts_with('-') {
59 return Err(format!("Short option cannot be a dash: '{}'", name));
60 }
61 Ok((stripped.to_string(), false))
62 } else {
63 Err(format!(
64 "Option name must start with '-' or '--': '{}'",
65 name
66 ))
67 }
68}
69
70pub fn split_option_names(names: &[&str]) -> Result<(Vec<String>, Vec<String>), String> {
80 let mut long = Vec::new();
81 let mut short = Vec::new();
82
83 for name in names {
84 let (_, is_long) = parse_option_name(name)?;
85 if is_long {
86 long.push(name.to_string());
87 } else {
88 short.push(name.to_string());
89 }
90 }
91
92 if long.is_empty() && short.is_empty() {
93 return Err("At least one option name is required".to_string());
94 }
95
96 Ok((long, short))
97}
98
99fn derive_param_name(long: &[String], short: &[String]) -> String {
103 let source = if !long.is_empty() {
104 long.iter()
106 .max_by_key(|s| s.len())
107 .map(|s| s.trim_start_matches('-'))
108 .unwrap_or("")
109 } else if !short.is_empty() {
110 short
111 .first()
112 .map(|s| s.trim_start_matches('-'))
113 .unwrap_or("")
114 } else {
115 ""
116 };
117
118 source.replace('-', "_")
119}
120
121#[derive(Clone)]
137pub struct ClickOption {
138 pub config: ParameterConfig,
140
141 pub long: Vec<String>,
143
144 pub short: Vec<String>,
146
147 pub is_flag: bool,
149
150 pub is_bool_flag: bool,
152
153 pub flag_value: Option<String>,
155
156 pub secondary_value: Option<String>,
158
159 pub count: bool,
161
162 pub prompt: Option<String>,
164
165 pub confirmation_prompt: bool,
167
168 pub hide_input: bool,
170
171 pub show_default: bool,
173
174 pub show_envvar: bool,
176
177 pub default: Option<String>,
179
180 type_name: String,
182 type_metavar: Option<String>,
184 type_converter: Arc<dyn AnyTypeConverter>,
186
187 shell_complete_callback: Option<ShellCompleteCallback>,
189}
190
191impl fmt::Debug for ClickOption {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 f.debug_struct("ClickOption")
194 .field("config", &self.config)
195 .field("long", &self.long)
196 .field("short", &self.short)
197 .field("is_flag", &self.is_flag)
198 .field("is_bool_flag", &self.is_bool_flag)
199 .field("flag_value", &self.flag_value)
200 .field("secondary_value", &self.secondary_value)
201 .field("count", &self.count)
202 .field("prompt", &self.prompt)
203 .field("confirmation_prompt", &self.confirmation_prompt)
204 .field("hide_input", &self.hide_input)
205 .field("show_default", &self.show_default)
206 .field("show_envvar", &self.show_envvar)
207 .field("default", &self.default)
208 .field("type_name", &self.type_name)
209 .field("has_shell_complete", &self.shell_complete_callback.is_some())
210 .field("has_type_converter", &true)
211 .finish()
212 }
213}
214
215impl ClickOption {
216 #[allow(clippy::new_ret_no_self)]
224 pub fn new(names: &[&str]) -> OptionBuilder {
225 OptionBuilder::new(names)
226 }
227
228 pub fn opts_string(&self) -> String {
230 let mut parts: Vec<&str> = Vec::new();
231 for s in &self.short {
232 parts.push(s);
233 }
234 for l in &self.long {
235 parts.push(l);
236 }
237 parts.join(", ")
238 }
239
240 pub fn primary_opt(&self) -> &str {
242 self.long
243 .first()
244 .or(self.short.first())
245 .map(|s| s.as_str())
246 .unwrap_or("")
247 }
248
249 pub fn type_converter(&self) -> &dyn AnyTypeConverter {
251 self.type_converter.as_ref()
252 }
253
254 pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
257 self.type_converter.convert_any(value)
258 }
259
260 pub fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String> {
263 self.type_converter.convert_multi(values)
264 }
265
266 pub fn get_completions(&self, ctx: &Context, incomplete: &str) -> Vec<CompletionItem> {
271 if let Some(ref callback) = self.shell_complete_callback {
272 callback(ctx, incomplete)
273 } else {
274 self.type_converter.shell_complete(incomplete)
275 }
276 }
277
278 pub fn has_shell_complete_callback(&self) -> bool {
280 self.shell_complete_callback.is_some()
281 }
282}
283
284impl Parameter for ClickOption {
285 fn name(&self) -> &str {
286 &self.config.name
287 }
288
289 fn human_readable_name(&self) -> String {
290 self.opts_string()
291 }
292
293 fn nargs(&self) -> Nargs {
294 self.config.nargs
295 }
296
297 fn multiple(&self) -> bool {
298 self.config.multiple
299 }
300
301 fn is_eager(&self) -> bool {
302 self.config.is_eager
303 }
304
305 fn expose_value(&self) -> bool {
306 self.config.expose_value
307 }
308
309 fn required(&self) -> bool {
310 self.config.required
311 }
312
313 fn envvar(&self) -> Option<&[String]> {
314 self.config.envvar.as_deref()
315 }
316
317 fn help(&self) -> Option<&str> {
318 self.config.help.as_deref()
319 }
320
321 fn hidden(&self) -> bool {
322 self.config.hidden
323 }
324
325 fn get_metavar(&self) -> Option<String> {
326 if let Some(ref mv) = self.config.metavar {
328 return Some(mv.clone());
329 }
330 if let Some(ref mv) = self.type_metavar {
332 if mv.contains('|') && !(mv.starts_with('[') && mv.ends_with(']')) {
333 return Some(format!("[{}]", mv));
334 }
335 return Some(mv.clone());
336 }
337 if !self.type_name.is_empty() {
338 return Some(self.type_name.clone());
339 }
340 None
341 }
342
343 fn get_help_record(&self) -> Option<(String, String)> {
344 if self.hidden() {
345 return None;
346 }
347
348 let mut opt_parts = Vec::new();
350
351 for s in &self.short {
353 opt_parts.push(s.clone());
354 }
355
356 for l in &self.long {
358 opt_parts.push(l.clone());
359 }
360
361 let mut opt_str = opt_parts.join(", ");
362
363 if !self.is_flag && !self.count {
365 if let Some(metavar) = self.get_metavar() {
366 opt_str.push(' ');
367 opt_str.push_str(&metavar);
368 }
369 }
370
371 let mut help = self.help().unwrap_or("").to_string();
373 let mut extras = Vec::new();
374
375 if self.show_envvar {
377 if let Some(envvars) = self.envvar() {
378 extras.push(format!("env var: {}", envvars.join(", ")));
379 }
380 }
381
382 if self.show_default {
384 if let Some(ref default) = self.default {
385 if !default.is_empty() {
386 extras.push(format!("default: {}", default));
387 }
388 }
389 }
390
391 if self.required() {
393 extras.push("required".to_string());
394 }
395
396 if !extras.is_empty() {
398 let extra_str = extras.join("; ");
399 if help.is_empty() {
400 help = format!("[{}]", extra_str);
401 } else {
402 help = format!("{} [{}]", help, extra_str);
403 }
404 }
405
406 Some((opt_str, help))
407 }
408
409 fn param_type_name(&self) -> &str {
410 "option"
411 }
412}
413
414pub struct OptionBuilder {
420 long: Vec<String>,
421 short: Vec<String>,
422 name: String,
423 help: Option<String>,
424 is_flag: bool,
425 is_bool_flag: bool,
426 flag_value: Option<String>,
427 secondary_value: Option<String>,
428 count: bool,
429 required: bool,
430 default: Option<String>,
431 envvar: Option<Vec<String>>,
432 prompt: Option<String>,
433 confirmation_prompt: bool,
434 hide_input: bool,
435 multiple: bool,
436 hidden: bool,
437 eager: bool,
438 show_default: bool,
439 show_envvar: bool,
440 metavar: Option<String>,
441 type_name: String,
442 type_metavar: Option<String>,
443 type_converter: Option<Arc<dyn AnyTypeConverter>>,
444 shell_complete_callback: Option<ShellCompleteCallback>,
445 nargs: Nargs,
446 callback: Option<ParameterCallback>,
447}
448
449impl OptionBuilder {
450 pub fn new(names: &[&str]) -> Self {
456 let (long, short) = split_option_names(names).expect("Invalid option names");
457 let name = derive_param_name(&long, &short);
458
459 Self {
460 long,
461 short,
462 name,
463 help: None,
464 is_flag: false,
465 is_bool_flag: false,
466 flag_value: None,
467 secondary_value: None,
468 count: false,
469 required: false,
470 default: None,
471 envvar: None,
472 prompt: None,
473 confirmation_prompt: false,
474 hide_input: false,
475 multiple: false,
476 hidden: false,
477 eager: false,
478 show_default: false,
479 show_envvar: false,
480 metavar: None,
481 type_name: TypeConverter::name(&STRING).to_string(),
482 type_metavar: TypeConverter::get_metavar(&STRING),
483 type_converter: None,
484 shell_complete_callback: None,
485 nargs: Nargs::Count(1),
486 callback: None,
487 }
488 }
489
490 pub fn dest(mut self, name: &str) -> Self {
494 self.name = name.to_string();
495 self
496 }
497
498 pub fn help(mut self, help: &str) -> Self {
500 self.help = Some(help.to_string());
501 self
502 }
503
504 pub fn flag(mut self, value: &str) -> Self {
508 self.is_flag = true;
509 self.flag_value = Some(value.to_string());
510 self
511 }
512
513 pub fn bool_flag(mut self) -> Self {
517 self.is_flag = true;
518 self.is_bool_flag = true;
519 self.flag_value = Some("true".to_string());
520 self.secondary_value = Some("false".to_string());
521 self
522 }
523
524 pub fn count(mut self) -> Self {
528 self.count = true;
529 self.is_flag = true;
530 self.default = Some("0".to_string());
531 self
532 }
533
534 pub fn required(mut self) -> Self {
536 self.required = true;
537 self
538 }
539
540 pub fn default(mut self, value: impl Into<String>) -> Self {
542 self.default = Some(value.into());
543 self
544 }
545
546 pub fn envvar(mut self, name: &str) -> Self {
548 self.envvar = Some(vec![name.to_string()]);
549 self
550 }
551
552 pub fn envvars(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
554 self.envvar = Some(names.into_iter().map(|n| n.into()).collect());
555 self
556 }
557
558 pub fn prompt(mut self, text: &str) -> Self {
560 self.prompt = Some(text.to_string());
561 self
562 }
563
564 pub fn confirmation_prompt(mut self, confirm: bool) -> Self {
566 self.confirmation_prompt = confirm;
567 self
568 }
569
570 pub fn hide_input(mut self, hide: bool) -> Self {
572 self.hide_input = hide;
573 self
574 }
575
576 pub fn multiple(mut self) -> Self {
578 self.multiple = true;
579 self
580 }
581
582 pub fn callback<F>(mut self, callback: F) -> Self
584 where
585 F: Fn(&Context, &dyn Parameter, Arc<dyn Any + Send + Sync>)
586 -> Result<Arc<dyn Any + Send + Sync>, ClickError>
587 + Send
588 + Sync
589 + 'static,
590 {
591 self.callback = Some(Arc::new(callback));
592 self
593 }
594
595 pub fn shell_complete<F>(mut self, callback: F) -> Self
597 where
598 F: Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync + 'static,
599 {
600 self.shell_complete_callback = Some(Arc::new(callback));
601 self
602 }
603
604 pub fn hidden(mut self) -> Self {
606 self.hidden = true;
607 self
608 }
609
610 pub fn eager(mut self) -> Self {
615 self.eager = true;
616 self
617 }
618
619 pub fn show_default(mut self) -> Self {
621 self.show_default = true;
622 self
623 }
624
625 pub fn show_envvar(mut self) -> Self {
627 self.show_envvar = true;
628 self
629 }
630
631 pub fn metavar(mut self, metavar: &str) -> Self {
633 self.metavar = Some(metavar.to_string());
634 self
635 }
636
637 pub fn type_<T: TypeConverter<Value = String> + Send + Sync + 'static>(
639 mut self,
640 type_: T,
641 ) -> Self {
642 self.type_name = type_.name().to_string();
643 self.type_metavar = type_.get_metavar();
644 self.type_converter = Some(Arc::new(type_));
645 self
646 }
647
648 pub fn type_any<V: Send + Sync + 'static, T: TypeConverter<Value = V> + Send + Sync + 'static>(
650 mut self,
651 type_: T,
652 ) -> Self {
653 self.type_name = type_.name().to_string();
654 self.type_metavar = type_.get_metavar();
655 self.type_converter = Some(Arc::new(type_));
656 self
657 }
658
659 pub fn nargs(mut self, nargs: Nargs) -> Self {
661 self.nargs = nargs;
662 self
663 }
664
665 pub fn build(self) -> ClickOption {
667 let config = ParameterConfig {
668 name: self.name,
669 nargs: self.nargs,
670 multiple: self.multiple,
671 is_eager: self.eager,
672 expose_value: true,
673 required: self.required,
674 envvar: self.envvar,
675 help: self.help,
676 hidden: self.hidden,
677 metavar: self.metavar,
678 deprecated: None,
679 callback: self.callback,
680 };
681
682 let type_converter: Arc<dyn AnyTypeConverter> =
683 self.type_converter.unwrap_or_else(|| Arc::new(StringType));
684
685 ClickOption {
686 config,
687 long: self.long,
688 short: self.short,
689 is_flag: self.is_flag,
690 is_bool_flag: self.is_bool_flag,
691 flag_value: self.flag_value,
692 secondary_value: self.secondary_value,
693 count: self.count,
694 prompt: self.prompt,
695 confirmation_prompt: self.confirmation_prompt,
696 hide_input: self.hide_input,
697 show_default: self.show_default,
698 show_envvar: self.show_envvar,
699 default: self.default,
700 type_name: self.type_name,
701 type_metavar: self.type_metavar,
702 type_converter,
703 shell_complete_callback: self.shell_complete_callback,
704 }
705 }
706}
707
708#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::types::{Choice, INT};
716
717 #[test]
718 fn test_parse_option_name_long() {
719 let (name, is_long) = parse_option_name("--name").unwrap();
720 assert_eq!(name, "name");
721 assert!(is_long);
722
723 let (name, is_long) = parse_option_name("--full-name").unwrap();
724 assert_eq!(name, "full-name");
725 assert!(is_long);
726 }
727
728 #[test]
729 fn test_parse_option_name_short() {
730 let (name, is_long) = parse_option_name("-n").unwrap();
731 assert_eq!(name, "n");
732 assert!(!is_long);
733
734 let (name, is_long) = parse_option_name("-v").unwrap();
735 assert_eq!(name, "v");
736 assert!(!is_long);
737 }
738
739 #[test]
740 fn test_parse_option_name_errors() {
741 assert!(parse_option_name("--").is_err());
742 assert!(parse_option_name("-").is_err());
743 assert!(parse_option_name("---name").is_err());
744 assert!(parse_option_name("-ab").is_err()); assert!(parse_option_name("name").is_err()); }
747
748 #[test]
749 fn test_split_option_names() {
750 let (long, short) = split_option_names(&["--name", "-n", "--full-name"]).unwrap();
751 assert_eq!(long, vec!["--name", "--full-name"]);
752 assert_eq!(short, vec!["-n"]);
753 }
754
755 #[test]
756 fn test_split_option_names_only_short() {
757 let (long, short) = split_option_names(&["-n", "-N"]).unwrap();
758 assert!(long.is_empty());
759 assert_eq!(short, vec!["-n", "-N"]);
760 }
761
762 #[test]
763 fn test_split_option_names_empty() {
764 let result = split_option_names(&[]);
765 assert!(result.is_err());
766 }
767
768 #[test]
769 fn test_derive_param_name() {
770 let long = vec!["--name".to_string(), "--full-name".to_string()];
771 let short = vec!["-n".to_string()];
772 assert_eq!(derive_param_name(&long, &short), "full_name");
774
775 let long = vec![];
776 let short = vec!["-n".to_string()];
777 assert_eq!(derive_param_name(&long, &short), "n");
778 }
779
780 #[test]
781 fn test_option_builder_basic() {
782 let opt = ClickOption::new(&["--name", "-n"])
783 .help("The name to greet")
784 .build();
785
786 assert_eq!(opt.name(), "name");
787 assert_eq!(opt.long, vec!["--name"]);
788 assert_eq!(opt.short, vec!["-n"]);
789 assert_eq!(opt.help(), Some("The name to greet"));
790 assert!(!opt.is_flag);
791 assert!(!opt.required());
792 }
793
794 #[test]
795 fn test_option_builder_dest_override() {
796 let opt = ClickOption::new(&["--moored"]).dest("ty").flag("moored").build();
797 assert_eq!(opt.name(), "ty");
798 assert_eq!(opt.long, vec!["--moored"]);
799 assert_eq!(opt.flag_value, Some("moored".to_string()));
800 }
801
802 #[test]
803 fn test_option_flag() {
804 let opt = ClickOption::new(&["--verbose", "-v"]).flag("true").build();
805
806 assert!(opt.is_flag);
807 assert_eq!(opt.flag_value, Some("true".to_string()));
808 }
809
810 #[test]
811 fn test_option_bool_flag() {
812 let opt = ClickOption::new(&["--debug"]).bool_flag().build();
813
814 assert!(opt.is_flag);
815 assert!(opt.is_bool_flag);
816 assert_eq!(opt.flag_value, Some("true".to_string()));
817 assert_eq!(opt.secondary_value, Some("false".to_string()));
818 }
819
820 #[test]
821 fn test_option_count() {
822 let opt = ClickOption::new(&["--verbose", "-v"]).count().build();
823
824 assert!(opt.count);
825 assert!(opt.is_flag);
826 assert_eq!(opt.default, Some("0".to_string()));
827 }
828
829 #[test]
830 fn test_option_help_record_value() {
831 let opt = ClickOption::new(&["-n", "--name"])
832 .help("Your name")
833 .build();
834
835 let (opts, help) = opt.get_help_record().unwrap();
836 assert_eq!(opts, "-n, --name TEXT");
837 assert_eq!(help, "Your name");
838 }
839
840 #[test]
841 fn test_option_help_record_flag() {
842 let opt = ClickOption::new(&["-v", "--verbose"])
843 .flag("true")
844 .help("Enable verbose mode")
845 .build();
846
847 let (opts, help) = opt.get_help_record().unwrap();
848 assert_eq!(opts, "-v, --verbose");
849 assert_eq!(help, "Enable verbose mode");
850 }
851
852 #[test]
853 fn test_option_help_record_count() {
854 let opt = ClickOption::new(&["--verbose", "-v"])
855 .count()
856 .help("Increase verbosity")
857 .build();
858
859 let (opts, help) = opt.get_help_record().unwrap();
860 assert_eq!(opts, "-v, --verbose");
862 assert_eq!(help, "Increase verbosity");
863 }
864
865 #[test]
866 fn test_option_help_record_with_default() {
867 let opt = ClickOption::new(&["--name"])
868 .default("World")
869 .show_default()
870 .build();
871
872 let (opts, help) = opt.get_help_record().unwrap();
873 assert_eq!(opts, "--name TEXT");
874 assert!(help.contains("default: World"));
875 }
876
877 #[test]
878 fn test_option_help_record_with_envvar() {
879 let opt = ClickOption::new(&["--name"])
880 .envvar("MY_NAME")
881 .show_envvar()
882 .build();
883
884 let (opts, help) = opt.get_help_record().unwrap();
885 assert_eq!(opts, "--name TEXT");
886 assert!(help.contains("env var: MY_NAME"));
887 }
888
889 #[test]
890 fn test_option_help_record_required() {
891 let opt = ClickOption::new(&["--name"]).required().build();
892
893 let (opts, help) = opt.get_help_record().unwrap();
894 assert_eq!(opts, "--name TEXT");
895 assert!(help.contains("required"));
896 }
897
898 #[test]
899 fn test_option_hidden() {
900 let opt = ClickOption::new(&["--secret"]).hidden().build();
901
902 assert!(opt.hidden());
903 assert!(opt.get_help_record().is_none());
904 }
905
906 #[test]
907 fn test_option_with_custom_type() {
908 let opt = ClickOption::new(&["--count"]).type_any(INT).build();
909
910 assert_eq!(opt.get_metavar(), Some("INTEGER".to_string()));
911 }
912
913 #[test]
914 fn test_option_with_choice_type() {
915 let opt = ClickOption::new(&["--format"])
916 .type_(Choice::new(["json", "xml", "csv"]))
917 .build();
918
919 assert_eq!(opt.get_metavar(), Some("[json|xml|csv]".to_string()));
920 }
921
922 #[test]
923 fn test_option_with_metavar_override() {
924 let opt = ClickOption::new(&["--file"]).metavar("PATH").build();
925
926 assert_eq!(opt.get_metavar(), Some("PATH".to_string()));
927 }
928
929 #[test]
930 fn test_option_prompt() {
931 let opt = ClickOption::new(&["--password"])
932 .prompt("Enter password")
933 .hide_input(true)
934 .confirmation_prompt(true)
935 .build();
936
937 assert_eq!(opt.prompt, Some("Enter password".to_string()));
938 assert!(opt.hide_input);
939 assert!(opt.confirmation_prompt);
940 }
941
942 #[test]
943 fn test_option_multiple() {
944 let opt = ClickOption::new(&["--file", "-f"]).multiple().build();
945
946 assert!(opt.multiple());
947 }
948
949 #[test]
950 fn test_option_eager() {
951 let opt = ClickOption::new(&["--help", "-h"]).eager().build();
952
953 assert!(opt.is_eager());
954 }
955
956 #[test]
957 fn test_option_human_readable_name() {
958 let opt = ClickOption::new(&["-n", "--name", "--full-name"]).build();
959
960 assert_eq!(opt.human_readable_name(), "-n, --name, --full-name");
961 }
962
963 #[test]
964 fn test_option_primary_opt() {
965 let opt = ClickOption::new(&["-n", "--name"]).build();
966 assert_eq!(opt.primary_opt(), "--name");
967
968 let opt = ClickOption::new(&["-n", "-N"]).build();
969 assert_eq!(opt.primary_opt(), "-n");
970 }
971
972 #[test]
973 fn test_option_param_type_name() {
974 let opt = ClickOption::new(&["--name"]).build();
975 assert_eq!(opt.param_type_name(), "option");
976 }
977}