1use crate::error::{UResult, USimpleError};
15use crate::locale::translate;
16
17use clap::error::{ContextKind, ErrorKind};
18use clap::{ArgMatches, Command, Error};
19
20use std::error::Error as StdError;
21use std::ffi::OsString;
22
23use std::io::Write as _;
24use std::io::stderr;
25
26#[derive(Debug, Clone, Copy)]
28pub enum Color {
29 Red,
30 Yellow,
31 Green,
32}
33
34impl Color {
35 fn code(self) -> &'static str {
36 match self {
37 Self::Red => "31",
38 Self::Yellow => "33",
39 Self::Green => "32",
40 }
41 }
42}
43
44fn get_color_choice() -> clap::ColorChoice {
46 if std::env::var("NO_COLOR").is_ok() {
47 clap::ColorChoice::Never
48 } else if std::env::var("CLICOLOR_FORCE").is_ok() || std::env::var("FORCE_COLOR").is_ok() {
49 clap::ColorChoice::Always
50 } else {
51 clap::ColorChoice::Auto
52 }
53}
54
55fn should_use_color_for_stream<S: std::io::IsTerminal>(stream: &S) -> bool {
57 match get_color_choice() {
58 clap::ColorChoice::Always => true,
59 clap::ColorChoice::Never => false,
60 clap::ColorChoice::Auto => {
61 stream.is_terminal() && std::env::var("TERM").unwrap_or_default() != "dumb"
62 }
63 }
64}
65
66struct ColorManager(bool);
68
69impl ColorManager {
70 fn from_env() -> Self {
72 Self(should_use_color_for_stream(&stderr()))
73 }
74
75 fn colorize(&self, text: &str, color: Color) -> String {
77 if self.0 {
78 format!("\x1b[{}m{text}\x1b[0m", color.code())
79 } else {
80 text.to_string()
81 }
82 }
83}
84
85pub struct ErrorFormatter<'a> {
87 color_mgr: ColorManager,
88 util_name: &'a str,
89}
90
91impl<'a> ErrorFormatter<'a> {
92 pub fn new(util_name: &'a str) -> Self {
93 Self {
94 color_mgr: ColorManager::from_env(),
95 util_name,
96 }
97 }
98
99 fn print_error_and_exit(&self, err: &Error, exit_code: i32) -> ! {
101 self.print_error_and_exit_with_callback(err, exit_code, || {})
102 }
103
104 pub fn print_error_and_exit_with_callback<F>(
106 &self,
107 err: &Error,
108 exit_code: i32,
109 callback: F,
110 ) -> !
111 where
112 F: FnOnce(),
113 {
114 let code = self.print_error(err, exit_code);
115 callback();
116 std::process::exit(code);
117 }
118
119 pub fn print_error(&self, err: &Error, exit_code: i32) -> i32 {
121 match err.kind() {
122 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => self.handle_display_errors(err),
123 ErrorKind::UnknownArgument => self.handle_unknown_argument(err, exit_code),
124 ErrorKind::InvalidValue | ErrorKind::ValueValidation => {
125 self.handle_invalid_value(err, exit_code)
126 }
127 ErrorKind::MissingRequiredArgument => self.handle_missing_required(err, exit_code),
128 ErrorKind::TooFewValues | ErrorKind::TooManyValues | ErrorKind::WrongNumberOfValues => {
129 let _ = write!(stderr(), "{}", err.render());
131 exit_code
132 }
133 _ => self.handle_generic_error(err, exit_code),
134 }
135 }
136
137 fn handle_display_errors(&self, err: &Error) -> i32 {
139 print!("{}", err.render());
140 0
141 }
142
143 fn handle_unknown_argument(&self, err: &Error, exit_code: i32) -> i32 {
145 if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) {
146 let arg_str = invalid_arg.to_string();
147 let error_word = translate!("common-error");
148
149 let _ = write!(
151 stderr(),
152 "{}\n\n",
153 translate!(
154 "clap-error-unexpected-argument",
155 "arg" => self.color_mgr.colorize(&arg_str, Color::Yellow),
156 "error_word" => self.color_mgr.colorize(&error_word, Color::Red)
157 )
158 );
159
160 if let Some(suggested_arg) = err.get(ContextKind::SuggestedArg) {
162 let tip_word = translate!("common-tip");
163 let _ = write!(
164 stderr(),
165 "{}\n\n",
166 translate!(
167 "clap-error-similar-argument",
168 "tip_word" => self.color_mgr.colorize(&tip_word, Color::Green),
169 "suggestion" => self.color_mgr.colorize(&suggested_arg.to_string(), Color::Green)
170 )
171 );
172 } else {
173 self.print_clap_tips(err);
175 }
176
177 self.print_usage_and_help();
178 } else {
179 self.print_simple_error_msg(&translate!("clap-error-unexpected-argument-simple"));
180 }
181 exit_code
182 }
183
184 fn handle_invalid_value(&self, err: &Error, exit_code: i32) -> i32 {
186 let invalid_arg = err.get(ContextKind::InvalidArg);
187 let invalid_value = err.get(ContextKind::InvalidValue);
188
189 if let (Some(arg), Some(value)) = (invalid_arg, invalid_value) {
190 let option = arg.to_string();
191 let value = value.to_string();
192
193 if value.is_empty() {
194 let error_word = translate!("common-error");
196 let _ = writeln!(
197 stderr(),
198 "{}",
199 translate!("clap-error-value-required",
200 "error_word" => self.color_mgr.colorize(&error_word, Color::Red),
201 "option" => self.color_mgr.colorize(&option, Color::Green))
202 );
203 } else {
204 let error_word = translate!("common-error");
206 let error_msg = translate!(
207 "clap-error-invalid-value",
208 "error_word" => self.color_mgr.colorize(&error_word, Color::Red),
209 "value" => self.color_mgr.colorize(&value, Color::Yellow),
210 "option" => self.color_mgr.colorize(&option, Color::Green)
211 );
212 match err.source() {
214 Some(source) if matches!(err.kind(), ErrorKind::ValueValidation) => {
215 let _ = writeln!(stderr(), "{error_msg}: {source}");
216 }
217 _ => eprintln!("{error_msg}"),
218 }
219 }
220
221 if matches!(err.kind(), ErrorKind::InvalidValue) {
223 if let Some(valid_values) = err.get(ContextKind::ValidValue) {
224 if !valid_values.to_string().is_empty() {
225 let _ = writeln!(
226 stderr(),
227 "\n [{}: {valid_values}]",
228 translate!("clap-error-possible-values")
229 );
230 }
231 }
232 }
233 let _ = writeln!(stderr(), "\n{}", translate!("common-help-suggestion"));
234 } else {
235 self.print_simple_error_msg(&err.render().to_string());
236 }
237
238 if matches!(err.kind(), ErrorKind::InvalidValue) && exit_code < 125 {
243 1 } else {
245 exit_code }
247 }
248
249 fn handle_missing_required(&self, err: &Error, exit_code: i32) -> i32 {
251 let rendered_str = err.render().to_string();
252 let lines: Vec<&str> = rendered_str.lines().collect();
253
254 match lines.first() {
255 Some(first_line)
256 if first_line
257 .starts_with("error: the following required arguments were not provided:") =>
258 {
259 let error_word = translate!("common-error");
260 let _ = writeln!(
261 stderr(),
262 "{}",
263 translate!(
264 "clap-error-missing-required-arguments",
265 "error_word" => self.color_mgr.colorize(&error_word, Color::Red)
266 )
267 );
268
269 for line in lines.iter().skip(1) {
271 if line.starts_with(" ") {
272 let _ = writeln!(stderr(), "{line}");
273 } else if line.starts_with("Usage:") || line.starts_with("For more information")
274 {
275 break;
276 }
277 }
278 let _ = writeln!(stderr());
279
280 lines
282 .iter()
283 .skip_while(|line| !line.starts_with("Usage:"))
284 .for_each(|line| {
285 if line.starts_with("For more information, try '--help'.") {
286 let _ = writeln!(stderr(), "{}", translate!("common-help-suggestion"));
287 } else {
288 let _ = writeln!(stderr(), "{line}");
289 }
290 });
291 }
292 _ => eprint!("{}", err.render()),
293 }
294 exit_code
295 }
296
297 fn handle_generic_error(&self, err: &Error, exit_code: i32) -> i32 {
299 let rendered_str = err.render().to_string();
300 if let Some(main_error_line) = rendered_str.lines().next() {
301 self.print_localized_error_line(main_error_line);
302 let _ = writeln!(stderr(), "\n{}", translate!("common-help-suggestion"));
303 } else {
304 let _ = write!(stderr(), "{}", err.render());
305 }
306 exit_code
307 }
308
309 fn print_simple_error_msg(&self, message: &str) {
311 let error_word = translate!("common-error");
312 let _ = writeln!(
313 stderr(),
314 "{}: {message}",
315 self.color_mgr.colorize(&error_word, Color::Red)
316 );
317 }
318
319 fn print_localized_error_line(&self, line: &str) {
321 let error_word = translate!("common-error");
322 let colored_error = self.color_mgr.colorize(&error_word, Color::Red);
323
324 if let Some(colon_pos) = line.find(':') {
325 let after_colon = &line[colon_pos..];
326 let _ = writeln!(stderr(), "{colored_error}{after_colon}");
327 } else {
328 let _ = writeln!(stderr(), "{line}");
329 }
330 }
331
332 fn print_clap_tips(&self, err: &Error) {
334 let rendered_str = err.render().to_string();
335 for line in rendered_str.lines() {
336 let trimmed = line.trim_start();
337 if trimmed.starts_with("tip:") && !line.contains("similar argument") {
338 let tip_word = translate!("common-tip");
339 if let Some(colon_pos) = trimmed.find(':') {
340 let after_colon = &trimmed[colon_pos..];
341 let _ = writeln!(
342 stderr(),
343 " {}{after_colon}",
344 self.color_mgr.colorize(&tip_word, Color::Green)
345 );
346 } else {
347 let _ = writeln!(stderr(), "{line}");
348 }
349 let _ = writeln!(stderr());
350 }
351 }
352 }
353
354 fn print_usage_and_help(&self) {
356 let usage_key = format!("{}-usage", self.util_name);
357 let usage_text = translate!(&usage_key);
358 let formatted_usage = crate::format_usage(&usage_text);
359 let usage_label = translate!("common-usage");
360 let _ = writeln!(
361 stderr(),
362 "{usage_label}: {formatted_usage}\n\n{}",
363 translate!("common-help-suggestion")
364 );
365 }
366}
367
368pub fn handle_clap_result<I, T>(cmd: Command, itr: I) -> UResult<ArgMatches>
395where
396 I: IntoIterator<Item = T>,
397 T: Into<OsString> + Clone,
398{
399 handle_clap_result_with_exit_code(cmd, itr, 1)
400}
401
402pub fn handle_clap_result_with_exit_code<I, T>(
435 cmd: Command,
436 itr: I,
437 exit_code: i32,
438) -> UResult<ArgMatches>
439where
440 I: IntoIterator<Item = T>,
441 T: Into<OsString> + Clone,
442{
443 cmd.try_get_matches_from(itr).map_err(|e| {
444 if e.exit_code() == 0 {
445 e.into() } else {
447 let formatter = ErrorFormatter::new(crate::util_name());
448 let code = formatter.print_error(&e, exit_code);
449 USimpleError::new(code, "")
450 }
451 })
452}
453
454pub fn handle_clap_error_with_exit_code(err: Error, exit_code: i32) -> ! {
482 let formatter = ErrorFormatter::new(crate::util_name());
483 formatter.print_error_and_exit(&err, exit_code);
484}
485
486pub fn configure_localized_command(mut cmd: Command) -> Command {
519 let color_choice = get_color_choice();
520 cmd = cmd.color(color_choice);
521
522 let colors_enabled = should_use_color_for_stream(&std::io::stdout());
524
525 cmd = cmd.help_template(crate::localized_help_template_with_colors(
526 crate::util_name(),
527 colors_enabled,
528 ));
529 cmd
530}
531
532#[cfg(test)]
534mod tests {
535 use super::*;
536 use clap::{Arg, Command};
537 use std::ffi::OsString;
538
539 #[test]
540 fn test_color_codes() {
541 assert_eq!(Color::Red.code(), "31");
542 assert_eq!(Color::Yellow.code(), "33");
543 assert_eq!(Color::Green.code(), "32");
544 }
545
546 #[test]
547 fn test_color_manager() {
548 let mgr = ColorManager(true);
549 let red_text = mgr.colorize("error", Color::Red);
550 assert_eq!(red_text, "\x1b[31merror\x1b[0m");
551
552 let mgr_disabled = ColorManager(false);
553 let plain_text = mgr_disabled.colorize("error", Color::Red);
554 assert_eq!(plain_text, "error");
555 }
556
557 fn create_test_command() -> Command {
558 Command::new("test")
559 .arg(
560 Arg::new("input")
561 .short('i')
562 .long("input")
563 .value_name("FILE")
564 .help("Input file"),
565 )
566 .arg(
567 Arg::new("output")
568 .short('o')
569 .long("output")
570 .value_name("FILE")
571 .help("Output file"),
572 )
573 .arg(
574 Arg::new("format")
575 .long("format")
576 .value_parser(["json", "xml", "csv"])
577 .help("Output format"),
578 )
579 }
580
581 #[test]
582 fn test_handle_clap_result_with_valid_args() {
583 let cmd = create_test_command();
584 let result = handle_clap_result(cmd, vec!["test", "--input", "file.txt"]);
585 assert!(result.is_ok());
586 let matches = result.unwrap();
587 assert_eq!(matches.get_one::<String>("input").unwrap(), "file.txt");
588 }
589
590 #[test]
591 fn test_handle_clap_result_with_osstring() {
592 let args: Vec<OsString> = vec!["test".into(), "--output".into(), "out.txt".into()];
593 let cmd = create_test_command();
594 let result = handle_clap_result(cmd, args);
595 assert!(result.is_ok());
596 let matches = result.unwrap();
597 assert_eq!(matches.get_one::<String>("output").unwrap(), "out.txt");
598 }
599
600 #[test]
601 fn test_configure_localized_command() {
602 let cmd = Command::new("test");
603 let configured = configure_localized_command(cmd);
604 assert_eq!(configured.get_name(), "test");
607 }
608
609 #[test]
610 fn test_color_environment_vars() {
611 use std::env;
612
613 unsafe {
615 env::set_var("NO_COLOR", "1");
616 }
617 assert_eq!(get_color_choice(), clap::ColorChoice::Never);
618 assert!(!should_use_color_for_stream(&stderr()));
619 let mgr = ColorManager::from_env();
620 assert!(!mgr.0);
621 unsafe {
622 env::remove_var("NO_COLOR");
623 }
624
625 unsafe {
627 env::set_var("CLICOLOR_FORCE", "1");
628 }
629 assert_eq!(get_color_choice(), clap::ColorChoice::Always);
630 assert!(should_use_color_for_stream(&stderr()));
631 let mgr = ColorManager::from_env();
632 assert!(mgr.0);
633 unsafe {
634 env::remove_var("CLICOLOR_FORCE");
635 }
636
637 unsafe {
639 env::set_var("FORCE_COLOR", "1");
640 }
641 assert_eq!(get_color_choice(), clap::ColorChoice::Always);
642 assert!(should_use_color_for_stream(&stderr()));
643 unsafe {
644 env::remove_var("FORCE_COLOR");
645 }
646 }
647
648 #[test]
649 fn test_error_formatter_creation() {
650 let formatter = ErrorFormatter::new("test");
651 assert_eq!(formatter.util_name, "test");
652 }
654
655 #[test]
656 fn test_localization_keys_exist() {
657 use crate::locale::{get_message, setup_localization};
658
659 let _ = setup_localization("test");
660
661 let required_keys = [
662 "common-error",
663 "common-usage",
664 "common-tip",
665 "common-help-suggestion",
666 "clap-error-unexpected-argument",
667 "clap-error-invalid-value",
668 "clap-error-missing-required-arguments",
669 "clap-error-similar-argument",
670 "clap-error-possible-values",
671 "clap-error-value-required",
672 ];
673
674 for key in &required_keys {
675 let message = get_message(key);
676 assert_ne!(message, *key, "Translation missing for key: {key}");
677 }
678 }
679
680 #[test]
681 fn test_french_localization() {
682 use crate::locale::{get_message, setup_localization};
683 use std::env;
684
685 let original_lang = env::var_os("LANG").unwrap_or_default();
686
687 unsafe {
688 env::set_var("LANG", "fr_FR.UTF-8");
689 }
690
691 if setup_localization("test").is_ok() {
692 assert_eq!(get_message("common-error"), "erreur");
693 assert_eq!(get_message("common-usage"), "Utilisation");
694 assert_eq!(get_message("common-tip"), "conseil");
695 }
696
697 unsafe {
698 if original_lang.is_empty() {
699 env::remove_var("LANG");
700 } else {
701 env::set_var("LANG", original_lang);
702 }
703 }
704 }
705}
706