From b886beb288c675516e4c2e8f80e203694ca9fcfd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 16 Aug 2026 21:26:00 +0200 Subject: [PATCH] seq: point the caret at the failing part of a format --- docs/src/extensions-errors.md | 1 + src/uu/seq/locales/en-US.ftl | 3 ++ src/uu/seq/locales/fr-FR.ftl | 3 ++ src/uu/seq/src/diagnostics.rs | 48 +++++++++++++++++++++++++++ src/uu/seq/src/seq.rs | 26 ++++++++++----- tests/by-util/test_seq.rs | 62 +++++++++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 src/uu/seq/src/diagnostics.rs diff --git a/docs/src/extensions-errors.md b/docs/src/extensions-errors.md index 5c0f70bcaa2..b6b4b5c9a76 100644 --- a/docs/src/extensions-errors.md +++ b/docs/src/extensions-errors.md @@ -279,6 +279,7 @@ the difference: | `sort` | the failing part of a `-k`/`--key` or field specification, or of the SIZE given to `-S` | [`sort -k2.3x fruits.txt`](https://uutils.org/playground/?cmd=sort+-k2.3x+fruits.txt) | | `numfmt` | the failing part of a `--field` or `--format` specification | [`numfmt --format=%q 1000`](https://uutils.org/playground/?cmd=numfmt+--format%3D%25q+1000) | | `printf` | the failing conversion or escape in the format string | [`printf %5.2c q`](https://uutils.org/playground/?cmd=printf+%255.2c+q) | +| `seq` | the failing conversion in the format given to `-f`/`--format` | [`seq -f %5.2c 1 3`](https://uutils.org/playground/?cmd=seq+-f+%255.2c+1+3) | | `env` | the failing part of a `-S`/`--split-string` string | [`env -S 'echo ${1FOO}'`](https://uutils.org/playground/?cmd=env+-S+%27echo+%24%7B1FOO%7D%27) | | `cut` | the failing range in the list given to `-b`, `-c`, `-f` or `-F` | [`cut -f 1,4-2 fruits.txt`](https://uutils.org/playground/?cmd=cut+-f+1%2C4-2+fruits.txt) | | `split` | the failing part of the SIZE given to `-b`, `-C` or `-l` | [`split -b 7zq fruits.txt`](https://uutils.org/playground/?cmd=split+-b+7zq+fruits.txt) | diff --git a/src/uu/seq/locales/en-US.ftl b/src/uu/seq/locales/en-US.ftl index 34a97d43868..285727f24af 100644 --- a/src/uu/seq/locales/en-US.ftl +++ b/src/uu/seq/locales/en-US.ftl @@ -18,3 +18,6 @@ seq-error-format-and-equal-width = format string may not be specified when print # Parse error types seq-parse-error-type-float = floating point seq-parse-error-type-nan = 'not-a-number' + +# Diagnostics +seq-diag-help-format = a format holds exactly one float conversion: %f, %e, %g or %a, as in -f%.3f diff --git a/src/uu/seq/locales/fr-FR.ftl b/src/uu/seq/locales/fr-FR.ftl index ed804982051..df98846079d 100644 --- a/src/uu/seq/locales/fr-FR.ftl +++ b/src/uu/seq/locales/fr-FR.ftl @@ -18,3 +18,6 @@ seq-error-format-and-equal-width = la chaîne de format ne peut pas être spéci # Types d'erreur d'analyse seq-parse-error-type-float = nombre à virgule flottante seq-parse-error-type-nan = 'non-un-nombre' + +# Diagnostics +seq-diag-help-format = un format contient exactement une conversion flottante : %f, %e, %g ou %a, comme dans -f%.3f diff --git a/src/uu/seq/src/diagnostics.rs b/src/uu/seq/src/diagnostics.rs new file mode 100644 index 00000000000..35e87340cf7 --- /dev/null +++ b/src/uu/seq/src/diagnostics.rs @@ -0,0 +1,48 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Maps a [`FormatError`] onto the part of the `-f` format it came from, so +//! that [`uucore::diagnostics`] can render it with a caret. + +use std::ffi::OsString; +use std::ops::Range; + +use uucore::diagnostics::Snapshot; +use uucore::format::FormatError; +use uucore::translate; + +/// Render `error` against `args` — the whole argument list, program name +/// included — where `format` is the value of `-f`/`--format` as typed. +/// +/// # Returns +/// +/// `false` when the error is not about the format string, or when the format +/// cannot be found among the arguments, in which case the caller should fall +/// back to the plain one-line message. +pub fn render(args: &[OsString], format: &str, error: &FormatError) -> bool { + let span: Range = match error { + FormatError::SpecError(_, span) + | FormatError::MissingHex(Some(span)) + | FormatError::InvalidCharacter(_, _, Some(span)) => span.clone(), + // These are about the format as a whole — it holds no directive, or + // more than one, or one seq cannot print a number with — so the caret + // takes all of it. + FormatError::TooManySpecs(_) + | FormatError::NeedAtLeastOneSpec(_) + | FormatError::EndsWithPercent(_) + | FormatError::WrongSpecType => 0..format.len(), + _ => return false, + }; + + Snapshot::with_program(args).render_option_value( + format, + Some('f'), + Some("format"), + span, + &error.to_string(), + None, + Some(&translate!("seq-diag-help-format")), + ) +} diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 64ab7100c4c..a02647b6ba6 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -11,12 +11,13 @@ use num_bigint::BigUint; use num_traits::ToPrimitive; use num_traits::Zero; -use uucore::error::{FromIo, UResult}; +use uucore::error::{FromIo, UResult, quiet_if_reported}; use uucore::extendedbigdecimal::ExtendedBigDecimal; use uucore::format::num_format::FloatVariant; use uucore::format::{Format, num_format}; use uucore::{fast_inc::fast_inc, format_usage}; +mod diagnostics; mod error; // public to allow fuzzing @@ -94,8 +95,14 @@ fn select_precision( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = - uucore::clap_localization::handle_clap_result(uu_app(), split_short_args_with_value(args))?; + let raw_args: Vec = args.collect(); + // Captured before `-f%q` is split into two arguments, so that the caret + // echoes the command line as it was typed. + let diag_args = uucore::diagnostics::capture(&raw_args); + let matches = uucore::clap_localization::handle_clap_result( + uu_app(), + split_short_args_with_value(raw_args.into_iter()), + )?; let numbers_option = matches.get_many::(ARG_NUMBERS); @@ -152,11 +159,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // If a format was passed on the command line, use that. // If not, use some default format based on parameters precision. let (format, padding, fast_allowed) = if let Some(str) = options.format { - ( - Format::::parse(str)?, - 0, - false, - ) + let format = + Format::::parse(str).map_err(|error| { + let reported = diag_args + .as_deref() + .is_some_and(|args| diagnostics::render(args, str, &error)); + quiet_if_reported(reported, error) + })?; + (format, 0, false) } else { let precision = select_precision(&first, &increment, &last); diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index ec3c266bc4d..7d9c48e33fd 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -1172,3 +1172,65 @@ fn test_equalize_widths_corner_cases() { .succeeds() .stdout_is("1.0625\n2.06252\n"); } + +#[cfg(all(feature = "feat_diagnostics", not(wasi_runner)))] +mod diagnostics { + use super::*; + + #[cfg(unix)] + #[test] + fn test_snippet_points_at_the_failing_conversion() { + let result = new_ucmd!() + .terminal_sim_stderr() + .args(&["-f", "%5.2c", "1", "3"]) + .fails_with_code(1); + + assert_eq!( + result.stderr_as_displayed(), + "\ +seq: %5.2c: invalid conversion specification + ╭─[ seq:1:8 ] + │ + 1 │ seq -f %5.2c 1 3 + │ ───── + │ + │ Help: a format holds exactly one float conversion: %f, %e, %g or %a, as in -f%.3f +───╯" + ); + } + + #[cfg(unix)] + #[test] + fn test_snippet_points_inside_a_glued_short_option() { + // `-f%q` is split for clap, but the report echoes what was typed. + let result = new_ucmd!() + .terminal_sim_stderr() + .args(&["-f%q", "1", "3"]) + .fails_with_code(1); + let stderr = result.stderr_as_displayed(); + + assert!(stderr.contains("1 │ seq -f%q 1 3"), "{stderr}"); + assert!(stderr.contains("seq:1:7"), "{stderr}"); + } + + #[cfg(unix)] + #[test] + fn test_snippet_underlines_a_format_with_no_directive() { + let result = new_ucmd!() + .terminal_sim_stderr() + .args(&["--format=abc", "1", "3"]) + .fails_with_code(1); + let stderr = result.stderr_as_displayed(); + + assert!(stderr.contains("seq:1:14"), "{stderr}"); + assert!(stderr.contains("has no % directive"), "{stderr}"); + } + + #[test] + fn test_plain_message_when_stderr_is_a_pipe() { + new_ucmd!() + .args(&["-f", "%5.2c", "1", "3"]) + .fails_with_code(1) + .stderr_is("seq: %5.2c: invalid conversion specification\n"); + } +}