Merge #6497 from justinmk/win-quot

win: system('...'): special-case cmd.exe
This commit is contained in:
Justin M. Keyes 2017-04-12 03:21:21 +02:00 committed by GitHub
commit dd391bfca1
13 changed files with 214 additions and 105 deletions

View File

@ -1524,13 +1524,18 @@ v:errors Errors found by assert functions, such as |assert_true()|.
list by the assert function.
*v:event* *event-variable*
v:event Dictionary of event data for the current |autocommand|. The
available keys differ per event type and are specified at the
documentation for each |event|. The possible keys are:
operator The operation performed. Unlike
|v:operator|, it is set also for an Ex
mode command. For instance, |:yank| is
translated to "|y|".
v:event Dictionary of event data for the current |autocommand|. Valid
only during the autocommand lifetime: storing or passing
`v:event` is invalid. Copy it instead: >
au TextYankPost * let g:foo = deepcopy(v:event)
< Keys vary by event; see the documentation for the specific
event, e.g. |TextYankPost|.
KEY DESCRIPTION ~
operator The current |operator|. Also set for
Ex commands (unlike |v:operator|). For
example if |TextYankPost| is triggered
by the |:yank| Ex command then
`v:event['operator']` is "y".
regcontents Text stored in the register as a
|readfile()|-style list of lines.
regname Requested register (e.g "x" for "xyy)
@ -4847,16 +4852,18 @@ jobstart({cmd}[, {opts}]) {Nvim} *jobstart()*
Spawns {cmd} as a job. If {cmd} is a |List| it is run
directly. If {cmd} is a |String| it is processed like this: >
:call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}'])
< NOTE: This only shows the idea; see |shell-unquoting| before
constructing lists with 'shell' or 'shellcmdflag'.
< (Only shows the idea; see |shell-unquoting| for full details.)
NOTE: On Windows if {cmd} is a List, cmd[0] must be a valid
executable (.exe, .com). If the executable is in $PATH it can
be called by name, with or without an extension: >
NOTE: on Windows if {cmd} is a List:
- cmd[0] must be an executable (not a "built-in"). If it is
in $PATH it can be called by name, without an extension: >
:call jobstart(['ping', 'neovim.io'])
< If it is a path (not a name), it must include the extension: >
< If it is a full or partial path, extension is required: >
:call jobstart(['System32\ping.exe', 'neovim.io'])
<
< - {cmd} is collapsed to a string of quoted args as expected
by CommandLineToArgvW https://msdn.microsoft.com/bb776391
unless cmd[0] is some form of "cmd.exe".
{opts} is a dictionary with these keys:
on_stdout: stdout event handler (function name or |Funcref|)
on_stderr: stderr event handler (function name or |Funcref|)

View File

@ -2765,8 +2765,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'grepprg'* *'gp'*
'grepprg' 'gp' string (default "grep -n ",
Unix: "grep -n $* /dev/null",
Win32: "findstr /n" or "grep -n")
Unix: "grep -n $* /dev/null")
global or local to buffer |global-local|
Program to use for the |:grep| command. This option may contain '%'
and '#' characters, which are expanded like when used in a command-
@ -2781,8 +2780,6 @@ A jump table for the options with a short description can be found at |Q_op|.
|:vimgrepadd| and |:lgrepadd| like |:lvimgrepadd|.
See also the section |:make_makeprg|, since most of the comments there
apply equally to 'grepprg'.
For Win32, the default is "findstr /n" if "findstr.exe" can be found,
otherwise it's "grep -n".
This option cannot be set from a |modeline| or in the |sandbox|, for
security reasons.
@ -5251,9 +5248,7 @@ A jump table for the options with a short description can be found at |Q_op|.
security reasons.
*'shellcmdflag'* *'shcf'*
'shellcmdflag' 'shcf' string (default: "-c";
Windows, when 'shell' does not
contain "sh" somewhere: "/c")
'shellcmdflag' 'shcf' string (default: "-c"; Windows: "/c")
global
Flag passed to the shell to execute "!" and ":!" commands; e.g.,
"bash.exe -c ls" or "cmd.exe /c dir". For Windows
@ -5264,15 +5259,12 @@ A jump table for the options with a short description can be found at |Q_op|.
See |option-backslash| about including spaces and backslashes.
See |shell-unquoting| which talks about separating this option into
multiple arguments.
Also see |dos-shell| for Windows.
This option cannot be set from a |modeline| or in the |sandbox|, for
security reasons.
*'shellpipe'* *'sp'*
'shellpipe' 'sp' string (default ">", "| tee", "|& tee" or "2>&1| tee")
global
{not available when compiled without the |+quickfix|
feature}
String to be used to put the output of the ":make" command in the
error file. See also |:make_makeprg|. See |option-backslash| about
including spaces and backslashes.
@ -5314,7 +5306,7 @@ A jump table for the options with a short description can be found at |Q_op|.
third-party shells on Windows systems, such as the MKS Korn Shell
or bash, where it should be "\"". The default is adjusted according
the value of 'shell', to reduce the need to set this option by the
user. See |dos-shell|.
user.
This option cannot be set from a |modeline| or in the |sandbox|, for
security reasons.
@ -5346,7 +5338,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'shellslash'* *'ssl'* *'noshellslash'* *'nossl'*
'shellslash' 'ssl' boolean (default off)
global
{only for MSDOS and MS-Windows}
{only for Windows}
When set, a forward slash is used when expanding file names. This is
useful when a Unix-like shell is used instead of command.com or
cmd.exe. Backward slashes can still be typed, but they are changed to
@ -5363,10 +5355,7 @@ A jump table for the options with a short description can be found at |Q_op|.
global
When on, use temp files for shell commands. When off use a pipe.
When using a pipe is not possible temp files are used anyway.
Currently a pipe is only supported on Unix and MS-Windows 2K and
later. You can check it with: >
:if has("filterpipe")
< The advantage of using a pipe is that nobody can read the temp file
The advantage of using a pipe is that nobody can read the temp file
and the 'shell' command does not need to support redirection.
The advantage of using a temp file is that the file type and encoding
can be detected.
@ -5376,19 +5365,14 @@ A jump table for the options with a short description can be found at |Q_op|.
|system()| does not respect this option, it always uses pipes.
*'shellxescape'* *'sxe'*
'shellxescape' 'sxe' string (default: "";
for Windows: "\"&|<>()@^")
'shellxescape' 'sxe' string (default: "")
global
When 'shellxquote' is set to "(" then the characters listed in this
option will be escaped with a '^' character. This makes it possible
to execute most external commands with cmd.exe.
*'shellxquote'* *'sxq'*
'shellxquote' 'sxq' string (default: "";
for Win32, when 'shell' is cmd.exe: "("
for Win32, when 'shell' contains "sh"
somewhere: "\""
for Unix, when using system(): "\"")
'shellxquote' 'sxq' string (default: "")
global
Quoting character(s), put around the command passed to the shell, for
the "!" and ":!" commands. Includes the redirection. See
@ -5397,12 +5381,6 @@ A jump table for the options with a short description can be found at |Q_op|.
When the value is '(' then ')' is appended. When the value is '"('
then ')"' is appended.
When the value is '(' then also see 'shellxescape'.
This is an empty string by default on most systems, but is known to be
useful for on Win32 version, either for cmd.exe which automatically
strips off the first and last quote on a command, or 3rd-party shells
such as the MKS Korn Shell or bash, where it should be "\"". The
default is adjusted according the value of 'shell', to reduce the need
to set this option by the user. See |dos-shell|.
This option cannot be set from a |modeline| or in the |sandbox|, for
security reasons.
@ -6413,8 +6391,6 @@ A jump table for the options with a short description can be found at |Q_op|.
*'title'* *'notitle'*
'title' boolean (default off, on when title can be restored)
global
{not available when compiled without the |+title|
feature}
When on, the title of the window will be set to the value of
'titlestring' (if it is not empty), or to:
filename [+=-] (path) - VIM
@ -6426,16 +6402,10 @@ A jump table for the options with a short description can be found at |Q_op|.
=+ indicates the file is read-only and modified
(path) is the path of the file being edited
- VIM the server name |v:servername| or "VIM"
Only works if the terminal supports setting window titles
(currently Win32 console, all GUI versions and terminals with a non-
empty 't_ts' option - this is Unix xterm by default, where 't_ts' is
taken from the builtin termcap).
*'titlelen'*
'titlelen' number (default 85)
global
{not available when compiled without the |+title|
feature}
Gives the percentage of 'columns' to use for the length of the window
title. When the title is longer, only the end of the path name is
shown. A '<' character before the path name is used to indicate this.
@ -6449,8 +6419,6 @@ A jump table for the options with a short description can be found at |Q_op|.
*'titleold'*
'titleold' string (default "Thanks for flying Vim")
global
{only available when compiled with the |+title|
feature}
This option will be used for the window title when exiting Vim if the
original title cannot be restored. Only happens if 'title' is on or
'titlestring' is not empty.
@ -6459,13 +6427,8 @@ A jump table for the options with a short description can be found at |Q_op|.
*'titlestring'*
'titlestring' string (default "")
global
{not available when compiled without the |+title|
feature}
When this option is not empty, it will be used for the title of the
window. This happens only when the 'title' option is on.
Only works if the terminal supports setting window titles (currently
Win32 console, all GUI versions and terminals with a non-empty 't_ts'
option).
When this option contains printf-style '%' items, they will be
expanded according to the rules used for 'statusline'.
Example: >

View File

@ -8,6 +8,7 @@
#include "nvim/event/process.h"
#include "nvim/event/libuv_process.h"
#include "nvim/log.h"
#include "nvim/os/os.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "event/libuv_process.c.generated.h"
@ -24,6 +25,13 @@ int libuv_process_spawn(LibuvProcess *uvproc)
if (proc->detach) {
uvproc->uvopts.flags |= UV_PROCESS_DETACHED;
}
#ifdef WIN32
// libuv collapses the argv to a CommandLineToArgvW()-style string. cmd.exe
// expects a different syntax (must be prepared by the caller before now).
if (os_shell_is_cmdexe(proc->argv[0])) {
uvproc->uvopts.flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS;
}
#endif
uvproc->uvopts.exit_cb = exit_cb;
uvproc->uvopts.cwd = proc->cwd;
uvproc->uvopts.env = NULL;

View File

@ -495,6 +495,13 @@ bool strequal(const char *a, const char *b)
return (a == NULL && b == NULL) || (a && b && strcmp(a, b) == 0);
}
/// Case-insensitive `strequal`.
bool striequal(const char *a, const char *b)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
return (a == NULL && b == NULL) || (a && b && STRICMP(a, b) == 0);
}
/*
* Avoid repeating the error message many times (they take 1 second each).
* Did_outofmem_msg is reset when a character is read.

View File

@ -2678,7 +2678,8 @@ void fast_breakcheck(void)
}
}
// Call shell. Calls os_call_shell, with 'shellxquote' added.
// os_call_shell wrapper. Handles 'verbose', :profile, and v:shell_error.
// Invalidates cached tags.
int call_shell(char_u *cmd, ShellOpts opts, char_u *extra_shell_arg)
{
int retval;
@ -2686,8 +2687,7 @@ int call_shell(char_u *cmd, ShellOpts opts, char_u *extra_shell_arg)
if (p_verbose > 3) {
verbose_enter();
smsg(_("Calling shell to execute: \"%s\""),
cmd == NULL ? p_sh : cmd);
smsg(_("Calling shell to execute: \"%s\""), cmd == NULL ? p_sh : cmd);
ui_putc('\n');
verbose_leave();
}

View File

@ -2051,7 +2051,11 @@ return {
secure=true,
vi_def=true,
varname='p_srr',
defaults={if_true={vi=">"}}
defaults={
condition='WIN32',
if_true={vi=">%s 2>&1"},
if_false={vi=">"}
}
},
{
full_name='shellslash', abbreviation='ssl',

View File

@ -1,11 +1,8 @@
// env.c -- environment variable access
// Environment inspection
#include <assert.h>
#include <uv.h>
// vim.h must be included before charset.h (and possibly others) or things
// blow up
#include "nvim/vim.h"
#include "nvim/ascii.h"
#include "nvim/charset.h"
@ -919,3 +916,20 @@ bool os_term_is_nice(void)
|| NULL != os_getenv("KONSOLE_DBUS_SESSION");
#endif
}
/// Returns true if `sh` looks like it resolves to "cmd.exe".
bool os_shell_is_cmdexe(const char *sh)
FUNC_ATTR_NONNULL_ALL
{
if (*sh == NUL) {
return false;
}
if (striequal(sh, "$COMSPEC")) {
const char *comspec = os_getenv("COMSPEC");
return striequal("cmd.exe", (char *)path_tail((char_u *)comspec));
}
if (striequal(sh, "cmd.exe") || striequal(sh, "cmd")) {
return true;
}
return striequal("cmd.exe", (char *)path_tail((char_u *)sh));
}

View File

@ -124,11 +124,9 @@ int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_args)
}
size_t nread;
int exitcode = 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);
if (output) {

View File

@ -198,8 +198,16 @@ char_u *vim_strsave_shellescape(const char_u *string,
/* First count the number of extra bytes required. */
size_t length = STRLEN(string) + 3; // two quotes and a trailing NUL
for (const char_u *p = string; *p != NUL; mb_ptr_adv(p)) {
if (*p == '\'')
length += 3; /* ' => '\'' */
#ifdef WIN32
if (!p_ssl) {
if (*p == '"') {
length++; // " -> ""
}
} else
#endif
if (*p == '\'') {
length += 3; // ' => '\''
}
if ((*p == '\n' && (csh_like || do_newline))
|| (*p == '!' && (csh_like || do_special))) {
++length; /* insert backslash */
@ -216,10 +224,25 @@ char_u *vim_strsave_shellescape(const char_u *string,
escaped_string = xmalloc(length);
d = escaped_string;
/* add opening quote */
// add opening quote
#ifdef WIN32
if (!p_ssl) {
*d++ = '"';
} else
#endif
*d++ = '\'';
for (const char_u *p = string; *p != NUL; ) {
#ifdef WIN32
if (!p_ssl) {
if (*p == '"') {
*d++ = '"';
*d++ = '"';
p++;
continue;
}
} else
#endif
if (*p == '\'') {
*d++ = '\'';
*d++ = '\\';
@ -246,7 +269,12 @@ char_u *vim_strsave_shellescape(const char_u *string,
MB_COPY_CHAR(p, d);
}
/* add terminating quote and finish with a NUL */
// add terminating quote and finish with a NUL
# ifdef WIN32
if (!p_ssl) {
*d++ = '"';
} else
# endif
*d++ = '\'';
*d = NUL;

View File

@ -4,6 +4,8 @@ local nvim_dir = helpers.nvim_dir
local eq, call, clear, eval, feed_command, feed, nvim =
helpers.eq, helpers.call, helpers.clear, helpers.eval, helpers.feed_command,
helpers.feed, helpers.nvim
local command = helpers.command
local iswin = helpers.iswin
local Screen = require('test.functional.ui.screen')
@ -33,8 +35,7 @@ describe('system()', function()
describe('command passed as a List', function()
local function printargs_path()
return nvim_dir..'/printargs-test'
.. (helpers.os_name() == 'windows' and '.exe' or '')
return nvim_dir..'/printargs-test' .. (iswin() and '.exe' or '')
end
it('sets v:shell_error if cmd[0] is not executable', function()
@ -88,15 +89,23 @@ describe('system()', function()
end)
it('does NOT run in shell', function()
if helpers.os_name() ~= 'windows' then
if not iswin() then
eq("* $PATH %PATH%\n", eval("system(['echo', '*', '$PATH', '%PATH%'])"))
end
end)
end)
if helpers.pending_win32(pending) then return end
it('sets v:shell_error', function()
if iswin() then
eval([[system("cmd.exe /c exit")]])
eq(0, eval('v:shell_error'))
eval([[system("cmd.exe /c exit 1")]])
eq(1, eval('v:shell_error'))
eval([[system("cmd.exe /c exit 5")]])
eq(5, eval('v:shell_error'))
eval([[system('this-should-not-exist')]])
eq(1, eval('v:shell_error'))
else
eval([[system("sh -c 'exit'")]])
eq(0, eval('v:shell_error'))
eval([[system("sh -c 'exit 1'")]])
@ -105,6 +114,7 @@ describe('system()', function()
eq(5, eval('v:shell_error'))
eval([[system('this-should-not-exist')]])
eq(127, eval('v:shell_error'))
end
end)
describe('executes shell function if passed a string', function()
@ -120,6 +130,40 @@ describe('system()', function()
screen:detach()
end)
if iswin() then
it('with shell=cmd.exe', function()
command('set shell=cmd.exe')
eq('""\n', eval([[system('echo ""')]]))
eq('"a b"\n', eval([[system('echo "a b"')]]))
eq('a \nb\n', eval([[system('echo a & echo b')]]))
eq('a \n', eval([[system('echo a 2>&1')]]))
eval([[system('cd "C:\Program Files"')]])
eq(0, eval('v:shell_error'))
end)
it('with shell=cmd', function()
command('set shell=cmd')
eq('"a b"\n', eval([[system('echo "a b"')]]))
end)
it('with shell=$COMSPEC', function()
local comspecshell = eval("fnamemodify($COMSPEC, ':t')")
if comspecshell == 'cmd.exe' then
command('set shell=$COMSPEC')
eq('"a b"\n', eval([[system('echo "a b"')]]))
else
pending('$COMSPEC is not cmd.exe: ' .. comspecshell)
end
end)
it('works with powershell', function()
helpers.set_shell_powershell()
eq('a\nb\n', eval([[system('echo a b')]]))
eq('C:\\\n', eval([[system('cd c:\; (Get-Location).Path')]]))
eq('a b\n', eval([[system('echo "a b"')]]))
end)
end
it('`echo` and waits for its return', function()
feed(':call system("echo")<cr>')
screen:expect([[
@ -180,7 +224,11 @@ describe('system()', function()
describe('passing no input', function()
it('returns the program output', function()
if iswin() then
eq("echoed\n", eval('system("echo echoed")'))
else
eq("echoed", eval('system("echo -n echoed")'))
end
end)
it('to backgrounded command does not crash', function()
-- This is indeterminate, just exercise the codepath. May get E5677.
@ -277,13 +325,21 @@ describe('system()', function()
end)
end)
if helpers.pending_win32(pending) then return end
describe('systemlist()', function()
-- Similar to `system()`, but returns List instead of String.
before_each(clear)
it('sets the v:shell_error variable', function()
it('sets v:shell_error', function()
if iswin() then
eval([[systemlist("cmd.exe /c exit")]])
eq(0, eval('v:shell_error'))
eval([[systemlist("cmd.exe /c exit 1")]])
eq(1, eval('v:shell_error'))
eval([[systemlist("cmd.exe /c exit 5")]])
eq(5, eval('v:shell_error'))
eval([[systemlist('this-should-not-exist')]])
eq(1, eval('v:shell_error'))
else
eval([[systemlist("sh -c 'exit'")]])
eq(0, eval('v:shell_error'))
eval([[systemlist("sh -c 'exit 1'")]])
@ -292,6 +348,7 @@ describe('systemlist()', function()
eq(5, eval('v:shell_error'))
eval([[systemlist('this-should-not-exist')]])
eq(127, eval('v:shell_error'))
end
end)
describe('exectues shell function', function()
@ -389,6 +446,7 @@ describe('systemlist()', function()
after_each(delete_file(fname))
it('replaces NULs by newline characters', function()
if helpers.pending_win32(pending) then return end
eq({'part1\npart2\npart3'}, eval('systemlist("cat '..fname..'")'))
end)
end)

View File

@ -348,7 +348,7 @@ end
local function set_shell_powershell()
source([[
set shell=powershell shellquote=\" shellpipe=\| shellredir=>
set shellcmdflag=\ -ExecutionPolicy\ RemoteSigned\ -Command
set shellcmdflag=\ -NoProfile\ -ExecutionPolicy\ RemoteSigned\ -Command
let &shellxquote=' '
]])
end

View File

@ -45,11 +45,8 @@ describe(':edit term://*', function()
local bufcontents = {}
local winheight = curwinmeths.get_height()
local buf_cont_start = rep_size - sb - winheight + 2
local function bufline (i)
return ('%d: foobar'):format(i)
end
for i = buf_cont_start,(rep_size - 1) do
bufcontents[#bufcontents + 1] = bufline(i)
bufcontents[#bufcontents + 1] = ('%d: foobar'):format(i)
end
bufcontents[#bufcontents + 1] = ''
bufcontents[#bufcontents + 1] = '[Process exited 0]'

View File

@ -67,12 +67,37 @@ describe('env function', function()
end)
end)
describe('os_shell_is_cmdexe', function()
itp('returns true for expected names', function()
eq(true, cimp.os_shell_is_cmdexe(to_cstr('cmd.exe')))
eq(true, cimp.os_shell_is_cmdexe(to_cstr('cmd')))
eq(true, cimp.os_shell_is_cmdexe(to_cstr('CMD.EXE')))
eq(true, cimp.os_shell_is_cmdexe(to_cstr('CMD')))
os_setenv('COMSPEC', '/foo/bar/cmd.exe', 0)
eq(true, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC')))
os_setenv('COMSPEC', [[C:\system32\cmd.exe]], 0)
eq(true, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC')))
end)
itp('returns false for unexpected names', function()
eq(false, cimp.os_shell_is_cmdexe(to_cstr('')))
eq(false, cimp.os_shell_is_cmdexe(to_cstr('powershell')))
eq(false, cimp.os_shell_is_cmdexe(to_cstr(' cmd.exe ')))
eq(false, cimp.os_shell_is_cmdexe(to_cstr('cm')))
eq(false, cimp.os_shell_is_cmdexe(to_cstr('md')))
eq(false, cimp.os_shell_is_cmdexe(to_cstr('cmd.ex')))
os_setenv('COMSPEC', '/foo/bar/cmd', 0)
eq(false, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC')))
end)
end)
describe('os_getenv', function()
itp('reads an env variable', function()
local name = 'NVIM_UNIT_TEST_GETENV_1N'
local value = 'NVIM_UNIT_TEST_GETENV_1V'
eq(NULL, os_getenv(name))
-- need to use os_setenv, because lua dosn't have a setenv function
-- Use os_setenv because Lua dosen't have setenv.
os_setenv(name, value, 1)
eq(value, os_getenv(name))
end)