Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/extensions-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
3 changes: 3 additions & 0 deletions src/uu/seq/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/uu/seq/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 48 additions & 0 deletions src/uu/seq/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -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<usize> = 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")),
)
}
26 changes: 18 additions & 8 deletions src/uu/seq/src/seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<OsString> = 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::<String>(ARG_NUMBERS);

Expand Down Expand Up @@ -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::<num_format::Float, &ExtendedBigDecimal>::parse(str)?,
0,
false,
)
let format =
Format::<num_format::Float, &ExtendedBigDecimal>::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);

Expand Down
62 changes: 62 additions & 0 deletions tests/by-util/test_seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Loading