1use std::any::Any;
12use std::fmt;
13use std::sync::Arc;
14
15use crate::argument::AnyTypeConverter;
16use crate::context::Context;
17use crate::error::ClickError;
18use crate::parameter::{Nargs, Parameter, ParameterCallback, ParameterConfig};
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(
210 "has_shell_complete",
211 &self.shell_complete_callback.is_some(),
212 )
213 .field("has_type_converter", &true)
214 .finish()
215 }
216}
217
218impl ClickOption {
219 #[allow(clippy::new_ret_no_self)]
227 pub fn new(names: &[&str]) -> OptionBuilder {
228 OptionBuilder::new(names)
229 }
230
231 pub fn opts_string(&self) -> String {
233 let mut parts: Vec<&str> = Vec::new();
234 for s in &self.short {
235 parts.push(s);
236 }
237 for l in &self.long {
238 parts.push(l);
239 }
240 parts.join(", ")
241 }
242
243 pub fn primary_opt(&self) -> &str {
245 self.long
246 .first()
247 .or(self.short.first())
248 .map(|s| s.as_str())
249 .unwrap_or("")
250 }
251
252 pub fn type_converter(&self) -> &dyn AnyTypeConverter {
254 self.type_converter.as_ref()
255 }
256
257 pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
260 self.type_converter.convert_any(value)
261 }
262
263 pub fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String> {
266 self.type_converter.convert_multi(values)
267 }
268
269 pub fn get_completions(&self, ctx: &Context, incomplete: &str) -> Vec<CompletionItem> {
274 if let Some(ref callback) = self.shell_complete_callback {
275 callback(ctx, incomplete)
276 } else {
277 self.type_converter.shell_complete(incomplete)
278 }
279 }
280
281 pub fn has_shell_complete_callback(&self) -> bool {
283 self.shell_complete_callback.is_some()
284 }
285}
286
287impl Parameter for ClickOption {
288 fn name(&self) -> &str {
289 &self.config.name
290 }
291
292 fn human_readable_name(&self) -> String {
293 self.opts_string()
294 }
295
296 fn nargs(&self) -> Nargs {
297 self.config.nargs
298 }
299
300 fn multiple(&self) -> bool {
301 self.config.multiple
302 }
303
304 fn is_eager(&self) -> bool {
305 self.config.is_eager
306 }
307
308 fn expose_value(&self) -> bool {
309 self.config.expose_value
310 }
311
312 fn required(&self) -> bool {
313 self.config.required
314 }
315
316 fn envvar(&self) -> Option<&[String]> {
317 self.config.envvar.as_deref()
318 }
319
320 fn help(&self) -> Option<&str> {
321 self.config.help.as_deref()
322 }
323
324 fn hidden(&self) -> bool {
325 self.config.hidden
326 }
327
328 fn get_metavar(&self) -> Option<String> {
329 if let Some(ref mv) = self.config.metavar {
331 return Some(mv.clone());
332 }
333 if let Some(ref mv) = self.type_metavar {
335 if mv.contains('|') && !(mv.starts_with('[') && mv.ends_with(']')) {
336 return Some(format!("[{}]", mv));
337 }
338 return Some(mv.clone());
339 }
340 if !self.type_name.is_empty() {
341 return Some(self.type_name.clone());
342 }
343 None
344 }
345
346 fn get_help_record(&self) -> Option<(String, String)> {
347 if self.hidden() {
348 return None;
349 }
350
351 let mut opt_parts = Vec::new();
353
354 for s in &self.short {
356 opt_parts.push(s.clone());
357 }
358
359 for l in &self.long {
361 opt_parts.push(l.clone());
362 }
363
364 let mut opt_str = opt_parts.join(", ");
365
366 if !self.is_flag && !self.count {
368 if let Some(metavar) = self.get_metavar() {
369 opt_str.push(' ');
370 opt_str.push_str(&metavar);
371 }
372 }
373
374 let mut help = self.help().unwrap_or("").to_string();
376 let mut extras = Vec::new();
377
378 if self.show_envvar {
380 if let Some(envvars) = self.envvar() {
381 extras.push(format!("env var: {}", envvars.join(", ")));
382 }
383 }
384
385 if self.show_default {
387 if let Some(ref default) = self.default {
388 if !default.is_empty() {
389 extras.push(format!("default: {}", default));
390 }
391 }
392 }
393
394 if self.required() {
396 extras.push("required".to_string());
397 }
398
399 if !extras.is_empty() {
401 let extra_str = extras.join("; ");
402 if help.is_empty() {
403 help = format!("[{}]", extra_str);
404 } else {
405 help = format!("{} [{}]", help, extra_str);
406 }
407 }
408
409 Some((opt_str, help))
410 }
411
412 fn param_type_name(&self) -> &str {
413 "option"
414 }
415}
416
417pub struct OptionBuilder {
423 long: Vec<String>,
424 short: Vec<String>,
425 name: String,
426 help: Option<String>,
427 is_flag: bool,
428 is_bool_flag: bool,
429 flag_value: Option<String>,
430 secondary_value: Option<String>,
431 count: bool,
432 required: bool,
433 default: Option<String>,
434 envvar: Option<Vec<String>>,
435 prompt: Option<String>,
436 confirmation_prompt: bool,
437 hide_input: bool,
438 multiple: bool,
439 hidden: bool,
440 eager: bool,
441 show_default: bool,
442 show_envvar: bool,
443 metavar: Option<String>,
444 type_name: String,
445 type_metavar: Option<String>,
446 type_converter: Option<Arc<dyn AnyTypeConverter>>,
447 shell_complete_callback: Option<ShellCompleteCallback>,
448 nargs: Nargs,
449 callback: Option<ParameterCallback>,
450}
451
452impl OptionBuilder {
453 pub fn new(names: &[&str]) -> Self {
459 let (long, short) = split_option_names(names).expect("Invalid option names");
460 let name = derive_param_name(&long, &short);
461
462 Self {
463 long,
464 short,
465 name,
466 help: None,
467 is_flag: false,
468 is_bool_flag: false,
469 flag_value: None,
470 secondary_value: None,
471 count: false,
472 required: false,
473 default: None,
474 envvar: None,
475 prompt: None,
476 confirmation_prompt: false,
477 hide_input: false,
478 multiple: false,
479 hidden: false,
480 eager: false,
481 show_default: false,
482 show_envvar: false,
483 metavar: None,
484 type_name: TypeConverter::name(&STRING).to_string(),
485 type_metavar: TypeConverter::get_metavar(&STRING),
486 type_converter: None,
487 shell_complete_callback: None,
488 nargs: Nargs::Count(1),
489 callback: None,
490 }
491 }
492
493 pub fn dest(mut self, name: &str) -> Self {
497 self.name = name.to_string();
498 self
499 }
500
501 pub fn help(mut self, help: &str) -> Self {
503 self.help = Some(help.to_string());
504 self
505 }
506
507 pub fn flag(mut self, value: &str) -> Self {
511 self.is_flag = true;
512 self.flag_value = Some(value.to_string());
513 self
514 }
515
516 pub fn bool_flag(mut self) -> Self {
520 self.is_flag = true;
521 self.is_bool_flag = true;
522 self.flag_value = Some("true".to_string());
523 self.secondary_value = Some("false".to_string());
524 self
525 }
526
527 pub fn count(mut self) -> Self {
531 self.count = true;
532 self.is_flag = true;
533 self.default = Some("0".to_string());
534 self
535 }
536
537 pub fn required(mut self) -> Self {
539 self.required = true;
540 self
541 }
542
543 pub fn default(mut self, value: impl Into<String>) -> Self {
545 self.default = Some(value.into());
546 self
547 }
548
549 pub fn envvar(mut self, name: &str) -> Self {
551 self.envvar = Some(vec![name.to_string()]);
552 self
553 }
554
555 pub fn envvars(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
557 self.envvar = Some(names.into_iter().map(|n| n.into()).collect());
558 self
559 }
560
561 pub fn prompt(mut self, text: &str) -> Self {
563 self.prompt = Some(text.to_string());
564 self
565 }
566
567 pub fn confirmation_prompt(mut self, confirm: bool) -> Self {
569 self.confirmation_prompt = confirm;
570 self
571 }
572
573 pub fn hide_input(mut self, hide: bool) -> Self {
575 self.hide_input = hide;
576 self
577 }
578
579 pub fn multiple(mut self) -> Self {
581 self.multiple = true;
582 self
583 }
584
585 pub fn callback<F>(mut self, callback: F) -> Self
587 where
588 F: Fn(
589 &Context,
590 &dyn Parameter,
591 Arc<dyn Any + Send + Sync>,
592 ) -> Result<Arc<dyn Any + Send + Sync>, ClickError>
593 + Send
594 + Sync
595 + 'static,
596 {
597 self.callback = Some(Arc::new(callback));
598 self
599 }
600
601 pub fn shell_complete<F>(mut self, callback: F) -> Self
603 where
604 F: Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync + 'static,
605 {
606 self.shell_complete_callback = Some(Arc::new(callback));
607 self
608 }
609
610 pub fn hidden(mut self) -> Self {
612 self.hidden = true;
613 self
614 }
615
616 pub fn eager(mut self) -> Self {
621 self.eager = true;
622 self
623 }
624
625 pub fn show_default(mut self) -> Self {
627 self.show_default = true;
628 self
629 }
630
631 pub fn show_envvar(mut self) -> Self {
633 self.show_envvar = true;
634 self
635 }
636
637 pub fn metavar(mut self, metavar: &str) -> Self {
639 self.metavar = Some(metavar.to_string());
640 self
641 }
642
643 pub fn type_<T: TypeConverter<Value = String> + Send + Sync + 'static>(
645 mut self,
646 type_: T,
647 ) -> Self {
648 self.type_name = type_.name().to_string();
649 self.type_metavar = type_.get_metavar();
650 self.type_converter = Some(Arc::new(type_));
651 self
652 }
653
654 pub fn type_any<
656 V: Send + Sync + 'static,
657 T: TypeConverter<Value = V> + Send + Sync + 'static,
658 >(
659 mut self,
660 type_: T,
661 ) -> Self {
662 self.type_name = type_.name().to_string();
663 self.type_metavar = type_.get_metavar();
664 self.type_converter = Some(Arc::new(type_));
665 self
666 }
667
668 pub fn nargs(mut self, nargs: Nargs) -> Self {
670 self.nargs = nargs;
671 self
672 }
673
674 pub fn build(self) -> ClickOption {
676 let config = ParameterConfig {
677 name: self.name,
678 nargs: self.nargs,
679 multiple: self.multiple,
680 is_eager: self.eager,
681 expose_value: true,
682 required: self.required,
683 envvar: self.envvar,
684 help: self.help,
685 hidden: self.hidden,
686 metavar: self.metavar,
687 deprecated: None,
688 callback: self.callback,
689 };
690
691 let type_converter: Arc<dyn AnyTypeConverter> =
692 self.type_converter.unwrap_or_else(|| Arc::new(StringType));
693
694 ClickOption {
695 config,
696 long: self.long,
697 short: self.short,
698 is_flag: self.is_flag,
699 is_bool_flag: self.is_bool_flag,
700 flag_value: self.flag_value,
701 secondary_value: self.secondary_value,
702 count: self.count,
703 prompt: self.prompt,
704 confirmation_prompt: self.confirmation_prompt,
705 hide_input: self.hide_input,
706 show_default: self.show_default,
707 show_envvar: self.show_envvar,
708 default: self.default,
709 type_name: self.type_name,
710 type_metavar: self.type_metavar,
711 type_converter,
712 shell_complete_callback: self.shell_complete_callback,
713 }
714 }
715}
716
717#[cfg(test)]
722mod tests {
723 use super::*;
724 use crate::types::{Choice, INT};
725
726 #[test]
727 fn test_parse_option_name_long() {
728 let (name, is_long) = parse_option_name("--name").unwrap();
729 assert_eq!(name, "name");
730 assert!(is_long);
731
732 let (name, is_long) = parse_option_name("--full-name").unwrap();
733 assert_eq!(name, "full-name");
734 assert!(is_long);
735 }
736
737 #[test]
738 fn test_parse_option_name_short() {
739 let (name, is_long) = parse_option_name("-n").unwrap();
740 assert_eq!(name, "n");
741 assert!(!is_long);
742
743 let (name, is_long) = parse_option_name("-v").unwrap();
744 assert_eq!(name, "v");
745 assert!(!is_long);
746 }
747
748 #[test]
749 fn test_parse_option_name_errors() {
750 assert!(parse_option_name("--").is_err());
751 assert!(parse_option_name("-").is_err());
752 assert!(parse_option_name("---name").is_err());
753 assert!(parse_option_name("-ab").is_err()); assert!(parse_option_name("name").is_err()); }
756
757 #[test]
758 fn test_split_option_names() {
759 let (long, short) = split_option_names(&["--name", "-n", "--full-name"]).unwrap();
760 assert_eq!(long, vec!["--name", "--full-name"]);
761 assert_eq!(short, vec!["-n"]);
762 }
763
764 #[test]
765 fn test_split_option_names_only_short() {
766 let (long, short) = split_option_names(&["-n", "-N"]).unwrap();
767 assert!(long.is_empty());
768 assert_eq!(short, vec!["-n", "-N"]);
769 }
770
771 #[test]
772 fn test_split_option_names_empty() {
773 let result = split_option_names(&[]);
774 assert!(result.is_err());
775 }
776
777 #[test]
778 fn test_derive_param_name() {
779 let long = vec!["--name".to_string(), "--full-name".to_string()];
780 let short = vec!["-n".to_string()];
781 assert_eq!(derive_param_name(&long, &short), "full_name");
783
784 let long = vec![];
785 let short = vec!["-n".to_string()];
786 assert_eq!(derive_param_name(&long, &short), "n");
787 }
788
789 #[test]
790 fn test_option_builder_basic() {
791 let opt = ClickOption::new(&["--name", "-n"])
792 .help("The name to greet")
793 .build();
794
795 assert_eq!(opt.name(), "name");
796 assert_eq!(opt.long, vec!["--name"]);
797 assert_eq!(opt.short, vec!["-n"]);
798 assert_eq!(opt.help(), Some("The name to greet"));
799 assert!(!opt.is_flag);
800 assert!(!opt.required());
801 }
802
803 #[test]
804 fn test_option_builder_dest_override() {
805 let opt = ClickOption::new(&["--moored"])
806 .dest("ty")
807 .flag("moored")
808 .build();
809 assert_eq!(opt.name(), "ty");
810 assert_eq!(opt.long, vec!["--moored"]);
811 assert_eq!(opt.flag_value, Some("moored".to_string()));
812 }
813
814 #[test]
815 fn test_option_flag() {
816 let opt = ClickOption::new(&["--verbose", "-v"]).flag("true").build();
817
818 assert!(opt.is_flag);
819 assert_eq!(opt.flag_value, Some("true".to_string()));
820 }
821
822 #[test]
823 fn test_option_bool_flag() {
824 let opt = ClickOption::new(&["--debug"]).bool_flag().build();
825
826 assert!(opt.is_flag);
827 assert!(opt.is_bool_flag);
828 assert_eq!(opt.flag_value, Some("true".to_string()));
829 assert_eq!(opt.secondary_value, Some("false".to_string()));
830 }
831
832 #[test]
833 fn test_option_count() {
834 let opt = ClickOption::new(&["--verbose", "-v"]).count().build();
835
836 assert!(opt.count);
837 assert!(opt.is_flag);
838 assert_eq!(opt.default, Some("0".to_string()));
839 }
840
841 #[test]
842 fn test_option_help_record_value() {
843 let opt = ClickOption::new(&["-n", "--name"])
844 .help("Your name")
845 .build();
846
847 let (opts, help) = opt.get_help_record().unwrap();
848 assert_eq!(opts, "-n, --name TEXT");
849 assert_eq!(help, "Your name");
850 }
851
852 #[test]
853 fn test_option_help_record_flag() {
854 let opt = ClickOption::new(&["-v", "--verbose"])
855 .flag("true")
856 .help("Enable verbose mode")
857 .build();
858
859 let (opts, help) = opt.get_help_record().unwrap();
860 assert_eq!(opts, "-v, --verbose");
861 assert_eq!(help, "Enable verbose mode");
862 }
863
864 #[test]
865 fn test_option_help_record_count() {
866 let opt = ClickOption::new(&["--verbose", "-v"])
867 .count()
868 .help("Increase verbosity")
869 .build();
870
871 let (opts, help) = opt.get_help_record().unwrap();
872 assert_eq!(opts, "-v, --verbose");
874 assert_eq!(help, "Increase verbosity");
875 }
876
877 #[test]
878 fn test_option_help_record_with_default() {
879 let opt = ClickOption::new(&["--name"])
880 .default("World")
881 .show_default()
882 .build();
883
884 let (opts, help) = opt.get_help_record().unwrap();
885 assert_eq!(opts, "--name TEXT");
886 assert!(help.contains("default: World"));
887 }
888
889 #[test]
890 fn test_option_help_record_with_envvar() {
891 let opt = ClickOption::new(&["--name"])
892 .envvar("MY_NAME")
893 .show_envvar()
894 .build();
895
896 let (opts, help) = opt.get_help_record().unwrap();
897 assert_eq!(opts, "--name TEXT");
898 assert!(help.contains("env var: MY_NAME"));
899 }
900
901 #[test]
902 fn test_option_help_record_required() {
903 let opt = ClickOption::new(&["--name"]).required().build();
904
905 let (opts, help) = opt.get_help_record().unwrap();
906 assert_eq!(opts, "--name TEXT");
907 assert!(help.contains("required"));
908 }
909
910 #[test]
911 fn test_option_hidden() {
912 let opt = ClickOption::new(&["--secret"]).hidden().build();
913
914 assert!(opt.hidden());
915 assert!(opt.get_help_record().is_none());
916 }
917
918 #[test]
919 fn test_option_with_custom_type() {
920 let opt = ClickOption::new(&["--count"]).type_any(INT).build();
921
922 assert_eq!(opt.get_metavar(), Some("INTEGER".to_string()));
923 }
924
925 #[test]
926 fn test_option_with_choice_type() {
927 let opt = ClickOption::new(&["--format"])
928 .type_(Choice::new(["json", "xml", "csv"]))
929 .build();
930
931 assert_eq!(opt.get_metavar(), Some("[json|xml|csv]".to_string()));
932 }
933
934 #[test]
935 fn test_option_with_metavar_override() {
936 let opt = ClickOption::new(&["--file"]).metavar("PATH").build();
937
938 assert_eq!(opt.get_metavar(), Some("PATH".to_string()));
939 }
940
941 #[test]
942 fn test_option_prompt() {
943 let opt = ClickOption::new(&["--password"])
944 .prompt("Enter password")
945 .hide_input(true)
946 .confirmation_prompt(true)
947 .build();
948
949 assert_eq!(opt.prompt, Some("Enter password".to_string()));
950 assert!(opt.hide_input);
951 assert!(opt.confirmation_prompt);
952 }
953
954 #[test]
955 fn test_option_multiple() {
956 let opt = ClickOption::new(&["--file", "-f"]).multiple().build();
957
958 assert!(opt.multiple());
959 }
960
961 #[test]
962 fn test_option_eager() {
963 let opt = ClickOption::new(&["--help", "-h"]).eager().build();
964
965 assert!(opt.is_eager());
966 }
967
968 #[test]
969 fn test_option_human_readable_name() {
970 let opt = ClickOption::new(&["-n", "--name", "--full-name"]).build();
971
972 assert_eq!(opt.human_readable_name(), "-n, --name, --full-name");
973 }
974
975 #[test]
976 fn test_option_primary_opt() {
977 let opt = ClickOption::new(&["-n", "--name"]).build();
978 assert_eq!(opt.primary_opt(), "--name");
979
980 let opt = ClickOption::new(&["-n", "-N"]).build();
981 assert_eq!(opt.primary_opt(), "-n");
982 }
983
984 #[test]
985 fn test_option_param_type_name() {
986 let opt = ClickOption::new(&["--name"]).build();
987 assert_eq!(opt.param_type_name(), "option");
988 }
989}