Skip to content

Commit 1afeb7a

Browse files
committed
csplit: create final empty file with --suppress-matched to match GNU (fixes #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
1 parent aaf742d commit 1afeb7a

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

src/uu/csplit/src/csplit.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,28 @@ where
120120
.enumerate();
121121
let mut input_iter = InputSplitter::new(enumerated_input_lines);
122122
let mut split_writer = SplitWriter::new(options);
123-
let patterns: Vec<patterns::Pattern> = patterns::get_patterns(patterns)?;
124-
let ret = do_csplit(&mut split_writer, patterns, &mut input_iter);
123+
let patterns_vec: Vec<patterns::Pattern> = patterns::get_patterns(patterns)?;
124+
let all_up_to_line = patterns_vec
125+
.iter()
126+
.all(|p| matches!(p, patterns::Pattern::UpToLine(_, _)));
127+
let ret = do_csplit(&mut split_writer, patterns_vec, &mut input_iter);
125128

126129
// consume the rest, unless there was an error
127130
if ret.is_ok() {
128131
input_iter.rewind_buffer();
129132
if let Some((_, line)) = input_iter.next() {
133+
// There is remaining input: create a final split and copy remainder
130134
split_writer.new_writer()?;
131135
split_writer.writeln(&line?)?;
132136
for (_, line) in input_iter {
133137
split_writer.writeln(&line?)?;
134138
}
135139
split_writer.finish_split();
140+
} else if all_up_to_line && options.suppress_matched {
141+
// GNU semantics for integer patterns with --suppress-matched:
142+
// even if no remaining input, create a final (possibly empty) split
143+
split_writer.new_writer()?;
144+
split_writer.finish_split();
136145
}
137146
}
138147
// delete files on error by default

tests/by-util/test_csplit.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,44 @@ fn generate(from: u32, to: u32) -> String {
1212
(from..to).fold(String::new(), |acc, v| format!("{acc}{v}\n"))
1313
}
1414

15+
#[test]
16+
fn test_line_numbers_suppress_matched_final_empty() {
17+
// Repro for #7286
18+
let (at, mut ucmd) = at_and_ucmd!();
19+
ucmd.args(&["--suppress-matched", "-", "2", "4", "6"]) // stdin, split at 2/4/6
20+
.pipe_in("1\n2\n3\n4\n5\n6\n")
21+
.succeeds()
22+
.stdout_only("2\n2\n2\n0\n");
23+
24+
// Expect four files: xx00:"1\n", xx01:"3\n", xx02:"5\n", xx03:""
25+
let count = glob(&at.plus_as_string("xx*"))
26+
.expect("there should be splits created")
27+
.count();
28+
assert_eq!(count, 4);
29+
assert_eq!(at.read("xx00"), "1\n");
30+
assert_eq!(at.read("xx01"), "3\n");
31+
assert_eq!(at.read("xx02"), "5\n");
32+
assert_eq!(at.read("xx03"), "");
33+
}
34+
35+
#[test]
36+
fn test_line_numbers_suppress_matched_final_empty_elided_with_z() {
37+
let (at, mut ucmd) = at_and_ucmd!();
38+
ucmd.args(&["--suppress-matched", "-z", "-", "2", "4", "6"]) // elide empty
39+
.pipe_in("1\n2\n3\n4\n5\n6\n")
40+
.succeeds()
41+
.stdout_only("2\n2\n2\n");
42+
43+
// Expect three files: xx00:"1\n", xx01:"3\n", xx02:"5\n"
44+
let count = glob(&at.plus_as_string("xx*"))
45+
.expect("there should be splits created")
46+
.count();
47+
assert_eq!(count, 3);
48+
assert_eq!(at.read("xx00"), "1\n");
49+
assert_eq!(at.read("xx01"), "3\n");
50+
assert_eq!(at.read("xx02"), "5\n");
51+
}
52+
1553
#[test]
1654
fn test_invalid_arg() {
1755
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);

0 commit comments

Comments
 (0)