From 9364d89d5d42d937eba1e48fbeb635017057ff04 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Fri, 30 Oct 2026 03:36:26 +0800 Subject: [PATCH 1/3] split: accept a leading '+' in every number --- src/uu/split/src/strategy.rs | 22 +++++++++++++--------- tests/by-util/test_split.rs | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/uu/split/src/strategy.rs b/src/uu/split/src/strategy.rs index 99c61486994..0583f0b1df6 100644 --- a/src/uu/split/src/strategy.rs +++ b/src/uu/split/src/strategy.rs @@ -117,13 +117,17 @@ impl NumberType { /// or if `K` is greater than `N` /// then this function returns [`NumberTypeError`]. fn from(s: &str) -> Result { + /// GNU accepts a leading '+' on every number, so strip it first. + fn parse_number(value: &str) -> Result { + parse_size_u64(value.strip_prefix('+').unwrap_or(value)) + } fn is_invalid_chunk(chunk_number: u64, num_chunks: u64) -> bool { chunk_number > num_chunks || chunk_number == 0 } let mut parts = s.splitn(4, '/'); match (parts.next(), parts.next(), parts.next(), parts.next()) { (Some(n_str), None, None, None) => { - let num_chunks = parse_size_u64(n_str) + let num_chunks = parse_number(n_str) .map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?; if num_chunks > 0 { Ok(Self::Bytes(num_chunks)) @@ -134,9 +138,9 @@ impl NumberType { (Some(k_str), Some(n_str), None, None) if !k_str.starts_with('l') && !k_str.starts_with('r') => { - let num_chunks = parse_size_u64(n_str) + let num_chunks = parse_number(n_str) .map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?; - let chunk_number = parse_size_u64(k_str) + let chunk_number = parse_number(k_str) .map_err(|_| NumberTypeError::ChunkNumber(k_str.to_string()))?; if is_invalid_chunk(chunk_number, num_chunks) { return Err(NumberTypeError::ChunkNumber(k_str.to_string())); @@ -144,7 +148,7 @@ impl NumberType { Ok(Self::KthBytes(chunk_number, num_chunks)) } (Some("l"), Some(n_str), None, None) => { - let num_chunks = parse_size_u64(n_str) + let num_chunks = parse_number(n_str) .map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?; if num_chunks == 0 { return Err(NumberTypeError::NumberOfChunks(n_str.to_string())); @@ -152,9 +156,9 @@ impl NumberType { Ok(Self::Lines(num_chunks)) } (Some("l"), Some(k_str), Some(n_str), None) => { - let num_chunks = parse_size_u64(n_str) + let num_chunks = parse_number(n_str) .map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?; - let chunk_number = parse_size_u64(k_str) + let chunk_number = parse_number(k_str) .map_err(|_| NumberTypeError::ChunkNumber(k_str.to_string()))?; if is_invalid_chunk(chunk_number, num_chunks) { return Err(NumberTypeError::ChunkNumber(k_str.to_string())); @@ -162,7 +166,7 @@ impl NumberType { Ok(Self::KthLines(chunk_number, num_chunks)) } (Some("r"), Some(n_str), None, None) => { - let num_chunks = parse_size_u64(n_str) + let num_chunks = parse_number(n_str) .map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?; if num_chunks == 0 { return Err(NumberTypeError::NumberOfChunks(n_str.to_string())); @@ -170,9 +174,9 @@ impl NumberType { Ok(Self::RoundRobin(num_chunks)) } (Some("r"), Some(k_str), Some(n_str), None) => { - let num_chunks = parse_size_u64(n_str) + let num_chunks = parse_number(n_str) .map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?; - let chunk_number = parse_size_u64(k_str) + let chunk_number = parse_number(k_str) .map_err(|_| NumberTypeError::ChunkNumber(k_str.to_string()))?; if is_invalid_chunk(chunk_number, num_chunks) { return Err(NumberTypeError::ChunkNumber(k_str.to_string())); diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 9840c786e84..03e1f5a757f 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2369,3 +2369,19 @@ fn test_obsolete_lines_not_read_after_double_dash() { .fails() .stderr_contains("cannot open '-1' for reading"); } + +#[test] +fn test_number_with_plus_prefix() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a.txt", "a\nb\n"); + ucmd.args(&["-n", "+2", "a.txt"]).succeeds(); + assert_eq!(at.read("xaa"), "a\n"); + assert_eq!(at.read("xab"), "b\n"); + + // Every spelling that carries a number accepts the prefix. + for number in ["l/+2", "+1/2", "l/+1/2", "r/+2", "r/+1/2"] { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a.txt", "a\nb\n"); + ucmd.args(&["-n", number, "a.txt"]).succeeds(); + } +} From c31f6d45cc406a91dbe7e1081cc0a80dc5e7e63b Mon Sep 17 00:00:00 2001 From: Logic769 Date: Mon, 2 Nov 2026 14:14:15 +0800 Subject: [PATCH 2/3] test(split): check the prefixed spellings split like the plain ones --- tests/by-util/test_split.rs | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 03e1f5a757f..38751f92746 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2378,10 +2378,44 @@ fn test_number_with_plus_prefix() { assert_eq!(at.read("xaa"), "a\n"); assert_eq!(at.read("xab"), "b\n"); - // Every spelling that carries a number accepts the prefix. - for number in ["l/+2", "+1/2", "l/+1/2", "r/+2", "r/+1/2"] { - let (at, mut ucmd) = at_and_ucmd!(); - at.write("a.txt", "a\nb\n"); - ucmd.args(&["-n", number, "a.txt"]).succeeds(); + // Every spelling that carries a number accepts the prefix, and the + // prefixed spelling splits exactly like the plain one. + for (prefixed, plain) in [ + ("l/+2", "l/2"), + ("r/+2", "r/2"), + ("+1/2", "1/2"), + ("l/+1/2", "l/1/2"), + ("r/+1/2", "r/1/2"), + ] { + let prefixed_chunks = split_into_chunks(prefixed); + let plain_chunks = split_into_chunks(plain); + assert_eq!( + prefixed_chunks, plain_chunks, + "-n {prefixed} does not split like -n {plain}" + ); } } + +/// Run `split -n a.txt` in a fresh fixture and return what it produced: +/// the concatenated `x*` files when chunks were written, or the stdout when the +/// spelling selects a single chunk. +fn split_into_chunks(number: &str) -> (Vec<(String, String)>, String) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a.txt", "a\nb\nc\nd\n"); + let stdout = ucmd + .args(&["-n", number, "a.txt"]) + .succeeds() + .stdout_move_str(); + + let mut files: Vec<(String, String)> = fs::read_dir(at.as_string()) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .filter(|name| name.to_string_lossy().starts_with('x')) + .map(|name| { + let contents = at.read(at.plus(&name)); + (name.to_string_lossy().into_owned(), contents) + }) + .collect(); + files.sort(); + (files, stdout) +} From aa4d64b03a14b260cc0af67bfbcfe70604e2004e Mon Sep 17 00:00:00 2001 From: Logic769 Date: Tue, 3 Nov 2026 08:15:05 +0800 Subject: [PATCH 3/3] test(split): turn the number prefix test into rstest cases --- tests/by-util/test_split.rs | 52 +++++++++++++++---------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 38751f92746..a55b3ab0945 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -9,6 +9,7 @@ use rand::{RngExt as _, SeedableRng, rng}; use regex::Regex; #[cfg(target_os = "linux")] use rlimit::Resource; +use rstest::rstest; #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; use std::path::Path; @@ -2370,42 +2371,26 @@ fn test_obsolete_lines_not_read_after_double_dash() { .stderr_contains("cannot open '-1' for reading"); } -#[test] -fn test_number_with_plus_prefix() { - let (at, mut ucmd) = at_and_ucmd!(); - at.write("a.txt", "a\nb\n"); - ucmd.args(&["-n", "+2", "a.txt"]).succeeds(); - assert_eq!(at.read("xaa"), "a\n"); - assert_eq!(at.read("xab"), "b\n"); - - // Every spelling that carries a number accepts the prefix, and the - // prefixed spelling splits exactly like the plain one. - for (prefixed, plain) in [ - ("l/+2", "l/2"), - ("r/+2", "r/2"), - ("+1/2", "1/2"), - ("l/+1/2", "l/1/2"), - ("r/+1/2", "r/1/2"), - ] { - let prefixed_chunks = split_into_chunks(prefixed); - let plain_chunks = split_into_chunks(plain); - assert_eq!( - prefixed_chunks, plain_chunks, - "-n {prefixed} does not split like -n {plain}" - ); - } -} - -/// Run `split -n a.txt` in a fresh fixture and return what it produced: -/// the concatenated `x*` files when chunks were written, or the stdout when the -/// spelling selects a single chunk. -fn split_into_chunks(number: &str) -> (Vec<(String, String)>, String) { +#[rstest] +#[case::bytes("+2", &[("xaa", "a\nb\n"), ("xab", "c\nd\n")], "")] +#[case::lines("l/+2", &[("xaa", "a\nb\n"), ("xab", "c\nd\n")], "")] +#[case::round_robin("r/+2", &[("xaa", "a\nc\n"), ("xab", "b\nd\n")], "")] +#[case::bytes_first_half("+1/2", &[], "a\nb\n")] +#[case::lines_first_half("l/+1/2", &[], "a\nb\n")] +#[case::round_robin_first_half("r/+1/2", &[], "a\nc\n")] +fn test_number_with_plus_prefix( + #[case] number: &str, + #[case] expected_files: &[(&str, &str)], + #[case] expected_stdout: &str, +) { let (at, mut ucmd) = at_and_ucmd!(); at.write("a.txt", "a\nb\nc\nd\n"); + let stdout = ucmd .args(&["-n", number, "a.txt"]) .succeeds() .stdout_move_str(); + assert_eq!(stdout, expected_stdout, "-n {number}"); let mut files: Vec<(String, String)> = fs::read_dir(at.as_string()) .unwrap() @@ -2417,5 +2402,10 @@ fn split_into_chunks(number: &str) -> (Vec<(String, String)>, String) { }) .collect(); files.sort(); - (files, stdout) + + let expected_files: Vec<(String, String)> = expected_files + .iter() + .map(|(name, contents)| (name.to_string(), contents.to_string())) + .collect(); + assert_eq!(files, expected_files, "-n {number}"); }