diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index e68e177d9c..45d5cea1ca 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -1097,17 +1097,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // The list is the value of the option that selected the mode, so the // caret can be put under the one range that is at fault. let (short, long) = mode_arg_names(mode_arg); - let reported = diag_args.as_ref().is_some_and(|args| { - e.render_option_value( - args, - list, - Some(short), - long, - &translate!("cut-diag-label-zero-bound"), - &translate!("cut-diag-help-list-syntax"), - ) - }); - uucore::error::quiet_if_reported(reported, UUsageError::new(1, e.message)) + uucore::diagnostics::error_after_report( + diag_args.as_deref(), + UUsageError::new(1, e.message.clone()), + |args, _| { + e.render_option_value( + args, + list, + Some(short), + long, + &translate!("cut-diag-label-zero-bound"), + &translate!("cut-diag-help-list-syntax"), + ) + }, + ) })?; let mode = match mode_arg { diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index e462bf1bae..723add1f14 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1494,10 +1494,9 @@ fn is_fifo(filename: &str) -> bool { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let raw_args: Vec = args.collect(); - // Kept for the caret in operand diagnostics, which echoes the command line. - let diag_args = uucore::diagnostics::capture(&raw_args); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), raw_args)?; + // The command line is kept for the caret in operand diagnostics. + let (matches, diag_args) = + uucore::clap_localization::handle_clap_result_with_diagnostics(uu_app(), args.collect())?; let settings: Settings = Parser::new().parse_with_diagnostics( matches diff --git a/src/uu/dd/src/diagnostics.rs b/src/uu/dd/src/diagnostics.rs index fa498859d4..eb234fabf7 100644 --- a/src/uu/dd/src/diagnostics.rs +++ b/src/uu/dd/src/diagnostics.rs @@ -14,8 +14,8 @@ use std::ffi::{OsStr, OsString}; use std::ops::Range; -use uucore::diagnostics::Snapshot; -use uucore::error::{UError, quiet_if_reported}; +use uucore::diagnostics::{Snapshot, list_items}; +use uucore::error::UError; use uucore::translate; use crate::parseargs::ParseError; @@ -37,8 +37,9 @@ pub fn operand_error( operand: &str, error: ParseError, ) -> Box { - let reported = diag_args.is_some_and(|args| render(args, operand, &error)); - quiet_if_reported(reported, error) + uucore::diagnostics::error_after_report(diag_args, error, |args, error| { + render(args, operand, error) + }) } /// Render `error` against `args`, with a caret under the part of `operand` @@ -53,20 +54,12 @@ fn render(args: &[OsString], operand: &str, error: &ParseError) -> bool { // The value starts past the `=`, or ends the operand when there is none. let value_start = operand.len().min(key_end + 1); let value = || value_start..operand.len(); - // A flag inside a comma-separated value. The list is walked the way the - // parser walks it rather than searched for the flag's text, which would - // match inside an earlier flag the failing one is a prefix of — the `noc` - // of `nocache,noc`. + // A flag inside a comma-separated value, at its place in the list rather + // than wherever its text first turns up. let flag = |flag: &str| { - let mut at = value_start; - for part in operand[value_start..].split(',') { - if part == flag { - return Some(at..at + part.len()); - } - // Every separator is one byte wide. - at += part.len() + 1; - } - None + list_items(&operand[value_start..], &[',']) + .find(|&(part, _)| part == flag) + .map(|(_, span)| value_start + span.start..value_start + span.end) }; let (span, help): (Range, &str) = match error { diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 7392046f33..7f3049c15d 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -17,6 +17,7 @@ use std::os::fd::AsFd; use std::path::Path; use std::path::PathBuf; use thiserror::Error; +use uucore::diagnostics::OptionValue; use uucore::display::{Quotable, print_verbatim}; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::line_ending::LineEnding; @@ -85,9 +86,7 @@ impl Default for Mode { /// made of it. pub struct SizeError { pub message: String, - value: String, - short: char, - long: &'static str, + option: OptionValue, error: ParseSizeError, } @@ -97,11 +96,9 @@ impl SizeError { fn into_error(self, diag_args: Option<&[OsString]>) -> Box { self.error.size_value_error( diag_args, - &self.value, + &self.option, // The parser never saw the sign; the caret has to count it back in. - number_offset(&self.value), - self.short, - self.long, + number_offset(&self.option.value), &self.message, HeadError::MatchOption(self.message.clone()), ) @@ -116,12 +113,10 @@ impl Mode { long: &'static str, key: &'static str, ) -> impl FnOnce(ParseSizeError) -> SizeError { - let value = value.to_string(); + let option = OptionValue::new(value, short, long); move |error| SizeError { message: translate!(key, "err" => &error), - value, - short, - long, + option, error, } } diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 08a683edbb..52129b4aed 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -16,8 +16,9 @@ use std::num::IntErrorKind; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use thiserror::Error; +use uucore::diagnostics::OptionValue; use uucore::display::Quotable; -use uucore::error::{FromIo, UError, UResult, USimpleError, quiet_if_reported, set_exit_code}; +use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code}; use uucore::i18n::collator::{ AlternateHandling, CollatorOptions, locale_cmp, should_use_locale_collation, try_init_collator, }; @@ -788,27 +789,23 @@ fn parse_settings(matches: &clap::ArgMatches, diag_args: Option<&[OsString]>) -> settings.autoformat = true; } else { let mut specs = vec![]; - // Where the current field sits in the value, so that the caret can - // take the one field that is at fault out of a long list. - let mut at = 0; - for part in format.split([' ', ',', '\t']) { + // `-o` has no long form. + let option = OptionValue::with_names(format.clone(), Some('o'), None); + // Each field carries its place in the value, so that the caret can + // take the one that is at fault out of a long list. + for (part, span) in uucore::diagnostics::list_items(format, &[' ', ',', '\t']) { specs.push(Spec::parse(part).map_err(|error| { let message = error.to_string(); - let reported = diag_args.is_some_and(|args| { - uucore::diagnostics::Snapshot::with_program(args).render_option_value( - format, - Some('o'), - None, - at..at + part.len(), + uucore::diagnostics::error_after_report(diag_args, error, |args, _| { + uucore::diagnostics::Snapshot::with_program(args).render_option( + &option, + span, &message, None, Some(&translate!("join-diag-help-format")), ) - }); - quiet_if_reported(reported, error) + }) })?); - // Every separator is one byte wide. - at += part.len() + 1; } settings.format = specs; } @@ -837,10 +834,10 @@ fn parse_settings(matches: &clap::ArgMatches, diag_args: Option<&[OsString]>) -> #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let raw_args: Vec = args.collect(); - // Kept for the caret in `-o` diagnostics, which needs the list as typed. - let diag_args = uucore::diagnostics::capture(&raw_args); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), raw_args)?; + // The command line is kept for the caret in `-o` diagnostics, which needs + // the list as typed. + let (matches, diag_args) = + uucore::clap_localization::handle_clap_result_with_diagnostics(uu_app(), args.collect())?; let mut opts = CollatorOptions::default(); opts.alternate_handling = Some(AlternateHandling::Shifted); diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index ac1567c250..e6844d84ad 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -19,7 +19,7 @@ use std::io::{BufRead, Write as _, stderr}; use std::str::FromStr; use uucore::display::Quotable; -use uucore::error::{UResult, quiet_if_reported}; +use uucore::error::UResult; use uucore::i18n::decimal::locale_grouping_separator; use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; @@ -132,10 +132,13 @@ fn handle_args<'a>( // Only this mode stops on the first bad number; the others carry // on, where a report per line would bury the output. Err(error) => { - let reported = snapshot.is_some_and(|args| { - diagnostics::render_input(args, l, n, &error.to_string(), options) - }); - return Err(quiet_if_reported(reported, error)); + return Err(uucore::diagnostics::error_after_report( + snapshot, + error, + |args, error| { + diagnostics::render_input(args, l, n, &error.to_string(), options) + }, + )); } } } @@ -443,34 +446,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // A format error still knows where in the format string it happened, // so it is the one error worth a caret. Err(ParseError::Format(error)) => { - let reported = format_args - .as_ref() - .zip(matches.get_one::(FORMAT)) - .is_some_and(|(args, format)| diagnostics::render(args, format, &error)); - return Err(quiet_if_reported( - reported, - NumfmtError::IllegalArgument(error.message), + return Err(uucore::diagnostics::error_after_report( + format_args.as_deref(), + NumfmtError::IllegalArgument(error.message.clone()), + |args, _| { + matches + .get_one::(FORMAT) + .is_some_and(|format| diagnostics::render(args, format, &error)) + }, )); } // As for a format, a field list knows which of its ranges is at fault. Err(ParseError::Field(error)) => { - let reported = format_args - .as_ref() - .zip(matches.get_one::(FIELD)) - .is_some_and(|(args, fields)| diagnostics::render_field(args, fields, &error)); - return Err(quiet_if_reported( - reported, - NumfmtError::IllegalArgument(error.message), + return Err(uucore::diagnostics::error_after_report( + format_args.as_deref(), + NumfmtError::IllegalArgument(error.message.clone()), + |args, _| { + matches + .get_one::(FIELD) + .is_some_and(|fields| diagnostics::render_field(args, fields, &error)) + }, )); } // An option value that is wrong as a whole: underline it where typed. Err(ParseError::Value(error)) => { - let reported = format_args - .as_ref() - .is_some_and(|args| diagnostics::render_value(args, &error)); - return Err(quiet_if_reported( - reported, - NumfmtError::IllegalArgument(error.message), + return Err(uucore::diagnostics::error_after_report( + format_args.as_deref(), + NumfmtError::IllegalArgument(error.message.clone()), + |args, _| diagnostics::render_value(args, &error), )); } Err(ParseError::Other(message)) => { diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 1fea9c0feb..41363668bd 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -41,8 +41,9 @@ use crate::prn_char::format_ascii_dump; use clap::ArgAction; use clap::{Arg, ArgMatches, Command, parser::ValueSource}; use std::ffi::OsString; +use uucore::diagnostics::OptionValue; use uucore::display::Quotable; -use uucore::error::{UError, UResult, USimpleError, quiet_if_reported}; +use uucore::error::{UResult, USimpleError}; use uucore::translate; use uucore::parser::parse_size::ParseSizeError; @@ -78,40 +79,12 @@ struct OdOptions { string_min_length: Option, } -/// The error to raise for a SIZE that does not parse. -/// -/// Draws a caret under the part of the value at fault when stderr is a -/// terminal, and quiets the message when it did, since the report has already -/// said everything it would. -/// -/// # Arguments -/// -/// * `error` - What the size parser made of the value. -/// * `args` - The whole argument list, program name included. -/// * `value` - The value as typed. -/// * `short` - The short name of the option it was given to, if it has one. -/// * `long` - Its long name. -/// * `message` - The headline, already localized. -fn size_error( - error: &ParseSizeError, - args: &[String], - value: &str, - short: Option, - long: &str, - message: String, -) -> Box { - let reported = uucore::diagnostics::enabled() && { - let diag_args: Vec = args.iter().map(OsString::from).collect(); - error.render_size_value(&diag_args, value, 0, short, Some(long), &message) - }; - quiet_if_reported(reported, USimpleError::new(1, message)) -} - /// Helper function to parse bytes with error handling fn parse_bytes_option( matches: &ArgMatches, args: &[String], - option_name: &str, + diag_args: Option<&[OsString]>, + option_name: &'static str, short: Option, ) -> UResult> { match matches.get_one::(option_name) { @@ -121,14 +94,21 @@ fn parse_bytes_option( Err(e) => { let message = format_error_message(&e, s, &option_display_name(args, option_name, short)); - Err(size_error(&e, args, s, short, option_name, message)) + let option = OptionValue::with_names(s.clone(), short, Some(option_name)); + Err(e.size_value_error( + diag_args, + &option, + 0, + &message, + USimpleError::new(1, message.clone()), + )) } }, } } impl OdOptions { - fn new(matches: &ArgMatches, args: &[String]) -> UResult { + fn new(matches: &ArgMatches, args: &[String], diag_args: Option<&[OsString]>) -> UResult { let byte_order = if let Some(s) = matches.get_one::(options::ENDIAN) { match s.as_str() { "little" => ByteOrder::Little, @@ -145,7 +125,8 @@ impl OdOptions { }; let mut skip_bytes = - parse_bytes_option(matches, args, options::SKIP_BYTES, Some('j'))?.unwrap_or(0); + parse_bytes_option(matches, args, diag_args, options::SKIP_BYTES, Some('j'))? + .unwrap_or(0); let mut label: Option = None; @@ -168,7 +149,13 @@ impl OdOptions { let width_display = option_display_name(args, options::WIDTH, Some('w')); let parsed = parse_number_of_bytes(s).map_err(|e| { let message = format_error_message(&e, s, &width_display); - size_error(&e, args, s, Some('w'), options::WIDTH, message) + e.size_value_error( + diag_args, + &OptionValue::new(s, 'w', options::WIDTH), + 0, + &message, + USimpleError::new(1, message.clone()), + ) })?; if parsed == 0 { return Err(USimpleError::new( @@ -207,9 +194,11 @@ impl OdOptions { let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES); - let read_bytes = parse_bytes_option(matches, args, options::READ_BYTES, Some('N'))?; + let read_bytes = + parse_bytes_option(matches, args, diag_args, options::READ_BYTES, Some('N'))?; - let string_min_length = match parse_bytes_option(matches, args, options::STRINGS, Some('S'))? { + let strings = parse_bytes_option(matches, args, diag_args, options::STRINGS, Some('S'))?; + let string_min_length = match strings { None => None, Some(n) => Some(usize::try_from(n).map_err(|_| { USimpleError::new( @@ -268,12 +257,15 @@ impl OdOptions { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_ignore(); + let raw_args: Vec = args.iter().map(OsString::from).collect(); let clap_opts = uu_app(); let clap_matches = uucore::clap_localization::handle_clap_result(clap_opts, &args)?; - let od_options = OdOptions::new(&clap_matches, &args)?; + // Kept for the caret in SIZE diagnostics, which echoes the command line. + let diag_args = uucore::diagnostics::capture(&raw_args); + let od_options = OdOptions::new(&clap_matches, &args, diag_args.as_deref())?; let mut out = std::io::stdout().lock(); // Check if we're in strings mode diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 8e13a3d1e7..3666400ea0 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -7,7 +7,7 @@ use std::ffi::OsString; use std::io::{Write, stdout}; use std::ops::ControlFlow; use uucore::display::Quotable; -use uucore::error::{FromIo, UError, UResult, UUsageError, quiet_if_reported}; +use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::format::{ FormatArgument, FormatArguments, FormatError, FormatItem, parse_spec_and_escape, }; @@ -63,10 +63,9 @@ fn print_formatted(args: impl uucore::Args) -> UResult<()> { // A parse error is rendered against the argument list when stderr is a // terminal; the plain one-line message is kept anywhere else. let raise = |error: FormatError| -> Box { - let reported = diag_args - .as_ref() - .is_some_and(|args| diagnostics::render(args, format, &error)); - quiet_if_reported(reported, error) + uucore::diagnostics::error_after_report(diag_args.as_deref(), error, |args, error| { + diagnostics::render(args, format, error) + }) }; let mut format_seen = false; diff --git a/src/uu/seq/src/diagnostics.rs b/src/uu/seq/src/diagnostics.rs index 35e87340cf..6cdb09205b 100644 --- a/src/uu/seq/src/diagnostics.rs +++ b/src/uu/seq/src/diagnostics.rs @@ -9,7 +9,7 @@ use std::ffi::OsString; use std::ops::Range; -use uucore::diagnostics::Snapshot; +use uucore::diagnostics::{OptionValue, Snapshot}; use uucore::format::FormatError; use uucore::translate; @@ -36,10 +36,8 @@ pub fn render(args: &[OsString], format: &str, error: &FormatError) -> bool { _ => return false, }; - Snapshot::with_program(args).render_option_value( - format, - Some('f'), - Some("format"), + Snapshot::with_program(args).render_option( + &OptionValue::new(format, 'f', crate::OPT_FORMAT), span, &error.to_string(), None, diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index a02647b6ba..2c5a82f411 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -11,7 +11,7 @@ use num_bigint::BigUint; use num_traits::ToPrimitive; use num_traits::Zero; -use uucore::error::{FromIo, UResult, quiet_if_reported}; +use uucore::error::{FromIo, UResult}; use uucore::extendedbigdecimal::ExtendedBigDecimal; use uucore::format::num_format::FloatVariant; use uucore::format::{Format, num_format}; @@ -36,7 +36,7 @@ use uucore::translate; const OPT_SEPARATOR: &str = "separator"; const OPT_TERMINATOR: &str = "terminator"; const OPT_EQUAL_WIDTH: &str = "equal-width"; -const OPT_FORMAT: &str = "format"; +pub(crate) const OPT_FORMAT: &str = "format"; const ARG_NUMBERS: &str = "numbers"; @@ -161,10 +161,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (format, padding, fast_allowed) = if let Some(str) = options.format { 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) + uucore::diagnostics::error_after_report( + diag_args.as_deref(), + error, + |args, error| diagnostics::render(args, str, error), + ) })?; (format, 0, false) } else { diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 52a9c1662d..b18e33850d 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -16,6 +16,7 @@ use std::io::{self, Read, Seek, SeekFrom, Write}; #[cfg(unix)] use std::os::unix::prelude::PermissionsExt; use std::path::{Path, PathBuf}; +use uucore::diagnostics::OptionValue; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::parser::parse_size::parse_size_u64; @@ -244,10 +245,10 @@ impl BytesWriter { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let raw_args: Vec = args.collect(); - // Kept for the caret in size diagnostics, which needs the size as typed. - let diag_args = uucore::diagnostics::capture(&raw_args); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), raw_args)?; + // The command line is kept for the caret in size diagnostics, which needs + // the size as typed. + let (matches, diag_args) = + uucore::clap_localization::handle_clap_result_with_diagnostics(uu_app(), args.collect())?; if !matches.contains_id(options::FILE) { return Err(UUsageError::new( @@ -420,10 +421,8 @@ fn get_size(size_str_opt: Option, diag_args: Option<&[OsString]>) -> URe let message = translate!("shred-invalid-file-size", "size" => size.quote()); Err(error.size_value_error( diag_args, - &size, + &OptionValue::new(&size, 's', options::SIZE), 0, - 's', - options::SIZE, &message, USimpleError::new(1, message.clone()), )) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index e654a8113a..314868589d 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -45,9 +45,10 @@ use std::path::PathBuf; use std::str::Utf8Error; use std::sync::OnceLock; use thiserror::Error; +use uucore::diagnostics::OptionValue; use uucore::display::Quotable; use uucore::error::{FromIo, strip_errno}; -use uucore::error::{UError, UResult, USimpleError, UUsageError, quiet_if_reported}; +use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::extendedbigdecimal::ExtendedBigDecimal; #[cfg(feature = "i18n-collator")] use uucore::i18n::collator::{compute_sort_key_utf8, locale_cmp}; @@ -2232,10 +2233,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let message = format_error_message(&error, size_str, options::BUF_SIZE); error.size_value_error( key_args.as_deref(), - size_str, + &OptionValue::new(size_str, 'S', options::BUF_SIZE), 0, - 'S', - options::BUF_SIZE, &message, USimpleError::new(2, message.clone()), ) @@ -2385,10 +2384,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let selector = match FieldSelector::parse(value, &settings) { Ok(selector) => selector, Err(error) => { - let reported = key_args - .as_ref() - .is_some_and(|args| diagnostics::render(args, value, &error)); - return Err(quiet_if_reported(reported, error)); + return Err(uucore::diagnostics::error_after_report( + key_args.as_deref(), + error, + |args, error| diagnostics::render(args, value, error), + )); } }; settings.selectors.push(selector); diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 7b154971b7..20c6748a19 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -26,9 +26,7 @@ use std::io::{BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write, stdin} use std::path::Path; use thiserror::Error; use uucore::display::Quotable; -use uucore::error::{ - FromIo, UResult, USimpleError, UUsageError, quiet_if_reported, set_exit_code, strip_errno, -}; +use uucore::error::{FromIo, UResult, USimpleError, UUsageError, set_exit_code, strip_errno}; use uucore::parser::parse_size::parse_size_u64; use uucore::translate; @@ -43,13 +41,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = Settings::from(&matches, obs_lines.as_deref()).map_err(|e| { let message = format!("{e}"); if e.requires_usage() { - UUsageError::new(1, message) - } else { - let reported = diag_args - .as_deref() - .is_some_and(|args| e.render(args, &message)); - quiet_if_reported(reported, USimpleError::new(1, message)) + return UUsageError::new(1, message); } + uucore::diagnostics::error_after_report( + diag_args.as_deref(), + USimpleError::new(1, message.clone()), + |args, _| match &e { + SettingsError::Strategy(error) => error.render(args, &message), + // The rest is about how the options combine rather than about + // one of them, so there is nothing to point a caret at. + _ => false, + }, + ) })?; // When using --filter, we write to a child process's stdin which may @@ -280,15 +283,6 @@ enum SettingsError { } impl SettingsError { - /// Draw a caret under the part of the argument that is at fault, when this - /// error is one that knows where it came from. - fn render(&self, diag_args: &[OsString], message: &str) -> bool { - match self { - Self::Strategy(error) => error.render(diag_args, message), - _ => false, - } - } - /// Whether the error demands a usage message. fn requires_usage(&self) -> bool { matches!( diff --git a/src/uu/split/src/strategy.rs b/src/uu/split/src/strategy.rs index 01ad2f5aaa..0388cb5de1 100644 --- a/src/uu/split/src/strategy.rs +++ b/src/uu/split/src/strategy.rs @@ -10,6 +10,7 @@ use clap::{ArgMatches, parser::ValueSource}; use std::ffi::OsString; use thiserror::Error; use uucore::{ + diagnostics::OptionValue, display::Quotable, parser::parse_size::{ParseSizeError, parse_size_u64, parse_size_u64_max}, translate, @@ -202,28 +203,20 @@ pub enum Strategy { Number(NumberType), } -/// The option a failing SIZE was given to, and the value as typed. +/// An error when parsing a chunking strategy from command-line arguments. /// -/// Kept next to the error so that a caret knows which argument to point at; -/// `None` for a size that did not come from an option, such as the obsolete +/// A bad size carries the option it was given to, so that a caret can point +/// inside it — `None` when it came from no option, as with the obsolete /// `split -22` spelling. -#[derive(Debug)] -pub struct SizeOrigin { - value: String, - short: char, - long: &'static str, -} - -/// An error when parsing a chunking strategy from command-line arguments. #[derive(Debug, Error)] pub enum StrategyError { /// Invalid number of lines. #[error("{}", translate!("split-error-invalid-number-of-lines", "error" => .0))] - Lines(ParseSizeError, Option), + Lines(ParseSizeError, Option), /// Invalid number of bytes. #[error("{}", translate!("split-error-invalid-number-of-bytes", "error" => .0))] - Bytes(ParseSizeError, Option), + Bytes(ParseSizeError, Option), /// Invalid number type. #[error("{0}")] @@ -248,20 +241,10 @@ impl StrategyError { /// nothing could be drawn; the caller then falls back to the plain /// one-line message. pub fn render(&self, diag_args: &[OsString], message: &str) -> bool { - let (Self::Lines(error, origin) | Self::Bytes(error, origin)) = self else { - return false; - }; - let Some(origin) = origin else { + let (Self::Lines(error, Some(option)) | Self::Bytes(error, Some(option))) = self else { return false; }; - error.render_size_value( - diag_args, - &origin.value, - 0, - Some(origin.short), - Some(origin.long), - message, - ) + error.render_size_value(diag_args, option, 0, message) } } @@ -273,16 +256,12 @@ impl Strategy { option: &'static str, short: char, strategy: fn(u64) -> Strategy, - error: fn(ParseSizeError, Option) -> StrategyError, + error: fn(ParseSizeError, Option) -> StrategyError, ) -> Result { let s = matches.get_one::(option).unwrap(); - let origin = || { - Some(SizeOrigin { - value: s.clone(), - short, - long: option, - }) - }; + // `None` for a size that did not come from an option, such as the + // obsolete `split -22` spelling: there is nothing to point at. + let origin = || Some(OptionValue::new(s, short, option)); let n = parse_size_u64_max(s).map_err(|e| error(e, origin()))?; if n > 0 { Ok(strategy(n)) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index c2a177ad95..8760f6e293 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -5,7 +5,8 @@ // spell-checker:ignore datetime use std::ops::Range; -use uucore::error::{UError, UResult, USimpleError, quiet_if_reported}; +use uucore::diagnostics::OptionValue; +use uucore::error::{UError, UResult, USimpleError}; use uucore::i18n::UEncoding; use uucore::quoting_style::{QuotingStyle as UucoreQuotingStyle, escape_name}; use uucore::translate; @@ -93,6 +94,18 @@ fn check_bound(slice: &str, bound: usize, beg: usize, end: usize) -> Result<(), Ok(()) } +/// Converts a character index to a byte index in a UTF-8 string +/// +/// This is necessary because Rust strings are UTF-8 encoded, so character +/// positions don't always align with byte positions for multi-byte characters. +/// An index past the last character gives the end of the string. +fn char_index_to_byte_index(format_str: &str, char_index: usize) -> usize { + format_str + .char_indices() + .nth(char_index) + .map_or(format_str.len(), |(byte_idx, _)| byte_idx) +} + /// A directive stat does not know, and where it sat in the format string. /// /// The message is the one stat always printed; the byte range is what a caret @@ -111,15 +124,10 @@ impl DirectiveError { /// * `beg`, `end` - Its char indices in `format_str`; `end` may sit past /// the end, for a directive the format stops in the middle of. fn new(format_str: &str, directive: &str, beg: usize, end: usize) -> Self { - let byte_of = |char_index: usize| { - format_str - .char_indices() - .nth(char_index) - .map_or(format_str.len(), |(byte_index, _)| byte_index) - }; Self { directive: directive.quote().to_string(), - span: byte_of(beg)..byte_of(end.min(format_str.chars().count())), + span: char_index_to_byte_index(format_str, beg) + ..char_index_to_byte_index(format_str, end), } } @@ -130,34 +138,33 @@ impl DirectiveError { /// /// * `diag_args` - The arguments as typed, or `None` when they were not /// kept. - /// * `format_str` - The format the directive came from, as typed. - /// * `option` - The short and long names of the option the format was - /// given to, or `None` for a format stat built itself, which is not on - /// the command line and has nothing to point at. + /// * `option` - The format as typed and the option it was given to, or + /// `None` for a format stat built itself, which is not on the command + /// line and has nothing to point at. fn into_error( self, diag_args: Option<&[OsString]>, - format_str: &str, - option: Option<(Option, &str)>, + option: Option<&OptionValue>, ) -> Box { let message = StatError::InvalidDirective { directive: self.directive, } .to_string(); - let reported = option.is_some_and(|(short, long)| { - diag_args.is_some_and(|args| { - uucore::diagnostics::Snapshot::with_program(args).render_option_value( - format_str, - short, - Some(long), - self.span.clone(), - &message, - None, - Some(&translate!("stat-diag-help-directive")), - ) - }) - }); - quiet_if_reported(reported, USimpleError::new(1, message)) + uucore::diagnostics::error_after_report( + diag_args, + USimpleError::new(1, message.clone()), + |args, _| { + option.is_some_and(|option| { + uucore::diagnostics::Snapshot::with_program(args).render_option( + option, + self.span.clone(), + &message, + None, + Some(&translate!("stat-diag-help-directive")), + ) + }) + }, + ) } } @@ -823,16 +830,6 @@ impl Stater { } } - /// Converts a character index to a byte index in a UTF-8 string - /// This is necessary because Rust strings are UTF-8 encoded, so character positions - /// don't always align with byte positions for multi-byte characters - fn char_index_to_byte_index(format_str: &str, char_index: usize) -> usize { - format_str - .char_indices() - .nth(char_index) - .map_or(format_str.len(), |(byte_idx, _)| byte_idx) - } - fn handle_percent_case( chars: &[char], i: &mut usize, @@ -857,7 +854,7 @@ impl Stater { let mut precision = Precision::NotSpecified; let mut j = *i; - let j_byte = Self::char_index_to_byte_index(format_str, j); + let j_byte = char_index_to_byte_index(format_str, j); if let Some((field_width, offset)) = format_str[j_byte..].scan_num::() { width = field_width; j += offset; @@ -880,7 +877,7 @@ impl Stater { j += 1; check_bound(format_str, bound, old, j)?; - let j_byte = Self::char_index_to_byte_index(format_str, j); + let j_byte = char_index_to_byte_index(format_str, j); match format_str[j_byte..].scan_num::() { Some((value, offset)) => { if value >= 0 { @@ -961,7 +958,7 @@ impl Stater { // Parse hexadecimal escape sequence (\xNN format) // Uses UTF-8 safe byte indexing to handle multi-byte characters properly if *i + 1 < bound { - let byte_index = Self::char_index_to_byte_index(format_str, *i + 1); + let byte_index = char_index_to_byte_index(format_str, *i + 1); if let Some((c, offset)) = format_str[byte_index..].scan_char(16) { *i += offset; Token::Byte(c as u8) @@ -1061,21 +1058,27 @@ impl Stater { // Only the format the user typed can be pointed at; the ones stat // builds for itself never fail, and are not on the command line. // `--printf` has no short form; `--format` also answers to `-c`. - let given_option = if use_printf { - (None, options::PRINTF) - } else { - (Some('c'), options::FORMAT) + let given_option = || { + OptionValue::with_names( + format_str, + if use_printf { None } else { Some('c') }, + Some(if use_printf { + options::PRINTF + } else { + options::FORMAT + }), + ) }; let default_tokens = if format_str.is_empty() { Self::generate_tokens(&Self::default_format(show_fs, terse, false), use_printf) - .map_err(|e| e.into_error(diag_args, format_str, None))? + .map_err(|e| e.into_error(diag_args, None))? } else { Self::generate_tokens(format_str, use_printf) - .map_err(|e| e.into_error(diag_args, format_str, Some(given_option)))? + .map_err(|e| e.into_error(diag_args, Some(&given_option())))? }; let default_dev_tokens = Self::generate_tokens(&Self::default_format(show_fs, terse, true), use_printf) - .map_err(|e| e.into_error(diag_args, format_str, None))?; + .map_err(|e| e.into_error(diag_args, None))?; // mount points aren't displayed when showing filesystem information, or // whenever the format string does not request the mount point. @@ -1453,10 +1456,10 @@ impl Stater { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let raw_args: Vec = args.collect(); - // Kept for the caret in format diagnostics, which needs the format as typed. - let diag_args = uucore::diagnostics::capture(&raw_args); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), raw_args)?; + // The command line is kept for the caret in format diagnostics, which + // needs the format as typed. + let (matches, diag_args) = + uucore::clap_localization::handle_clap_result_with_diagnostics(uu_app(), args.collect())?; let stater = Stater::new(&matches, diag_args.as_deref())?; let exit_status = stater.exec(); diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index e25b4dae90..024ea672b5 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -14,8 +14,9 @@ use std::process; use tempfile::TempDir; use tempfile::tempdir; use thiserror::Error; +use uucore::diagnostics::OptionValue; use uucore::display::Quotable; -use uucore::error::{UResult, USimpleError, UUsageError, quiet_if_reported, strip_errno}; +use uucore::error::{UResult, USimpleError, UUsageError, strip_errno}; use uucore::format_usage; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; use uucore::translate; @@ -74,13 +75,11 @@ impl TryFrom<&ArgMatches> for ProgramOptions { /// A buffering mode that did not parse as a size, and where it came from. /// /// The message is built where it always was; the rest is what a caret needs: -/// the mode as typed, the option it was given to, and what the size parser +/// the mode as typed with the option it was given to, and what the size parser /// made of it. #[derive(Debug)] struct ModeError { - value: String, - short: char, - long: &'static str, + option: OptionValue, error: ParseSizeError, } @@ -94,34 +93,6 @@ enum ProgramOptionsError { ValueTooLarge(String), } -impl ProgramOptionsError { - /// Draw a caret under the part of the mode that is at fault. - /// - /// # Arguments - /// - /// * `diag_args` - The arguments as typed, program name included. - /// * `message` - The headline, already localized. - /// - /// # Returns - /// - /// `false` when this error is not about a mode that failed to parse as a - /// size, or when nothing could be drawn; the caller then falls back to the - /// plain one-line message. - fn render(&self, diag_args: &[OsString], message: &str) -> bool { - let Self::InvalidMode(mode) = self else { - return false; - }; - mode.error.render_size_value( - diag_args, - &mode.value, - 0, - Some(mode.short), - Some(mode.long), - message, - ) - } -} - #[cfg(all(unix, not(target_vendor = "apple"), not(target_os = "cygwin")))] fn preload_strings() -> (&'static str, &'static str) { ("LD_PRELOAD", "so") @@ -154,9 +125,7 @@ fn check_option( x => parse_size_u64(x).map_or_else( |error| { Err(ProgramOptionsError::InvalidMode(Box::new(ModeError { - value: x.to_string(), - short, - long: name, + option: OptionValue::new(x, short, name), error, }))) }, @@ -251,10 +220,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = ProgramOptions::try_from(&matches).map_err(|e| { let message = e.to_string(); - let reported = diag_args - .as_deref() - .is_some_and(|args| e.render(args, &message)); - quiet_if_reported(reported, UUsageError::new(125, message)) + uucore::diagnostics::error_after_report( + diag_args.as_deref(), + UUsageError::new(125, message.clone()), + |args, _| match &e { + ProgramOptionsError::InvalidMode(mode) => { + mode.error + .render_size_value(args, &mode.option, 0, &message) + } + // The rest is not about a mode that failed to parse, so there + // is nothing to point a caret at. + _ => false, + }, + ) })?; let mut command_values = matches diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 25a1f1dab7..6f10f7c704 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -12,6 +12,7 @@ use same_file::Handle; use std::ffi::OsString; use std::io::{IsTerminal, Write}; use std::time::Duration; +use uucore::diagnostics::OptionValue; use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::parser::parse_signed_num::{SignPrefix, number_offset, parse_signed_num_max}; use uucore::parser::parse_size::ParseSizeError; @@ -76,11 +77,9 @@ impl FilterMode { let raise = |message: String, arg: &str, short, long, error: &ParseSizeError| { error.size_value_error( diag_args, - arg, + &OptionValue::new(arg, short, long), // The parser never saw the sign; the caret has to count it back in. number_offset(arg), - short, - long, &message, USimpleError::new(1, message.clone()), ) diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 965ce713c1..9743ee45bf 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -82,12 +82,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { match parse(args).and_then(|mut stack| eval(&mut stack)) { Ok(true) => Ok(()), Ok(false) => Err(1.into()), - Err(e) => { - let reported = expression - .as_ref() - .is_some_and(|expression| diagnostics::render(expression, &e)); - Err(uucore::error::quiet_if_reported(reported, e)) - } + Err(e) => Err(uucore::diagnostics::error_after_report( + expression.as_deref(), + e, + diagnostics::render, + )), } } diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index d92551a52f..a6bc546fb3 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -17,7 +17,7 @@ use simd::process_input; use std::ffi::OsString; use std::io::{stdin, stdout}; use uucore::display::Quotable; -use uucore::error::{UResult, USimpleError, UUsageError, quiet_if_reported}; +use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::fs::is_stdin_directory; use uucore::translate; use uucore::{format_usage, os_str_as_bytes, show}; @@ -118,10 +118,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (set1, set2) = match solved { Ok(sets_solved) => sets_solved, Err(error) => { - let reported = set_args - .as_ref() - .is_some_and(|args| diagnostics::render(args, &sets, &error)); - return Err(quiet_if_reported(reported, error)); + return Err(uucore::diagnostics::error_after_report( + set_args.as_deref(), + error, + |args, error| diagnostics::render(args, &sets, error), + )); } }; diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 2fc9928cac..a2f6652503 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -12,6 +12,7 @@ use std::io::ErrorKind; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use std::path::Path; +use uucore::diagnostics::OptionValue; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::format_usage; @@ -314,12 +315,10 @@ fn truncate( let message = translate!("truncate-error-invalid-number", "error" => &error); return Err(error.size_value_error( diag_args, - string, + &OptionValue::new(string, 's', "size"), // The parser never saw the mode character; the caret has // to count it back in. size_offset(string, is_modifier), - 's', - "size", &message, USimpleError::new(1, message.clone()), )); diff --git a/src/uucore/src/lib/features/diagnostics.rs b/src/uucore/src/lib/features/diagnostics.rs index 4cf6720815..6ae44e15cd 100644 --- a/src/uucore/src/lib/features/diagnostics.rs +++ b/src/uucore/src/lib/features/diagnostics.rs @@ -93,7 +93,35 @@ pub fn operands(args: &[OsString]) -> Option> { capture(args.get(1..).unwrap_or_default()) } -pub use crate::features::diagnostics_boundary::{char_span, floor_boundary}; +pub use crate::features::diagnostics_boundary::{ + OptionValue, char_span, floor_boundary, list_items, +}; + +/// The error to raise for something a caret may have just explained. +/// +/// Draws the report when the arguments as typed were kept, and quiets `error` +/// when it did: the report has already said everything the one-line message +/// would, and the exit code is all that is left to carry. Every caret +/// diagnostic ends this way, so it is written once here. +/// +/// # Arguments +/// +/// * `diag_args` - The arguments as typed, program name included, or `None` +/// when they were not kept — as [`capture`] returns them. +/// * `error` - The error to raise when nothing was drawn. It is lent to `draw` +/// rather than moved into it, since it is usually the error the report is +/// about as well. +/// * `draw` - Draws the report against the arguments, and returns `false` when +/// it could not — because the error is not about any one of them, or because +/// none of them turned out to carry what the caret would point at. +pub fn error_after_report>>( + diag_args: Option<&[OsString]>, + error: E, + draw: impl FnOnce(&[OsString], &E) -> bool, +) -> Box { + let reported = diag_args.is_some_and(|args| draw(args, &error)); + crate::error::quiet_if_reported(reported, error) +} /// An argument list rendered as a single line, with the position of every /// argument inside it. @@ -464,6 +492,39 @@ impl Snapshot { self.render_inside_at(index, operand, range, message, label, help) } + /// Write a report pointing at `range` inside the value of an option. + /// + /// As [`Snapshot::render_option_value`], for a value that travels with the + /// option it was given to. + /// + /// # Arguments + /// + /// * `option` - The value at fault and the option it came from. + /// * `range` - Byte range inside the value to point at. An empty range + /// marks the character it starts at. + /// * `message` - The error message, already localized. + /// * `label` - Text placed under the caret, already localized, or `None` + /// for a bare underline. + /// * `help` - An optional line of advice, already localized. + pub fn render_option( + &self, + option: &OptionValue, + range: Range, + message: &str, + label: Option<&str>, + help: Option<&str>, + ) -> bool { + self.render_option_value( + &option.value, + option.short, + option.long, + range, + message, + label, + help, + ) + } + /// Byte range covered by `range` — an offset inside `operand` — within the /// argument at `index`. fn locate_at(&self, index: usize, operand: &str, range: Range) -> Option> { diff --git a/src/uucore/src/lib/features/diagnostics_boundary.rs b/src/uucore/src/lib/features/diagnostics_boundary.rs index 3548c19baa..8d4a19fe97 100644 --- a/src/uucore/src/lib/features/diagnostics_boundary.rs +++ b/src/uucore/src/lib/features/diagnostics_boundary.rs @@ -3,12 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! The character-boundary arithmetic behind the caret diagnostics. +//! The part of the caret diagnostics that is real even without them. //! //! Both [`crate::diagnostics`] and its no-op stand-in re-export these, and both -//! do so for the same reason: a caller may floor an offset before it knows -//! whether anything will be drawn, so the arithmetic has to be real even when -//! the rendering is compiled out. Keeping it here means the two cannot drift. +//! do so for the same reason: a caller locates what a caret would point at — +//! flooring an offset, walking a list, keeping a value next to the option it +//! came from — before it knows whether anything will be drawn, so that much has +//! to work even when the rendering is compiled out. Keeping it here means the +//! two cannot drift. use std::ops::Range; @@ -52,9 +54,74 @@ pub fn char_span(text: &str, offset: usize) -> Range { } } +/// The value of an option, and the option it was given to. +/// +/// An option's value can be spelled many ways — `-S 1Q`, `-S1Q`, +/// `--buffer-size=1Q` — so a caret pointing inside one has to know which option +/// carried it before it can know which argument to draw under. A utility that +/// may want a caret keeps the value and the two names together from the moment +/// the parse fails until the report is drawn. +#[derive(Debug)] +pub struct OptionValue { + /// The value as typed. + pub value: String, + /// The option's short name, if it has one. + pub short: Option, + /// The option's long name, if it has one. + pub long: Option<&'static str>, +} + +impl OptionValue { + /// The value of an option answering to both a short and a long name. + pub fn new(value: impl Into, short: char, long: &'static str) -> Self { + Self::with_names(value, Some(short), Some(long)) + } + + /// The value of an option that is missing one of the two names, or whose + /// names are only known once the parse has failed — `stat` blames `-c` or + /// `--printf` depending on which one it was given. + pub fn with_names( + value: impl Into, + short: Option, + long: Option<&'static str>, + ) -> Self { + Self { + value: value.into(), + short, + long, + } + } +} + +/// The items of a separated list, each with its byte range inside `list`. +/// +/// A caret pointing at one item of a list — a `dd` conversion flag, a `join` +/// output field — needs to know where that item was written. The list is walked +/// rather than searched for the item's text, which would also match inside an +/// earlier item the wanted one is a prefix of: the `noc` of `nocache,noc`. +/// +/// # Arguments +/// +/// * `list` - The list as typed. +/// * `separators` - The characters it is split on, of any width. +pub fn list_items<'a>( + list: &'a str, + separators: &'a [char], +) -> impl DoubleEndedIterator)> { + let base = list.as_ptr() as usize; + list.split(separators).map(move |item| { + // Every item is a slice of `list`, so its address gives away where it + // was written: no running count to keep, which would have tied the + // spans to walking the list once, in order, past separators of a width + // the count assumed. + let start = item.as_ptr() as usize - base; + (item, start..start + item.len()) + }) +} + #[cfg(test)] mod tests { - use super::{char_span, floor_boundary}; + use super::{char_span, floor_boundary, list_items}; #[test] fn floors_into_a_multibyte_character() { @@ -78,4 +145,32 @@ mod tests { fn spans_nothing_at_the_end() { assert_eq!(char_span("ab", 2), 2..2); } + + #[test] + fn spans_every_item_of_a_list() { + let items: Vec<_> = list_items("ab,,cde", &[',']).collect(); + assert_eq!(items, vec![("ab", 0..2), ("", 3..3), ("cde", 4..7)]); + } + + /// The whole point of walking: "sy" also occurs at the start of "sync". + #[test] + fn spans_an_item_an_earlier_one_starts_with() { + let items: Vec<_> = list_items("sync sy", &[' ']).collect(); + assert_eq!(items[1], ("sy", 5..7)); + } + + /// The spans are a property of the list, not of the walk: taking the items + /// out of order, or splitting on a separator that is more than one byte + /// wide, points at the same text. + #[test] + fn spans_an_item_wherever_it_is_reached() { + let items: Vec<_> = list_items("aé§bb§c", &['\u{a7}']).rev().collect(); + assert_eq!(items, vec![("c", 9..10), ("bb", 5..7), ("aé", 0..3)]); + } + + #[test] + fn spans_a_list_of_one() { + let items: Vec<_> = list_items("solo", &[',', ' ']).collect(); + assert_eq!(items, vec![("solo", 0..4)]); + } } diff --git a/src/uucore/src/lib/features/diagnostics_stub.rs b/src/uucore/src/lib/features/diagnostics_stub.rs index 857c46c1f3..3c9c839f02 100644 --- a/src/uucore/src/lib/features/diagnostics_stub.rs +++ b/src/uucore/src/lib/features/diagnostics_stub.rs @@ -33,7 +33,18 @@ pub fn operands(_args: &[OsString]) -> Option> { // an offset before it knows whether anything will be drawn. It is the one part // of this module that is not a no-op, so it is shared with the real one rather // than restated here. -pub use crate::features::diagnostics_boundary::{char_span, floor_boundary}; +pub use crate::features::diagnostics_boundary::{ + OptionValue, char_span, floor_boundary, list_items, +}; + +/// Always the error itself: nothing is ever drawn to replace it. +pub fn error_after_report>>( + _diag_args: Option<&[OsString]>, + error: E, + _draw: impl FnOnce(&[OsString], &E) -> bool, +) -> Box { + error.into() +} /// A snapshot of nothing: it finds nothing and renders nothing. /// @@ -98,6 +109,17 @@ impl Snapshot { false } + pub fn render_option( + &self, + _option: &OptionValue, + _range: Range, + _message: &str, + _label: Option<&str>, + _help: Option<&str>, + ) -> bool { + false + } + #[allow(clippy::too_many_arguments)] pub fn render_option_value( &self, diff --git a/src/uucore/src/lib/features/parser/parse_size.rs b/src/uucore/src/lib/features/parser/parse_size.rs index b30a2f99a3..97a714a566 100644 --- a/src/uucore/src/lib/features/parser/parse_size.rs +++ b/src/uucore/src/lib/features/parser/parse_size.rs @@ -636,7 +636,7 @@ impl ParseSizeError { } } - /// Render this error against `args`, with a caret under the part of the + /// Render this error against `snapshot`, with a caret under the part of the /// SIZE that is at fault. /// /// Every utility taking a SIZE takes the same syntax, so the label and the @@ -646,32 +646,27 @@ impl ParseSizeError { /// /// * `args` - The whole argument list, program name included — as /// [`crate::diagnostics::capture`] returns it. - /// * `operand` - The option's value as typed. It may carry something in - /// front of the size — `truncate` takes a mode character, as in `+2K`, - /// `head` and `tail` a sign — which the caret has to count but the parser - /// never saw. - /// * `size_at` - Where the size itself starts inside `operand`, zero when + /// * `option` - The option's value as typed, and the option it was given + /// to. The value may carry something in front of the size — `truncate` + /// takes a mode character, as in `+2K`, `head` and `tail` a sign — which + /// the caret has to count but the parser never saw. + /// * `size_at` - Where the size itself starts inside the value, zero when /// the whole of it is the size. - /// * `short` - The short name of the option it was given to, if it has one. - /// * `long` - Its long name, if it has one. /// * `message` - The headline, already localized. It differs between /// utilities, so it is passed in rather than built here. /// /// # Returns /// - /// `false` when no argument carries `size` as that option's value, in + /// `false` when no argument carries the value as that option's value, in /// which case the caller should fall back to the plain one-line message. - #[allow(clippy::too_many_arguments)] pub fn render_size_value( &self, args: &[std::ffi::OsString], - operand: &str, + option: &crate::diagnostics::OptionValue, size_at: usize, - short: Option, - long: Option<&str>, message: &str, ) -> bool { - let Some(size) = operand.get(size_at..) else { + let Some(size) = option.value.get(size_at..) else { return false; }; // Labelled only where a label would add to the message, per the @@ -682,10 +677,8 @@ impl ParseSizeError { Self::ParseFailure(_) | Self::PhysicalMem(_) => None, }; let span = self.span(size); - crate::diagnostics::Snapshot::with_program(args).render_option_value( - operand, - short, - long, + crate::diagnostics::Snapshot::with_program(args).render_option( + option, size_at + span.start..size_at + span.end, message, label.as_deref(), @@ -704,24 +697,19 @@ impl ParseSizeError { /// /// * `diag_args` - The arguments as typed, or `None` when they were not /// kept — as [`crate::diagnostics::capture`] returns them. - /// * `operand`, `size_at`, `short`, `long`, `message` - As for - /// [`Self::render_size_value`]. + /// * `option`, `size_at`, `message` - As for [`Self::render_size_value`]. /// * `error` - The error to raise if nothing was drawn. - #[allow(clippy::too_many_arguments)] pub fn size_value_error( &self, diag_args: Option<&[std::ffi::OsString]>, - operand: &str, + option: &crate::diagnostics::OptionValue, size_at: usize, - short: char, - long: &str, message: &str, error: impl Into>, ) -> Box { - let reported = diag_args.is_some_and(|args| { - self.render_size_value(args, operand, size_at, Some(short), Some(long), message) - }); - crate::error::quiet_if_reported(reported, error) + crate::diagnostics::error_after_report(diag_args, error, |args, _| { + self.render_size_value(args, option, size_at, message) + }) } fn size_too_big(s: &str) -> Self { diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 0249089c14..8198498ed7 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -399,6 +399,33 @@ where handle_clap_result_with_exit_code(cmd, itr, 1) } +/// Parses the command line as [`handle_clap_result`] does, keeping a copy of +/// it for a caret diagnostic first. +/// +/// Parsing consumes the argument list, and a caret echoes it as it was typed, +/// so the copy has to be taken before — which is what this saves every caller +/// from spelling out. A utility that rewrites its arguments before parsing +/// keeps capturing on its own, since only it knows which of the two lists the +/// caret should echo. +/// +/// # Arguments +/// +/// * `cmd` - The clap `Command` to parse arguments against +/// * `args` - The command line, program name included +/// +/// # Returns +/// +/// The parsed arguments, and the command line as typed — `None` when +/// diagnostics are off, so that nothing is copied for a report no one will +/// see. +pub fn handle_clap_result_with_diagnostics( + cmd: Command, + args: Vec, +) -> UResult<(ArgMatches, Option>)> { + let diag_args = crate::diagnostics::capture(&args); + Ok((handle_clap_result(cmd, args)?, diag_args)) +} + /// Handles clap command parsing with a custom exit code for errors. /// /// Similar to `handle_clap_result` but allows specifying a custom exit code