diff --git a/src/uu/sort/locales/en-US.ftl b/src/uu/sort/locales/en-US.ftl index a5c5d01b69b..f2d97cd77df 100644 --- a/src/uu/sort/locales/en-US.ftl +++ b/src/uu/sort/locales/en-US.ftl @@ -32,6 +32,15 @@ sort-field-index-cannot-be-zero = field index can not be 0 sort-failed-parse-char-index = failed to parse character index {$char}: {$error} sort-invalid-option = invalid option: '{$option}' sort-invalid-char-index-zero-start = invalid character index 0 for the start position of a field +sort-invalid-field-spec = {$msg}: invalid field specification {$spec} +sort-invalid-count-at-start-of = invalid count at start of {$string} +sort-invalid-number-at-field-start = invalid number at field start +sort-invalid-number-after-dash = invalid number after '-' +sort-invalid-number-after-dot = invalid number after '.' +sort-invalid-number-after-comma = invalid number after ',' +sort-field-number-is-zero = field number is zero +sort-character-offset-is-zero = character offset is zero +sort-stray-character-field-spec = stray character in field spec sort-invalid-batch-size-arg = invalid --batch-size argument '{$arg}' sort-minimum-batch-size-two = minimum --batch-size argument is '2' sort-batch-size-too-large = --batch-size argument {$arg} too large diff --git a/src/uu/sort/locales/fr-FR.ftl b/src/uu/sort/locales/fr-FR.ftl index 4dbc05a49aa..1a01ebb6be6 100644 --- a/src/uu/sort/locales/fr-FR.ftl +++ b/src/uu/sort/locales/fr-FR.ftl @@ -32,6 +32,15 @@ sort-field-index-cannot-be-zero = l'index de champ ne peut pas être 0 sort-failed-parse-char-index = échec d'analyse de l'index de caractère {$char} : {$error} sort-invalid-option = option invalide : '{$option}' sort-invalid-char-index-zero-start = index de caractère 0 invalide pour la position de début d'un champ +sort-invalid-field-spec = {$msg} : spécification de champ invalide {$spec} +sort-invalid-count-at-start-of = nombre invalide au début de {$string} +sort-invalid-number-at-field-start = nombre invalide au début du champ +sort-invalid-number-after-dash = nombre invalide après '-' +sort-invalid-number-after-dot = nombre invalide après '.' +sort-invalid-number-after-comma = nombre invalide après ',' +sort-field-number-is-zero = le numéro de champ est zéro +sort-character-offset-is-zero = le décalage de caractère est zéro +sort-stray-character-field-spec = caractère parasite dans la spécification de champ sort-invalid-batch-size-arg = argument --batch-size invalide '{$arg}' sort-minimum-batch-size-two = l'argument --batch-size minimum est '2' sort-batch-size-too-large = argument --batch-size {$arg} trop grand diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 071163c5aee..5e1ff303b11 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -67,15 +67,6 @@ mod options { pub const GENERAL_NUMERIC: &str = "general-numeric-sort"; pub const VERSION: &str = "version-sort"; pub const RANDOM: &str = "random-sort"; - - pub const ALL_SORT_MODES: [&str; 6] = [ - GENERAL_NUMERIC, - HUMAN_NUMERIC, - MONTH, - NUMERIC, - VERSION, - RANDOM, - ]; } pub mod check { @@ -139,9 +130,6 @@ pub enum SortError { error: std::io::Error, }, - #[error("{}", translate!("sort-parse-key-error", "key" => .key.quote(), "msg" => .msg.clone()))] - ParseKeyError { key: String, msg: String }, - #[error("{}", translate!("sort-cannot-read", "path" => format!("{}", .path.maybe_quote()), "error" => strip_errno(.error)))] ReadFailed { path: PathBuf, @@ -207,20 +195,6 @@ enum SortMode { Default, } -impl SortMode { - fn get_short_name(&self) -> Option { - match self { - Self::Numeric => Some('n'), - Self::HumanNumeric => Some('h'), - Self::GeneralNumeric => Some('g'), - Self::Month => Some('M'), - Self::Version => Some('V'), - Self::Random => Some('R'), - Self::Default => None, - } - } -} - /// Return the length of the byte slice while ignoring embedded NULs (used for debug underline alignment). fn count_non_null_bytes(bytes: &[u8]) -> usize { bytes.iter().filter(|&&c| c != b'\0').count() @@ -430,53 +404,7 @@ struct KeySettings { reverse: bool, } -impl KeySettings { - /// Checks if the supplied combination of `mode`, `ignore_non_printing` and `dictionary_order` is allowed. - fn check_compatibility( - mode: SortMode, - ignore_non_printing: bool, - dictionary_order: bool, - ) -> Result<(), String> { - if matches!( - mode, - SortMode::Numeric | SortMode::HumanNumeric | SortMode::GeneralNumeric | SortMode::Month - ) { - if dictionary_order { - return Err( - translate!("sort-options-incompatible", "opt1" => "d", "opt2" => mode.get_short_name().unwrap()), - ); - } else if ignore_non_printing { - return Err( - translate!("sort-options-incompatible", "opt1" => "i", "opt2" => mode.get_short_name().unwrap()), - ); - } - } - Ok(()) - } - - fn set_sort_mode(&mut self, mode: SortMode) -> Result<(), String> { - if self.mode != SortMode::Default && self.mode != mode { - return Err( - translate!("sort-options-incompatible", "opt1" => self.mode.get_short_name().unwrap(), "opt2" => mode.get_short_name().unwrap()), - ); - } - Self::check_compatibility(mode, self.ignore_non_printing, self.dictionary_order)?; - self.mode = mode; - Ok(()) - } - - fn set_dictionary_order(&mut self) -> Result<(), String> { - Self::check_compatibility(self.mode, self.ignore_non_printing, true)?; - self.dictionary_order = true; - Ok(()) - } - - fn set_ignore_non_printing(&mut self) -> Result<(), String> { - Self::check_compatibility(self.mode, true, self.dictionary_order)?; - self.ignore_non_printing = true; - Ok(()) - } -} +impl KeySettings {} impl From<&GlobalSettings> for KeySettings { fn from(settings: &GlobalSettings) -> Self { @@ -496,6 +424,122 @@ impl Default for KeySettings { Self::from(&GlobalSettings::default()) } } + +#[derive(Clone, Copy, Debug, Default)] +struct ModeFlags { + numeric: bool, + general_numeric: bool, + human_numeric: bool, + month: bool, + version: bool, + random: bool, +} + +impl ModeFlags { + fn from_mode(mode: SortMode) -> Self { + let mut flags = Self::default(); + match mode { + SortMode::Numeric => flags.numeric = true, + SortMode::GeneralNumeric => flags.general_numeric = true, + SortMode::HumanNumeric => flags.human_numeric = true, + SortMode::Month => flags.month = true, + SortMode::Version => flags.version = true, + SortMode::Random => flags.random = true, + SortMode::Default => {} + } + flags + } + + fn to_mode(self) -> SortMode { + if self.numeric { + SortMode::Numeric + } else if self.general_numeric { + SortMode::GeneralNumeric + } else if self.human_numeric { + SortMode::HumanNumeric + } else if self.month { + SortMode::Month + } else if self.random { + SortMode::Random + } else if self.version { + SortMode::Version + } else { + SortMode::Default + } + } +} + +fn ordering_opts_string( + flags: ModeFlags, + dictionary_order: bool, + ignore_non_printing: bool, + ignore_case: bool, +) -> String { + let mut opts = String::new(); + if dictionary_order { + opts.push('d'); + } + if ignore_case { + opts.push('f'); + } + if flags.general_numeric { + opts.push('g'); + } + if flags.human_numeric { + opts.push('h'); + } + if !dictionary_order && ignore_non_printing { + opts.push('i'); + } + if flags.month { + opts.push('M'); + } + if flags.numeric { + opts.push('n'); + } + if flags.random { + opts.push('R'); + } + if flags.version { + opts.push('V'); + } + opts +} + +fn ordering_incompatible( + flags: ModeFlags, + dictionary_order: bool, + ignore_non_printing: bool, +) -> bool { + let mut count = 0; + if flags.numeric { + count += 1; + } + if flags.general_numeric { + count += 1; + } + if flags.human_numeric { + count += 1; + } + if flags.month { + count += 1; + } + if flags.version || flags.random || dictionary_order || ignore_non_printing { + count += 1; + } + count > 1 +} + +fn incompatible_options_error(opts: &str) -> Box { + USimpleError::new( + 2, + translate!( + "sort-options-incompatible", + "opt1" => opts, + "opt2" => "" + ), + ) +} enum Selection<'a> { AsBigDecimal(GeneralBigDecimalParseResult), WithNumInfo(&'a [u8], NumInfo), @@ -772,42 +816,6 @@ struct KeyPosition { ignore_blanks: bool, } -impl KeyPosition { - fn new(key: &str, default_char_index: usize, ignore_blanks: bool) -> Result { - let mut field_and_char = key.split('.'); - - let field = field_and_char - .next() - .ok_or_else(|| translate!("sort-invalid-key", "key" => key.quote()))?; - let char = field_and_char.next(); - - let field = match field.parse::() { - Ok(f) => f, - Err(e) if *e.kind() == IntErrorKind::PosOverflow => usize::MAX, - Err(e) => { - return Err( - translate!("sort-failed-parse-field-index", "field" => field.quote(), "error" => e), - ); - } - }; - if field == 0 { - return Err(translate!("sort-field-index-cannot-be-zero")); - } - - let char = char.map_or(Ok(default_char_index), |char| { - char.parse().map_err(|e: std::num::ParseIntError| { - translate!("sort-failed-parse-char-index", "char" => char.quote(), "error" => e) - }) - })?; - - Ok(Self { - field, - char, - ignore_blanks, - }) - } -} - impl Default for KeyPosition { fn default() -> Self { Self { @@ -818,6 +826,88 @@ impl Default for KeyPosition { } } +fn bad_field_spec(spec: &str, msg_key: &str) -> Box { + USimpleError::new( + 2, + translate!( + "sort-invalid-field-spec", + "msg" => translate!(msg_key), + "spec" => spec.quote() + ), + ) +} + +fn invalid_count_error(msg_key: &str, input: &str) -> Box { + USimpleError::new( + 2, + format!( + "{}: {}", + translate!(msg_key), + translate!("sort-invalid-count-at-start-of", "string" => input.quote()) + ), + ) +} + +fn parse_field_count<'a>(input: &'a str, msg_key: &str) -> UResult<(usize, &'a str)> { + let bytes = input.as_bytes(); + let mut idx = 0; + while idx < bytes.len() && bytes[idx].is_ascii_digit() { + idx += 1; + } + if idx == 0 { + return Err(invalid_count_error(msg_key, input)); + } + let (num_str, rest) = input.split_at(idx); + let value = match num_str.parse::() { + Ok(v) => v, + Err(e) if *e.kind() == IntErrorKind::PosOverflow => usize::MAX, + Err(_) => return Err(invalid_count_error(msg_key, input)), + }; + Ok((value, rest)) +} + +fn is_ordering_option_char(byte: u8) -> bool { + matches!( + byte, + b'b' | b'd' | b'f' | b'g' | b'h' | b'i' | b'M' | b'n' | b'R' | b'r' | b'V' + ) +} + +fn parse_ordering_options<'a>( + input: &'a str, + settings: &mut KeySettings, + flags: &mut ModeFlags, +) -> (&'a str, bool) { + let mut ignore_blanks = false; + let bytes = input.as_bytes(); + let mut idx = 0; + while idx < bytes.len() { + match bytes[idx] { + b'b' => ignore_blanks = true, + b'd' => { + settings.dictionary_order = true; + settings.ignore_non_printing = false; + } + b'f' => settings.ignore_case = true, + b'g' => flags.general_numeric = true, + b'h' => flags.human_numeric = true, + b'i' => { + if !settings.dictionary_order { + settings.ignore_non_printing = true; + } + } + b'M' => flags.month = true, + b'n' => flags.numeric = true, + b'R' => flags.random = true, + b'r' => settings.reverse = true, + b'V' => flags.version = true, + _ => break, + } + idx += 1; + } + (&input[idx..], ignore_blanks) +} + #[derive(Clone, PartialEq, Debug, Default)] struct FieldSelector { from: KeyPosition, @@ -831,91 +921,106 @@ struct FieldSelector { } impl FieldSelector { - /// Splits this position into the actual position and the attached options. - fn split_key_options(position: &str) -> (&str, &str) { - if let Some((options_start, _)) = position.char_indices().find(|(_, c)| c.is_alphabetic()) { - position.split_at(options_start) + fn parse(key: &str, global_settings: &GlobalSettings) -> UResult { + let has_options = key.as_bytes().iter().copied().any(is_ordering_option_char); + let mut settings = if has_options { + KeySettings::default() } else { - (position, "") - } - } + KeySettings::from(global_settings) + }; + let mut flags = if has_options { + ModeFlags::default() + } else { + ModeFlags::from_mode(settings.mode) + }; - fn parse(key: &str, global_settings: &GlobalSettings) -> UResult { - let mut from_to = key.split(','); - let (from, from_options) = Self::split_key_options(from_to.next().unwrap()); - let to = from_to.next().map(Self::split_key_options); - let options_are_empty = from_options.is_empty() && matches!(to, None | Some((_, ""))); - - if options_are_empty { - // Inherit the global settings if there are no options attached to this key. - (|| { - // This would be ideal for a try block, I think. In the meantime this closure allows - // to use the `?` operator here. - Self::new( - KeyPosition::new(from, 1, global_settings.ignore_leading_blanks)?, - to.map(|(to, _)| { - KeyPosition::new(to, 0, global_settings.ignore_leading_blanks) - }) - .transpose()?, - KeySettings::from(global_settings), - ) - })() + let mut from_ignore_blanks = if has_options { + false + } else { + settings.ignore_blanks + }; + let mut to_ignore_blanks = if has_options { + false } else { - // Do not inherit from `global_settings`, as there are options attached to this key. - Self::parse_with_options((from, from_options), to) + settings.ignore_blanks + }; + + let (from_field, mut rest) = parse_field_count(key, "sort-invalid-number-at-field-start")?; + if from_field == 0 { + return Err(bad_field_spec(key, "sort-field-number-is-zero")); } - .map_err(|msg| { - SortError::ParseKeyError { - key: key.to_owned(), - msg, + + let mut from_char = 1; + if let Some(stripped) = rest.strip_prefix('.') { + let (char_idx, rest_after) = + parse_field_count(stripped, "sort-invalid-number-after-dot")?; + if char_idx == 0 { + return Err(bad_field_spec(key, "sort-character-offset-is-zero")); } - .into() - }) - } + from_char = char_idx; + rest = rest_after; + } - fn parse_with_options( - (from, from_options): (&str, &str), - to: Option<(&str, &str)>, - ) -> Result { - /// Applies `options` to `key_settings`, returning if the 'b'-flag (ignore blanks) was present. - fn parse_key_settings( - options: &str, - key_settings: &mut KeySettings, - ) -> Result { - let mut ignore_blanks = false; - for option in options.chars() { - match option { - 'M' => key_settings.set_sort_mode(SortMode::Month)?, - 'b' => ignore_blanks = true, - 'd' => key_settings.set_dictionary_order()?, - 'f' => key_settings.ignore_case = true, - 'g' => key_settings.set_sort_mode(SortMode::GeneralNumeric)?, - 'h' => key_settings.set_sort_mode(SortMode::HumanNumeric)?, - 'i' => key_settings.set_ignore_non_printing()?, - 'n' => key_settings.set_sort_mode(SortMode::Numeric)?, - 'R' => key_settings.set_sort_mode(SortMode::Random)?, - 'r' => key_settings.reverse = true, - 'V' => key_settings.set_sort_mode(SortMode::Version)?, - c => { - return Err(translate!("sort-invalid-option", "option" => c)); - } - } + let (rest_after_opts, ignore_blanks) = + parse_ordering_options(rest, &mut settings, &mut flags); + if ignore_blanks { + from_ignore_blanks = true; + } + + let mut to = None; + if let Some(rest_after_comma) = rest_after_opts.strip_prefix(',') { + let (to_field, mut rest) = + parse_field_count(rest_after_comma, "sort-invalid-number-after-comma")?; + if to_field == 0 { + return Err(bad_field_spec(key, "sort-field-number-is-zero")); + } + + let mut to_char = 0; + if let Some(stripped) = rest.strip_prefix('.') { + let (char_idx, rest_after) = + parse_field_count(stripped, "sort-invalid-number-after-dot")?; + to_char = char_idx; + rest = rest_after; + } + + let (rest, ignore_blanks_end) = parse_ordering_options(rest, &mut settings, &mut flags); + if ignore_blanks_end { + to_ignore_blanks = true; } - Ok(ignore_blanks) + if !rest.is_empty() { + return Err(bad_field_spec(key, "sort-stray-character-field-spec")); + } + to = Some(KeyPosition { + field: to_field, + char: to_char, + ignore_blanks: to_ignore_blanks, + }); + } else if !rest_after_opts.is_empty() { + return Err(bad_field_spec(key, "sort-stray-character-field-spec")); } - let mut key_settings = KeySettings::default(); - let from = parse_key_settings(from_options, &mut key_settings) - .map(|ignore_blanks| KeyPosition::new(from, 1, ignore_blanks))??; - let to = if let Some((to, to_options)) = to { - Some( - parse_key_settings(to_options, &mut key_settings) - .map(|ignore_blanks| KeyPosition::new(to, 0, ignore_blanks))??, - ) - } else { - None + if ordering_incompatible( + flags, + settings.dictionary_order, + settings.ignore_non_printing, + ) { + let opts = ordering_opts_string( + flags, + settings.dictionary_order, + settings.ignore_non_printing, + settings.ignore_case, + ); + return Err(incompatible_options_error(&opts)); + } + + settings.mode = flags.to_mode(); + + let from = KeyPosition { + field: from_field, + char: from_char, + ignore_blanks: from_ignore_blanks, }; - Self::new(from, to, key_settings) + Self::new(from, to, settings).map_err(|msg| USimpleError::new(2, msg)) } fn new( @@ -1074,11 +1179,6 @@ fn make_sort_mode_arg(mode: &'static str, short: char, help: String) -> Arg { .long(mode) .help(help) .action(ArgAction::SetTrue) - .conflicts_with_all( - options::modes::ALL_SORT_MODES - .iter() - .filter(|&&m| m != mode), - ) } #[cfg(target_os = "linux")] @@ -1342,49 +1442,59 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap_or_default() }; - settings.mode = if matches.get_flag(options::modes::HUMAN_NUMERIC) - || matches - .get_one::(options::modes::SORT) - .is_some_and(|s| s == "human-numeric") - { - SortMode::HumanNumeric - } else if matches.get_flag(options::modes::MONTH) - || matches - .get_one::(options::modes::SORT) - .is_some_and(|s| s == "month") - { - SortMode::Month - } else if matches.get_flag(options::modes::GENERAL_NUMERIC) - || matches - .get_one::(options::modes::SORT) - .is_some_and(|s| s == "general-numeric") - { - SortMode::GeneralNumeric - } else if matches.get_flag(options::modes::NUMERIC) - || matches - .get_one::(options::modes::SORT) - .is_some_and(|s| s == "numeric") - { - SortMode::Numeric - } else if matches.get_flag(options::modes::VERSION) - || matches - .get_one::(options::modes::SORT) - .is_some_and(|s| s == "version") - { - SortMode::Version - } else if matches.get_flag(options::modes::RANDOM) - || matches - .get_one::(options::modes::SORT) - .is_some_and(|s| s == "random") - { + let mut mode_flags = ModeFlags::default(); + if matches.get_flag(options::modes::HUMAN_NUMERIC) { + mode_flags.human_numeric = true; + } + if matches.get_flag(options::modes::MONTH) { + mode_flags.month = true; + } + if matches.get_flag(options::modes::GENERAL_NUMERIC) { + mode_flags.general_numeric = true; + } + if matches.get_flag(options::modes::NUMERIC) { + mode_flags.numeric = true; + } + if matches.get_flag(options::modes::VERSION) { + mode_flags.version = true; + } + if matches.get_flag(options::modes::RANDOM) { + mode_flags.random = true; + } + if let Some(sort_arg) = matches.get_one::(options::modes::SORT) { + match sort_arg.as_str() { + "human-numeric" => mode_flags.human_numeric = true, + "month" => mode_flags.month = true, + "general-numeric" => mode_flags.general_numeric = true, + "numeric" => mode_flags.numeric = true, + "version" => mode_flags.version = true, + "random" => mode_flags.random = true, + _ => {} + } + } + + let dictionary_order = matches.get_flag(options::DICTIONARY_ORDER); + let ignore_non_printing = matches.get_flag(options::IGNORE_NONPRINTING); + let ignore_case = matches.get_flag(options::IGNORE_CASE); + + if ordering_incompatible(mode_flags, dictionary_order, ignore_non_printing) { + let opts = ordering_opts_string( + mode_flags, + dictionary_order, + ignore_non_printing, + ignore_case, + ); + return Err(incompatible_options_error(&opts)); + } + + settings.mode = mode_flags.to_mode(); + if mode_flags.random { settings.salt = Some(get_rand_string()); - SortMode::Random - } else { - SortMode::Default - }; + } - settings.dictionary_order = matches.get_flag(options::DICTIONARY_ORDER); - settings.ignore_non_printing = matches.get_flag(options::IGNORE_NONPRINTING); + settings.dictionary_order = dictionary_order; + settings.ignore_non_printing = ignore_non_printing; + settings.ignore_case = ignore_case; if matches.contains_id(options::PARALLEL) { // "0" is default - threads = num of cores settings.threads = matches @@ -1480,6 +1590,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.merge = matches.get_flag(options::MERGE); settings.check = matches.contains_id(options::check::CHECK); + if settings.check && matches.get_flag(options::check::CHECK_SILENT) { + return Err(incompatible_options_error("cC")); + } if matches.get_flag(options::check::CHECK_SILENT) || matches!( matches @@ -1492,7 +1605,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.check = true; } - settings.ignore_case = matches.get_flag(options::IGNORE_CASE); + if matches.contains_id(options::OUTPUT) && settings.check { + let opts = if settings.check_silent { "Co" } else { "co" }; + return Err(incompatible_options_error(opts)); + } settings.ignore_leading_blanks = matches.get_flag(options::IGNORE_LEADING_BLANKS); @@ -1612,8 +1728,7 @@ pub fn uu_app() -> Command { "numeric", "version", "random", - ])) - .conflicts_with_all(options::modes::ALL_SORT_MODES), + ])), ) .arg(make_sort_mode_arg( options::modes::HUMAN_NUMERIC, @@ -1650,12 +1765,6 @@ pub fn uu_app() -> Command { .short('d') .long(options::DICTIONARY_ORDER) .help(translate!("sort-help-dictionary-order")) - .conflicts_with_all([ - options::modes::NUMERIC, - options::modes::GENERAL_NUMERIC, - options::modes::HUMAN_NUMERIC, - options::modes::MONTH, - ]) .action(ArgAction::SetTrue), ) .arg( @@ -1676,14 +1785,12 @@ pub fn uu_app() -> Command { options::check::QUIET, options::check::DIAGNOSE_FIRST, ])) - .conflicts_with_all([options::OUTPUT, options::check::CHECK_SILENT]) .help(translate!("sort-help-check")), ) .arg( Arg::new(options::check::CHECK_SILENT) .short('C') .long(options::check::CHECK_SILENT) - .conflicts_with_all([options::OUTPUT, options::check::CHECK]) .help(translate!("sort-help-check-silent")) .action(ArgAction::SetTrue), ) @@ -1699,12 +1806,6 @@ pub fn uu_app() -> Command { .short('i') .long(options::IGNORE_NONPRINTING) .help(translate!("sort-help-ignore-nonprinting")) - .conflicts_with_all([ - options::modes::NUMERIC, - options::modes::GENERAL_NUMERIC, - options::modes::HUMAN_NUMERIC, - options::modes::MONTH, - ]) .action(ArgAction::SetTrue), ) .arg( diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 6330f759df0..0ff93996d72 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) ints (linux) NOFILE +// spell-checker:ignore (words) ints (linux) NOFILE dfgi #![allow(clippy::cast_possible_wrap)] use std::env; @@ -620,7 +620,7 @@ fn test_keys_invalid_field() { new_ucmd!() .args(&["-k", "1."]) .fails() - .stderr_only("sort: failed to parse key '1.': failed to parse character index '': cannot parse integer from empty string\n"); + .stderr_only("sort: invalid number after '.': invalid count at start of ''\n"); } #[test] @@ -628,7 +628,7 @@ fn test_keys_invalid_field_option() { new_ucmd!() .args(&["-k", "1.1x"]) .fails() - .stderr_only("sort: failed to parse key '1.1x': invalid option: 'x'\n"); + .stderr_only("sort: stray character in field spec: invalid field specification '1.1x'\n"); } #[test] @@ -636,7 +636,7 @@ fn test_keys_invalid_field_zero() { new_ucmd!() .args(&["-k", "0.1"]) .fails() - .stderr_only("sort: failed to parse key '0.1': field index can not be 0\n"); + .stderr_only("sort: field number is zero: invalid field specification '0.1'\n"); } #[test] @@ -644,7 +644,73 @@ fn test_keys_invalid_char_zero() { new_ucmd!() .args(&["-k", "1.0"]) .fails() - .stderr_only("sort: failed to parse key '1.0': invalid character index 0 for the start position of a field\n"); + .stderr_only("sort: character offset is zero: invalid field specification '1.0'\n"); +} + +#[test] +fn test_keys_invalid_number_formats() { + new_ucmd!() + .args(&["-k", "0"]) + .fails_with_code(2) + .stderr_only("sort: field number is zero: invalid field specification '0'\n"); + + new_ucmd!() + .args(&["-k", "2.,3"]) + .fails_with_code(2) + .stderr_only("sort: invalid number after '.': invalid count at start of ',3'\n"); + + new_ucmd!() + .args(&["-k", "2,"]) + .fails_with_code(2) + .stderr_only("sort: invalid number after ',': invalid count at start of ''\n"); + + new_ucmd!() + .args(&["-k", "1.1,-k0"]) + .fails_with_code(2) + .stderr_only("sort: invalid number after ',': invalid count at start of '-k0'\n"); +} + +#[test] +fn test_incompatible_options() { + new_ucmd!() + .arg("-hn") + .fails_with_code(2) + .stderr_only("sort: options '-hn' are incompatible\n"); + + new_ucmd!() + .arg("-in") + .fails_with_code(2) + .stderr_only("sort: options '-in' are incompatible\n"); + + new_ucmd!() + .arg("-nR") + .fails_with_code(2) + .stderr_only("sort: options '-nR' are incompatible\n"); + + new_ucmd!() + .arg("-dfgiMnR") + .fails_with_code(2) + .stderr_only("sort: options '-dfgMnR' are incompatible\n"); + + new_ucmd!() + .args(&["--sort=random", "-n"]) + .fails_with_code(2) + .stderr_only("sort: options '-nR' are incompatible\n"); + + new_ucmd!() + .args(&["-c", "-o", "out"]) + .fails_with_code(2) + .stderr_only("sort: options '-co' are incompatible\n"); + + new_ucmd!() + .args(&["-C", "-o", "out"]) + .fails_with_code(2) + .stderr_only("sort: options '-Co' are incompatible\n"); + + new_ucmd!() + .args(&["-c", "-C"]) + .fails_with_code(2) + .stderr_only("sort: options '-cC' are incompatible\n"); } #[test] @@ -1154,16 +1220,22 @@ fn test_sigpipe_panic() { #[test] fn test_conflict_check_out() { - let check_flags = ["-c=silent", "-c=quiet", "-c=diagnose-first", "-c", "-C"]; - for check_flag in &check_flags { + let cases = [ + ("-c=silent", "sort: options '-Co' are incompatible\n"), + ("-c=quiet", "sort: options '-Co' are incompatible\n"), + ( + "-c=diagnose-first", + "sort: options '-co' are incompatible\n", + ), + ("-c", "sort: options '-co' are incompatible\n"), + ("-C", "sort: options '-Co' are incompatible\n"), + ]; + for (check_flag, expected) in &cases { new_ucmd!() .arg(check_flag) .arg("-o=/dev/null") .fails() - .stderr_contains( - // the rest of the message might be subject to change - "error: the argument", - ); + .stderr_contains(expected); } } @@ -1204,7 +1276,7 @@ fn test_verifies_files_after_keys() { "nonexistent_dir/input_file", ]) .fails_with_code(2) - .stderr_contains("failed to parse key"); + .stderr_contains("invalid field specification '0'"); } #[test] @@ -1735,8 +1807,14 @@ fn test_clap_localization_missing_required_argument() { #[test] fn test_clap_localization_invalid_value() { let test_cases = vec![ - ("en_US.UTF-8", "sort: failed to parse key 'invalid'"), - ("fr_FR.UTF-8", "sort: échec d'analyse de la clé 'invalid'"), + ( + "en_US.UTF-8", + "sort: invalid number at field start: invalid count at start of 'invalid'", + ), + ( + "fr_FR.UTF-8", + "sort: nombre invalide au début du champ: nombre invalide au début de 'invalid'", + ), ]; for (locale, expected_message) in test_cases {