diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index d683bb34328..d150299143b 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)) }) } @@ -161,9 +166,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 u8::try_from(*c).is_ok_and(|c| SHORT_OPTS_WITH_VALUE.contains(&c)) { + 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 @@ -225,10 +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, "-b" | "-C" | "-l" | "-n" | "-a" | "-t"); + // 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 fb39a91b045..148fc435e78 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() { @@ -637,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!());