fix(shell): preserve CR when :! outputs to binary-mode buffer #39558

Problem:
When `:!` writes shell output to a buffer, write_output() splits on `\r`, `\n`,
and `\r\n`, replacing the terminator byte with NUL. For a binary-mode buffer
this is wrong: `\r` should be preserved verbatim, not treated as a line
terminator. This wrong behavior causes a file like `\r\n` round-trips through
`:%!cat` to `\n`.

This was masked when 'shelltemp' was enabled, because output went through a temp
file and the regular file I/O path handled binary-mode correctly. Switching the
default to 'noshelltemp' exposed the bug, since output is now piped directly
into write_output().

Solution:
In `write_output()`, skip the `\r` and `\r\n` splits for a binary-mode buffer;
only split on `\n`.

(cherry picked from commit 832a68835b)
This commit is contained in:
Alexej Kowalew
2026-05-08 09:54:30 +02:00
committed by github-actions[bot]
parent d8a94658f5
commit 2389cf2e39
2 changed files with 22 additions and 2 deletions

View File

@@ -1254,7 +1254,8 @@ static size_t write_output(char *output, size_t remaining, bool eof)
size_t off = 0;
while (off < remaining) {
// CRLF
if (output[off] == CAR && output[off + 1] == NL) {
// special case: for binary mode, don't remove CR.
if (output[off] == CAR && output[off + 1] == NL && !curbuf->b_p_bin) {
output[off] = NUL;
ml_append(curwin->w_cursor.lnum++, output, (int)off + 1, false);
size_t skip = off + 2;
@@ -1262,7 +1263,7 @@ static size_t write_output(char *output, size_t remaining, bool eof)
remaining -= skip;
off = 0;
continue;
} else if (output[off] == CAR || output[off] == NL) {
} else if ((output[off] == CAR && !curbuf->b_p_bin) || output[off] == NL) {
// Insert the line
output[off] = NUL;
ml_append(curwin->w_cursor.lnum++, output, (int)off + 1, false);

View File

@@ -567,6 +567,25 @@ end)
describe('shell :!', function()
before_each(clear)
it('preserves newlines (including CR) in binary-mode buffer #39424', function()
local fname = 'Xbinaryfile'
finally(function()
os.remove(fname)
end)
t.write_file(fname, '\r\n', true)
command('edit ++bin ' .. fname)
command('%!cat')
command('w')
eq('\r\n', t.read_file(fname))
t.write_file(fname, '\r', true)
command('edit ++bin ' .. fname)
command('%!cat')
command('w')
eq('\r', t.read_file(fname))
end)
it(':{range}! works when the first char is NUL #34163', function()
api.nvim_buf_set_lines(0, 0, -1, true, { '\0hello', 'hello' })
command('%!cat')