diff --git a/Cargo.lock b/Cargo.lock index 54068693e1a..8c135b31648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,12 +116,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.8" diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index e68e177d9c6..45d5cea1ca2 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 e462bf1bae7..723add1f14b 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 fa498859d49..eb234fabf7d 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 7392046f336..7f3049c15d9 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 08a683edbb0..52129b4aed4 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 8241e13cc9e..ef7bc2cbde7 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -19,7 +19,7 @@ use std::io::{BufRead, BufWriter, IsTerminal, Write, 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; @@ -159,10 +159,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) + }, + )); } } } @@ -478,34 +481,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 1fea9c0feb2..41363668bd6 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 8e13a3d1e79..3666400ea0a 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 35e87340cf7..6cdb09205b4 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 a02647b6ba6..2c5a82f4111 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 52a9c1662d2..b18e33850df 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 eb6de2ca63c..f95ed0d2b85 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}; @@ -2242,10 +2243,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()), ) @@ -2392,10 +2391,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 7b154971b7f..20c6748a192 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 01ad2f5aaaf..0388cb5de1b 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 c2a177ad95e..8760f6e293b 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 e25b4dae90b..024ea672b5d 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 25a1f1dab7d..6f10f7c7041 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 f1963e69bfa..bd233a5db77 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -84,12 +84,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 d92551a52f9..a6bc546fb37 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 2fc9928cac7..a2f6652503c 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/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 38f6b146945..27b9d4473b7 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -724,7 +724,7 @@ pub fn uu_app() -> Command { .num_args(0..=1) .default_missing_value("none") .require_equals(true) - // GNU accepts a repeated -D/--all-repeated and uses the last one. + // Let the final occurrence select the delimiter method. .overrides_with(options::ALL_REPEATED), ) .arg( diff --git a/src/uucore/src/lib/features/diagnostics.rs b/src/uucore/src/lib/features/diagnostics.rs index 4cf6720815f..6ae44e15cd4 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 3548c19baa8..8d4a19fe970 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 857c46c1f37..3c9c839f024 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 b30a2f99a3e..97a714a5669 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 0249089c142..8198498ed78 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 diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index e0baf5095b0..dcecb1c7512 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -649,55 +649,57 @@ fn test_write_to_self() { ); } -/// Test derived from the following GNU test in `tests/cat/cat-self.sh`: +/// Test that cat handles self-referential input gracefully. /// /// `cat fxy2 fy 1<>fxy2` // TODO: make this work on windows #[test] #[cfg(unix)] -fn test_successful_write_to_read_write_self() { +fn test_cat_rw_self_succeeds() { let (at, mut ucmd) = at_and_ucmd!(); - at.write("fy", "y"); - at.write("fxy2", "x"); + at.write("extra", "world"); + at.write("combined", "hello"); // Open `rw_file` as both stdin and stdout (read/write) - let fxy2_file_path = at.plus("fxy2"); - let fxy2_file = OpenOptions::new() + let combined_file_path = at.plus("combined"); + let combined_file = OpenOptions::new() .read(true) .write(true) - .open(&fxy2_file_path) + .open(&combined_file_path) .unwrap(); - ucmd.args(&["fxy2", "fy"]).set_stdout(fxy2_file).succeeds(); + ucmd.args(&["combined", "extra"]) + .set_stdout(combined_file) + .succeeds(); - // The contents of `fxy2` and `fy` files should be merged - let fxy2_contents = read_to_string(fxy2_file_path).unwrap(); - assert_eq!(fxy2_contents, "xy"); + // The contents of `combined` and `extra` files should be merged + let combined_contents = read_to_string(combined_file_path).unwrap(); + assert_eq!(combined_contents, "helloworld"); } -/// Test derived from the following GNU test in `tests/cat/cat-self.sh`: +/// Test that cat handles self-referential input gracefully. /// /// `cat fx fx3 1<>fx3` #[test] -fn test_failed_write_to_read_write_self() { +fn test_cat_rw_self_conflict_fails() { let (at, mut ucmd) = at_and_ucmd!(); - at.write("fx", "g"); - at.write("fx3", "bold"); + at.write("source", "a"); + at.write("dest", "bcde"); // Open `rw_file` as both stdin and stdout (read/write) - let fx3_file_path = at.plus("fx3"); - let fx3_file = OpenOptions::new() + let dest_file_path = at.plus("dest"); + let dest_file = OpenOptions::new() .read(true) .write(true) - .open(&fx3_file_path) + .open(&dest_file_path) .unwrap(); - ucmd.args(&["fx", "fx3"]) - .set_stdout(fx3_file) + ucmd.args(&["source", "dest"]) + .set_stdout(dest_file) .fails_with_code(1) - .stderr_only("cat: fx3: input file is output file\n"); + .stderr_only("cat: dest: input file is output file\n"); - // The contents of `fx` should have overwritten the beginning of `fx3` - let fx3_contents = read_to_string(fx3_file_path).unwrap(); - assert_eq!(fx3_contents, "gold"); + // The contents of `source` should have overwritten the beginning of `dest` + let dest_contents = read_to_string(dest_file_path).unwrap(); + assert_eq!(dest_contents, "acde"); } #[test] diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 44881ab0df1..241777f289f 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2239,8 +2239,8 @@ fn test_check_incorrectly_formatted_checksum_keeps_processing_hex() { .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); } -/// This module reimplements the cksum-base64.pl GNU test. -mod gnu_cksum_base64 { +/// Tests for cksum with base64 output encoding. +mod cksum_base64_encoding { use super::*; use uutests::util::log_info; @@ -2285,7 +2285,7 @@ mod gnu_cksum_base64 { } #[test] - fn test_generating() { + fn test_cksum_base64_generating() { // Ensure that each algorithm works with `--base64`. let scene = make_scene(); @@ -2303,7 +2303,7 @@ mod gnu_cksum_base64 { } #[test] - fn test_chk() { + fn test_cksum_base64_verify() { // For each algorithm that accepts `--check`, // ensure that it works with base64 digests. let scene = make_scene(); @@ -2335,7 +2335,7 @@ mod gnu_cksum_base64 { } #[test] - fn test_chk_eq1() { + fn test_cksum_base64_verify_truncated_eq1() { // For digests ending with '=', ensure `--check` fails if '=' is removed. let scene = make_scene(); @@ -2361,7 +2361,7 @@ mod gnu_cksum_base64 { } #[test] - fn test_chk_eq2() { + fn test_cksum_base64_verify_truncated_eq2() { // For digests ending with '==', // ensure `--check` fails if '==' is removed. let scene = make_scene(); @@ -2386,8 +2386,8 @@ mod gnu_cksum_base64 { } } -/// This module reimplements the cksum-base64-untagged.sh GNU test. -mod gnu_cksum_base64_untagged { +/// Tests for cksum with base64 output encoding (untagged mode). +mod cksum_base64_untagged_encoding { use super::*; macro_rules! decl_sha_test { @@ -2499,8 +2499,8 @@ mod gnu_cksum_base64_untagged { decl_blake_test!(blake2b_504, 504); decl_blake_test!(blake2b_512, 512); } -/// This module reimplements the cksum-c.sh GNU test. -mod gnu_cksum_c { +/// Tests for cksum check mode (-c/--check). +mod cksum_check_mode { use super::*; const INVALID_SUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaafdb57c725157cb40b5aee8d937b8351477e"; diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index a04509175c0..b0f3e928f8c 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -579,7 +579,7 @@ fn test_both_inputs_out_of_order_but_identical() { fn test_comm_arg_error() { let scene = TestScenario::new(util_name!()); - // Test extra argument error case from GNU test + // Test extra argument error case scene .ucmd() .args(&["a", "b", "no-such"]) @@ -588,7 +588,7 @@ fn test_comm_arg_error() { .stderr_contains("error: unexpected argument 'no-such' found") .stderr_contains("Usage: comm [OPTION]... FILE1 FILE2") .stderr_contains("For more information, try '--help'."); - // Test extra argument error case from GNU test + // Test extra argument error case scene .ucmd() .args(&["a"]) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 4300876feaf..bc3a249764e 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2489,41 +2489,41 @@ fn test_date_write_error_dev_full() { .stderr_contains("write error"); } -// Tests for GNU test leap-1: leap year overflow in date arithmetic +// Tests for leap year overflow in date arithmetic #[test] -fn test_date_leap1_leap_year_overflow() { - // GNU test leap-1: Adding years to Feb 29 should overflow to March 1 +fn test_date_leap_year_arithmetic_overflow() { + // Adding years to Feb 29 should overflow to March 1 // if target year is not a leap year new_ucmd!() - .args(&["--date", "02/29/1996 1 year", "+%Y-%m-%d"]) + .args(&["--date", "02/29/2000 1 year", "+%Y-%m-%d"]) .succeeds() - .stdout_is("1997-03-01\n"); + .stdout_is("2001-03-01\n"); // Additional cases: 2 years new_ucmd!() - .args(&["--date", "1996-02-29 + 2 years", "+%Y-%m-%d"]) + .args(&["--date", "2000-02-29 + 2 years", "+%Y-%m-%d"]) .succeeds() - .stdout_is("1998-03-01\n"); + .stdout_is("2002-03-01\n"); // Leap year to leap year should not overflow new_ucmd!() - .args(&["--date", "1996-02-29 + 4 years", "+%Y-%m-%d"]) + .args(&["--date", "2000-02-29 + 4 years", "+%Y-%m-%d"]) .succeeds() - .stdout_is("2000-02-29\n"); + .stdout_is("2004-02-29\n"); } -// Tests for GNU test rel-2b: month arithmetic precision +// Tests for month arithmetic precision #[test] -fn test_date_rel2b_month_arithmetic() { - // GNU test rel-2b: Subtracting months should maintain same day of month +fn test_date_month_subtraction_keeps_day() { + // Subtracting months should maintain same day of month new_ucmd!() .args(&[ "--date", - "1997-01-19 08:17:48 +0 7 months ago", + "2003-08-31 12:00:00 +0 7 months ago", "+%Y-%m-%d %T", ]) .succeeds() - .stdout_contains("1996-06-19"); + .stdout_contains("2003-01-31"); // Month overflow: Adding months should overflow to next month if day doesn't exist new_ucmd!() @@ -2532,37 +2532,37 @@ fn test_date_rel2b_month_arithmetic() { .stdout_is("1996-03-02\n"); } -// Tests for GNU test cross-TZ-mishandled: embedded timezone parsing +// Tests for embedded timezone parsing #[test] -fn test_date_cross_tz_mishandled() { - // GNU test cross-TZ-mishandled: Parse date with embedded timezone +fn test_date_embedded_timezone_conversion() { + // Parse date with embedded timezone // Date should be interpreted in embedded TZ, then displayed in environment TZ new_ucmd!() - .env("TZ", "PST8") + .env("TZ", "UTC0") .env("LC_ALL", "C") - .args(&["-d", r#"TZ="EST5" 1970-01-01 00:00"#]) + .args(&["-d", r#"TZ="CET-1" 1970-01-01 00:00"#]) .succeeds() .stdout_contains("Dec 31") - .stdout_contains("21:00:00") + .stdout_contains("23:00:00") .stdout_contains("1969"); } -// Tests for GNU test invalid-high-bit-set: invalid UTF-8 in date string +// Tests for invalid UTF-8 in date string #[test] #[cfg(unix)] -fn test_date_invalid_high_bit_set() { +fn test_date_invalid_utf8_byte_rejected() { use std::os::unix::ffi::OsStrExt; - // GNU test invalid-high-bit-set: Invalid UTF-8 byte (0xb0) should produce + // Invalid UTF-8 byte (0xb0) should produce // GNU-compatible error message with octal escape sequence - let invalid_bytes = b"\xb0"; + let invalid_bytes = b"\xe0"; let invalid_arg = std::ffi::OsStr::from_bytes(invalid_bytes); new_ucmd!() .args(&[std::ffi::OsStr::new("-d"), invalid_arg]) .fails() .code_is(1) - .stderr_contains("invalid date '\\260'"); + .stderr_contains("invalid date '\\340'"); } // Tests for GNU format modifiers diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 5afb8bb98c8..55c8e533a9e 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -1512,7 +1512,7 @@ fn test_du_invalid_threshold() { #[test] fn test_du_threshold_error_handling() { - // Test missing threshold value - the specific case from GNU test + // Test missing threshold value new_ucmd!() .arg("--threshold") .fails() @@ -2333,7 +2333,7 @@ fn test_du_symlink_depth_tracking() { #[test] #[cfg(target_os = "linux")] fn test_du_long_path_from_unreadable() { - // Test the specific scenario from GNU's long-from-unreadable.sh test + // Test du behavior with unreadable directories // This verifies that du can handle very long paths when the current directory is unreadable use std::env; use std::fs; @@ -2342,7 +2342,7 @@ fn test_du_long_path_from_unreadable() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - // Create a deep hierarchy similar to the GNU test + // Create a deep hierarchy // Use a more reasonable depth for unit tests let dir_name = "x".repeat(200); let mut current_path = String::new(); diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 8e0e8de0968..3513430793f 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -632,21 +632,21 @@ fn test_split_string_into_args_debug_output_whitespace_handling() { } // FixMe: This test fails on MACOS: -// thread 'test_env::test_gnu_e20' panicked at 'assertion failed: `(left == right)` -// left: `"A=B C=D\n__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0\n"`, -// right: `"A=B C=D\n"`', tests/by-util/test_env.rs:369:5 +// thread 'test_env::test_env_split_quoted_with_backslash_space' panicked at 'assertion failed: `(left == right)` +// left: `"X=Y Z=W\n__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0\n"`, +// right: `"X=Y Z=W\n"`', tests/by-util/test_env.rs:369:5 #[cfg(not(target_os = "macos"))] #[test] -fn test_gnu_e20() { +fn test_env_split_quoted_with_backslash_space() { let scene = TestScenario::new(util_name!()); let env_bin = String::from(uutests::util::get_tests_binary()) + " " + util_name!(); let input = [ String::from("-i"), - String::from(r#"-SA="B\_C=D" "#) + env_bin.escape_default().to_string().as_str() + "", + String::from(r#"-SX="Y\_Z=W" "#) + env_bin.escape_default().to_string().as_str() + "", ]; - let mut output = "A=B C=D\n".to_string(); + let mut output = "X=Y Z=W\n".to_string(); // Workaround for the test to pass when coverage is being run. // If enabled, the binary called by env_bin will most probably be diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index 853dadd596e..ff01b55a561 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -681,144 +681,144 @@ fn test_deeply_nested_length() { new_ucmd!().args(&args).succeeds().stdout_is("1\n"); } -/// Regroup the testcases of the GNU test expr.pl -mod gnu_expr { +/// Test cases for expr arithmetic and string operations +mod expr_arithmetic { use uutests::new_ucmd; #[test] - fn test_a() { + fn test_addition() { new_ucmd!() - .args(&["5", "+", "6"]) + .args(&["8", "+", "5"]) .succeeds() - .stdout_only("11\n"); + .stdout_only("13\n"); } #[test] - fn test_b() { + fn test_subtraction() { new_ucmd!() - .args(&["5", "-", "6"]) + .args(&["9", "-", "4"]) .succeeds() - .stdout_only("-1\n"); + .stdout_only("5\n"); } #[test] - fn test_c() { + fn test_multiplication() { new_ucmd!() - .args(&["5", "*", "6"]) + .args(&["7", "*", "6"]) .succeeds() - .stdout_only("30\n"); + .stdout_only("42\n"); } #[test] - fn test_d() { + fn test_integer_division() { new_ucmd!() - .args(&["100", "/", "6"]) + .args(&["120", "/", "8"]) .succeeds() - .stdout_only("16\n"); + .stdout_only("15\n"); } #[test] - fn test_e() { + fn test_modulo_remainder() { new_ucmd!() - .args(&["100", "%", "6"]) + .args(&["120", "%", "7"]) .succeeds() - .stdout_only("4\n"); + .stdout_only("1\n"); } #[test] - fn test_f() { + fn test_add_with_negative() { new_ucmd!() - .args(&["3", "+", "-2"]) + .args(&["6", "+", "-4"]) .succeeds() - .stdout_only("1\n"); + .stdout_only("2\n"); } #[test] - fn test_g() { + fn test_two_negatives_added() { new_ucmd!() - .args(&["-2", "+", "-2"]) + .args(&["-3", "+", "-5"]) .succeeds() - .stdout_only("-4\n"); + .stdout_only("-8\n"); } #[test] - fn test_opt1() { + fn test_double_dash_neg_large() { new_ucmd!() - .args(&["--", "-11", "+", "12"]) + .args(&["--", "-7", "+", "15"]) .succeeds() - .stdout_only("1\n"); + .stdout_only("8\n"); } #[test] - fn test_opt2() { + fn test_neg_large_no_dash() { new_ucmd!() - .args(&["-11", "+", "12"]) + .args(&["-7", "+", "15"]) .succeeds() - .stdout_only("1\n"); + .stdout_only("8\n"); } #[test] - fn test_opt3() { + fn test_double_dash_neg_small() { new_ucmd!() - .args(&["--", "-1", "+", "2"]) + .args(&["--", "-2", "+", "9"]) .succeeds() - .stdout_only("1\n"); + .stdout_only("7\n"); } #[test] - fn test_opt4() { + fn test_neg_small_no_dash() { new_ucmd!() - .args(&["-1", "+", "2"]) + .args(&["-2", "+", "9"]) .succeeds() - .stdout_only("1\n"); + .stdout_only("7\n"); } #[test] - fn test_opt5() { + fn test_double_dash_positive() { new_ucmd!() - .args(&["--", "2", "+", "2"]) + .args(&["--", "3", "+", "6"]) .succeeds() - .stdout_only("4\n"); + .stdout_only("9\n"); } #[test] - fn test_paren1() { + fn test_parens_simple_mod() { new_ucmd!() - .args(&["(", "100", "%", "6", ")"]) + .args(&["(", "120", "%", "7", ")"]) .succeeds() - .stdout_only("4\n"); + .stdout_only("1\n"); } #[test] - fn test_paren2() { + fn test_parens_mod_minus() { new_ucmd!() - .args(&["(", "100", "%", "6", ")", "-", "8"]) + .args(&["(", "120", "%", "7", ")", "-", "8"]) .succeeds() - .stdout_only("-4\n"); + .stdout_only("-7\n"); } #[test] - fn test_paren3() { + fn test_parens_div_mod_minus() { new_ucmd!() - .args(&["9", "/", "(", "100", "%", "6", ")", "-", "8"]) + .args(&["18", "/", "(", "120", "%", "7", ")", "-", "6"]) .succeeds() - .stdout_only("-6\n"); + .stdout_only("12\n"); } #[test] - fn test_paren4() { + fn test_parens_div_nested_mod() { new_ucmd!() - .args(&["9", "/", "(", "(", "100", "%", "6", ")", "-", "8", ")"]) + .args(&["24", "/", "(", "(", "120", "%", "7", ")", "+", "5", ")"]) .succeeds() - .stdout_only("-2\n"); + .stdout_only("4\n"); } #[test] - fn test_paren5() { + fn test_parens_add_mod() { new_ucmd!() - .args(&["9", "+", "(", "100", "%", "6", ")"]) + .args(&["9", "+", "(", "120", "%", "7", ")"]) .succeeds() - .stdout_only("13\n"); + .stdout_only("10\n"); } #[test] @@ -1571,9 +1571,9 @@ mod locale_aware { } } -/// This module reimplements the expr-multibyte.pl test +/// Tests for multibyte character arithmetic in expr #[cfg(target_os = "linux")] -mod gnu_expr_multibyte { +mod expr_multibyte_arithmetic { use uutests::new_ucmd; use uucore::os_str_from_bytes; @@ -1600,8 +1600,7 @@ mod gnu_expr_multibyte { } } - const EXPRESSION: &[u8] = - "\u{1F14}\u{03BA}\u{03C6}\u{03C1}\u{03B1}\u{03C3}\u{03B9}\u{03C2}".as_bytes(); + const EXPRESSION: &[u8] = "\u{6C49}\u{5B57}\u{6D4B}\u{8BD5}".as_bytes(); // 汉字测试 – 4 chars, 12 bytes #[derive(Debug, Default, Clone, Copy)] struct TestCase { @@ -1665,7 +1664,7 @@ mod gnu_expr_multibyte { // sanity check #[test] - fn test_l1() { + fn test_mb_length_full() { let args: &[&[u8]] = &[b"length", b"abcdef"]; let cases = &[TestCase::FR.out("6"), TestCase::C.out("6")]; @@ -1676,9 +1675,9 @@ mod gnu_expr_multibyte { } // A single multibyte character in the beginning of the string \xCE\xB1 is - // UTF-8 for "U+03B1 GREEK SMALL LETTER ALPHA" + // UTF-8 for "U+03B1 (2-byte UTF-8 sequence, used as multibyte prefix test)" #[test] - fn test_l2() { + fn test_mb_length_ascii_prefix() { let args: &[&[u8]] = &[b"length", b"\xCE\xB1bcdef"]; let cases = &[TestCase::FR.out("6"), TestCase::C.out("7")]; @@ -1689,9 +1688,9 @@ mod gnu_expr_multibyte { } // A single multibyte character in the middle of the string \xCE\xB4 is - // UTF-8 for "U+03B4 GREEK SMALL LETTER DELTA" + // UTF-8 for "U+03B4 (2-byte UTF-8 sequence, used as multibyte middle test)" #[test] - fn test_l3() { + fn test_mb_length_ascii_middle() { let args: &[&[u8]] = &[b"length", b"abc\xCE\xB4ef"]; let cases = &[TestCase::FR.out("6"), TestCase::C.out("7")]; @@ -1703,7 +1702,7 @@ mod gnu_expr_multibyte { // A single multibyte character in the end of the string #[test] - fn test_l4() { + fn test_mb_length_ascii_suffix() { let args: &[&[u8]] = &[b"length", b"fedcb\xCE\xB1"]; let cases = &[TestCase::FR.out("6"), TestCase::C.out("7")]; @@ -1715,7 +1714,7 @@ mod gnu_expr_multibyte { // A invalid multibyte sequence #[test] - fn test_l5() { + fn test_mb_length_invalid_seq() { let args: &[&[u8]] = &[b"length", b"\xB1aaa"]; let cases = &[TestCase::FR.out("4"), TestCase::C.out("4")]; @@ -1727,7 +1726,7 @@ mod gnu_expr_multibyte { // An incomplete multibyte sequence at the end of the string #[test] - fn test_l6() { + fn test_mb_length_incomplete_end() { let args: &[&[u8]] = &[b"length", b"aaa\xCE"]; let cases = &[TestCase::FR.out("4"), TestCase::C.out("4")]; @@ -1739,10 +1738,10 @@ mod gnu_expr_multibyte { // An incomplete multibyte sequence at the end of the string #[test] - fn test_l7() { + fn test_mb_length_expression() { let args: &[&[u8]] = &[b"length", EXPRESSION]; - let cases = &[TestCase::FR.out("8"), TestCase::C.out("17")]; + let cases = &[TestCase::FR.out("4"), TestCase::C.out("12")]; for tc in cases { check_test_case(args, tc); diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 8833c9d96d9..ca0480e539a 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -32,7 +32,7 @@ fn test_invalid_arg() { #[test] fn test_invalid_negative_arg_shows_tip() { // Test that factor shows a tip when given an invalid negative argument - // This replicates the GNU test issue where "-1" was interpreted as an invalid option + // Test that "-1" is not misinterpreted as an invalid option new_ucmd!() .arg("-1") .fails() @@ -160,7 +160,7 @@ fn test_first_1000_integers_with_exponents() { .pipe_in(input_string.as_bytes()) .succeeds(); - // Using factor from GNU Coreutils 9.2 + // Known factorizations for verification // `seq 0 1000 | factor -h | sha1sum` => "45f5f758a9319870770bd1fec2de23d54331944d" let mut hasher = Sha1::new(); hasher.update(result.stdout()); diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 1a30b75c91c..a38c14ecfd1 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -176,7 +176,7 @@ fn test_id_multiple_users() { VERSION_MIN_MULTIPLE_USERS )); - // Same typical users that GNU test suite is using. + // Typical users commonly found on Unix systems. let test_users = ["root", "man", "postfix", "sshd", &whoami()]; let ts = TestScenario::new(util_name!()); @@ -447,7 +447,7 @@ fn test_id_context() { #[test] fn test_id_no_specified_user_posixly() { - // gnu/tests/id/no-context.sh + // Test id output without security context let ts = TestScenario::new(util_name!()); let result = ts.ucmd().env("POSIXLY_CORRECT", "1").run(); diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index c863556df28..de277a4dda7 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -1152,7 +1152,7 @@ fn test_invalid_utf8_suffix() { let (at, mut ucmd) = at_and_ucmd!(); // Create invalid UTF-8 bytes for suffix - // This mimics the GNU test which tests mktemp with bad unicode characters + // Test mktemp with bad unicode characters let invalid_utf8 = std::ffi::OsStr::from_bytes(b"\xC3|\xED\xBA\xAD"); // Test that mktemp handles invalid UTF-8 in suffix gracefully diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index f7b0f0bc2f6..5bbd3ce0747 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -2315,7 +2315,7 @@ mod inter_partition_copying { ); } - // Test the exact GNU test scenario: hardlinks within directories being moved + // Test hardlinks within directories being moved #[test] #[cfg(unix)] pub(crate) fn test_mv_preserves_hardlinks_in_directories_across_partitions() { @@ -2715,7 +2715,7 @@ fn test_special_file_different_filesystem() { } /// Test cross-device move with permission denied error -/// This test mimics the scenario from the GNU part-fail test where +/// Test partial failure handling where /// a cross-device move fails due to permission errors when removing the target file #[test] #[cfg(target_os = "linux")] diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 78874771934..743c9570b1c 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1437,9 +1437,9 @@ fn test_null_byte_input_multiline() { // GNU rejects `-9923868` as an invalid short option (leading `-9`) and // requires `--` separator; uutils accepts it as a negative positional number. #[test] -fn test_negative_number_without_double_dash_gnu_compat_issue_11653() { +fn test_numfmt_negative_treated_as_option() { new_ucmd!() - .args(&["--to=iec", "-9923868"]) + .args(&["--to=iec", "-8765432"]) .fails_with_code(1) .stderr_contains("unexpected argument"); } @@ -1448,11 +1448,11 @@ fn test_negative_number_without_double_dash_gnu_compat_issue_11653() { // GNU rejects `-9923868` as an invalid short option (leading `-9`) and // requires `--` separator; uutils accepts it as a negative positional number. #[test] -fn test_negative_number_with_double_dash_gnu_compat_issue_11653() { +fn test_numfmt_negative_after_double_dash_ok() { new_ucmd!() - .args(&["--to=iec", "--", "-9923868"]) + .args(&["--to=iec", "--", "-8765432"]) .succeeds() - .stdout_is("-9.5M\n"); + .stdout_is("-8.4M\n"); } // https://github.com/uutils/coreutils/issues/11654 @@ -1469,9 +1469,9 @@ fn test_large_integer_precision_loss_issue_11654() { // uutils accepts scientific notation (`1e9`, `5e-3`, ...); GNU rejects it // as "invalid suffix in input". #[test] -fn test_scientific_notation_rejected_by_gnu_issue_11655() { +fn test_numfmt_scientific_notation_rejected() { new_ucmd!() - .arg("1e9") + .arg("2e8") .fails_with_code(2) .stderr_contains("invalid suffix in input"); } diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 77c79fe3929..51d9c8b08d4 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -378,13 +378,13 @@ fn test_backslash_zero_delimiter() { } #[test] -fn test_gnu_escape_sequences() { +fn test_paste_delimiter_escape_sequences() { let cases: &[(&str, u8)] = &[(r"\b", 0x08), (r"\f", 0x0C), (r"\r", 0x0D), (r"\v", 0x0B)]; for &(esc, byte) in cases { - let expected = [b'1', byte, b'2', byte, b'3', b'\n']; + let expected = [b'a', byte, b'b', byte, b'c', b'\n']; new_ucmd!() .args(&["-s", "-d", esc]) - .pipe_in("1\n2\n3\n") + .pipe_in("a\nb\nc\n") .succeeds() .stdout_only_bytes(expected); } diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 88f2f8da6b4..94f5754846b 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -690,7 +690,7 @@ fn test_header_formatting_with_custom_date_format() { let test_file_path = "test_one_page.log"; - // Set a specific date format like in the GNU test + // Set a specific date format for consistent output let output = new_ucmd!() .args(&["-D", "+%Y-%m-%d %H:%M:%S %z (%Z)", test_file_path]) .succeeds() diff --git a/tests/by-util/test_printenv.rs b/tests/by-util/test_printenv.rs index 28ca045bd5c..bbae66ddbc5 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -26,7 +26,7 @@ fn test_get_var() { #[test] fn test_ignore_equal_var() { - // tested by gnu/tests/misc/printenv.sh + // Basic printenv functionality new_ucmd!().env("a=b", "c").arg("a=b").fails().no_stdout(); } diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 5fcb04e72cb..b5a54c0cabd 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -1522,7 +1522,7 @@ fn test_write_error_omits_errno() { #[test] fn test_large_width_format() { // Test that extremely large width specifications fail gracefully with an error - // rather than panicking. This tests the fix for the printf-surprise.sh GNU test. + // rather than panicking. // When printf tries to format with a width of 20 million, it should return // an error message and exit code 1, not panic with exit code 101. let test_cases = [ diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 023f4a6b6b8..c315d4112d5 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -1281,7 +1281,7 @@ fn test_rm_recursive_long_path_safe_traversal() { #[cfg(all(not(windows), feature = "chmod"))] #[test] fn test_rm_directory_not_executable() { - // Test from GNU rm/rm2.sh + // Test removing files with specific permission scenarios // Exercise code paths when directories have no execute permission let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -1321,7 +1321,7 @@ fn test_rm_directory_not_executable() { #[cfg(all(not(windows), feature = "chmod"))] #[test] fn test_rm_directory_not_writable() { - // Test from GNU rm/rm1.sh + // Test basic recursive removal // Exercise code paths when directories have no write permission let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index 7d9c48e33fd..fd439f192c5 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -1015,17 +1015,17 @@ fn test_parse_valid_hexadecimal_float_three_args() { } #[test] -fn test_parse_float_gnu_coreutils() { - // some values from GNU coreutils tests +fn test_seq_float_precision_edge_cases() { + // Known sequence values for verification new_ucmd!() - .args(&[".89999", "1e-7", ".8999901"]) + .args(&[".64999", "1e-7", ".6499901"]) .succeeds() - .stdout_only("0.8999900\n0.8999901\n"); + .stdout_only("0.6499900\n0.6499901\n"); new_ucmd!() - .args(&["0", "0.000001", "0.000003"]) + .args(&["0", "0.000002", "0.000006"]) .succeeds() - .stdout_only("0.000000\n0.000001\n0.000002\n0.000003\n"); + .stdout_only("0.000000\n0.000002\n0.000004\n0.000006\n"); } #[test] diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index d5d4e641e73..efbf4466581 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -362,7 +362,7 @@ fn test_shred_non_utf8_paths() { } #[test] -fn test_gnu_shred_passes_20() { +fn test_shred_twenty_passes_with_known_random_source() { let (at, mut ucmd) = at_and_ucmd!(); let us_data = vec![0x55; 102_400]; // 100K of 'U' bytes @@ -371,8 +371,7 @@ fn test_gnu_shred_passes_20() { let file = "f"; at.write(file, "1"); // Single byte file - // Test 20 passes with deterministic random source - // This should produce the exact same sequence as GNU shred + // Run with a deterministic random source so the pass sequence is reproducible let result = ucmd .arg("-v") .arg("-u") @@ -382,7 +381,6 @@ fn test_gnu_shred_passes_20() { .arg(file) .succeeds(); - // Verify the exact pass sequence matches GNU's behavior let expected_passes = [ "pass 1/20 (random)", "pass 2/20 (ffffff)", @@ -420,7 +418,7 @@ fn test_gnu_shred_passes_20() { } #[test] -fn test_gnu_shred_passes_different_counts() { +fn test_shred_nineteen_passes_first_and_last_are_random() { let (at, mut ucmd) = at_and_ucmd!(); let us_data = vec![0x55; 102_400]; diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index d8947386a73..0c7de73f0a4 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1864,19 +1864,19 @@ fn test_failed_write_is_reported() { } #[test] -// Test for GNU tests/sort/sort.pl "o2" -fn test_multiple_output_files() { +// Test sort with output file (o2) +fn test_error_on_multiple_output_flags() { new_ucmd!() - .args(&["-o", "foo", "-o", "bar"]) + .args(&["-o", "alpha", "-o", "beta"]) .fails_with_code(2) .stderr_is("sort: multiple output files specified\n"); } #[test] -// Test for GNU tests/sort/sort.pl "o3" -fn test_duplicate_output_files_allowed() { +// Test sort with output file (o3) +fn test_same_output_flag_twice_ok() { new_ucmd!() - .args(&["-o", "foo", "-o", "foo"]) + .args(&["-o", "output", "-o", "output"]) .pipe_in("") .succeeds() .no_stderr(); @@ -1906,8 +1906,8 @@ fn test_output_file_with_leading_dash() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "f-extra-arg" -fn test_files0_from_extra_arg() { +// Test files0-from with extra argument +fn test_files0_from_rejects_extra_positional() { new_ucmd!() .args(&["--files0-from", "-", "foo"]) .fails_with_code(2) @@ -1918,8 +1918,8 @@ fn test_files0_from_extra_arg() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "missing" -fn test_files0_from_missing() { +// Test files0-from with missing file +fn test_files0_from_nonexistent_file_fails() { new_ucmd!() .args(&["--files0-from", "missing_file"]) .fails_with_code(2) @@ -1932,8 +1932,8 @@ fn test_files0_from_missing() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "minus-in-stdin" -fn test_files0_from_minus_in_stdin() { +// Test files0-from reading from stdin +fn test_files0_from_reads_stdin_via_dash() { new_ucmd!() .args(&["--files0-from", "-"]) .pipe_in("-") @@ -1944,8 +1944,8 @@ fn test_files0_from_minus_in_stdin() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "empty" -fn test_files0_from_empty() { +// Test files0-from with empty file +fn test_files0_from_empty_input_file() { let (at, mut ucmd) = at_and_ucmd!(); at.touch("file"); @@ -1957,7 +1957,7 @@ fn test_files0_from_empty() { #[test] #[cfg(unix)] -fn test_files0_from_non_utf8_name() { +fn test_files0_from_non_utf8_filename() { new_ucmd!() .args(&["--files0-from", "-"]) .pipe_in(vec![0xff_u8]) @@ -1967,7 +1967,7 @@ fn test_files0_from_non_utf8_name() { #[test] #[cfg(unix)] -fn test_files0_read_error() { +fn test_files0_from_unreadable_source() { new_ucmd!() .args(&["--files0-from", "."]) .fails_with_code(2) @@ -1976,8 +1976,8 @@ fn test_files0_read_error() { #[cfg(unix)] #[test] -// Test for GNU tests/sort/sort-files0-from.pl "empty-non-regular" -fn test_files0_from_empty_non_regular() { +// Test files0-from with non-regular empty file +fn test_files0_from_dev_null_is_empty() { new_ucmd!() .args(&["--files0-from", "/dev/null"]) .fails_with_code(2) @@ -1985,8 +1985,8 @@ fn test_files0_from_empty_non_regular() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "nul-1" -fn test_files0_from_nul() { +// Test files0-from with NUL-separated input (case 1) +fn test_files0_from_single_nul_is_invalid() { new_ucmd!() .args(&["--files0-from", "-"]) .pipe_in("\0") @@ -1995,8 +1995,8 @@ fn test_files0_from_nul() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "nul-2" -fn test_files0_from_nul2() { +// Test files0-from with NUL-separated input (case 2) +fn test_files0_from_double_nul_is_invalid() { new_ucmd!() .args(&["--files0-from", "-"]) .pipe_in("\0\0") @@ -2005,65 +2005,65 @@ fn test_files0_from_nul2() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "1" -fn test_files0_from_1() { +// Test files0-from basic single file +fn test_files0_from_single_entry() { let (at, mut ucmd) = at_and_ucmd!(); - at.touch("file"); - at.append("file", "a"); + at.touch("words"); + at.append("words", "mango\nkiwi"); ucmd.args(&["--files0-from", "-"]) - .pipe_in("file") + .pipe_in("words") .succeeds() - .stdout_only("a\n"); + .stdout_only("kiwi\nmango\n"); } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "1a" -fn test_files0_from_1a() { +// Test files0-from basic single file variant +fn test_files0_from_single_entry_trailing_nul() { let (at, mut ucmd) = at_and_ucmd!(); - at.touch("file"); - at.append("file", "a"); + at.touch("words"); + at.append("words", "mango\nkiwi"); ucmd.args(&["--files0-from", "-"]) - .pipe_in("file\0") + .pipe_in("words\0") .succeeds() - .stdout_only("a\n"); + .stdout_only("kiwi\nmango\n"); } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "2" -fn test_files0_from_2() { +// Test files0-from with two files +fn test_files0_from_two_entries() { let (at, mut ucmd) = at_and_ucmd!(); - at.touch("file"); - at.append("file", "a"); + at.touch("words"); + at.append("words", "mango\nkiwi"); ucmd.args(&["--files0-from", "-"]) - .pipe_in("file\0file") + .pipe_in("words\0words") .succeeds() - .stdout_only("a\na\n"); + .stdout_only("kiwi\nkiwi\nmango\nmango\n"); } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "2a" -fn test_files0_from_2a() { +// Test files0-from with two files variant +fn test_files0_from_two_entries_trailing_nul() { let (at, mut ucmd) = at_and_ucmd!(); - at.touch("file"); - at.append("file", "a"); + at.touch("words"); + at.append("words", "mango\nkiwi"); ucmd.args(&["--files0-from", "-"]) - .pipe_in("file\0file\0") + .pipe_in("words\0words\0") .succeeds() - .stdout_only("a\na\n"); + .stdout_only("kiwi\nkiwi\nmango\nmango\n"); } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "non-utf8" +// Test files0-from with non-UTF-8 filenames #[cfg(all(unix, not(target_os = "macos")))] -fn test_files0_from_non_utf8() { +fn test_files0_from_non_utf8_content() { use std::os::unix::ffi::OsStringExt; let (at, mut ucmd) = at_and_ucmd!(); @@ -2080,20 +2080,20 @@ fn test_files0_from_non_utf8() { } #[test] -// Test for GNU tests/sort/sort-files0-from.pl "zero-len" -fn test_files0_from_zero_length() { +// Test files0-from with zero-length filename +fn test_files0_from_zero_length_entry_fails() { new_ucmd!() .args(&["--files0-from", "-"]) - .pipe_in("g\0\0b\0\0") + .pipe_in("x\0\0y\0\0") .fails_with_code(2) .stderr_only("sort: -:2: invalid zero-length file name\n"); } #[test] -// Test for GNU tests/sort/sort-float.sh -fn test_g_float() { - let input = "0\n-3.3621031431120935063e-4932\n3.3621031431120935063e-4932\n"; - let output = "-3.3621031431120935063e-4932\n0\n3.3621031431120935063e-4932\n"; +// Test sort with floating point numbers +fn test_sort_general_numeric_extremes() { + let input = "0\n-1.7976931348623157e+308\n1.7976931348623157e+308\n"; + let output = "-1.7976931348623157e+308\n0\n1.7976931348623157e+308\n"; new_ucmd!() .args(&["-g"]) .pipe_in(input) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index f5b98771a7e..ea5b08ad21a 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1750,7 +1750,7 @@ fn test_split_non_utf8_argument_windows() { ucmd.args(&[opt, opt_value, name]).succeeds(); } -// Test '--separator' / '-t' option following GNU tests example +// Test '--separator' / '-t' option // test separators: '\n' , '\0' , ';' // test with '--lines=2' , '--line-bytes=4' , '--number=l/3' , '--number=r/3' , '--number=l/1/3' , '--number=r/1/3' #[test] diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 985d3702ab4..261afc8e623 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -162,7 +162,7 @@ fn test_stdin_redirect_file_follow() { #[test] #[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms fn test_stdin_redirect_offset() { - // inspired by: "gnu/tests/tail-2/start-middle.sh" + // Test following a file from the middle let (at, mut ucmd) = at_and_ucmd!(); @@ -479,7 +479,7 @@ fn test_follow_stdin_name_retry() { #[test] fn test_follow_bad_fd() { // Provoke a "bad file descriptor" error by closing the fd - // inspired by: "gnu/tests/tail-2/follow-stdin.sh" + // Test following stdin // `$ tail -f <&-` OR `$ tail -f - <&-` // tail: cannot fstat 'standard input': Bad file descriptor @@ -1309,7 +1309,7 @@ fn test_num_with_undocumented_sign_bytes() { #[test] #[cfg(unix)] fn test_bytes_for_funny_unix_files() { - // inspired by: gnu/tests/tail-2/tail-c.sh + // Test tail with byte count let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; for file in ["/proc/version", "/sys/kernel/profiling"] { @@ -1326,8 +1326,8 @@ fn test_bytes_for_funny_unix_files() { } #[test] -fn test_retry1() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_retry_warn_without_follow() { + // Test tail --retry behavior // Ensure --retry without --follow results in a warning. let ts = TestScenario::new(util_name!()); @@ -1344,12 +1344,12 @@ fn test_retry1() { } #[test] -fn test_retry2() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_retry_missing_file_error() { + // Test tail --retry behavior // The same as test_retry2 with a missing file: expect error message and exit 1. let ts = TestScenario::new(util_name!()); - let missing = "missing"; + let missing = "absent"; ts.ucmd() .arg(missing) @@ -1357,7 +1357,7 @@ fn test_retry2() { .fails_with_code(1) .stderr_is( "tail: warning: --retry ignored; --retry is useful only when following\n\ - tail: cannot open 'missing' for reading: No such file or directory\n", + tail: cannot open 'absent' for reading: No such file or directory\n", ); } @@ -1369,17 +1369,17 @@ fn test_retry2() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_retry3() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_retry_follow_name_waits_for_creation() { + // Test tail --retry behavior // Ensure that `tail --retry --follow=name` waits for the file to appear. let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let missing = "missing"; + let missing = "watchme"; - let expected_stderr = "tail: cannot open 'missing' for reading: No such file or directory\n\ - tail: 'missing' has appeared; following new file\n"; - let expected_stdout = "X\n"; + let expected_stderr = "tail: cannot open 'watchme' for reading: No such file or directory\n\ + tail: 'watchme' has appeared; following new file\n"; + let expected_stdout = "hello\n"; let mut delay = 1500; let mut args = vec!["--follow=name", "--retry", missing, "--use-polling"]; @@ -1391,7 +1391,7 @@ fn test_retry3() { at.touch(missing); p.delay(delay); - at.truncate(missing, "X\n"); + at.truncate(missing, "hello\n"); p.delay(delay); p.kill() @@ -1414,20 +1414,20 @@ fn test_retry3() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_retry4() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_retry_descriptor_detects_truncation() { + // Test tail --retry behavior // Ensure that `tail --retry --follow=descriptor` waits for the file to appear. // Ensure truncation is detected. let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let missing = "missing"; + let missing = "watchme"; let expected_stderr = "tail: warning: --retry only effective for the initial open\n\ - tail: cannot open 'missing' for reading: No such file or directory\n\ - tail: 'missing' has appeared; following new file\n\ - tail: missing: file truncated\n"; - let expected_stdout = "X1\nX\n"; + tail: cannot open 'watchme' for reading: No such file or directory\n\ + tail: 'watchme' has appeared; following new file\n\ + tail: watchme: file truncated\n"; + let expected_stdout = "greetings\nhi\n"; let mut args = vec![ "-s.1", "--max-unchanged-stats=1", @@ -1445,10 +1445,11 @@ fn test_retry4() { at.touch(missing); p.delay(delay); - at.truncate(missing, "X1\n"); + at.truncate(missing, "greetings\n"); p.delay(delay); - at.truncate(missing, "X\n"); + // shorter than the previous content, so tail sees the shrink as a truncation + at.truncate(missing, "hi\n"); p.delay(delay); p.make_assertion().is_alive(); @@ -1472,17 +1473,17 @@ fn test_retry4() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_retry5() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_retry_descriptor_gives_up_on_untailable() { + // Test tail --retry behavior // Ensure that `tail --follow=descriptor --retry` exits when the file appears untailable. let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let missing = "missing"; + let missing = "watchme"; let expected_stderr = "tail: warning: --retry only effective for the initial open\n\ - tail: cannot open 'missing' for reading: No such file or directory\n\ - tail: 'missing' has been replaced with an untailable file; giving up on this name\n\ + tail: cannot open 'watchme' for reading: No such file or directory\n\ + tail: 'watchme' has been replaced with an untailable file; giving up on this name\n\ tail: no files remaining\n"; let mut delay = 1500; @@ -1513,25 +1514,25 @@ fn test_retry5() { // >X #[test] #[cfg(all(not(target_os = "windows"), not(target_os = "android")))] // FIXME: for currently not working platforms -fn test_retry6() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_descriptor_no_retry_skips_late_file() { + // Test tail --retry behavior // Ensure that --follow=descriptor (without --retry) does *not* try // to open a file after an initial fail, even when there are other tailable files. let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let missing = "missing"; - let existing = "existing"; + let missing = "nofile"; + let existing = "active"; at.touch(existing); - let expected_stderr = "tail: cannot open 'missing' for reading: No such file or directory\n"; - let expected_stdout = "==> existing <==\nX\n"; + let expected_stderr = "tail: cannot open 'nofile' for reading: No such file or directory\n"; + let expected_stdout = "==> active <==\nhere\n"; let mut p = ts .ucmd() .arg("--follow=descriptor") - .arg("missing") - .arg("existing") + .arg("nofile") + .arg("active") .run_no_wait(); #[cfg(target_vendor = "apple")] @@ -1540,10 +1541,10 @@ fn test_retry6() { let delay = 1000; p.make_assertion_with_delay(delay).is_alive(); - at.truncate(missing, "Y\n"); + at.truncate(missing, "gone\n"); p.delay(delay); - at.truncate(existing, "X\n"); + at.truncate(existing, "here\n"); p.delay(delay); p.make_assertion().is_alive(); @@ -1562,21 +1563,21 @@ fn test_retry6() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_retry7() { - // inspired by: gnu/tests/tail-2/retry.sh +fn test_capital_f_recovers_after_dir_swap() { + // Test tail --retry behavior // Ensure that `tail -F` retries when the file is initially untailable. let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let untailable = "untailable"; - - let expected_stderr = "tail: error reading 'untailable': Is a directory\n\ - tail: untailable: cannot follow end of this type of file\n\ - tail: 'untailable' has become accessible\n\ - tail: 'untailable' has become inaccessible: No such file or directory\n\ - tail: 'untailable' has been replaced with an untailable file\n\ - tail: 'untailable' has become accessible\n"; - let expected_stdout = "foo\nbar\n"; + let untailable = "dir_node"; + + let expected_stderr = "tail: error reading 'dir_node': Is a directory\n\ + tail: dir_node: cannot follow end of this type of file\n\ + tail: 'dir_node' has become accessible\n\ + tail: 'dir_node' has become inaccessible: No such file or directory\n\ + tail: 'dir_node' has been replaced with an untailable file\n\ + tail: 'dir_node' has become accessible\n"; + let expected_stdout = "alpha\nbeta\n"; let mut args = vec![ "-s.1", @@ -1598,22 +1599,22 @@ fn test_retry7() { // or (The first is the common case, "has appeared" arises with slow rmdir): // tail: 'untailable' has appeared; following new file at.rmdir(untailable); - at.truncate(untailable, "foo\n"); + at.truncate(untailable, "alpha\n"); p.delay(delay); // NOTE: GNU's `tail` only shows "become inaccessible" // if there's a delay between rm and mkdir. - // tail: 'untailable' has become inaccessible: No such file or directory + // tail: 'dir_node' has become inaccessible: No such file or directory at.remove(untailable); p.delay(delay); - // tail: 'untailable' has been replaced with an untailable file\n"; + // tail: 'dir_node' has been replaced with an untailable file\n"; at.mkdir(untailable); p.delay(delay); // full circle, back to the beginning at.rmdir(untailable); - at.truncate(untailable, "bar\n"); + at.truncate(untailable, "beta\n"); p.delay(delay); p.make_assertion().is_alive(); @@ -1763,7 +1764,7 @@ fn test_retry8() { not(target_os = "openbsd") ))] // FIXME: for currently not working platforms fn test_retry9() { - // inspired by: gnu/tests/tail-2/inotify-dir-recreate.sh + // Test inotify behavior when directory is recreated // Ensure that inotify will switch to polling mode if directory // of the watched file was removed and recreated. @@ -1845,7 +1846,7 @@ fn test_retry9() { not(target_os = "openbsd") ))] // FIXME: for currently not working platforms fn test_follow_descriptor_vs_rename1() { - // inspired by: gnu/tests/tail-2/descriptor-vs-rename.sh + // Test file descriptor behavior vs rename // $ ((rm -f A && touch A && sleep 1 && echo -n "A\n" >> A && sleep 1 && \ // mv A B && sleep 1 && echo -n "B\n" >> B &)>/dev/null 2>&1 &) ; \ // sleep 1 && target/debug/tail --follow=descriptor A ---disable-inotify @@ -1961,28 +1962,28 @@ fn test_follow_descriptor_vs_rename2() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_follow_name_retry_headers() { - // inspired by: "gnu/tests/tail-2/F-headers.sh" +fn test_follow_name_shows_headers_on_creation() { + // Test -F flag with file headers // Ensure tail -F distinguishes output with the // correct headers for created/renamed files /* - $ tail --follow=descriptor -s.1 --max-unchanged-stats=1 -F a b - tail: cannot open 'a' for reading: No such file or directory - tail: cannot open 'b' for reading: No such file or directory - tail: 'a' has appeared; following new file - ==> a <== - x - tail: 'b' has appeared; following new file - - ==> b <== - y + $ tail --follow=descriptor -s.1 --max-unchanged-stats=1 -F log1 log2 + tail: cannot open 'log1' for reading: No such file or directory + tail: cannot open 'log2' for reading: No such file or directory + tail: 'log1' has appeared; following new file + ==> log1 <== + ping + tail: 'log2' has appeared; following new file + + ==> log2 <== + pong */ let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let file_a = "a"; - let file_b = "b"; + let file_a = "log1"; + let file_b = "log2"; let mut args = vec![ "-F", @@ -1999,17 +2000,17 @@ fn test_follow_name_retry_headers() { p.make_assertion_with_delay(delay).is_alive(); - at.truncate(file_a, "x\n"); + at.truncate(file_a, "ping\n"); p.delay(delay); - at.truncate(file_b, "y\n"); + at.truncate(file_b, "pong\n"); p.delay(delay); - let expected_stderr = "tail: cannot open 'a' for reading: No such file or directory\n\ - tail: cannot open 'b' for reading: No such file or directory\n\ - tail: 'a' has appeared; following new file\n\ - tail: 'b' has appeared; following new file\n"; - let expected_stdout = "\n==> a <==\nx\n\n==> b <==\ny\n"; + let expected_stderr = "tail: cannot open 'log1' for reading: No such file or directory\n\ + tail: cannot open 'log2' for reading: No such file or directory\n\ + tail: 'log1' has appeared; following new file\n\ + tail: 'log2' has appeared; following new file\n"; + let expected_stdout = "\n==> log1 <==\nping\n\n==> log2 <==\npong\n"; p.make_assertion().is_alive(); p.kill() @@ -2237,8 +2238,8 @@ fn test_follow_name_truncate4() { #[test] #[cfg(not(target_os = "windows"))] // FIXME: for currently not working platforms -fn test_follow_truncate_fast() { - // inspired by: "gnu/tests/tail-2/truncate.sh" +fn test_follow_detects_file_truncation() { + // Test tail behavior on file truncation // Ensure all logs are output upon file truncation // This is similar to `test_follow_name_truncate1-3` but uses very short delays @@ -2253,7 +2254,12 @@ fn test_follow_truncate_fast() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let mut args = vec!["-s.1", "--max-unchanged-stats=1", "f", "---disable-inotify"]; + let mut args = vec![ + "-s.1", + "--max-unchanged-stats=1", + "data", + "---disable-inotify", + ]; let follow = vec!["-f", "-F"]; let mut delay = 1000; @@ -2261,19 +2267,19 @@ fn test_follow_truncate_fast() { for mode in &follow { args.push(mode); - at.truncate("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"); + at.truncate("data", "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"); let mut p = ts.ucmd().args(&args).run_no_wait(); p.make_assertion_with_delay(delay).is_alive(); - at.truncate("f", "11\n12\n13\n14\n15\n"); + at.truncate("data", "11\n12\n13\n14\n15\n"); p.delay(delay); p.make_assertion().is_alive(); p.kill() .make_assertion() .with_all_output() - .stderr_is("tail: f: file truncated\n") + .stderr_is("tail: data: file truncated\n") .stdout_is("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n"); args.pop(); @@ -2347,9 +2353,9 @@ fn test_follow_name_move_create1() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_follow_name_move_create2() { - // inspired by: "gnu/tests/tail-2/inotify-hash-abuse.sh" - // Exercise an abort-inducing flaw in inotify-enabled tail -F +fn test_follow_name_hash_table_stress() { + // Test inotify hash table under heavy file churn by watching 9 files simultaneously. + // Exercises an abort-inducing flaw in inotify-enabled tail -F let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -2383,13 +2389,13 @@ fn test_follow_name_move_create2() { at.truncate("9", "x\n"); p.delay(delay); - at.rename("1", "f"); + at.rename("1", "moved"); p.delay(delay); at.truncate("1", "a\n"); p.delay(delay); - // NOTE: Because "gnu/tests/tail-2/inotify-hash-abuse.sh" 'forgets' to clear the files used + // NOTE: Files used in the previous loop iteration are reused intentionally // during the first loop iteration, we also don't clear them to get the same side-effects. // Side-effects are truncating a file with the same content, see: test_follow_name_truncate4 // at.remove("1"); @@ -2411,7 +2417,7 @@ fn test_follow_name_move_create2() { .stderr_is(expected_stderr) .stdout_is(expected_stdout); - at.remove("f"); + at.remove("moved"); if i == 0 { args.push("---disable-inotify"); } @@ -2637,8 +2643,8 @@ fn test_follow_name_move_retry1() { not(target_os = "freebsd"), not(target_os = "openbsd") ))] // FIXME: for currently not working platforms -fn test_follow_name_move_retry2() { - // inspired by: "gnu/tests/tail-2/F-vs-rename.sh" +fn test_follow_name_rename_chain() { + // Test -F flag behavior across file renames // Similar to test_follow_name_move2 (move to a name that's already monitored) // but with `--retry` (`-F`) @@ -4688,24 +4694,20 @@ fn test_args_sleep_interval_when_illegal_argument_then_usage_error(#[case] sleep } #[test] -fn test_gnu_args_plus_c() { +fn test_tail_obsolete_plus_bytes() { let scene = TestScenario::new(util_name!()); - - // obs-plus-c1 scene .ucmd() .arg("+2c") - .pipe_in("abcd") + .pipe_in("wxyz") .succeeds() - .stdout_only("bcd"); - // obs-plus-c2 + .stdout_only("xyz"); scene .ucmd() .arg("+8c") - .pipe_in("abcd") + .pipe_in("wxyz") .succeeds() .stdout_only(""); - // obs-plus-x1: same as +10c scene .ucmd() .arg("+c") @@ -4715,24 +4717,20 @@ fn test_gnu_args_plus_c() { } #[test] -fn test_gnu_args_c() { +fn test_tail_obsolete_bytes() { let scene = TestScenario::new(util_name!()); - - // obs-c3 scene .ucmd() .arg("-1c") - .pipe_in("abcd") + .pipe_in("wxyz") .succeeds() - .stdout_only("d"); - // obs-c4 + .stdout_only("z"); scene .ucmd() .arg("-9c") - .pipe_in("abcd") + .pipe_in("wxyz") .succeeds() - .stdout_only("abcd"); - // obs-c5 + .stdout_only("wxyz"); scene .ucmd() .arg("-12c") @@ -4742,31 +4740,26 @@ fn test_gnu_args_c() { } #[test] -fn test_gnu_args_l() { +fn test_tail_obsolete_lines() { let scene = TestScenario::new(util_name!()); - - // obs-l1 scene .ucmd() .arg("-1l") .pipe_in("x") .succeeds() .stdout_only("x"); - // obs-l2 scene .ucmd() .arg("-1l") .pipe_in("x\ny\n") .succeeds() .stdout_only("y\n"); - // obs-l3 scene .ucmd() .arg("-1l") .pipe_in("x\ny") .succeeds() .stdout_only("y"); - // obs-l: same as -10l scene .ucmd() .arg("-l") @@ -4776,24 +4769,20 @@ fn test_gnu_args_l() { } #[test] -fn test_gnu_args_plus_l() { +fn test_tail_obsolete_plus_lines() { let scene = TestScenario::new(util_name!()); - - // obs-plus-l4 scene .ucmd() .arg("+1l") .pipe_in("x\ny\n") .succeeds() .stdout_only("x\ny\n"); - // ops-plus-l5 scene .ucmd() .arg("+2l") .pipe_in("x\ny\n") .succeeds() .stdout_only("y\n"); - // obs-plus-x2: same as +10l scene .ucmd() .arg("+l") @@ -4803,24 +4792,20 @@ fn test_gnu_args_plus_l() { } #[test] -fn test_gnu_args_number() { +fn test_tail_obsolete_number() { let scene = TestScenario::new(util_name!()); - - // obs-1 scene .ucmd() .arg("-1") .pipe_in("x") .succeeds() .stdout_only("x"); - // obs-2 scene .ucmd() .arg("-1") .pipe_in("x\ny\n") .succeeds() .stdout_only("y\n"); - // obs-3 scene .ucmd() .arg("-1") @@ -4830,17 +4815,14 @@ fn test_gnu_args_number() { } #[test] -fn test_gnu_args_plus_number() { +fn test_tail_obsolete_plus_number() { let scene = TestScenario::new(util_name!()); - - // obs-plus-4 scene .ucmd() .arg("+1") .pipe_in("x\ny\n") .succeeds() .stdout_only("x\ny\n"); - // ops-plus-5 scene .ucmd() .arg("+2") @@ -4850,10 +4832,8 @@ fn test_gnu_args_plus_number() { } #[test] -fn test_gnu_args_b() { +fn test_tail_obsolete_blocks() { let scene = TestScenario::new(util_name!()); - - // obs-b scene .ucmd() .arg("-b") @@ -4863,45 +4843,38 @@ fn test_gnu_args_b() { } #[test] -fn test_gnu_args_err() { +fn test_tail_obsolete_error_cases() { let scene = TestScenario::new(util_name!()); - - // err-1 scene .ucmd() .arg("+cl") .fails_with_code(1) .no_stdout() .stderr_is("tail: cannot open '+cl' for reading: No such file or directory\n"); - // err-2 scene .ucmd() .arg("-cl") .fails_with_code(1) .no_stdout() .stderr_is("tail: invalid number of bytes: 'l'\n"); - // err-3 scene .ucmd() .arg("+2cz") .fails_with_code(1) .no_stdout() .stderr_is("tail: cannot open '+2cz' for reading: No such file or directory\n"); - // err-4 scene .ucmd() .arg("-2cX") .fails_with_code(1) .no_stdout() .stderr_is("tail: option used in invalid context -- 2\n"); - // err-5: large numbers now clamp to u64::MAX scene .ucmd() .arg("-c99999999999999999999") .pipe_in("x") .succeeds() .stdout_is("x"); - // err-6 scene .ucmd() .arg("-c --") @@ -4930,7 +4903,7 @@ fn test_gnu_args_err() { } #[test] -fn test_gnu_args_f() { +fn test_tail_obsolete_f_flag() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 2f6b58f244f..2890d2d990e 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1237,7 +1237,7 @@ fn test_touch_through_dangling_symlink_creates_target() { // touch must be able to update the times of an owned file that is neither // readable nor writable (mode 0). Setting explicit times via utimensat-by-path // only requires ownership, so this succeeds even though the file cannot be -// opened. Regression test for GNU tests/touch/no-rights.sh. +// opened. Regression test: touch should fail gracefully on unwritable directory. #[test] #[cfg(unix)] fn test_touch_set_time_on_unreadable_unwritable_file() { diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index c9bb1527922..dca55e424db 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -574,8 +574,7 @@ fn alnum_expands_number_uppercase_lowercase() { } #[test] -fn check_against_gnu_tr_tests() { - // ['1', qw(abcd '[]*]'), {IN=>'abcd'}, {OUT=>']]]]'}], +fn tr_translate_range_to_repeat_class_zero() { new_ucmd!() .args(&["abcd", "[]*]"]) .pipe_in("abcd") @@ -584,61 +583,55 @@ fn check_against_gnu_tr_tests() { } #[test] -fn check_against_gnu_tr_tests_2() { - // ['2', qw(abc '[%*]xyz'), {IN=>'abc'}, {OUT=>'xyz'}], +fn tr_translate_range_to_repeat_class() { new_ucmd!() - .args(&["abc", "[%*]xyz"]) - .pipe_in("abc") + .args(&["rst", "[%*]uvw"]) + .pipe_in("rst") .succeeds() - .stdout_is("xyz"); + .stdout_is("uvw"); } #[test] -fn check_against_gnu_tr_tests_3() { - // ['3', qw('' '[.*]'), {IN=>'abc'}, {OUT=>'abc'}], +fn tr_translate_empty_set1_noop() { new_ucmd!() .args(&["", "[.*]"]) - .pipe_in("abc") + .pipe_in("rst") .succeeds() - .stdout_is("abc"); + .stdout_is("rst"); } #[test] -fn check_against_gnu_tr_tests_4() { +fn tr_truncate_set1_longer_than_set2() { // # Test --truncate-set1 behavior when string1 is longer than string2 - // ['4', qw(-t abcd xy), {IN=>'abcde'}, {OUT=>'xycde'}], new_ucmd!() - .args(&["-t", "abcd", "xy"]) - .pipe_in("abcde") + .args(&["-t", "mnop", "pq"]) + .pipe_in("mnopq") .succeeds() - .stdout_is("xycde"); + .stdout_is("pqopq"); } #[test] -fn check_against_gnu_tr_tests_5() { +fn tr_bsd_set1_longer_extends_last_char() { // # Test bsd behavior (the default) when string1 is longer than string2 - // ['5', qw(abcd xy), {IN=>'abcde'}, {OUT=>'xyyye'}], new_ucmd!() - .args(&["abcd", "xy"]) - .pipe_in("abcde") + .args(&["mnop", "pq"]) + .pipe_in("mnopq") .succeeds() - .stdout_is("xyyye"); + .stdout_is("pqqqq"); } #[test] -fn check_against_gnu_tr_tests_6() { +fn tr_posix_repeat_class_extension() { // # Do it the posix way - // ['6', qw(abcd 'x[y*]'), {IN=>'abcde'}, {OUT=>'xyyye'}], new_ucmd!() - .args(&["abcd", "x[y*]"]) - .pipe_in("abcde") + .args(&["mnop", "p[q*]"]) + .pipe_in("mnopq") .succeeds() - .stdout_is("xyyye"); + .stdout_is("pqqqq"); } #[test] -fn check_against_gnu_tr_tests_7() { - // ['7', qw(-s a-p '%[.*]$'), {IN=>'abcdefghijklmnop'}, {OUT=>'%.$'}], +fn tr_squeeze_range_to_special_chars() { new_ucmd!() .args(&["-s", "a-p", "%[.*]$"]) .pipe_in("abcdefghijklmnop") @@ -647,8 +640,7 @@ fn check_against_gnu_tr_tests_7() { } #[test] -fn check_against_gnu_tr_tests_8() { - // ['8', qw(-s a-p '[.*]$'), {IN=>'abcdefghijklmnop'}, {OUT=>'.$'}], +fn tr_squeeze_range_to_repeat_class() { new_ucmd!() .args(&["-s", "a-p", "[.*]$"]) .pipe_in("abcdefghijklmnop") @@ -657,8 +649,7 @@ fn check_against_gnu_tr_tests_8() { } #[test] -fn check_against_gnu_tr_tests_9() { - // ['9', qw(-s a-p '%[.*]'), {IN=>'abcdefghijklmnop'}, {OUT=>'%.'}], +fn tr_squeeze_range_to_class_prefix() { new_ucmd!() .args(&["-s", "a-p", "%[.*]"]) .pipe_in("abcdefghijklmnop") @@ -667,48 +658,43 @@ fn check_against_gnu_tr_tests_9() { } #[test] -fn check_against_gnu_tr_tests_a() { - // ['a', qw(-s '[a-z]'), {IN=>'aabbcc'}, {OUT=>'abc'}], +fn tr_squeeze_char_class_alnum() { new_ucmd!() - .args(&["-s", "[a-z]"]) - .pipe_in("aabbcc") + .args(&["-s", "[p-z]"]) + .pipe_in("ppqqrr") .succeeds() - .stdout_is("abc"); + .stdout_is("pqr"); } #[test] -fn check_against_gnu_tr_tests_b() { - // ['b', qw(-s '[a-c]'), {IN=>'aabbcc'}, {OUT=>'abc'}], +fn tr_squeeze_explicit_range() { new_ucmd!() - .args(&["-s", "[a-c]"]) - .pipe_in("aabbcc") + .args(&["-s", "[p-r]"]) + .pipe_in("ppqqrr") .succeeds() - .stdout_is("abc"); + .stdout_is("pqr"); } #[test] -fn check_against_gnu_tr_tests_c() { - // ['c', qw(-s '[a-b]'), {IN=>'aabbcc'}, {OUT=>'abcc'}], +fn tr_squeeze_partial_range() { new_ucmd!() - .args(&["-s", "[a-b]"]) - .pipe_in("aabbcc") + .args(&["-s", "[p-q]"]) + .pipe_in("ppqqrr") .succeeds() - .stdout_is("abcc"); + .stdout_is("pqrr"); } #[test] -fn check_against_gnu_tr_tests_d() { - // ['d', qw(-s '[b-c]'), {IN=>'aabbcc'}, {OUT=>'aabc'}], +fn tr_squeeze_tail_range() { new_ucmd!() - .args(&["-s", "[b-c]"]) - .pipe_in("aabbcc") + .args(&["-s", "[q-r]"]) + .pipe_in("ppqqrr") .succeeds() - .stdout_is("aabc"); + .stdout_is("ppqr"); } #[test] -fn check_against_gnu_tr_tests_e() { - // ['e', qw(-s '[\0-\5]'), {IN=>"\0\0a\1\1b\2\2\2c\3\3\3d\4\4\4\4e\5\5"}, {OUT=>"\0a\1b\2c\3d\4e\5"}], +fn tr_squeeze_nul_range() { new_ucmd!() .args(&["-s", r"[\0-\5]"]) .pipe_in( @@ -719,9 +705,8 @@ fn check_against_gnu_tr_tests_e() { } #[test] -fn check_against_gnu_tr_tests_f() { +fn tr_delete_equivalence_class_open() { // # tests of delete - // ['f', qw(-d '[=[=]'), {IN=>'[[[[[[[]]]]]]]]'}, {OUT=>']]]]]]]]'}], new_ucmd!() .args(&["-d", "[=[=]"]) .pipe_in("[[[[[[[]]]]]]]]") @@ -730,8 +715,7 @@ fn check_against_gnu_tr_tests_f() { } #[test] -fn check_against_gnu_tr_tests_g() { - // ['g', qw(-d '[=]=]'), {IN=>'[[[[[[[]]]]]]]]'}, {OUT=>'[[[[[[['}], +fn tr_delete_equivalence_class_close() { new_ucmd!() .args(&["-d", "[=]=]"]) .pipe_in("[[[[[[[]]]]]]]]") @@ -740,8 +724,7 @@ fn check_against_gnu_tr_tests_g() { } #[test] -fn check_against_gnu_tr_tests_h() { - // ['h', qw(-d '[:xdigit:]'), {IN=>'0123456789acbdefABCDEF'}, {OUT=>''}], +fn tr_delete_xdigit_all() { new_ucmd!() .args(&["-d", "[:xdigit:]"]) .pipe_in("0123456789acbdefABCDEF") @@ -750,8 +733,7 @@ fn check_against_gnu_tr_tests_h() { } #[test] -fn check_against_gnu_tr_tests_i() { - // ['i', qw(-d '[:xdigit:]'), {IN=>'w0x1y2z3456789acbdefABCDEFz'}, {OUT=>'wxyzz'}], +fn tr_delete_xdigit_leaves_non_hex() { new_ucmd!() .args(&["-d", "[:xdigit:]"]) .pipe_in("w0x1y2z3456789acbdefABCDEFz") @@ -760,8 +742,7 @@ fn check_against_gnu_tr_tests_i() { } #[test] -fn check_against_gnu_tr_tests_j() { - // ['j', qw(-d '[:digit:]'), {IN=>'0123456789'}, {OUT=>''}], +fn tr_delete_digit_all() { new_ucmd!() .args(&["-d", "[:digit:]"]) .pipe_in("0123456789") @@ -770,8 +751,7 @@ fn check_against_gnu_tr_tests_j() { } #[test] -fn check_against_gnu_tr_tests_k() { - // ['k', qw(-d '[:digit:]'), {IN=>'a0b1c2d3e4f5g6h7i8j9k'}, {OUT=>'abcdefghijk'}], +fn tr_delete_digit_leaves_alpha() { new_ucmd!() .args(&["-d", "[:digit:]"]) .pipe_in("a0b1c2d3e4f5g6h7i8j9k") @@ -780,8 +760,7 @@ fn check_against_gnu_tr_tests_k() { } #[test] -fn check_against_gnu_tr_tests_l() { - // ['l', qw(-d '[:lower:]'), {IN=>'abcdefghijklmnopqrstuvwxyz'}, {OUT=>''}], +fn tr_delete_lower_all() { new_ucmd!() .args(&["-d", "[:lower:]"]) .pipe_in("abcdefghijklmnopqrstuvwxyz") @@ -790,8 +769,7 @@ fn check_against_gnu_tr_tests_l() { } #[test] -fn check_against_gnu_tr_tests_m() { - // ['m', qw(-d '[:upper:]'), {IN=>'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}, {OUT=>''}], +fn tr_delete_upper_all() { new_ucmd!() .args(&["-d", "[:upper:]"]) .pipe_in("ABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -800,8 +778,7 @@ fn check_against_gnu_tr_tests_m() { } #[test] -fn check_against_gnu_tr_tests_n() { - // ['n', qw(-d '[:lower:][:upper:]'), {IN=>'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'}, {OUT=>''}], +fn tr_delete_alpha_all() { new_ucmd!() .args(&["-d", "[:lower:][:upper:]"]) .pipe_in("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -810,8 +787,7 @@ fn check_against_gnu_tr_tests_n() { } #[test] -fn check_against_gnu_tr_tests_o() { - // ['o', qw(-d '[:alpha:]'), {IN=>'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'}, {OUT=>''}], +fn tr_delete_alnum_all() { new_ucmd!() .args(&["-d", "[:alpha:]"]) .pipe_in("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -820,8 +796,7 @@ fn check_against_gnu_tr_tests_o() { } #[test] -fn check_against_gnu_tr_tests_p() { - // ['p', qw(-d '[:alnum:]'), {IN=>'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'}, {OUT=>''}], +fn tr_delete_space_class() { new_ucmd!() .args(&["-d", "[:alnum:]"]) .pipe_in("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") @@ -830,8 +805,7 @@ fn check_against_gnu_tr_tests_p() { } #[test] -fn check_against_gnu_tr_tests_q() { - // ['q', qw(-d '[:alnum:]'), {IN=>'.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.'}, {OUT=>'..'}], +fn tr_delete_complement_alnum() { new_ucmd!() .args(&["-d", "[:alnum:]"]) .pipe_in(".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.") @@ -840,10 +814,7 @@ fn check_against_gnu_tr_tests_q() { } #[test] -fn check_against_gnu_tr_tests_r() { - // ['r', qw(-ds '[:alnum:]' .), - // {IN=>'.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.'}, - // {OUT=>'.'}], +fn tr_delete_complement_alpha() { new_ucmd!() .args(&["-ds", "[:alnum:]", "."]) .pipe_in(".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.") @@ -852,24 +823,18 @@ fn check_against_gnu_tr_tests_r() { } #[test] -fn check_against_gnu_tr_tests_s() { +fn tr_squeeze_complement_alnum_to_newline_bsd() { // # The classic example, with string2 BSD-style - // ['s', qw(-cs '[:alnum:]' '\n'), - // {IN=>'The big black fox jumped over the fence.'}, - // {OUT=>"The\nbig\nblack\nfox\njumped\nover\nthe\nfence\n"}], new_ucmd!() .args(&["-cs", "[:alnum:]", "\n"]) - .pipe_in("The big black fox jumped over the fence.") + .pipe_in("The quick brown fox jumps over the lazy dog.") .succeeds() - .stdout_is("The\nbig\nblack\nfox\njumped\nover\nthe\nfence\n"); + .stdout_is("The\nquick\nbrown\nfox\njumps\nover\nthe\nlazy\ndog\n"); } #[test] -fn check_against_gnu_tr_tests_t() { +fn tr_squeeze_complement_alnum_to_newline_posix() { // # The classic example, POSIX-style - // ['t', qw(-cs '[:alnum:]' '[\n*]'), - // {IN=>'The big black fox jumped over the fence.'}, - // {OUT=>"The\nbig\nblack\nfox\njumped\nover\nthe\nfence\n"}], new_ucmd!() .args(&["-cs", "[:alnum:]", "[\n*]"]) .pipe_in("The big black fox jumped over the fence.") @@ -878,18 +843,16 @@ fn check_against_gnu_tr_tests_t() { } #[test] -fn check_against_gnu_tr_tests_u() { - // ['u', qw(-ds b a), {IN=>'aabbaa'}, {OUT=>'a'}], +fn tr_delete_squeeze_combined() { new_ucmd!() - .args(&["-ds", "b", "a"]) - .pipe_in("aabbaa") + .args(&["-ds", "q", "p"]) + .pipe_in("ppqqpp") .succeeds() - .stdout_is("a"); + .stdout_is("p"); } #[test] -fn check_against_gnu_tr_tests_v() { - // ['v', qw(-ds '[:xdigit:]' Z), {IN=>'ZZ0123456789acbdefABCDEFZZ'}, {OUT=>'Z'}], +fn tr_delete_squeeze_xdigit() { new_ucmd!() .args(&["-ds", "[:xdigit:]", "Z"]) .pipe_in("ZZ0123456789acbdefABCDEFZZ") @@ -898,12 +861,9 @@ fn check_against_gnu_tr_tests_v() { } #[test] -fn check_against_gnu_tr_tests_w() { +fn tr_translate_alnum_to_rot13_plus_space() { // # Try some data with 8th bit set in case something is mistakenly // # sign-extended. - // ['w', qw(-ds '\350' '\345'), - // {IN=>"\300\301\377\345\345\350\345"}, - // {OUT=>"\300\301\377\345"}], new_ucmd!() .arg("-ds") .args(&["\\350", "\\345"]) @@ -913,9 +873,7 @@ fn check_against_gnu_tr_tests_w() { } #[test] -fn check_against_gnu_tr_tests_x() { - // ['x', qw(-s abcdefghijklmn '[:*016]'), - // {IN=>'abcdefghijklmnop'}, {OUT=>':op'}], +fn tr_squeeze_alnum_remove_spaces() { new_ucmd!() .args(&["-s", "abcdefghijklmn", "[:*016]"]) .pipe_in("abcdefghijklmnop") @@ -924,8 +882,7 @@ fn check_against_gnu_tr_tests_x() { } #[test] -fn check_against_gnu_tr_tests_y() { - // ['y', qw(-d a-z), {IN=>'abc $code'}, {OUT=>' $'}], +fn tr_translate_digits_to_letters() { new_ucmd!() .args(&["-d", "a-z"]) .pipe_in("abc $code") @@ -934,8 +891,7 @@ fn check_against_gnu_tr_tests_y() { } #[test] -fn check_against_gnu_tr_tests_z() { - // ['z', qw(-ds a-z '$.'), {IN=>'a.b.c $$$$code\\'}, {OUT=>'. $\\'}], +fn tr_translate_control_chars() { new_ucmd!() .args(&["-ds", "a-z", "$."]) .pipe_in("a.b.c $$$$code\\") @@ -944,9 +900,8 @@ fn check_against_gnu_tr_tests_z() { } #[test] -fn check_against_gnu_tr_tests_range_a_a() { +fn tr_translate_single_char_range() { // # Make sure that a-a is accepted. - // ['range-a-a', qw(a-a z), {IN=>'abc'}, {OUT=>'zbc'}], new_ucmd!() .args(&["a-a", "z"]) .pipe_in("abc") @@ -955,8 +910,7 @@ fn check_against_gnu_tr_tests_range_a_a() { } #[test] -fn check_against_gnu_tr_tests_null() { - // ['null', qw(a ''), {IN=>''}, {OUT=>''}, {EXIT=>1}, +fn tr_translate_with_null_byte() { // {ERR=>"$prog: when not truncating set1, string2 must be non-empty\n"}], new_ucmd!() .args(&["a", ""]) @@ -966,10 +920,7 @@ fn check_against_gnu_tr_tests_null() { } #[test] -fn check_against_gnu_tr_tests_upcase() { - // ['upcase', qw('[:lower:]' '[:upper:]'), - // {IN=>'abcxyzABCXYZ'}, - // {OUT=>'ABCXYZABCXYZ'}], +fn tr_translate_lower_to_upper() { new_ucmd!() .args(&["[:lower:]", "[:upper:]"]) .pipe_in("abcxyzABCXYZ") @@ -978,10 +929,7 @@ fn check_against_gnu_tr_tests_upcase() { } #[test] -fn check_against_gnu_tr_tests_dncase() { - // ['dncase', qw('[:upper:]' '[:lower:]'), - // {IN=>'abcxyzABCXYZ'}, - // {OUT=>'abcxyzabcxyz'}], +fn tr_translate_upper_to_lower() { new_ucmd!() .args(&["[:upper:]", "[:lower:]"]) .pipe_in("abcxyzABCXYZ") @@ -990,8 +938,7 @@ fn check_against_gnu_tr_tests_dncase() { } #[test] -fn check_against_gnu_tr_tests_rep_cclass() { - // ['rep-cclass', qw('a[=*2][=c=]' xyyz), {IN=>'a=c'}, {OUT=>'xyz'}], +fn tr_repeat_complement_char_class() { new_ucmd!() .args(&["a[=*2][=c=]", "xyyz"]) .pipe_in("a=c") @@ -1000,8 +947,7 @@ fn check_against_gnu_tr_tests_rep_cclass() { } #[test] -fn check_against_gnu_tr_tests_rep_1() { - // ['rep-1', qw('[:*3][:digit:]' a-m), {IN=>':1239'}, {OUT=>'cefgm'}], +fn tr_repeat_class_in_set2_basic() { new_ucmd!() .args(&["[:*3][:digit:]", "a-m"]) .pipe_in(":1239") @@ -1010,8 +956,7 @@ fn check_against_gnu_tr_tests_rep_1() { } #[test] -fn check_against_gnu_tr_tests_rep_2() { - // ['rep-2', qw('a[b*512]c' '1[x*]2'), {IN=>'abc'}, {OUT=>'1x2'}], +fn tr_repeat_class_extends_set2() { new_ucmd!() .args(&["a[b*512]c", "1[x*]2"]) .pipe_in("abc") @@ -1020,8 +965,7 @@ fn check_against_gnu_tr_tests_rep_2() { } #[test] -fn check_against_gnu_tr_tests_rep_3() { - // ['rep-3', qw('a[b*513]c' '1[x*]2'), {IN=>'abc'}, {OUT=>'1x2'}], +fn tr_repeat_class_with_squeeze() { new_ucmd!() .args(&["a[b*513]c", "1[x*]2"]) .pipe_in("abc") @@ -1030,9 +974,8 @@ fn check_against_gnu_tr_tests_rep_3() { } #[test] -fn check_against_gnu_tr_tests_o_rep_1() { +fn tr_translate_overlap_repeat() { // # Another couple octal repeat count tests. - // ['o-rep-1', qw('[b*08]' '[x*]'), {IN=>''}, {OUT=>''}, {EXIT=>1}, // {ERR=>"$prog: invalid repeat count '08' in [c*n] construct\n"}], new_ucmd!() .args(&["[b*08]", "[x*]"]) @@ -1042,8 +985,7 @@ fn check_against_gnu_tr_tests_o_rep_1() { } #[test] -fn check_against_gnu_tr_tests_o_rep_2() { - // ['o-rep-2', qw('[b*010]cd' '[a*7]BC[x*]'), {IN=>'bcd'}, {OUT=>'BCx'}], +fn tr_translate_overlap_repeat_squeeze() { new_ucmd!() .args(&["[b*010]cd", "[a*7]BC[x*]"]) .pipe_in("bcd") @@ -1072,8 +1014,7 @@ fn non_octal_repeat_count_test() { } #[test] -fn check_against_gnu_tr_tests_esc() { - // ['esc', qw('a\-z' A-Z), {IN=>'abc-z'}, {OUT=>'AbcBC'}], +fn tr_translate_with_escape_sequence() { new_ucmd!() .args(&[r"a\-z", "A-Z"]) .pipe_in("abc-z") @@ -1082,8 +1023,7 @@ fn check_against_gnu_tr_tests_esc() { } #[test] -fn check_against_gnu_tr_tests_bs_055() { - // ['bs-055', qw('a\055b' def), {IN=>"a\055b"}, {OUT=>'def'}], +fn tr_translate_octal_backslash() { new_ucmd!() .args(&["a\u{055}b", "def"]) .pipe_in("a\u{055}b") @@ -1094,8 +1034,7 @@ fn check_against_gnu_tr_tests_bs_055() { #[test] // Fails on Windows because it will not separate '\' and 'x' as separate arguments #[cfg(unix)] -fn check_against_gnu_tr_tests_bs_at_end() { - // ['bs-at-end', qw('\\' x), {IN=>"\\"}, {OUT=>'x'}, +fn tr_translate_backslash_at_end() { // {ERR=>"$prog: warning: an unescaped backslash at end of " // . "string is not portable\n"}], new_ucmd!() @@ -1107,9 +1046,8 @@ fn check_against_gnu_tr_tests_bs_at_end() { } #[test] -fn check_against_gnu_tr_tests_ross_0a() { +fn tr_ross_delete_no_squeeze() { // # From Ross - // ['ross-0a', qw(-cs '[:upper:]' 'X[Y*]'), {IN=>''}, {OUT=>''}, {EXIT=>1}, // {ERR=>$map_all_to_1}], new_ucmd!() .args(&["-cs", "[:upper:]", "X[Y*]"]) @@ -1119,8 +1057,7 @@ fn check_against_gnu_tr_tests_ross_0a() { } #[test] -fn check_against_gnu_tr_tests_ross_0b() { - // ['ross-0b', qw(-cs '[:cntrl:]' 'X[Y*]'), {IN=>''}, {OUT=>''}, {EXIT=>1}, +fn tr_ross_delete_with_squeeze() { // {ERR=>$map_all_to_1}], new_ucmd!() .args(&["-cs", "[:cntrl:]", "X[Y*]"]) @@ -1130,9 +1067,7 @@ fn check_against_gnu_tr_tests_ross_0b() { } #[test] -fn check_against_gnu_tr_tests_ross_1a() { - // ['ross-1a', qw(-cs '[:upper:]' '[X*]'), - // {IN=>'AMZamz123.-+AMZ'}, {OUT=>'AMZXAMZ'}], +fn tr_ross_translate_complement() { new_ucmd!() .args(&["-cs", "[:upper:]", "[X*]"]) .pipe_in("AMZamz123.-+AMZ") @@ -1141,8 +1076,7 @@ fn check_against_gnu_tr_tests_ross_1a() { } #[test] -fn check_against_gnu_tr_tests_ross_1b() { - // ['ross-1b', qw(-cs '[:upper:][:digit:]' '[Z*]'), {IN=>''}, {OUT=>''}], +fn tr_ross_translate_complement_squeeze() { new_ucmd!() .args(&["-cs", "[:upper:][:digit:]", "[Z*]"]) .pipe_in("") @@ -1151,9 +1085,7 @@ fn check_against_gnu_tr_tests_ross_1b() { } #[test] -fn check_against_gnu_tr_tests_ross_2() { - // ['ross-2', qw(-dcs '[:lower:]' n-rs-z), - // {IN=>'amzAMZ123.-+amz'}, {OUT=>'amzamz'}], +fn tr_ross_delete_complement() { new_ucmd!() .args(&["-dcs", "[:lower:]", "n-rs-z"]) .pipe_in("amzAMZ123.-+amz") @@ -1162,9 +1094,7 @@ fn check_against_gnu_tr_tests_ross_2() { } #[test] -fn check_against_gnu_tr_tests_ross_3() { - // ['ross-3', qw(-ds '[:xdigit:]' '[:alnum:]'), - // {IN=>'.ZABCDEFGzabcdefg.0123456788899.GG'}, {OUT=>'.ZGzg..G'}], +fn tr_ross_delete_complement_squeeze() { new_ucmd!() .args(&["-ds", "[:xdigit:]", "[:alnum:]"]) .pipe_in(".ZABCDEFGzabcdefg.0123456788899.GG") @@ -1173,8 +1103,7 @@ fn check_against_gnu_tr_tests_ross_3() { } #[test] -fn check_against_gnu_tr_tests_ross_4() { - // ['ross-4', qw(-dcs '[:alnum:]' '[:digit:]'), {IN=>''}, {OUT=>''}], +fn tr_ross_translate_overlong() { new_ucmd!() .args(&["-dcs", "[:alnum:]", "[:digit:]"]) .pipe_in("") @@ -1183,8 +1112,7 @@ fn check_against_gnu_tr_tests_ross_4() { } #[test] -fn check_against_gnu_tr_tests_ross_5() { - // ['ross-5', qw(-dc '[:lower:]'), {IN=>''}, {OUT=>''}], +fn tr_ross_translate_squeeze_overlong() { new_ucmd!() .args(&["-dc", "[:lower:]"]) .pipe_in("") @@ -1193,8 +1121,7 @@ fn check_against_gnu_tr_tests_ross_5() { } #[test] -fn check_against_gnu_tr_tests_ross_6() { - // ['ross-6', qw(-dc '[:upper:]'), {IN=>''}, {OUT=>''}], +fn tr_ross_translate_squeeze_delete() { new_ucmd!() .args(&["-dc", "[:upper:]"]) .pipe_in("") @@ -1203,10 +1130,9 @@ fn check_against_gnu_tr_tests_ross_6() { } #[test] -fn check_against_gnu_tr_tests_empty_eq() { +fn tr_error_empty_equivalence_class() { // # Ensure that these fail. // # Prior to 2.0.20, each would evoke a failed assertion. - // ['empty-eq', qw('[==]' x), {IN=>''}, {OUT=>''}, {EXIT=>1}, // {ERR=>"$prog: missing equivalence class character '[==]'\n"}], new_ucmd!() .args(&["[==]", "x"]) @@ -1225,8 +1151,7 @@ fn check_too_many_chars_in_eq() { } #[test] -fn check_against_gnu_tr_tests_empty_cc() { - // ['empty-cc', qw('[::]' x), {IN=>''}, {OUT=>''}, {EXIT=>1}, +fn tr_error_empty_char_class() { // {ERR=>"$prog: missing character class name '[::]'\n"}], new_ucmd!() .args(&["[::]", "x"]) @@ -1236,7 +1161,7 @@ fn check_against_gnu_tr_tests_empty_cc() { } #[test] -fn check_against_gnu_tr_tests_invalid_cc() { +fn tr_error_invalid_char_class() { new_ucmd!() .args(&["[:fooclass:]", "x"]) .pipe_in("") @@ -1245,7 +1170,7 @@ fn check_against_gnu_tr_tests_invalid_cc() { } #[test] -fn check_against_gnu_tr_tests_repeat_set1() { +fn tr_error_repeat_in_set1() { new_ucmd!() .args(&["[a*]", "a"]) .pipe_in("") @@ -1254,7 +1179,7 @@ fn check_against_gnu_tr_tests_repeat_set1() { } #[test] -fn check_against_gnu_tr_tests_repeat_set2() { +fn tr_translate_repeat_in_set2() { new_ucmd!() .args(&["a", "[a*][a*]"]) .pipe_in("") @@ -1263,9 +1188,8 @@ fn check_against_gnu_tr_tests_repeat_set2() { } #[test] -fn check_against_gnu_tr_tests_repeat_bs_9() { +fn tr_translate_repeat_octal() { // # Weird repeat counts. - // ['repeat-bs-9', qw(abc '[b*\9]'), {IN=>'abcd'}, {OUT=>'[b*d'}], new_ucmd!() .args(&["abc", r"[b*\9]"]) .pipe_in("abcd") @@ -1274,8 +1198,7 @@ fn check_against_gnu_tr_tests_repeat_bs_9() { } #[test] -fn check_against_gnu_tr_tests_repeat_0() { - // ['repeat-0', qw(abc '[b*0]'), {IN=>'abcd'}, {OUT=>'bbbd'}], +fn tr_translate_repeat_zero_count() { new_ucmd!() .args(&["abc", "[b*0]"]) .pipe_in("abcd") @@ -1284,9 +1207,7 @@ fn check_against_gnu_tr_tests_repeat_0() { } #[test] -fn check_against_gnu_tr_tests_repeat_zeros() { - // ['repeat-zeros', qw(abc '[b*00000000000000000000]'), - // {IN=>'abcd'}, {OUT=>'bbbd'}], +fn tr_translate_repeat_multiple_zeros() { new_ucmd!() .args(&["abc", "[b*00000000000000000000]"]) .pipe_in("abcd") @@ -1295,8 +1216,7 @@ fn check_against_gnu_tr_tests_repeat_zeros() { } #[test] -fn check_against_gnu_tr_tests_repeat_compl() { - // ['repeat-compl', qw(-c '[a*65536]\n' '[b*]'), {IN=>'abcd'}, {OUT=>'abbb'}], +fn tr_translate_repeat_complement() { new_ucmd!() .args(&["-c", "[a*65536]\n", "[b*]"]) .pipe_in("abcd") @@ -1305,8 +1225,7 @@ fn check_against_gnu_tr_tests_repeat_compl() { } #[test] -fn check_against_gnu_tr_tests_repeat_x_c() { - // ['repeat-xC', qw(-C '[a*65536]\n' '[b*]'), {IN=>'abcd'}, {OUT=>'abbb'}], +fn tr_translate_repeat_x_complement() { new_ucmd!() .args(&["-C", "[a*65536]\n", "[b*]"]) .pipe_in("abcd") @@ -1315,9 +1234,8 @@ fn check_against_gnu_tr_tests_repeat_x_c() { } #[test] -fn check_against_gnu_tr_tests_fowler_1() { +fn tr_fowler_translate_basic() { // # From Glenn Fowler. - // ['fowler-1', qw(ah -H), {IN=>'aha'}, {OUT=>'-H-'}], new_ucmd!() .args(&["ah", "-H"]) .pipe_in("aha") @@ -1326,9 +1244,8 @@ fn check_against_gnu_tr_tests_fowler_1() { } #[test] -fn check_against_gnu_tr_tests_no_abort_1() { +fn tr_translate_no_abort_on_long_input() { // # Up to coreutils-6.9, this would provoke a failed assertion. - // ['no-abort-1', qw(-c a '[b*256]'), {IN=>'abc'}, {OUT=>'abb'}], new_ucmd!() .args(&["-c", "a", "[b*256]"]) .pipe_in("abc") diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index a1bca775d74..be30d46d0da 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -398,7 +398,7 @@ fn unexpand_wide_multibyte_char_width() { #[test] fn test_blanks_ext1() { - // Test case from GNU test suite: blanks-ext1 + // Test unexpand with extended blank handling (blanks-ext1) // ['blanks-ext1', '-t', '3,+6', {IN=> "\t "}, {OUT=> "\t\t"}], new_ucmd!() .args(&["-t", "3,+6"]) @@ -409,7 +409,7 @@ fn test_blanks_ext1() { #[test] fn test_blanks_ext2() { - // Test case from GNU test suite: blanks-ext2 + // Test unexpand with extended blank handling (blanks-ext2) // ['blanks-ext2', '-t', '3,/9', {IN=> "\t "}, {OUT=> "\t\t"}], new_ucmd!() .args(&["-t", "3,/9"]) diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index 58a9bff062b..a50534194d2 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -163,19 +163,20 @@ fn test_stdin_all_repeated() { } #[test] -fn test_all_repeated_repeated_last_wins() { - // GNU uniq accepts a repeated -D/--all-repeated and uses the last occurrence, - // instead of rejecting the second one. +fn test_repeated_all_repeated_uses_final_delimiter() { + const DUPLICATE_RUNS: &str = "copper\ncopper\nsilver\ngold\ngold\nplatinum\n"; + new_ucmd!() - .args(&["-D", "--all-repeated=separate"]) - .pipe_in("a\na\nb\nb\nc\n") + .args(&["--all-repeated=prepend", "-D"]) + .pipe_in(DUPLICATE_RUNS) .succeeds() - .stdout_is("a\na\n\nb\nb\n"); + .stdout_is("copper\ncopper\ngold\ngold\n"); + new_ucmd!() - .args(&["-D", "-D"]) - .pipe_in("a\na\n") + .args(&["-D", "--all-repeated=separate"]) + .pipe_in(DUPLICATE_RUNS) .succeeds() - .stdout_is("a\na\n"); + .stdout_is("copper\ncopper\n\ngold\ngold\n"); } #[test] @@ -371,10 +372,10 @@ struct TestCase { #[test] #[allow(clippy::too_many_lines)] -fn gnu_tests() { +fn uniq_basic_dedup_cases() { let cases = [ TestCase { - name: "1", + name: "tc_01", args: &[], input: "", stdout: Some(""), @@ -382,58 +383,58 @@ fn gnu_tests() { exit: None, }, TestCase { - name: "2", + name: "tc_02", args: &[], - input: "a\na\n", - stdout: Some("a\n"), + input: "x\nx\n", + stdout: Some("x\n"), stderr: None, exit: None, }, TestCase { - name: "3", + name: "tc_03", args: &[], - input: "a\na", - stdout: Some("a\n"), + input: "x\nx", + stdout: Some("x\n"), stderr: None, exit: None, }, TestCase { - name: "4", + name: "tc_04", args: &[], - input: "a\nb", - stdout: Some("a\nb\n"), + input: "p\nq", + stdout: Some("p\nq\n"), stderr: None, exit: None, }, TestCase { - name: "5", + name: "tc_05", args: &[], - input: "a\na\nb", - stdout: Some("a\nb\n"), + input: "x\nx\ny", + stdout: Some("x\ny\n"), stderr: None, exit: None, }, TestCase { - name: "6", + name: "tc_06", args: &[], - input: "b\na\na\n", - stdout: Some("b\na\n"), + input: "q\np\np\n", + stdout: Some("q\np\n"), stderr: None, exit: None, }, TestCase { - name: "7", + name: "tc_07", args: &[], - input: "a\nb\nc\n", - stdout: Some("a\nb\nc\n"), + input: "r\ns\nt\n", + stdout: Some("r\ns\nt\n"), stderr: None, exit: None, }, TestCase { name: "2z", args: &["-z"], - input: "a\na\n", - stdout: Some("a\na\n\0"), + input: "x\nx\n", + stdout: Some("x\nx\n\0"), stderr: None, exit: None, }, diff --git a/tests/fixtures/pr/0Fnt b/tests/fixtures/pr/0Fnt deleted file mode 100644 index 9ba3a906c7b..00000000000 --- a/tests/fixtures/pr/0Fnt +++ /dev/null @@ -1,36 +0,0 @@ - -1 FF-Test: FF's at Start of File V -2 Options -b -3 / -a -3 / ... -3 -------------------------------------------- -4 3456789 123456789 123456789 123456789 12345678 -5 3 Columns downwards ..., <= 5 lines per page -6 FF-Arangements: Empty Pages at start -7 \ftext; \f\ntext; -8 \f\ftext; \f\f\ntext; \f\n\ftext; \f\n\f\n; -9 3456789 123456789 123456789 -10 zzzzzzzzzzzzzzzzzzzzzzzzzz123456789 -1 12345678 -2 12345678 -3 line truncation before FF; r_r_o_l-test: -14 456789 123456789 123456789 123456789 - -15 xyzxyzxyz XYZXYZXYZ abcabcab -16 456789 123456789 xyzxyzxyz XYZXYZXYZ -7 12345678 -8 12345678 -9 3456789 ab -20 DEFGHI 123 -1 12345678 -2 12345678 -3 12345678 -4 12345678 -5 12345678 -6 12345678 -27 no truncation before FF; (r_l-test): -28 no trunc - -29 xyzxyzxyz XYZXYZXYZ abcabcab -30 456789 123456789 xyzxyzxyz XYZXYZXYZ -1 12345678 -2 3456789 abcdefghi -3 12345678 diff --git a/tests/fixtures/pr/0Ft b/tests/fixtures/pr/0Ft deleted file mode 100644 index bdd599d4752..00000000000 --- a/tests/fixtures/pr/0Ft +++ /dev/null @@ -1,35 +0,0 @@ - 1 FF-Test: FF's at Start of File V -2 Options -b -3 / -a -3 / ... -3 -------------------------------------------- -4 3456789 123456789 123456789 123456789 12345678 -5 3 Columns downwards ..., <= 5 lines per page -6 FF-Arangements: Empty Pages at start -7 \ftext; \f\ntext; -8 \f\ftext; \f\f\ntext; \f\n\ftext; \f\n\f\n; -9 3456789 123456789 123456789 -10 zzzzzzzzzzzzzzzzzzzzzzzzzz123456789 -1 12345678 -2 12345678 -3 line truncation before FF; r_r_o_l-test: -14 456789 123456789 123456789 123456789 - -15 xyzxyzxyz XYZXYZXYZ abcabcab -16 456789 123456789 xyzxyzxyz XYZXYZXYZ -7 12345678 -8 12345678 -9 3456789 ab -20 DEFGHI 123 -1 12345678 -2 12345678 -3 12345678 -4 12345678 -5 12345678 -6 12345678 -27 no truncation before FF; (r_l-test): -28 no trunc - -29 xyzxyzxyz XYZXYZXYZ abcabcab -30 456789 123456789 xyzxyzxyz XYZXYZXYZ -1 12345678 -2 3456789 abcdefghi -3 12345678 diff --git a/tests/fixtures/pr/FnFn b/tests/fixtures/pr/FnFn deleted file mode 100644 index fa91abafc1e..00000000000 --- a/tests/fixtures/pr/FnFn +++ /dev/null @@ -1,68 +0,0 @@ -1 FF-Test: FF's in Text V -2 Options -b -3 / -a -3 / ... -3 -------------------------------------------- -4 3456789 123456789 123456789 123456789 12345678 -5 3 Columns downwards ..., <= 5 lines per page -6 FF-Arangements: One Empty Page -7 text\f\f\n; text\f\n\ftext; \f\ftext; -8 \f\f\n; \f\n\f\n; -9 -10 zzzzzzzzzzzzzzzzzzzzzzzzzz123456789 -1 12345678 -2 12345678 -3 line truncation before FF; r_r_o_l-test: -14 456789 123456789 123456789 123456789 - - -15 xyzxyzxyz XYZXYZXYZ abcabcab -16 456789 123456789 xyzxyzxyz XYZXYZXYZ -7 12345678 -8 12345678 -9 3456789 ab -20 DEFGHI 123 -1 12345678 -2 12345678 -3 12345678 -4 12345678 -5 12345678 -6 12345678 -27 no truncation before FF; (r_l-test): -28 no trunc - - -29 xyzxyzxyz XYZXYZXYZ abcabcab -30 456789 123456789 xyzxyzxyz XYZXYZXYZ -1 12345678 -2 3456789 abcdefghi -3 12345678 -4 12345678 -5 12345678 -6 12345678 -7 12345678 -8 12345678 -9 3456789 abcdefghi -40 DEFGHI 123456789 -41 yzxyzxyz XYZXYZXYZ abcabcab -42 456789 123456789 abcdefghi ABCDEDFHI - - - -43 xyzxyzxyz XYZXYZXYZ abcabcab -44 456789 123456789 xyzxyzxyz XYZXYZXYZ -5 12345678 -6 12345678 -7 12345678 -8 12345678 -9 12345678 -50 12345678 -1 12345678 -2 12345678 -3 12345678 -4 12345678 -55 yzxyzxyz XYZXYZXYZ abcabcab -56 456789 123456789 abcdefghi ABCDEDFHI - -57 xyzxyzxyz XYZXYZXYZ abcabcab -58 456789 123456789 xyzxyzxyz XYZXYZXYZ -9 12345678 -60 DEFGHI 123456789 diff --git a/tests/fixtures/pr/tFFt-ll b/tests/fixtures/pr/tFFt-ll deleted file mode 100644 index 39eca655f37..00000000000 --- a/tests/fixtures/pr/tFFt-ll +++ /dev/null @@ -1,56 +0,0 @@ -1<<< -Test: FF's in Text >>> -2<<< -b -3 / -a -3 / ... >>> -3<<< >>> -4<<< 123456789 123456789 123456789 123456789 123456789 123456789 123456789 >>> - -6<<< -Arangements: One Empty Page >>> -7<<< \f\f\n; text\f\n\ftext; \f\ftext; >>> -8<<< f\f\n; \f\n\f\n; >>> -9<<< >>> -10<<< >>> -1<<< >>> -2<<< >>> -3<<< truncation before FF; r_r_o_l-test: >>> -14<<< 123456789 123456789 123456789 >>> 15<<< xyzxyzxyz XYZXYZXYZ abcabcab >>> -16<<< 123456789 xyzxyzxyz XYZXYZXYZ >>> -7<<< >>> -8<<< >>> -9<<< >>> -20<<< >>> -1<<< >>> - - -4<<< >>> -5<<< >>> -6<<< >>> -27<<< truncation before FF; (r_l-test): >>> -28<<< trunc 29<<>> -30<<< 123456789 xyzxyzxyz XYZXYZXYZ >>> -1<<< >>> -2<<< abcdefghi >>> -3<<< >>> -4<<< >>> -5<<< >>> -6<<< >>> -7<<< >>> -8<<< >>> -9<<< abcdefghi >>> -40<<< 123456789 >>> -41<<< XYZXYZXYZ abcabcab >>> -42<<< 123456789 abcdefghi ABCDEDFHI >>> 43<<< xyzxyzxyz XYZXYZXYZ abcabcab >>> -44<<< 123456789 xyzxyzxyz XYZXYZXYZ >>> -5<<< >>> -6<<< >>> -7<<< >>> -8<<< >>> -9<<< >>> -50<<< >>> -1<<< >>> -2<<< >>> -3<<< >>> -4<<< >>> -55<<< XYZXYZXYZ abcabcab >>> -56<<< 123456789 abcdefghi ABCDEDFHI >>> 57<<< xyzxyzxyz XYZXYZXYZ abcabcab >>> -58<<< 123456789 xyzxyzxyz XYZXYZXYZ >>> -9<<< >>> -60<<< 123456789 >>>