Str

#186 Jun 2026

186. str::split_inclusive — Split a String and Keep the Separator With Each Chunk

"a\nb\n".split('\n') swallows every newline and hands you a phantom "" at the end. split_inclusive keeps each separator glued to the chunk it belongs to — no ghost element, and you can .concat() straight back to the original.

The split that loses information

The default split is destructive: the matched character is gone, and a trailing separator becomes an empty string.

1
2
3
4
5
let log = "INFO\nWARN\nERROR\n";

let parts: Vec<&str> = log.split('\n').collect();
assert_eq!(parts, ["INFO", "WARN", "ERROR", ""]);
//                                            ^^ phantom empty tail

That phantom empty string is the source of a hundred Stack Overflow questions. You usually paper over it with .filter(|s| !s.is_empty()) or trim_end() before splitting.

split_inclusive keeps the terminator

Each piece keeps the separator that ended it. The trailing newline isn’t an empty string — it’s the end of the last real chunk.

1
2
3
4
5
let log = "INFO\nWARN\nERROR\n";

let parts: Vec<&str> = log.split_inclusive('\n').collect();
assert_eq!(parts, ["INFO\n", "WARN\n", "ERROR\n"]);
assert_eq!(parts.concat(), log); // round-trips exactly

When you actually want this

Reformatting line by line without losing the trailing newline:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let src = "fn main() {\n    println!(\"hi\");\n}\n";

let numbered: String = src
    .split_inclusive('\n')
    .enumerate()
    .map(|(i, line)| format!("{:>2} | {line}", i + 1))
    .collect();

assert_eq!(
    numbered,
    " 1 | fn main() {\n 2 |     println!(\"hi\");\n 3 | }\n"
);

No newline added, none lost — every \n is already where it should be.

Works on slices too

[T]::split_inclusive exists with the same shape, taking a predicate instead of a pattern. Useful for batching consecutive items up to a delimiter element.

1
2
3
let bytes = [1, 2, 0, 3, 0, 4];
let chunks: Vec<&[u8]> = bytes.split_inclusive(|&b| b == 0).collect();
assert_eq!(chunks, [&[1, 2, 0][..], &[3, 0][..], &[4][..]]);

Reach for split_inclusive whenever the separator is part of the data — line endings, statement terminators, record markers — not noise to throw away.