From 443c1c8ce24044de7efb23db6dd25c0cc7b9763a Mon Sep 17 00:00:00 2001 From: Yunus Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:00:46 +0300 Subject: [PATCH 1/3] split: don't take an option's value in combined shorts as obsolete lines In '-dn10' the '10' is the value of -n, but the obsolete '-N' lines option was extracted from it, so split failed with 'cannot split in more than one way'. Stop looking for obsolete lines digits after an option that takes a value (-a, -b, -C, -l, -n, -t). --- src/uu/split/src/split.rs | 9 +++++++++ tests/by-util/test_split.rs | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index d683bb34328..1973bd22363 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -161,9 +161,18 @@ fn should_extract_obs_lines( fn handle_extract_obs_lines(slice: &str, obs_lines: &mut Option) -> Option { let mut obs_lines_extracted: Vec = vec![]; let mut obs_lines_end_reached = false; + let mut option_value_reached = false; let filtered_slice: Vec = slice .chars() .filter(|c| { + // The rest of the argument after an option that takes a value, + // as in '-dn10', is that value and not an obsolete lines value + if option_value_reached { + return true; + } + if matches!(c, 'a' | 'b' | 'C' | 'l' | 'n' | 't') { + option_value_reached = true; + } // To correctly process scenario like '-x200a4' // we need to stop extracting digits once alphabetic character is encountered // after we already have something in obs_lines_extracted diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index fb39a91b045..472adcb8515 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -525,6 +525,28 @@ fn test_split_obs_lines_within_combined_shorts() { assert_eq!(glob.collate(), at.read_bytes(name)); } +/// Digits that are the value of an option in the same combined short options, +/// as in `-dn10`, are not an obsolete lines option +#[test] +fn test_split_option_value_within_combined_shorts() { + let (at, mut ucmd) = at_and_ucmd!(); + let name = "option-value-within-shorts"; + RandomFile::new(&at, name).add_lines(100); + + ucmd.args(&["-dn10", name]).succeeds().no_output(); + let glob = Glob::new(&at, ".", r"x\d\d$"); + assert_eq!(glob.count(), 10); + assert_eq!(glob.collate(), at.read_bytes(name)); + + let (at, mut ucmd) = at_and_ucmd!(); + RandomFile::new(&at, name).add_lines(100); + + ucmd.args(&["-xl25", name]).succeeds().no_output(); + let glob = Glob::new(&at, ".", r"x[[:xdigit:]]{2}$"); + assert_eq!(glob.count(), 4); + assert_eq!(glob.collate(), at.read_bytes(name)); +} + /// Test for obsolete lines option as part of combined short options with tailing suffix length with value #[test] fn test_split_obs_lines_within_combined_shorts_tailing_suffix_length() { From e120922718df337648e70c66a1c3c3d5de076ea8 Mon Sep 17 00:00:00 2001 From: Yunus Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:42:56 +0300 Subject: [PATCH 2/3] split: share the list of short options that take a value --- src/uu/split/src/split.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 1973bd22363..7c2136ea8bf 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -140,6 +140,9 @@ fn filter_args( filter } +/// Short options that take a value, as in '-b 10' or '-b10' +const SHORT_OPTS_WITH_VALUE: &[u8] = b"abClnt"; // spell-checker:disable-line + /// Helper function to [`filter_args`] /// Checks if the slice is a true short option (and not hyphen prefixed value of an option) /// and if so, a short option that can contain obsolete lines value @@ -151,7 +154,9 @@ fn should_extract_obs_lines( !preceding_long_opt_req_value && !preceding_short_opt_req_value && slice.strip_prefix("-").is_some_and(|s| { - !s.as_bytes().first().is_some_and(|s| b"-abClnt".contains(s)) // spell-checker:disable-line + !s.as_bytes() + .first() + .is_some_and(|s| *s == b'-' || SHORT_OPTS_WITH_VALUE.contains(s)) }) } @@ -170,7 +175,7 @@ fn handle_extract_obs_lines(slice: &str, obs_lines: &mut Option) -> Opti if option_value_reached { return true; } - if matches!(c, 'a' | 'b' | 'C' | 'l' | 'n' | 't') { + if u8::try_from(*c).is_ok_and(|c| SHORT_OPTS_WITH_VALUE.contains(&c)) { option_value_reached = true; } // To correctly process scenario like '-x200a4' @@ -237,7 +242,8 @@ fn handle_preceding_options( // capture if current slice is a preceding short option that requires value and does not have value in the same slice (value separated by whitespace) // following slice should be treaded as value for this option // even if it starts with '-' (which would be treated as hyphen prefixed value) - *preceding_short_opt_req_value = matches!(slice, "-b" | "-C" | "-l" | "-n" | "-a" | "-t"); + *preceding_short_opt_req_value = + matches!(slice.as_bytes(), [b'-', c] if SHORT_OPTS_WITH_VALUE.contains(c)); } /// Parameters that control how a file gets split. From 866c17ccdc1ce077cc64f6d46d80274327bafbbf Mon Sep 17 00:00:00 2001 From: Yunus Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:26:46 +0300 Subject: [PATCH 3/3] split: treat a value option ending a short option group like a lone one --- src/uu/split/src/split.rs | 16 +++++++++++++--- tests/by-util/test_split.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 7c2136ea8bf..d150299143b 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -239,11 +239,21 @@ fn handle_preceding_options( | options::SEPARATOR ); } - // capture if current slice is a preceding short option that requires value and does not have value in the same slice (value separated by whitespace) + // capture if current slice is a short option, alone or ending a group like '-dl', that requires value and does not have value in the same slice (value separated by whitespace) // following slice should be treaded as value for this option // even if it starts with '-' (which would be treated as hyphen prefixed value) - *preceding_short_opt_req_value = - matches!(slice.as_bytes(), [b'-', c] if SHORT_OPTS_WITH_VALUE.contains(c)); + // the first short option in the group that requires value takes the rest of the group as its value, + // so only a group where that option is the last character leaves the following slice to it + *preceding_short_opt_req_value = slice + .strip_prefix('-') + .filter(|shorts| !shorts.starts_with('-')) + .and_then(|shorts| { + shorts + .bytes() + .position(|c| SHORT_OPTS_WITH_VALUE.contains(&c)) + .map(|i| i == shorts.len() - 1) + }) + .unwrap_or(false); } /// Parameters that control how a file gets split. diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 472adcb8515..148fc435e78 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -659,6 +659,32 @@ fn test_split_obs_lines_as_other_option_value() { .stderr_contains("split: invalid number of chunks: '-e200'\n"); } +/// A short option that requires value at the end of combined short options, as in `-dl`, +/// takes the following hyphen prefixed argument as its value +#[test] +fn test_split_obs_lines_as_value_of_option_ending_combined_shorts() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + + scene + .ucmd() + .args(&["-dl", "-200", "file"]) + .fails_with_code(1) + .stderr_only("split: invalid number of lines: '-200'\n"); + scene + .ucmd() + .args(&["-xa", "-200", "file"]) + .fails_with_code(1) + .stderr_only("split: invalid suffix length: '-200'\n"); + // `-a` takes `2l` as its value here, so `-5` is not the value of `-l` + scene + .ucmd() + .args(&["-a2l", "-5", "file"]) + .fails_with_code(1) + .stderr_only("split: invalid suffix length: '2l'\n"); +} + #[test] fn test_split_chunks_by_line_or_round_robin_zero_chunks() { let scene = TestScenario::new(util_name!());