1use crate::{ConfValueSource, FlattenedOptionalDebugInfo, ProgramOption};
2use clap::{Command, Error as ClapError, builder::Styles, error::ErrorKind};
3use std::{ffi::OsString, fmt, fmt::Write};
4
5#[derive(Debug)]
13pub struct Error(ClapError);
14
15impl fmt::Display for Error {
16 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
17 self.0.fmt(f)
18 }
19}
20
21impl std::error::Error for Error {
22 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
23 self.0.source()
24 }
25}
26
27impl Error {
28 pub fn print(&self) -> Result<(), std::io::Error> {
30 self.0.print()
31 }
32
33 pub fn exit(&self) -> ! {
36 self.0.exit()
37 }
38
39 pub fn exit_code(&self) -> i32 {
41 self.0.exit_code()
42 }
43
44 #[doc(hidden)]
46 pub fn skip_short_not_found(
47 not_found_chars: Vec<char>,
48 field_name: &'static str,
49 field_type_name: &'static str,
50 ) -> Self {
51 let buf = format!(
52 "Internal error (invalid skip short)\n When flattening {field_type_name} at {field_name}, these short options were not found: {not_found_chars:?}\n To fix this error, remove them from the skip_short attribute list."
53 );
54 ClapError::raw(ErrorKind::UnknownArgument, buf).into()
55 }
56
57 #[doc(hidden)]
59 pub fn positional_in_flatten_optional(
60 field_name: &str,
61 field_type_name: &str,
62 option_id: &str,
63 ) -> Self {
64 let buf = format!(
65 "Cannot use flatten optional with struct '{field_type_name}' at field '{field_name}' because it contains positional argument '{option_id}'. Positional arguments are not supported in flatten optional structs (but are supported in regular flatten)."
66 );
67 ClapError::raw(ErrorKind::ArgumentConflict, buf).into()
68 }
69}
70
71impl From<ClapError> for Error {
72 fn from(src: ClapError) -> Error {
73 Error(src)
74 }
75}
76
77impl From<fmt::Error> for Error {
78 fn from(src: fmt::Error) -> Error {
79 ClapError::from(src).into()
80 }
81}
82
83#[doc(hidden)]
86#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
87pub enum InnerError {
88 MissingRequiredParameter(
91 Box<ProgramOption>,
92 Option<Box<OwnedFlattenedOptionalDebugInfo>>,
93 bool,
94 ),
95 InvalidParameterValue(ConfValueSource<String>, String, Box<ProgramOption>, String),
98 TooFewArguments(
102 String,
103 String,
104 Vec<ProgramOption>,
105 Vec<String>,
106 Option<Box<OwnedFlattenedOptionalDebugInfo>>,
107 bool,
108 ),
109 TooManyArguments(
113 String,
114 String,
115 Vec<(ProgramOption, ConfValueSource<String>)>,
116 Vec<(String, ProgramOption, ConfValueSource<String>)>,
117 ),
118 ValidationFailed(String, String, String),
121 InvalidUtf8Env(String, Box<ProgramOption>, Option<OsString>),
124 MissingRequiredSubcommand(String, String, Vec<String>),
127 Serde(String, String, String),
130}
131
132impl InnerError {
133 pub fn invalid_value(
135 conf_value_source: ConfValueSource<&str>,
136 value_str: &str,
137 program_option: &ProgramOption,
138 err: impl fmt::Display,
139 ) -> Self {
140 let program_option = Box::new(program_option.clone());
141 Self::InvalidParameterValue(
142 conf_value_source.into_owned(),
143 value_str.to_owned(),
144 program_option,
145 err.to_string(),
146 )
147 }
148
149 pub fn invalid_value_os(
151 conf_value_source: ConfValueSource<&str>,
152 value_os: &std::ffi::OsStr,
153 program_option: &ProgramOption,
154 err: impl fmt::Display,
155 ) -> Self {
156 let program_option = Box::new(program_option.clone());
157 Self::InvalidParameterValue(
158 conf_value_source.into_owned(),
159 value_os.to_string_lossy().into_owned(),
160 program_option,
161 err.to_string(),
162 )
163 }
164
165 pub(crate) fn missing_required_parameter(
167 opt: &ProgramOption,
168 flattened_optional_debug_info: Option<FlattenedOptionalDebugInfo<'_>>,
169 serde_source_is_present: bool,
170 ) -> Self {
171 Self::MissingRequiredParameter(
172 Box::new(opt.clone()),
173 flattened_optional_debug_info.map(Into::into).map(Box::new),
174 serde_source_is_present,
175 )
176 }
177
178 pub(crate) fn too_few_arguments<'a>(
180 struct_name: &'static str,
181 instance_id_prefix: &str,
182 constraint_single_options: impl AsRef<[&'a ProgramOption]>,
183 constraint_flattened_ids: impl AsRef<[&'a str]>,
184 flattened_optional_debug_info: Option<FlattenedOptionalDebugInfo<'a>>,
185 serde_source_is_present: bool,
186 ) -> Self {
187 let constraint_single_options = constraint_single_options
188 .as_ref()
189 .iter()
190 .map(|opt| (*opt).clone())
191 .collect::<Vec<_>>();
192 let constraint_flattened_ids = constraint_flattened_ids
193 .as_ref()
194 .iter()
195 .map(|id| (*id).to_owned())
196 .collect::<Vec<_>>();
197 let flattened_optional_debug_info =
198 flattened_optional_debug_info.map(Into::into).map(Box::new);
199 Self::TooFewArguments(
200 struct_name.to_owned(),
201 instance_id_prefix.to_owned(),
202 constraint_single_options,
203 constraint_flattened_ids,
204 flattened_optional_debug_info,
205 serde_source_is_present,
206 )
207 }
208
209 pub(crate) fn too_many_arguments<'a>(
212 struct_name: &'static str,
213 instance_id_prefix: &'a str,
214 constraint_single_options: impl AsRef<[(&'a ProgramOption, ConfValueSource<&'a str>)]>,
215 constraint_flattened_data: impl AsRef<[(&'a str, &'a ProgramOption, ConfValueSource<&'a str>)]>,
216 ) -> Self {
217 let constraint_single_options = constraint_single_options
218 .as_ref()
219 .iter()
220 .map(|(opt, src)| ((*opt).clone(), (*src).into_owned()))
221 .collect::<Vec<_>>();
222 let constraint_flattened_data = constraint_flattened_data
223 .as_ref()
224 .iter()
225 .map(|(field_name, opt, src)| {
226 (
227 (*field_name).to_owned(),
228 (*opt).clone(),
229 (*src).into_owned(),
230 )
231 })
232 .collect::<Vec<_>>();
233 Self::TooManyArguments(
234 struct_name.to_owned(),
235 instance_id_prefix.to_owned(),
236 constraint_single_options,
237 constraint_flattened_data,
238 )
239 }
240
241 pub fn validation(
243 struct_name: &'static str,
244 instance_id_prefix: &str,
245 err: impl fmt::Display,
246 ) -> Self {
247 Self::ValidationFailed(
248 struct_name.to_owned(),
249 instance_id_prefix.to_owned(),
250 err.to_string(),
251 )
252 }
253
254 pub(crate) fn invalid_utf8_env(
256 env_var: &str,
257 program_option: &ProgramOption,
258 val: Option<&OsString>,
259 ) -> Self {
260 Self::InvalidUtf8Env(
261 env_var.to_owned(),
262 Box::new(program_option.clone()),
263 val.cloned(),
264 )
265 }
266
267 pub fn missing_required_subcommand(
269 struct_name: &str,
270 field_name: &str,
271 subcommand_names: &'static [&'static str],
272 ) -> Self {
273 Self::MissingRequiredSubcommand(
274 struct_name.to_owned(),
275 field_name.to_owned(),
276 subcommand_names.iter().map(|x| (*x).to_owned()).collect(),
277 )
278 }
279
280 pub fn serde(document_name: &str, field_name: &str, err: impl fmt::Display) -> Self {
282 Self::Serde(
283 document_name.to_owned(),
284 field_name.to_owned(),
285 err.to_string(),
286 )
287 }
288
289 fn title(&self) -> &'static str {
291 match self {
292 Self::InvalidUtf8Env(..) => "An env var contained invalid UTF8",
293 Self::MissingRequiredParameter(..) => "A required value was not provided",
294 Self::TooFewArguments(..) => "Too few arguments",
295 Self::TooManyArguments(..) => "Too many arguments",
296 Self::ValidationFailed(..) => "Validation failed",
297 Self::InvalidParameterValue(..) => "Invalid value",
298 Self::MissingRequiredSubcommand(..) => "Missing required subcommand",
299 Self::Serde(..) => "Parsing document",
300 }
301 }
302
303 fn error_kind(&self) -> ErrorKind {
305 match self {
306 Self::InvalidUtf8Env(..) => ErrorKind::InvalidValue,
307 Self::MissingRequiredParameter(..) => ErrorKind::MissingRequiredArgument,
308 Self::TooFewArguments(..) => ErrorKind::TooFewValues,
309 Self::TooManyArguments(..) => ErrorKind::TooManyValues,
310 Self::ValidationFailed(..) => ErrorKind::ValueValidation,
311 Self::InvalidParameterValue(..) => ErrorKind::InvalidValue,
312 Self::MissingRequiredSubcommand(..) => ErrorKind::MissingSubcommand,
313 Self::Serde(..) => ErrorKind::InvalidValue,
314 }
315 }
316
317 fn get_program_option(&self) -> Option<&ProgramOption> {
320 match self {
321 Self::InvalidUtf8Env(_, opt, _) => Some(opt),
322 Self::MissingRequiredParameter(opt, ..) => Some(opt),
323 Self::InvalidParameterValue(_src, _val_str, opt, _err) => Some(opt),
324 Self::TooFewArguments(..) => None,
325 Self::TooManyArguments(..) => None,
326 Self::ValidationFailed(..) => None,
327 Self::MissingRequiredSubcommand(..) => None,
328 Self::Serde(..) => None,
329 }
330 }
331
332 fn print_solo(
334 &self,
335 stream: &mut impl std::fmt::Write,
336 styles: &Styles,
337 ) -> Result<(), std::fmt::Error> {
338 writeln!(stream, "{}", self.title())?;
339
340 self.print_body(stream, styles)?;
341
342 if let Some(opt) = self.get_program_option() {
344 writeln!(stream)?;
345 writeln!(stream, "Help:")?;
346 opt.print(stream, None)?;
347 }
348
349 Ok(())
350 }
351
352 fn print_body(
354 &self,
355 stream: &mut impl std::fmt::Write,
356 styles: &Styles,
357 ) -> Result<(), std::fmt::Error> {
358 let invalid = styles.get_invalid();
361
362 match self {
363 Self::InvalidUtf8Env(name, opt, maybe_val) => {
364 let lossy_val = maybe_val
365 .as_ref()
366 .and_then(|val| {
367 if opt.is_secret() {
368 None
369 } else {
370 Some(val.to_string_lossy())
371 }
372 })
373 .unwrap_or_else(|| "***secret***".into());
374
375 writeln!(
376 stream,
377 " {name}: {}'{lossy_val}'{}",
378 invalid.render(),
379 invalid.render_reset()
380 )?;
381 }
382 Self::MissingRequiredParameter(
383 opt,
384 maybe_flatten_optional_debug_info,
385 serde_source_is_present,
386 ) => {
387 print_opt_requirements(stream, opt, "must be provided", *serde_source_is_present)?;
388 if let Some(flatten_optional) = maybe_flatten_optional_debug_info.as_ref() {
389 write!(stream, " ")?;
391 flatten_optional.print_required_opt_context(stream)?;
392 }
393 }
394 Self::InvalidParameterValue(value_source, value_str, opt, err) => {
395 let context = format!(
396 " when parsing {} value",
397 render_provided_opt(opt, value_source)
398 );
399 let mut estimated_len = context.len();
400 write!(stream, "{context}")?;
401 if !opt.is_secret() {
402 write!(
403 stream,
404 " {}'{value_str}'{}",
405 invalid.render(),
406 invalid.render_reset()
407 )?;
408 estimated_len += 3 + value_str.len();
409 }
410 writeln!(
411 stream,
412 ": {err_str}",
413 err_str = Self::format_err_str(err, estimated_len + 2)
414 )?;
415 }
416 Self::TooFewArguments(
417 struct_name,
418 instance_id_prefix,
419 single_opts,
420 flattened_opts,
421 maybe_flatten_optional_debug_info,
422 serde_source_is_present,
423 ) => {
424 let mut instance_id_prefix = instance_id_prefix.to_owned();
425 if !instance_id_prefix.is_empty() {
426 instance_id_prefix.insert_str(0, " @ .");
427 remove_trailing_dot(&mut instance_id_prefix);
428 }
429 writeln!(
430 stream,
431 " One of these must be provided: (constraint on {struct_name}{instance_id_prefix}): "
432 )?;
433 for opt in single_opts {
434 write!(stream, " ")?;
435 print_opt_requirements(stream, opt, "", *serde_source_is_present)?;
436 }
437 for field_name in flattened_opts {
438 writeln!(stream, " Argument group '{field_name}'")?;
439 }
440 if let Some(flatten_optional) = maybe_flatten_optional_debug_info.as_ref() {
441 write!(stream, " ")?;
442 flatten_optional.print_required_opt_context(stream)?;
443 }
444 }
445 Self::TooManyArguments(
446 struct_name,
447 instance_id_prefix,
448 single_opts,
449 flattened_opts,
450 ) => {
451 let mut instance_id_prefix = instance_id_prefix.to_owned();
452 if !instance_id_prefix.is_empty() {
453 instance_id_prefix.insert_str(0, " @ .");
454 remove_trailing_dot(&mut instance_id_prefix);
455 }
456 writeln!(
457 stream,
458 " Too many arguments, provide at most one of these: (constraint on {struct_name}{instance_id_prefix}): "
459 )?;
460 for (opt, source) in single_opts {
461 let provided_opt = render_provided_opt(opt, source);
462 writeln!(stream, " {provided_opt}")?;
463 }
464 for (field_name, opt, source) in flattened_opts {
465 let provided_opt = render_provided_opt(opt, source);
466 writeln!(
467 stream,
468 " {provided_opt} (part of argument group '{field_name}')"
469 )?;
470 }
471 }
472 Self::ValidationFailed(struct_name, instance_id_prefix, err) => {
473 let mut context = format!(" {struct_name} value was invalid");
474 if !instance_id_prefix.is_empty() {
475 context += &format!(" (@ .{instance_id_prefix})");
476 }
477 let estimated_len = context.len();
478 writeln!(
479 stream,
480 "{context}: {err_str}",
481 err_str = Self::format_err_str(err, estimated_len + 2)
482 )?;
483 }
484 Self::MissingRequiredSubcommand(struct_name, field_name, subcommand_names) => {
485 writeln!(
486 stream,
487 " A subcommand must be selected ({struct_name}::{field_name}):"
488 )?;
489 for name in subcommand_names {
490 writeln!(stream, " {name}")?;
491 }
492 }
493 Self::Serde(document_name, field_name, err) => {
494 let context = format!(" Parsing {document_name} (@ {field_name})");
495 let estimated_len = context.len();
496 writeln!(
497 stream,
498 "{context}: {err_str}",
499 err_str = Self::format_err_str(err, estimated_len + 2)
500 )?;
501 }
502 }
503 Ok(())
504 }
505
506 fn format_err_str(err_str: &str, estimated_line_length_so_far: usize) -> String {
509 const TARGET_LINE_LENGTH: usize = 100;
510 const INDENTATION: usize = 4;
511
512 let mut err_str = err_str.to_owned();
513 if err_str.len() + estimated_line_length_so_far > TARGET_LINE_LENGTH
514 || err_str.contains('\n')
515 {
516 err_str.insert(0, '\n');
517 }
518
519 let indented_newline = "\n".to_owned() + &" ".repeat(INDENTATION);
520 err_str.replace('\n', &indented_newline)
521 }
522}
523
524fn print_opt_requirements(
527 stream: &mut impl std::fmt::Write,
528 opt: &ProgramOption,
529 trailing_text: &str,
530 serde_source_is_present: bool,
531) -> fmt::Result {
532 let mut ways_to_provide = vec![];
533
534 if let Some(name) = opt.env_form.as_deref() {
536 ways_to_provide.push(format!("env '{name}'"));
537 }
538
539 if opt.is_positional {
541 let pos_name = format!("<{}>", opt.id);
542 ways_to_provide.push(format!("'{pos_name}'"));
543 }
544 else if let Some(switch) = render_help_switch(opt) {
546 ways_to_provide.push(format!("'{switch}'"));
547 }
548
549 if opt.has_serde_source && serde_source_is_present {
551 ways_to_provide.push(format!("'{}' in config file", opt.id));
552 }
553
554 if ways_to_provide.is_empty() {
556 writeln!(stream, " Required value '{}' cannot be provided", opt.id)?;
557 } else {
558 let joined = ways_to_provide.join(", or ");
559 if trailing_text.is_empty() {
560 writeln!(stream, " {joined}")?;
561 } else if ways_to_provide.len() == 1 {
562 writeln!(stream, " {joined} {trailing_text}")?;
564 } else {
565 writeln!(stream, " {joined}, {trailing_text}")?;
566 }
567 }
568
569 Ok(())
570}
571
572fn render_provided_opt(opt: &ProgramOption, value_source: &ConfValueSource<String>) -> String {
573 match value_source {
574 ConfValueSource::Args => {
575 let switch = render_help_switch(opt).unwrap_or_default();
578 format!("'{switch}'")
579 }
580 ConfValueSource::Default => "default value".into(),
581 ConfValueSource::Env(name) => {
582 format!("env '{name}'")
583 }
584 ConfValueSource::Document(name) => {
585 format!("document '{name}'")
586 }
587 }
588}
589
590fn render_help_switch(opt: &ProgramOption) -> Option<String> {
591 opt.long_form
593 .as_deref()
594 .map(|l| format!("--{l}"))
595 .or_else(|| opt.short_form.map(|s| format!("-{s}")))
596}
597
598fn remove_trailing_dot(string: &mut String) {
599 if let Some(c) = string.pop() {
600 if c != '.' {
601 string.push(c);
602 }
603 }
604}
605
606#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
608pub struct OwnedFlattenedOptionalDebugInfo {
609 pub struct_name: &'static str,
610 pub id_prefix: String,
611 pub option_appeared: Box<ProgramOption>,
612 pub value_source: ConfValueSource<String>,
613}
614
615impl<'a> From<FlattenedOptionalDebugInfo<'a>> for OwnedFlattenedOptionalDebugInfo {
616 fn from(src: FlattenedOptionalDebugInfo<'a>) -> Self {
617 Self {
618 struct_name: src.struct_name,
619 id_prefix: src.id_prefix,
620 option_appeared: Box::new(src.option_appeared.clone()),
621 value_source: src.value_source.into_owned(),
622 }
623 }
624}
625
626impl OwnedFlattenedOptionalDebugInfo {
627 fn print_required_opt_context(&self, stream: &mut impl std::fmt::Write) -> fmt::Result {
630 let provided_opt = render_provided_opt(&self.option_appeared, &self.value_source);
631 let mut context = self.struct_name.to_owned();
632 if !self.id_prefix.is_empty() {
633 context += " @ .";
634 context += &self.id_prefix;
635 remove_trailing_dot(&mut context);
636 }
637
638 writeln!(
639 stream,
640 "because {provided_opt} was provided (enabling argument group {context})"
641 )
642 }
643}
644
645impl fmt::Display for InnerError {
646 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
647 write!(fmt, "{}", self.title())
648 }
649}
650
651impl InnerError {
652 pub(crate) fn into_clap_error(self, command: &Command) -> Error {
653 let mut buf = String::new();
654
655 self.print_solo(&mut buf, command.get_styles()).unwrap();
656 ClapError::raw(self.error_kind(), buf)
657 .with_cmd(command)
658 .into()
659 }
660
661 pub(crate) fn vec_to_clap_error(mut src: Vec<InnerError>, command: &Command) -> Error {
662 assert!(!src.is_empty());
663 if src.len() == 1 {
664 return src.remove(0).into_clap_error(command);
665 }
666
667 src.sort();
668 let last_error_kind = src.last().unwrap().error_kind();
669
670 let styles = command.get_styles();
671 let error_sty = styles.get_error();
672
673 let mut buf = String::new();
674
675 let mut last_title = "";
676 for err in src {
677 if err.title() != last_title {
678 if !last_title.is_empty() {
679 write!(
680 &mut buf,
681 "{}error: {}",
682 error_sty.render(),
683 error_sty.render_reset()
684 )
685 .unwrap();
686 }
687 writeln!(&mut buf, "{}", err.title()).unwrap();
688 last_title = err.title();
689 }
690 err.print_body(&mut buf, styles).unwrap();
691 }
692 ClapError::raw(last_error_kind, buf)
693 .with_cmd(command)
694 .into()
695 }
696}