use crate::error::UResult;
use crate::locale::translate;
use clap::error::{ContextKind, ErrorKind};
use clap::{ArgMatches, Command, Error};
use std::error::Error as StdError;
use std::ffi::OsString;
#[derive(Debug, Clone, Copy)]
pub enum Color {
Red,
Yellow,
Green,
}
impl Color {
fn code(self) -> &'static str {
match self {
Self::Red => "31",
Self::Yellow => "33",
Self::Green => "32",
}
}
}
fn get_color_choice() -> clap::ColorChoice {
if std::env::var("NO_COLOR").is_ok() {
clap::ColorChoice::Never
} else if std::env::var("CLICOLOR_FORCE").is_ok() || std::env::var("FORCE_COLOR").is_ok() {
clap::ColorChoice::Always
} else {
clap::ColorChoice::Auto
}
}
fn should_use_color_for_stream<S: std::io::IsTerminal>(stream: &S) -> bool {
match get_color_choice() {
clap::ColorChoice::Always => true,
clap::ColorChoice::Never => false,
clap::ColorChoice::Auto => {
stream.is_terminal() && std::env::var("TERM").unwrap_or_default() != "dumb"
}
}
}
struct ColorManager(bool);
impl ColorManager {
fn from_env() -> Self {
Self(should_use_color_for_stream(&std::io::stderr()))
}
fn colorize(&self, text: &str, color: Color) -> String {
if self.0 {
format!("\x1b[{}m{text}\x1b[0m", color.code())
} else {
text.to_string()
}
}
}
pub struct ErrorFormatter<'a> {
color_mgr: ColorManager,
util_name: &'a str,
}
impl<'a> ErrorFormatter<'a> {
pub fn new(util_name: &'a str) -> Self {
Self {
color_mgr: ColorManager::from_env(),
util_name,
}
}
fn print_error_and_exit(&self, err: &Error, exit_code: i32) -> ! {
self.print_error_and_exit_with_callback(err, exit_code, || {})
}
pub fn print_error_and_exit_with_callback<F>(
&self,
err: &Error,
exit_code: i32,
callback: F,
) -> !
where
F: FnOnce(),
{
match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => self.handle_display_errors(err),
ErrorKind::UnknownArgument => {
self.handle_unknown_argument_with_callback(err, exit_code, callback)
}
ErrorKind::InvalidValue | ErrorKind::ValueValidation => {
self.handle_invalid_value_with_callback(err, exit_code, callback)
}
ErrorKind::MissingRequiredArgument => {
self.handle_missing_required_with_callback(err, exit_code, callback)
}
ErrorKind::TooFewValues | ErrorKind::TooManyValues | ErrorKind::WrongNumberOfValues => {
eprint!("{}", err.render());
callback();
std::process::exit(exit_code);
}
_ => self.handle_generic_error_with_callback(err, exit_code, callback),
}
}
fn handle_display_errors(&self, err: &Error) -> ! {
print!("{}", err.render());
std::process::exit(0);
}
fn handle_unknown_argument_with_callback<F>(
&self,
err: &Error,
exit_code: i32,
callback: F,
) -> !
where
F: FnOnce(),
{
if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) {
let arg_str = invalid_arg.to_string();
let error_word = translate!("common-error");
eprintln!(
"{}",
translate!(
"clap-error-unexpected-argument",
"arg" => self.color_mgr.colorize(&arg_str, Color::Yellow),
"error_word" => self.color_mgr.colorize(&error_word, Color::Red)
)
);
eprintln!();
if let Some(suggested_arg) = err.get(ContextKind::SuggestedArg) {
let tip_word = translate!("common-tip");
eprintln!(
"{}",
translate!(
"clap-error-similar-argument",
"tip_word" => self.color_mgr.colorize(&tip_word, Color::Green),
"suggestion" => self.color_mgr.colorize(&suggested_arg.to_string(), Color::Green)
)
);
eprintln!();
} else {
self.print_clap_tips(err);
}
self.print_usage_and_help();
} else {
self.print_simple_error_with_callback(
&translate!("clap-error-unexpected-argument-simple"),
exit_code,
|| {},
);
}
callback();
std::process::exit(exit_code);
}
fn handle_invalid_value_with_callback<F>(&self, err: &Error, exit_code: i32, callback: F) -> !
where
F: FnOnce(),
{
let invalid_arg = err.get(ContextKind::InvalidArg);
let invalid_value = err.get(ContextKind::InvalidValue);
if let (Some(arg), Some(value)) = (invalid_arg, invalid_value) {
let option = arg.to_string();
let value = value.to_string();
if value.is_empty() {
let error_word = translate!("common-error");
eprintln!(
"{}",
translate!("clap-error-value-required",
"error_word" => self.color_mgr.colorize(&error_word, Color::Red),
"option" => self.color_mgr.colorize(&option, Color::Green))
);
} else {
let error_word = translate!("common-error");
let error_msg = translate!(
"clap-error-invalid-value",
"error_word" => self.color_mgr.colorize(&error_word, Color::Red),
"value" => self.color_mgr.colorize(&value, Color::Yellow),
"option" => self.color_mgr.colorize(&option, Color::Green)
);
match err.source() {
Some(source) if matches!(err.kind(), ErrorKind::ValueValidation) => {
eprintln!("{error_msg}: {source}");
}
_ => eprintln!("{error_msg}"),
}
}
if matches!(err.kind(), ErrorKind::InvalidValue) {
if let Some(valid_values) = err.get(ContextKind::ValidValue) {
if !valid_values.to_string().is_empty() {
eprintln!();
eprintln!(
" [{}: {valid_values}]",
translate!("clap-error-possible-values")
);
}
}
}
eprintln!();
eprintln!("{}", translate!("common-help-suggestion"));
} else {
self.print_simple_error(&err.render().to_string(), exit_code);
}
let actual_exit_code = if matches!(err.kind(), ErrorKind::InvalidValue) && exit_code < 125 {
1 } else {
exit_code };
callback();
std::process::exit(actual_exit_code);
}
fn handle_missing_required_with_callback<F>(
&self,
err: &Error,
exit_code: i32,
callback: F,
) -> !
where
F: FnOnce(),
{
let rendered_str = err.render().to_string();
let lines: Vec<&str> = rendered_str.lines().collect();
match lines.first() {
Some(first_line)
if first_line
.starts_with("error: the following required arguments were not provided:") =>
{
let error_word = translate!("common-error");
eprintln!(
"{}",
translate!(
"clap-error-missing-required-arguments",
"error_word" => self.color_mgr.colorize(&error_word, Color::Red)
)
);
for line in lines.iter().skip(1) {
if line.starts_with(" ") {
eprintln!("{line}");
} else if line.starts_with("Usage:") || line.starts_with("For more information")
{
break;
}
}
eprintln!();
lines
.iter()
.skip_while(|line| !line.starts_with("Usage:"))
.for_each(|line| {
if line.starts_with("For more information, try '--help'.") {
eprintln!("{}", translate!("common-help-suggestion"));
} else {
eprintln!("{line}");
}
});
}
_ => eprint!("{}", err.render()),
}
callback();
std::process::exit(exit_code);
}
fn handle_generic_error_with_callback<F>(&self, err: &Error, exit_code: i32, callback: F) -> !
where
F: FnOnce(),
{
let rendered_str = err.render().to_string();
if let Some(main_error_line) = rendered_str.lines().next() {
self.print_localized_error_line(main_error_line);
eprintln!();
eprintln!("{}", translate!("common-help-suggestion"));
} else {
eprint!("{}", err.render());
}
callback();
std::process::exit(exit_code);
}
fn print_simple_error(&self, message: &str, exit_code: i32) -> ! {
self.print_simple_error_with_callback(message, exit_code, || {})
}
fn print_simple_error_with_callback<F>(&self, message: &str, exit_code: i32, callback: F) -> !
where
F: FnOnce(),
{
let error_word = translate!("common-error");
eprintln!(
"{}: {message}",
self.color_mgr.colorize(&error_word, Color::Red)
);
callback();
std::process::exit(exit_code);
}
fn print_localized_error_line(&self, line: &str) {
let error_word = translate!("common-error");
let colored_error = self.color_mgr.colorize(&error_word, Color::Red);
if let Some(colon_pos) = line.find(':') {
let after_colon = &line[colon_pos..];
eprintln!("{colored_error}{after_colon}");
} else {
eprintln!("{line}");
}
}
fn print_clap_tips(&self, err: &Error) {
let rendered_str = err.render().to_string();
for line in rendered_str.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("tip:") && !line.contains("similar argument") {
let tip_word = translate!("common-tip");
if let Some(colon_pos) = trimmed.find(':') {
let after_colon = &trimmed[colon_pos..];
eprintln!(
" {}{after_colon}",
self.color_mgr.colorize(&tip_word, Color::Green)
);
} else {
eprintln!("{line}");
}
eprintln!();
}
}
}
fn print_usage_and_help(&self) {
let usage_key = format!("{}-usage", self.util_name);
let usage_text = translate!(&usage_key);
let formatted_usage = crate::format_usage(&usage_text);
let usage_label = translate!("common-usage");
eprintln!("{usage_label}: {formatted_usage}");
eprintln!();
eprintln!("{}", translate!("common-help-suggestion"));
}
}
pub fn handle_clap_result<I, T>(cmd: Command, itr: I) -> UResult<ArgMatches>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
handle_clap_result_with_exit_code(cmd, itr, 1)
}
pub fn handle_clap_result_with_exit_code<I, T>(
cmd: Command,
itr: I,
exit_code: i32,
) -> UResult<ArgMatches>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
cmd.try_get_matches_from(itr).map_err(|e| {
if e.exit_code() == 0 {
e.into() } else {
handle_clap_error_with_exit_code(e, exit_code)
}
})
}
pub fn handle_clap_error_with_exit_code(err: Error, exit_code: i32) -> ! {
let formatter = ErrorFormatter::new(crate::util_name());
formatter.print_error_and_exit(&err, exit_code);
}
pub fn configure_localized_command(mut cmd: Command) -> Command {
let color_choice = get_color_choice();
cmd = cmd.color(color_choice);
let colors_enabled = should_use_color_for_stream(&std::io::stdout());
cmd = cmd.help_template(crate::localized_help_template_with_colors(
crate::util_name(),
colors_enabled,
));
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use clap::{Arg, Command};
use std::ffi::OsString;
#[test]
fn test_color_codes() {
assert_eq!(Color::Red.code(), "31");
assert_eq!(Color::Yellow.code(), "33");
assert_eq!(Color::Green.code(), "32");
}
#[test]
fn test_color_manager() {
let mgr = ColorManager(true);
let red_text = mgr.colorize("error", Color::Red);
assert_eq!(red_text, "\x1b[31merror\x1b[0m");
let mgr_disabled = ColorManager(false);
let plain_text = mgr_disabled.colorize("error", Color::Red);
assert_eq!(plain_text, "error");
}
fn create_test_command() -> Command {
Command::new("test")
.arg(
Arg::new("input")
.short('i')
.long("input")
.value_name("FILE")
.help("Input file"),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("Output file"),
)
.arg(
Arg::new("format")
.long("format")
.value_parser(["json", "xml", "csv"])
.help("Output format"),
)
}
#[test]
fn test_handle_clap_result_with_valid_args() {
let cmd = create_test_command();
let result = handle_clap_result(cmd, vec!["test", "--input", "file.txt"]);
assert!(result.is_ok());
let matches = result.unwrap();
assert_eq!(matches.get_one::<String>("input").unwrap(), "file.txt");
}
#[test]
fn test_handle_clap_result_with_osstring() {
let args: Vec<OsString> = vec!["test".into(), "--output".into(), "out.txt".into()];
let cmd = create_test_command();
let result = handle_clap_result(cmd, args);
assert!(result.is_ok());
let matches = result.unwrap();
assert_eq!(matches.get_one::<String>("output").unwrap(), "out.txt");
}
#[test]
fn test_configure_localized_command() {
let cmd = Command::new("test");
let configured = configure_localized_command(cmd);
assert_eq!(configured.get_name(), "test");
}
#[test]
fn test_color_environment_vars() {
use std::env;
unsafe {
env::set_var("NO_COLOR", "1");
}
assert_eq!(get_color_choice(), clap::ColorChoice::Never);
assert!(!should_use_color_for_stream(&std::io::stderr()));
let mgr = ColorManager::from_env();
assert!(!mgr.0);
unsafe {
env::remove_var("NO_COLOR");
}
unsafe {
env::set_var("CLICOLOR_FORCE", "1");
}
assert_eq!(get_color_choice(), clap::ColorChoice::Always);
assert!(should_use_color_for_stream(&std::io::stderr()));
let mgr = ColorManager::from_env();
assert!(mgr.0);
unsafe {
env::remove_var("CLICOLOR_FORCE");
}
unsafe {
env::set_var("FORCE_COLOR", "1");
}
assert_eq!(get_color_choice(), clap::ColorChoice::Always);
assert!(should_use_color_for_stream(&std::io::stderr()));
unsafe {
env::remove_var("FORCE_COLOR");
}
}
#[test]
fn test_error_formatter_creation() {
let formatter = ErrorFormatter::new("test");
assert_eq!(formatter.util_name, "test");
}
#[test]
fn test_localization_keys_exist() {
use crate::locale::{get_message, setup_localization};
let _ = setup_localization("test");
let required_keys = [
"common-error",
"common-usage",
"common-tip",
"common-help-suggestion",
"clap-error-unexpected-argument",
"clap-error-invalid-value",
"clap-error-missing-required-arguments",
"clap-error-similar-argument",
"clap-error-possible-values",
"clap-error-value-required",
];
for key in &required_keys {
let message = get_message(key);
assert_ne!(message, *key, "Translation missing for key: {key}");
}
}
#[test]
fn test_french_localization() {
use crate::locale::{get_message, setup_localization};
use std::env;
let original_lang = env::var("LANG").unwrap_or_default();
unsafe {
env::set_var("LANG", "fr_FR.UTF-8");
}
if setup_localization("test").is_ok() {
assert_eq!(get_message("common-error"), "erreur");
assert_eq!(get_message("common-usage"), "Utilisation");
assert_eq!(get_message("common-tip"), "conseil");
}
unsafe {
if original_lang.is_empty() {
env::remove_var("LANG");
} else {
env::set_var("LANG", original_lang);
}
}
}
}