diff --git a/Cargo.lock b/Cargo.lock index 44c6a1ca4..f8e977712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3027,6 +3027,7 @@ name = "uu_fmt" version = "0.1.0" dependencies = [ "clap", + "thiserror 2.0.12", "unicode-width 0.2.1", "uucore", ] diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index 65e03b80d..9dc8fccb7 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -21,6 +21,7 @@ path = "src/fmt.rs" clap = { workspace = true } unicode-width = { workspace = true } uucore = { workspace = true } +thiserror = { workspace = true } [[bin]] name = "fmt" diff --git a/src/uu/fmt/locales/en-US.ftl b/src/uu/fmt/locales/en-US.ftl index c572e7f5c..4270e0a89 100644 --- a/src/uu/fmt/locales/en-US.ftl +++ b/src/uu/fmt/locales/en-US.ftl @@ -1,2 +1,32 @@ -fmt-about = Reformat paragraphs from input files (or stdin) to stdout. -fmt-usage = fmt [-WIDTH] [OPTION]... [FILE]... +fmt-about = Reformat paragraphs from input (or standard input) to stdout. +fmt-usage = [OPTION]... [FILE]... + +# Help messages +fmt-crown-margin-help = First and second line of paragraph may have different indentations, in which case the first line's indentation is preserved, and each subsequent line's indentation matches the second line. +fmt-tagged-paragraph-help = Like -c, except that the first and second line of a paragraph *must* have different indentation or they are treated as separate paragraphs. +fmt-preserve-headers-help = Attempt to detect and preserve mail headers in the input. Be careful when combining this flag with -p. +fmt-split-only-help = Split lines only, do not reflow. +fmt-uniform-spacing-help = Insert exactly one space between words, and two between sentences. Sentence breaks in the input are detected as [?!.] followed by two spaces or a newline; other punctuation is not interpreted as a sentence break. +fmt-prefix-help = Reformat only lines beginning with PREFIX, reattaching PREFIX to reformatted lines. Unless -x is specified, leading whitespace will be ignored when matching PREFIX. +fmt-skip-prefix-help = Do not reformat lines beginning with PSKIP. Unless -X is specified, leading whitespace will be ignored when matching PSKIP +fmt-exact-prefix-help = PREFIX must match at the beginning of the line with no preceding whitespace. +fmt-exact-skip-prefix-help = PSKIP must match at the beginning of the line with no preceding whitespace. +fmt-width-help = Fill output lines up to a maximum of WIDTH columns, default 75. This can be specified as a negative number in the first argument. +fmt-goal-help = Goal width, default of 93% of WIDTH. Must be less than or equal to WIDTH. +fmt-quick-help = Break lines more quickly at the expense of a potentially more ragged appearance. +fmt-tab-width-help = Treat tabs as TABWIDTH spaces for determining line length, default 8. Note that this is used only for calculating line lengths; tabs are preserved in the output. + +# Error messages +fmt-error-invalid-goal = invalid goal: {$goal} +fmt-error-goal-greater-than-width = GOAL cannot be greater than WIDTH. +fmt-error-invalid-width = invalid width: {$width} +fmt-error-width-out-of-range = invalid width: '{$width}': Numerical result out of range +fmt-error-invalid-tabwidth = Invalid TABWIDTH specification: {$tabwidth} +fmt-error-first-option-width = invalid option -- {$option}; -WIDTH is recognized only when it is the first + option; use -w N instead + Try 'fmt --help' for more information. +fmt-error-read = read error +fmt-error-invalid-width-malformed = invalid width: {$width} +fmt-error-cannot-open-for-reading = cannot open {$file} for reading +fmt-error-cannot-get-metadata = cannot get metadata for {$file} +fmt-error-failed-to-write-output = failed to write output diff --git a/src/uu/fmt/locales/fr-FR.ftl b/src/uu/fmt/locales/fr-FR.ftl new file mode 100644 index 000000000..640942a8f --- /dev/null +++ b/src/uu/fmt/locales/fr-FR.ftl @@ -0,0 +1,32 @@ +fmt-about = Reformate les paragraphes depuis l'entrée (ou l'entrée standard) vers la sortie standard. +fmt-usage = [OPTION]... [FICHIER]... + +# Messages d'aide +fmt-crown-margin-help = La première et la deuxième ligne d'un paragraphe peuvent avoir des indentations différentes, auquel cas l'indentation de la première ligne est préservée, et chaque ligne suivante correspond à l'indentation de la deuxième ligne. +fmt-tagged-paragraph-help = Comme -c, sauf que la première et la deuxième ligne d'un paragraphe *doivent* avoir des indentations différentes ou elles sont traitées comme des paragraphes séparés. +fmt-preserve-headers-help = Tente de détecter et préserver les en-têtes de courrier dans l'entrée. Attention en combinant ce drapeau avec -p. +fmt-split-only-help = Divise les lignes seulement, ne les reformate pas. +fmt-uniform-spacing-help = Insère exactement un espace entre les mots, et deux entre les phrases. Les fins de phrase dans l'entrée sont détectées comme [?!.] suivies de deux espaces ou d'une nouvelle ligne ; les autres ponctuations ne sont pas interprétées comme des fins de phrase. +fmt-prefix-help = Reformate seulement les lignes commençant par PRÉFIXE, en rattachant PRÉFIXE aux lignes reformatées. À moins que -x soit spécifié, les espaces de début seront ignorés lors de la correspondance avec PRÉFIXE. +fmt-skip-prefix-help = Ne reformate pas les lignes commençant par PSKIP. À moins que -X soit spécifié, les espaces de début seront ignorés lors de la correspondance avec PSKIP +fmt-exact-prefix-help = PRÉFIXE doit correspondre au début de la ligne sans espace précédent. +fmt-exact-skip-prefix-help = PSKIP doit correspondre au début de la ligne sans espace précédent. +fmt-width-help = Remplit les lignes de sortie jusqu'à un maximum de WIDTH colonnes, par défaut 75. Cela peut être spécifié comme un nombre négatif dans le premier argument. +fmt-goal-help = Largeur objectif, par défaut 93% de WIDTH. Doit être inférieur ou égal à WIDTH. +fmt-quick-help = Divise les lignes plus rapidement au détriment d'un aspect potentiellement plus irrégulier. +fmt-tab-width-help = Traite les tabulations comme TABWIDTH espaces pour déterminer la longueur de ligne, par défaut 8. Notez que ceci n'est utilisé que pour calculer les longueurs de ligne ; les tabulations sont préservées dans la sortie. + +# Messages d'erreur +fmt-error-invalid-goal = objectif invalide : {$goal} +fmt-error-goal-greater-than-width = GOAL ne peut pas être supérieur à WIDTH. +fmt-error-invalid-width = largeur invalide : {$width} +fmt-error-width-out-of-range = largeur invalide : '{$width}' : Résultat numérique hors limites +fmt-error-invalid-tabwidth = Spécification TABWIDTH invalide : {$tabwidth} +fmt-error-first-option-width = option invalide -- {$option} ; -WIDTH n'est reconnu que lorsqu'il est la première + option ; utilisez -w N à la place + Essayez 'fmt --help' pour plus d'informations. +fmt-error-read = erreur de lecture +fmt-error-invalid-width-malformed = largeur invalide : {$width} +fmt-error-cannot-open-for-reading = impossible d'ouvrir {$file} en lecture +fmt-error-cannot-get-metadata = impossible d'obtenir les métadonnées pour {$file} +fmt-error-failed-to-write-output = échec de l'écriture de sortie diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index bb79c2c59..f6be36a3c 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -9,16 +9,45 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use std::fs::File; use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; +use uucore::error::{FromIo, UResult, USimpleError}; + use uucore::format_usage; use linebreak::break_lines; use parasplit::ParagraphStream; -use uucore::locale::get_message; +use std::collections::HashMap; +use thiserror::Error; +use uucore::locale::{get_message, get_message_with_args}; mod linebreak; mod parasplit; +#[derive(Debug, Error)] +enum FmtError { + #[error("{}", get_message_with_args("fmt-error-invalid-goal", HashMap::from([("goal".to_string(), .0.quote().to_string())])))] + InvalidGoal(String), + #[error("{}", get_message("fmt-error-goal-greater-than-width"))] + GoalGreaterThanWidth, + #[error("{}", get_message_with_args("fmt-error-invalid-width", HashMap::from([("width".to_string(), .0.quote().to_string())])))] + InvalidWidth(String), + #[error("{}", get_message_with_args("fmt-error-width-out-of-range", HashMap::from([("width".to_string(), .0.to_string())])))] + WidthOutOfRange(usize), + #[error("{}", get_message_with_args("fmt-error-invalid-tabwidth", HashMap::from([("tabwidth".to_string(), .0.quote().to_string())])))] + InvalidTabWidth(String), + #[error("{}", get_message_with_args("fmt-error-first-option-width", HashMap::from([("option".to_string(), .0.to_string())])))] + FirstOptionWidth(char), + #[error("{}", get_message("fmt-error-read"))] + ReadError, + #[error("{}", get_message_with_args("fmt-error-invalid-width-malformed", HashMap::from([("width".to_string(), .0.quote().to_string())])))] + InvalidWidthMalformed(String), +} + +impl From for Box { + fn from(err: FmtError) -> Self { + USimpleError::new(1, err.to_string()) + } +} + const MAX_WIDTH: usize = 2500; const DEFAULT_GOAL: usize = 70; const DEFAULT_WIDTH: usize = 75; @@ -92,10 +121,7 @@ impl FmtOptions { match goal_str.parse::() { Ok(goal) => Some(goal), Err(_) => { - return Err(USimpleError::new( - 1, - format!("invalid goal: {}", goal_str.quote()), - )); + return Err(FmtError::InvalidGoal(goal_str.clone()).into()); } } } else { @@ -105,7 +131,7 @@ impl FmtOptions { let (width, goal) = match (width_opt, goal_opt) { (Some(w), Some(g)) => { if g > w { - return Err(USimpleError::new(1, "GOAL cannot be greater than WIDTH.")); + return Err(FmtError::GoalGreaterThanWidth.into()); } (w, g) } @@ -119,7 +145,7 @@ impl FmtOptions { } (None, Some(g)) => { if g > DEFAULT_WIDTH { - return Err(USimpleError::new(1, "GOAL cannot be greater than WIDTH.")); + return Err(FmtError::GoalGreaterThanWidth.into()); } let w = (g * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO).max(g + 3); (w, g) @@ -132,21 +158,15 @@ impl FmtOptions { ); if width > MAX_WIDTH { - return Err(USimpleError::new( - 1, - format!("invalid width: '{width}': Numerical result out of range"), - )); + return Err(FmtError::WidthOutOfRange(width).into()); } let mut tabwidth = 8; if let Some(s) = matches.get_one::(options::TAB_WIDTH) { tabwidth = match s.parse::() { Ok(t) => t, - Err(e) => { - return Err(USimpleError::new( - 1, - format!("Invalid TABWIDTH specification: {}: {e}", s.quote()), - )); + Err(_) => { + return Err(FmtError::InvalidTabWidth(s.clone()).into()); } }; }; @@ -192,13 +212,22 @@ fn process_file( let mut fp = BufReader::new(match file_name { "-" => Box::new(stdin()) as Box, _ => { - let f = File::open(file_name) - .map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?; + let f = File::open(file_name).map_err_context(|| { + get_message_with_args( + "fmt-error-cannot-open-for-reading", + HashMap::from([("file".to_string(), file_name.quote().to_string())]), + ) + })?; if f.metadata() - .map_err_context(|| format!("cannot get metadata for {}", file_name.quote()))? + .map_err_context(|| { + get_message_with_args( + "fmt-error-cannot-get-metadata", + HashMap::from([("file".to_string(), file_name.quote().to_string())]), + ) + })? .is_dir() { - return Err(USimpleError::new(1, "read error".to_string())); + return Err(FmtError::ReadError.into()); } Box::new(f) as Box @@ -211,20 +240,20 @@ fn process_file( Err(s) => { ostream .write_all(s.as_bytes()) - .map_err_context(|| "failed to write output".to_string())?; + .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?; ostream .write_all(b"\n") - .map_err_context(|| "failed to write output".to_string())?; + .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?; } Ok(para) => break_lines(¶, fmt_opts, ostream) - .map_err_context(|| "failed to write output".to_string())?, + .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?, } } // flush the output after each file ostream .flush() - .map_err_context(|| "failed to write output".to_string())?; + .map_err_context(|| get_message("fmt-error-failed-to-write-output"))?; Ok(()) } @@ -251,10 +280,11 @@ fn extract_files(matches: &ArgMatches) -> UResult> { if in_first_pos && i == 0 { None } else { - let first_num = x.chars().nth(1).expect("a negative number should be at least two characters long"); - Some(Err( - UUsageError::new(1, format!("invalid option -- {first_num}; -WIDTH is recognized only when it is the first\noption; use -w N instead")) - )) + let first_num = x + .chars() + .nth(1) + .expect("a negative number should be at least two characters long"); + Some(Err(FmtError::FirstOptionWidth(first_num).into())) } } else { Some(Ok(x.clone())) @@ -275,10 +305,7 @@ fn extract_width(matches: &ArgMatches) -> UResult> { return if let Ok(width) = width_str.parse::() { Ok(Some(width)) } else { - Err(USimpleError::new( - 1, - format!("invalid width: {}", width_str.quote()), - )) + Err(FmtError::InvalidWidth(width_str.clone()).into()) }; } @@ -307,13 +334,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { && first_arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) && first_arg.chars().skip(2).any(|c| !c.is_ascii_digit()); if malformed_number { - return Err(USimpleError::new( - 1, - format!( - "invalid width: {}", - first_arg.strip_prefix('-').unwrap().quote() - ), - )); + return Err(FmtError::InvalidWidthMalformed( + first_arg.strip_prefix('-').unwrap().to_string(), + ) + .into()); } } @@ -343,102 +367,70 @@ pub fn uu_app() -> Command { Arg::new(options::CROWN_MARGIN) .short('c') .long(options::CROWN_MARGIN) - .help( - "First and second line of paragraph \ - may have different indentations, in which \ - case the first line's indentation is preserved, \ - and each subsequent line's indentation matches the second line.", - ) + .help(get_message("fmt-crown-margin-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::TAGGED_PARAGRAPH) .short('t') .long("tagged-paragraph") - .help( - "Like -c, except that the first and second line of a paragraph *must* \ - have different indentation or they are treated as separate paragraphs.", - ) + .help(get_message("fmt-tagged-paragraph-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::PRESERVE_HEADERS) .short('m') .long("preserve-headers") - .help( - "Attempt to detect and preserve mail headers in the input. \ - Be careful when combining this flag with -p.", - ) + .help(get_message("fmt-preserve-headers-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::SPLIT_ONLY) .short('s') .long("split-only") - .help("Split lines only, do not reflow.") + .help(get_message("fmt-split-only-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::UNIFORM_SPACING) .short('u') .long("uniform-spacing") - .help( - "Insert exactly one \ - space between words, and two between sentences. \ - Sentence breaks in the input are detected as [?!.] \ - followed by two spaces or a newline; other punctuation \ - is not interpreted as a sentence break.", - ) + .help(get_message("fmt-uniform-spacing-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::PREFIX) .short('p') .long("prefix") - .help( - "Reformat only lines \ - beginning with PREFIX, reattaching PREFIX to reformatted lines. \ - Unless -x is specified, leading whitespace will be ignored \ - when matching PREFIX.", - ) + .help(get_message("fmt-prefix-help")) .value_name("PREFIX"), ) .arg( Arg::new(options::SKIP_PREFIX) .short('P') .long("skip-prefix") - .help( - "Do not reformat lines \ - beginning with PSKIP. Unless -X is specified, leading whitespace \ - will be ignored when matching PSKIP", - ) + .help(get_message("fmt-skip-prefix-help")) .value_name("PSKIP"), ) .arg( Arg::new(options::EXACT_PREFIX) .short('x') .long("exact-prefix") - .help( - "PREFIX must match at the \ - beginning of the line with no preceding whitespace.", - ) + .help(get_message("fmt-exact-prefix-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::EXACT_SKIP_PREFIX) .short('X') .long("exact-skip-prefix") - .help( - "PSKIP must match at the \ - beginning of the line with no preceding whitespace.", - ) + .help(get_message("fmt-exact-skip-prefix-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::WIDTH) .short('w') .long("width") - .help("Fill output lines up to a maximum of WIDTH columns, default 75. This can be specified as a negative number in the first argument.") + .help(get_message("fmt-width-help")) // We must accept invalid values if they are overridden later. This is not supported by clap, so accept all strings instead. .value_name("WIDTH"), ) @@ -446,7 +438,7 @@ pub fn uu_app() -> Command { Arg::new(options::GOAL) .short('g') .long("goal") - .help("Goal width, default of 93% of WIDTH. Must be less than or equal to WIDTH.") + .help(get_message("fmt-goal-help")) // We must accept invalid values if they are overridden later. This is not supported by clap, so accept all strings instead. .value_name("GOAL"), ) @@ -454,21 +446,14 @@ pub fn uu_app() -> Command { Arg::new(options::QUICK) .short('q') .long("quick") - .help( - "Break lines more quickly at the \ - expense of a potentially more ragged appearance.", - ) + .help(get_message("fmt-quick-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::TAB_WIDTH) .short('T') .long("tab-width") - .help( - "Treat tabs as TABWIDTH spaces for \ - determining line length, default 8. Note that this is used only for \ - calculating line lengths; tabs are preserved in the output.", - ) + .help(get_message("fmt-tab-width-help")) .value_name("TABWIDTH"), ) .arg(