-*channel.txt* For Vim version 9.2. Last change: 2026 Apr 15
+*channel.txt* For Vim version 9.2. Last change: 2026 Apr 28
VIM REFERENCE MANUAL by Bram Moolenaar
input and one output handle, with no separate handle for
stderr.
+ Note: term_start() always uses RAW mode for its callbacks.
+ "out_cb" and "err_cb" receive the raw chunk of data as read
+ from the OS. A single callback invocation may contain
+ multiple lines separated by NL, and (for stdout via a pty)
+ each line may have a trailing CR from the line discipline
+ (ONLCR). If per-line handling is desired, the callback must
+ split "msg" on NL and strip the trailing CR itself.
+ Example: >
+ func Handle(ch, msg)
+ for line in split(a:msg, "\n")
+ echom substitute(line, '\r$', '', '')
+ endfor
+ endfunc
+<
There are extra options:
"term_name" name to use for the buffer name, instead
of the command name.
// invoke the channel callback
ch_log(channel, "Invoking channel callback %s",
(char *)callback->cb_name);
-#ifdef FEAT_TERMINAL
- // For a terminal job in RAW mode (term_start()), split msg on
- // NL and invoke the callback once per line with trailing CR
- // stripped. This ensures out_cb/err_cb receive one line at a
- // time regardless of how much data arrives in a single read.
- if (ch_mode == CH_MODE_RAW && msg != NULL
- && channel->ch_job != NULL
- && channel->ch_job->jv_tty_out != NULL)
- {
- char_u *cp = msg;
- char_u *nl;
-
- while ((nl = vim_strchr(cp, NL)) != NULL)
- {
- long_u len = (long_u)(nl - cp);
-
- if (len > 0 && cp[len - 1] == CAR)
- --len;
- argv[1].vval.v_string = vim_strnsave(cp, len);
- if (argv[1].vval.v_string != NULL)
- invoke_callback(channel, callback, argv);
- vim_free(argv[1].vval.v_string);
- cp = nl + 1;
- }
- if (*cp != NUL)
- {
- long_u len = STRLEN(cp);
-
- if (len > 0 && cp[len - 1] == CAR)
- --len;
- argv[1].vval.v_string = vim_strnsave(cp, len);
- if (argv[1].vval.v_string != NULL)
- invoke_callback(channel, callback, argv);
- vim_free(argv[1].vval.v_string);
- }
- argv[1].vval.v_string = msg;
- }
- else
-#endif
- invoke_callback(channel, callback, argv);
+ invoke_callback(channel, callback, argv);
}
}
}
unlet! g:out g:error
endfunc
-" Verify that term_start() with out_cb/err_cb delivers one line per callback
-" call (no embedded newlines, no trailing CR), matching the user's expectation.
-func Test_term_start_cb_per_line()
+" Verify that term_start() with out_cb/err_cb delivers data in RAW mode,
+" preserving embedded newlines in the raw chunk received from read(). If
+" per-line handling is desired, it is the callback's responsibility to split
+" on NL and strip the trailing CR.
+func Test_term_start_cb_raw_chunk()
CheckUnix
CheckFeature terminal
let g:Ch_msgs = []
- let script_file = 'Xterm_cb_per_line.sh'
+ let script_file = 'Xterm_cb_raw_chunk.sh'
call writefile(["#!/bin/sh",
\ "printf 'err:1\\nerr:2\\n' >&2",
\ "printf 'out:3\\n'"], script_file, 'D')
let ptybuf = term_start('./' .. script_file, {
\ 'out_cb': {ch, msg -> add(g:Ch_msgs, msg)},
\ 'err_cb': {ch, msg -> add(g:Ch_msgs, msg)}})
- call WaitForAssert({-> assert_equal(3, len(g:Ch_msgs))}, 5000)
- " Each line must arrive as a separate callback call with no embedded CR/NL.
- call assert_equal(['err:1', 'err:2', 'out:3'], g:Ch_msgs)
+ " Wait until both the raw stderr chunk and a stdout chunk have arrived.
+ call WaitForAssert({-> assert_true(
+ \ index(g:Ch_msgs, "err:1\nerr:2\n") >= 0
+ \ && match(g:Ch_msgs, 'out:3') >= 0)}, 5000)
+ " stderr (via pipe) arrives as a single raw chunk with embedded NL,
+ " not split per line. stdout (via PTY) is delivered, but its exact
+ " CR/LF shape depends on the PTY line discipline, so we only check that
+ " 'out:3' appears somewhere in the received chunks.
call job_stop(term_getjob(ptybuf))
+ exe 'bwipe! ' .. ptybuf
unlet g:Ch_msgs
endfunc
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 412,
/**/
411,
/**/