Merge pull request #2424 from splinterofchaos/term-no-sh

use an argument vector for termopen() / unify jobstart, termopen, and system
This commit is contained in:
Scott Prager 2015-05-02 10:30:30 -04:00
commit 1c2c90ab07
17 changed files with 284 additions and 109 deletions

View File

@ -296,7 +296,7 @@ get_compile_flags(NVIM_VERSION_CFLAGS)
add_subdirectory(test/includes)
add_subdirectory(config)
add_subdirectory(test/functional/job) # compile pty test program
add_subdirectory(test/functional/fixtures) # compile pty/shell test programs
# Setup some test-related bits. We do this after going down the tree because we

View File

@ -2013,8 +2013,8 @@ synIDattr( {synID}, {what} [, {mode}])
synIDtrans( {synID}) Number translated syntax ID of {synID}
synconcealed( {lnum}, {col}) List info about concealing
synstack( {lnum}, {col}) List stack of syntax IDs at {lnum} and {col}
system( {expr} [, {input}]) String output of shell command/filter {expr}
systemlist( {expr} [, {input}]) List output of shell command/filter {expr}
system( {cmd} [, {input}]) String output of shell command/filter {cmd}
systemlist( {cmd} [, {input}]) List output of shell command/filter {cmd}
tabpagebuflist( [{arg}]) List list of buffer numbers in tab page
tabpagenr( [{arg}]) Number number of current or last tab page
tabpagewinnr( {tabarg}[, {arg}])
@ -4031,9 +4031,12 @@ jobsend({job}, {data}) {Nvim} *jobsend()*
:call jobsend(j, ["abc", "123\n456", ""])
< will send "abc<NL>123<NUL>456<NL>".
jobstart({argv}[, {opts}]) {Nvim} *jobstart()*
Spawns {argv}(list) as a job. If passed, {opts} must be a
dictionary with any of the following keys:
jobstart({cmd}[, {opts}]) {Nvim} *jobstart()*
Spawns {cmd} as a job. If {cmd} is a |List|, it will be run
directly. If {cmd} is a |String|, it will be equivalent to >
:call jobstart([&shell, &shellcmdflag, '{cmd}'])
< If passed, {opts} must be a dictionary with any of the
following keys:
- on_stdout: stdout event handler
- on_stderr: stderr event handler
- on_exit: exit event handler
@ -4052,7 +4055,8 @@ jobstart({argv}[, {opts}]) {Nvim} *jobstart()*
- The job ID on success, which is used by |jobsend()| and
|jobstop()|
- 0 when the job table is full or on invalid arguments
- -1 when {argv}[0] is not executable
- -1 when {cmd}[0] is not executable. Will never fail if
{cmd} is a string unless 'shell' is not executable.
See |job-control| for more information.
jobstop({job}) {Nvim} *jobstop()*
@ -6151,9 +6155,10 @@ synstack({lnum}, {col}) *synstack()*
character in a line and the first column in an empty line are
valid positions.
system({expr} [, {input}]) *system()* *E677*
Get the output of the shell command {expr} as a string. See
|systemlist()| to get the output as a List.
system({cmd} [, {input}]) *system()* *E677*
Get the output of the shell command {cmd} as a |String|. {cmd}
will be run the same as in |jobstart()|. See |systemlist()|
to get the output as a |List|.
When {input} is given and is a string this string is written
to a file and passed as stdin to the command. The string is
@ -6167,7 +6172,7 @@ system({expr} [, {input}]) *system()* *E677*
Note: Use |shellescape()| or |::S| with |expand()| or
|fnamemodify()| to escape special characters in a command
argument. Newlines in {expr} may cause the command to fail.
argument. Newlines in {cmd} may cause the command to fail.
The characters in 'shellquote' and 'shellxquote' may also
cause trouble.
This is not to be used for interactive commands.
@ -6182,11 +6187,8 @@ system({expr} [, {input}]) *system()* *E677*
To avoid the string being truncated at a NUL, all NUL
characters are replaced with SOH (0x01).
The command executed is constructed using several options:
'shell' 'shellcmdflag' 'shellxquote' {expr} 'shellredir' {tmp} 'shellxquote'
({tmp} is an automatically generated file name).
For Unix braces are put around {expr} to allow for
concatenated commands.
The command executed is constructed using several options when
{cmd} is a string: 'shell' 'shellcmdflag' {cmd}
The command will be executed in "cooked" mode, so that a
CTRL-C will interrupt the command (on Unix at least).
@ -6201,7 +6203,7 @@ system({expr} [, {input}]) *system()* *E677*
Use |:checktime| to force a check.
systemlist({expr} [, {input} [, {keepempty}]]) *systemlist()*
systemlist({cmd} [, {input} [, {keepempty}]]) *systemlist()*
Same as |system()|, but returns a |List| with lines (parts of
output separated by NL) with NULs transformed into NLs. Output
is the same as |readfile()| will output with {binary} argument
@ -6299,15 +6301,19 @@ tempname() *tempname()* *temp-file-name*
For MS-Windows forward slashes are used when the 'shellslash'
option is set or when 'shellcmdflag' starts with '-'.
termopen({command}[, {opts}]) {Nvim} *termopen()*
Spawns {command} using the shell in a new pseudo-terminal
session connected to the current buffer. This function fails
if the current buffer is modified (all buffer contents are
destroyed). The {opts} dict is similar to the one passed to
|jobstart()|, but the `pty`, `width`, `height`, and `TERM` fields are
ignored: `height`/`width` are taken from the current window and
$TERM is set to "xterm-256color". Returns the same values as
|jobstart()|.
termopen({cmd}[, {opts}]) {Nvim} *termopen()*
Spawns {cmd} in a new pseudo-terminal session connected
to the current buffer. {cmd} is the same as the one passed to
|jobstart()|. This function fails if the current buffer is
modified (all buffer contents are destroyed).
The {opts} dict is similar to the one passed to |jobstart()|,
but the `pty`, `width`, `height`, and `TERM` fields are
ignored: `height`/`width` are taken from the current window
and `$TERM` is set to "xterm-256color", and it may have a
`name` field. `name`, if present, sets the buffer's name to
"term://{cwd}/{pid}:{name}".
Returns the same values as |jobstart()|.
See |nvim-terminal-emulator| for more information.

View File

@ -223,11 +223,18 @@ g8 Print the hex values of the bytes used in the
:sh[ell] Removed. |vim-differences| {Nvim}
*:term* *:terminal*
:term[inal][!] {cmd} Spawns {command} using the current value of 'shell'
in a new terminal buffer. This is equivalent to: >
:term[inal][!] {cmd} Spawns {cmd} using the current value of 'shell' and
'shellcmdflag' in a new terminal buffer. This is
equivalent to: >
:enew | call termopen('{cmd}') | startinsert
:enew
:call termopen([&sh, &shcf, '{cmd}'],
\{'name':'{cmd}'})
:startinsert
<
If no {cmd} is given, 'shellcmdflag' will not be sent
to |termopen()|.
Like |:enew|, it will fail if the current buffer is
modified, but can be forced with "!". See |termopen()|
and |nvim-terminal-emulator| for more information.

View File

@ -10779,6 +10779,59 @@ static void f_jobresize(typval_T *argvars, typval_T *rettv)
rettv->vval.v_number = 1;
}
static char **tv_to_argv(typval_T *cmd_tv, char **cmd)
{
if (cmd_tv->v_type == VAR_STRING) {
char *cmd_str = (char *)get_tv_string(cmd_tv);
if (cmd) {
*cmd = cmd_str;
}
return shell_build_argv(cmd_str, NULL);
}
if (cmd_tv->v_type != VAR_LIST) {
EMSG2(_(e_invarg2), "expected String or List");
return NULL;
}
list_T *argl = cmd_tv->vval.v_list;
int argc = argl->lv_len;
if (!argc) {
EMSG(_("Argument vector must have at least one item"));
return NULL;
}
assert(argl->lv_first);
const char_u *exe = get_tv_string_chk(&argl->lv_first->li_tv);
if (!exe || !os_can_exe(exe, NULL)) {
// String is not executable
if (exe) {
EMSG2(e_jobexe, exe);
}
return NULL;
}
if (cmd) {
*cmd = (char *)exe;
}
// Build the argument vector
int i = 0;
char **argv = xcalloc(argc + 1, sizeof(char *));
for (listitem_T *arg = argl->lv_first; arg != NULL; arg = arg->li_next) {
char *a = (char *)get_tv_string_chk(&arg->li_tv);
if (!a) {
// Did emsg in get_tv_string; just deallocate argv.
shell_free_argv(argv);
return NULL;
}
argv[i++] = xstrdup(a);
}
return argv;
}
// "jobstart()" function
static void f_jobstart(typval_T *argvars, typval_T *rettv)
{
@ -10789,34 +10842,15 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
return;
}
if (argvars[0].v_type != VAR_LIST
|| (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN)) {
char **argv = tv_to_argv(&argvars[0], NULL);
if (!argv) {
return; // Did error message in tv_to_argv.
}
if (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN) {
// Wrong argument types
EMSG(_(e_invarg));
return;
}
list_T *args = argvars[0].vval.v_list;
// Assert that all list items are strings
for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) {
if (arg->li_tv.v_type != VAR_STRING) {
EMSG(_(e_invarg));
return;
}
}
int argc = args->lv_len;
if (!argc) {
EMSG(_("Argument vector must have at least one item"));
return;
}
assert(args->lv_first);
const char_u *exe = get_tv_string(&args->lv_first->li_tv);
if (!os_can_exe(exe, NULL)) {
// String is not executable
EMSG2(e_jobexe, exe);
EMSG2(_(e_invarg2), "expected dictionary");
shell_free_argv(argv);
return;
}
@ -10825,17 +10859,11 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
if (argvars[1].v_type == VAR_DICT) {
job_opts = argvars[1].vval.v_dict;
if (!common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit)) {
shell_free_argv(argv);
return;
}
}
// Build the argument vector
int i = 0;
char **argv = xcalloc(argc + 1, sizeof(char *));
for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) {
argv[i++] = xstrdup((char *) get_tv_string(&arg->li_tv));
}
JobOptions opts = common_job_options(argv, on_stdout, on_stderr, on_exit,
job_opts);
@ -14927,12 +14955,16 @@ static void get_system_output_as_rettv(typval_T *argvars, typval_T *rettv,
}
// get shell command to execute
const char *cmd = (char *) get_tv_string(&argvars[0]);
char **argv = tv_to_argv(&argvars[0], NULL);
if (!argv) {
xfree(input);
return; // Already did emsg.
}
// execute the command
size_t nread = 0;
char *res = NULL;
int status = os_system(cmd, input, input_len, &res, &nread);
int status = os_system(argv, input, input_len, &res, &nread);
xfree(input);
@ -15155,10 +15187,16 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
return;
}
if (argvars[0].v_type != VAR_STRING
|| (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN)) {
// Wrong argument types
EMSG(_(e_invarg));
char *cmd;
char **argv = tv_to_argv(&argvars[0], &cmd);
if (!argv) {
return; // Did error message in tv_to_argv.
}
if (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN) {
// Wrong argument type
EMSG2(_(e_invarg2), "expected dictionary");
shell_free_argv(argv);
return;
}
@ -15167,11 +15205,11 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
if (argvars[1].v_type == VAR_DICT) {
job_opts = argvars[1].vval.v_dict;
if (!common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit)) {
shell_free_argv(argv);
return;
}
}
char **argv = shell_build_argv((char *)argvars[0].vval.v_string, NULL);
JobOptions opts = common_job_options(argv, on_stdout, on_stderr, on_exit,
job_opts);
opts.pty = true;
@ -15180,6 +15218,7 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
opts.term_name = xstrdup("xterm-256color");
Job *job = common_job_start(opts, rettv);
if (!job) {
shell_free_argv(argv);
return;
}
TerminalJobData *data = opts.data;
@ -15197,10 +15236,13 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
cwd = (char *)argvars[1].vval.v_string;
}
int pid = job_pid(job);
// Get the desired name of the buffer.
char *name = job_opts ?
(char *)get_dict_string(job_opts, (char_u *)"name", false) : cmd;
char buf[1024];
// format the title with the pid to conform with the term:// URI
snprintf(buf, sizeof(buf), "term://%s//%d:%s", cwd, pid,
(char *)argvars[0].vval.v_string);
snprintf(buf, sizeof(buf), "term://%s//%d:%s", cwd, pid, name);
// at this point the buffer has no terminal instance associated yet, so unset
// the 'swapfile' option to ensure no swap file will be created
curbuf->b_p_swf = false;
@ -15215,6 +15257,8 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
Terminal *term = terminal_open(topts);
data->term = term;
data->refcount++;
return;
}
/*

View File

@ -9410,9 +9410,22 @@ static void ex_folddo(exarg_T *eap)
static void ex_terminal(exarg_T *eap)
{
char cmd[512];
snprintf(cmd, sizeof(cmd), ":enew%s | call termopen('%s') | startinsert",
eap->forceit==TRUE ? "!" : "",
strcmp((char *)eap->arg, "") ? (char *)eap->arg : (char *)p_sh);
do_cmdline_cmd((uint8_t *)cmd);
// We will call termopen() with ['shell'] if not given a {cmd}.
char *name = (char *)p_sh;
char *lquote = "['";
char *rquote = "']";
if (*eap->arg != NUL) {
name = (char *)vim_strsave_escaped(eap->arg, (char_u *)"\"\\");
lquote = rquote = "\"";
}
char ex_cmd[512];
snprintf(ex_cmd, sizeof(ex_cmd),
":enew%s | call termopen(%s%s%s) | startinsert",
eap->forceit==TRUE ? "!" : "", lquote, name, rquote);
do_cmdline_cmd((uint8_t *)ex_cmd);
if (name != (char *)p_sh) {
xfree(name);
}
}

View File

@ -119,14 +119,14 @@ int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_args)
}
size_t nread;
int status = shell((const char *)cmd,
(const char *)extra_args,
input.data,
input.len,
output_ptr,
&nread,
emsg_silent,
forward_output);
int status = do_os_system(shell_build_argv((char *)cmd, (char *)extra_args),
input.data,
input.len,
output_ptr,
&nread,
emsg_silent,
forward_output);
xfree(input.data);
@ -152,9 +152,11 @@ int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_args)
/// example:
/// char *output = NULL;
/// size_t nread = 0;
/// int status = os_sytem("ls -la", NULL, 0, &output, &nread);
/// char *argv[] = {"ls", "-la", NULL};
/// int status = os_sytem(argv, NULL, 0, &output, &nread);
///
/// @param cmd The full commandline to be passed to the shell
/// @param argv The commandline arguments to be passed to the shell. `argv`
/// will be consumed.
/// @param input The input to the shell (NULL for no input), passed to the
/// stdin of the resulting process.
/// @param len The length of the input buffer (not used if `input` == NULL)
@ -166,23 +168,22 @@ int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_args)
/// returned buffer is not NULL)
/// @return the return code of the process, -1 if the process couldn't be
/// started properly
int os_system(const char *cmd,
int os_system(char **argv,
const char *input,
size_t len,
char **output,
size_t *nread) FUNC_ATTR_NONNULL_ARG(1)
{
return shell(cmd, NULL, input, len, output, nread, true, false);
return do_os_system(argv, input, len, output, nread, true, false);
}
static int shell(const char *cmd,
const char *extra_args,
const char *input,
size_t len,
char **output,
size_t *nread,
bool silent,
bool forward_output)
static int do_os_system(char **argv,
const char *input,
size_t len,
char **output,
size_t *nread,
bool silent,
bool forward_output)
{
// the output buffer
DynamicBuffer buf = DYNAMIC_BUFFER_INIT;
@ -197,7 +198,9 @@ static int shell(const char *cmd,
data_cb = NULL;
}
char **argv = shell_build_argv(cmd, extra_args);
// Copy the program name in case we need to report an error.
char prog[MAXPATHL];
xstrlcpy(prog, argv[0], MAXPATHL);
int status;
JobOptions opts = JOB_OPTIONS_INIT;
@ -211,11 +214,9 @@ static int shell(const char *cmd,
if (status <= 0) {
// Failed, probably due to `sh` not being executable
ELOG("Couldn't start job, command: '%s', error code: '%d'",
(cmd ? cmd : (char *)p_sh), status);
if (!silent) {
MSG_PUTS(_("\nCannot execute shell "));
msg_outtrans(p_sh);
MSG_PUTS(_("\nCannot execute "));
msg_outtrans((char_u *)prog);
msg_putchar('\n');
}
return -1;

View File

@ -1,2 +1,4 @@
add_executable(tty-test tty-test.c)
target_link_libraries(tty-test ${LIBUV_LIBRARIES})
add_executable(shell-test shell-test.c)

View File

@ -0,0 +1,25 @@
// A simple implementation of a shell for testing
// `termopen([&sh, &shcf, '{cmd'}])` and `termopen([&sh])`.
//
// If launched with no arguments, prints "ready $ ", otherwise prints
// "ready $ {cmd}\n".
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
fprintf(stderr, "ready $ ");
if (argc == 3) {
// argv should be {"terminal-test", "EXE", "prog args..."}
if (strcmp(argv[1], "EXE") != 0) {
fprintf(stderr, "first argument must be 'EXE'\n");
return 2;
}
fprintf(stderr, "%s\n", argv[2]);
}
return 0;
}

View File

@ -29,6 +29,13 @@ describe('jobs', function()
]])
end)
it('uses &shell and &shellcmdflag if passed a string', function()
nvim('command', "let $VAR = 'abc'")
nvim('command', "let j = jobstart('echo $VAR', g:job_opts)")
eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg())
eq({'notification', 'exit', {0, 0}}, next_msg())
end)
it('returns 0 when it fails to start', function()
local status, rv = pcall(eval, "jobstart([])")
eq(false, status)

View File

@ -44,7 +44,7 @@ describe('system()', function()
eq(127, eval('v:shell_error'))
end)
describe('executes shell function', function()
describe('executes shell function if passed a string', function()
local screen
before_each(function()
@ -192,6 +192,13 @@ describe('system()', function()
end)
end)
end
describe('command passed as a list', function()
it('does not execute &shell', function()
eq('* $NOTHING ~/file',
eval("system(['echo', '-n', '*', '$NOTHING', '~/file'])"))
end)
end)
end)
describe('systemlist()', function()

View File

@ -144,7 +144,7 @@ describe('cursor with customized highlighting', function()
[6] = {foreground = 130},
})
screen:attach(false)
execute('term "' ..nvim_dir.. '/tty-test"')
execute('call termopen(["'..nvim_dir..'/tty-test"]) | startinsert')
end)
it('overrides the default highlighting', function()

View File

@ -0,0 +1,61 @@
local helpers = require('test.functional.helpers')
local Screen = require('test.functional.ui.screen')
local clear, wait, nvim = helpers.clear, helpers.wait, helpers.nvim
local nvim_dir = helpers.nvim_dir
local execute, source = helpers.execute, helpers.source
local eq, neq = helpers.eq, helpers.neq
describe(':terminal', function()
local screen
before_each(function()
clear()
screen = Screen.new(50, 7)
screen:attach(false)
nvim('set_option', 'shell', nvim_dir..'/shell-test')
nvim('set_option', 'shellcmdflag', 'EXE')
end)
it('with no argument, acts like termopen()', function()
execute('terminal')
wait()
screen:expect([[
ready $ |
[Program exited, press any key to close] |
|
|
|
|
-- TERMINAL -- |
]])
end)
it('executes a given command through the shell', function()
execute('terminal echo hi')
wait()
screen:expect([[
ready $ echo hi |
|
[Program exited, press any key to close] |
|
|
|
-- TERMINAL -- |
]])
end)
it('allows quotes and slashes', function()
execute([[terminal echo 'hello' \ "world"]])
wait()
screen:expect([[
ready $ echo 'hello' \ "world" |
|
[Program exited, press any key to close] |
|
|
|
-- TERMINAL -- |
]])
end)
end)

View File

@ -56,7 +56,7 @@ local function screen_setup(extra_height)
-- tty-test puts the terminal into raw mode and echoes all input. tests are
-- done by feeding it with terminfo codes to control the display and
-- verifying output with screen:expect.
execute('term ' ..nvim_dir.. '/tty-test')
execute('enew | call termopen(["'..nvim_dir..'/tty-test"]) | startinsert')
-- wait for "tty ready" to be printed before each test or the terminal may
-- still be in canonical mode(will echo characters for example)
--

View File

@ -27,7 +27,7 @@ describe('terminal window highlighting', function()
[8] = {background = 11}
})
screen:attach(false)
execute('term "' ..nvim_dir.. '/tty-test"')
execute('enew | call termopen(["'..nvim_dir..'/tty-test"]) | startinsert')
screen:expect([[
tty ready |
|
@ -133,7 +133,7 @@ describe('terminal window highlighting with custom palette', function()
})
screen:attach(true)
nvim('set_var', 'terminal_color_3', '#123456')
execute('term "' ..nvim_dir.. '/tty-test"')
execute('enew | call termopen(["'..nvim_dir..'/tty-test"]) | startinsert')
screen:expect([[
tty ready |
|

View File

@ -332,7 +332,7 @@ describe('terminal prints more lines than the screen height and exits', function
clear()
local screen = Screen.new(50, 7)
screen:attach(false)
execute('term ' ..nvim_dir.. '/tty-test 10')
execute('call termopen(["'..nvim_dir..'/tty-test", "10"]) | startinsert')
wait()
screen:expect([[
line6 |

View File

@ -40,7 +40,9 @@ describe('shell functions', function()
local output = ffi.new('char *[1]')
local nread = ffi.new('size_t[1]')
local status = shell.os_system(to_cstr(cmd), input_or, input_len, output, nread)
local argv = ffi.cast('char**',
shell.shell_build_argv(to_cstr(cmd), nil))
local status = shell.os_system(argv, input_or, input_len, output, nread)
return status, intern(output[0], nread[0])
end