Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
csplit: create final empty file with --suppress-matched to match GNU (f…
…ixes #7286)

Problem
- With , uutils csplit failed to create the final empty output file when the last split point consumed the trailing input.
- GNU csplit always creates a final segment after processing patterns; with , that empty final segment is elided.
- This broke the GNU test (tests/csplit/csplit-suppress-matched.pl) and the scenario:

Root cause
- Final-file creation was conditional on there being remaining input after pattern processing. If none remained, no final file was created, contrary to GNU semantics.

Fix (minimal, targeted)
- In , after :
  - If there is remaining input, always create a final split and copy the remainder, then finish.
  - Else, if all patterns were integer-based and  is set, create a final (possibly empty) split and finish;  elides it when  is set.

Tests (Rust integration)
- Added two Rust tests under  to lock down GNU-compatible behavior:
  -  (expects sizes 2,2,2,0 and a final empty )
  -  (final empty file is correctly elided)

Verification
- All  tests pass locally. The originally reported case now matches GNU.

Relation to PR #7806
- #7806 proposes a broader refactor to fix multiple issues, but remains a draft and notes remaining GNU suppress-matched differences.
- This PR provides a small, reviewable fix specifically for #7286, plus precise integration tests to safeguard behavior.

Fixes #7286
  • Loading branch information
naoNao89 committed Sep 17, 2025
commit 1afeb7a6e3f06195e04a7aea6c798ceb496711d2
13 changes: 11 additions & 2 deletions src/uu/csplit/src/csplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,28 @@ where
.enumerate();
let mut input_iter = InputSplitter::new(enumerated_input_lines);
let mut split_writer = SplitWriter::new(options);
let patterns: Vec<patterns::Pattern> = patterns::get_patterns(patterns)?;
let ret = do_csplit(&mut split_writer, patterns, &mut input_iter);
let patterns_vec: Vec<patterns::Pattern> = patterns::get_patterns(patterns)?;
let all_up_to_line = patterns_vec
.iter()
.all(|p| matches!(p, patterns::Pattern::UpToLine(_, _)));
let ret = do_csplit(&mut split_writer, patterns_vec, &mut input_iter);

// consume the rest, unless there was an error
if ret.is_ok() {
input_iter.rewind_buffer();
if let Some((_, line)) = input_iter.next() {
// There is remaining input: create a final split and copy remainder
split_writer.new_writer()?;
split_writer.writeln(&line?)?;
for (_, line) in input_iter {
split_writer.writeln(&line?)?;
}
split_writer.finish_split();
} else if all_up_to_line && options.suppress_matched {
// GNU semantics for integer patterns with --suppress-matched:
// even if no remaining input, create a final (possibly empty) split
split_writer.new_writer()?;
split_writer.finish_split();
}
}
// delete files on error by default
Expand Down
38 changes: 38 additions & 0 deletions tests/by-util/test_csplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,44 @@ fn generate(from: u32, to: u32) -> String {
(from..to).fold(String::new(), |acc, v| format!("{acc}{v}\n"))
}

#[test]
fn test_line_numbers_suppress_matched_final_empty() {
// Repro for #7286
let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["--suppress-matched", "-", "2", "4", "6"]) // stdin, split at 2/4/6
.pipe_in("1\n2\n3\n4\n5\n6\n")
.succeeds()
.stdout_only("2\n2\n2\n0\n");

// Expect four files: xx00:"1\n", xx01:"3\n", xx02:"5\n", xx03:""
let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created")
.count();
assert_eq!(count, 4);
assert_eq!(at.read("xx00"), "1\n");
assert_eq!(at.read("xx01"), "3\n");
assert_eq!(at.read("xx02"), "5\n");
assert_eq!(at.read("xx03"), "");
}

#[test]
fn test_line_numbers_suppress_matched_final_empty_elided_with_z() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["--suppress-matched", "-z", "-", "2", "4", "6"]) // elide empty
.pipe_in("1\n2\n3\n4\n5\n6\n")
.succeeds()
.stdout_only("2\n2\n2\n");

// Expect three files: xx00:"1\n", xx01:"3\n", xx02:"5\n"
let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created")
.count();
assert_eq!(count, 3);
assert_eq!(at.read("xx00"), "1\n");
assert_eq!(at.read("xx01"), "3\n");
assert_eq!(at.read("xx02"), "5\n");
}

#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
Expand Down
Loading