win: defaults: 'shellcmdflag', 'shellxquote' #7343

closes #7698

Wrapping a command in double-quotes allows cmd.exe to safely dequote the
entire command as if the user entered the entire command in an
interactive prompt. This reduces the need to escape nested and uneven
double quotes.

The `/s` flag of cmd.exe makes the behaviour more reliable:

    :set shellcmdflag=/s\ /c

Before this patch, cmd.exe cannot use cygwin echo.exe (as opposed to
cmd.exe `echo` builtin) even if it is wrapped in double quotes.

Example:
:: internal echo
> cmd /s /c " echo foo\:bar" "
foo\:bar"

:: cygwin echo.exe
> cmd /s /c " "echo" foo\:bar" "
foo:bar
This commit is contained in:
Jan Edmund Lazo 2017-09-30 21:31:31 -04:00 committed by Justin M. Keyes
parent be67d926c5
commit 131aad953c
5 changed files with 45 additions and 15 deletions

View File

@ -5160,10 +5160,10 @@ 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: "/c")
'shellcmdflag' 'shcf' string (default: "-c"; Windows: "/s /c")
global
Flag passed to the shell to execute "!" and ":!" commands; e.g.,
"bash.exe -c ls" or "cmd.exe /c dir". For Windows
`bash.exe -c ls` or `cmd.exe /s /c "dir"`. For Windows
systems, the default is set according to the value of 'shell', to
reduce the need to set this option by the user.
On Unix it can have more than one flag. Each white space separated
@ -5284,7 +5284,7 @@ A jump table for the options with a short description can be found at |Q_op|.
to execute most external commands with cmd.exe.
*'shellxquote'* *'sxq'*
'shellxquote' 'sxq' string (default: "")
'shellxquote' 'sxq' string (default: "", Windows: "\"")
global
Quoting character(s), put around the command passed to the shell, for
the "!" and ":!" commands. Includes the redirection. See

View File

@ -2048,7 +2048,7 @@ return {
varname='p_shcf',
defaults={
condition='WIN32',
if_true={vi="/c"},
if_true={vi="/s /c"},
if_false={vi="-c"}
}
},
@ -2104,7 +2104,11 @@ return {
secure=true,
vi_def=true,
varname='p_sxq',
defaults={if_true={vi=""}}
defaults={
condition='WIN32',
if_true={vi="\""},
if_false={vi=""},
}
},
{
full_name='shellxescape', abbreviation='sxe',

View File

@ -120,33 +120,47 @@ describe('system()', function()
end
end)
describe('executes shell function if passed a string', function()
describe('executes shell function', function()
local screen
before_each(function()
clear()
screen = Screen.new()
screen:attach()
clear()
screen = Screen.new()
screen:attach()
end)
after_each(function()
screen:detach()
screen:detach()
end)
if iswin() then
local function test_more()
eq('root = true', eval([[get(split(system('"more" ".editorconfig"'), "\n"), 0, '')]]))
end
local function test_shell_unquoting()
eval([[system('"ping" "-n" "1" "127.0.0.1"')]])
eq(0, eval('v:shell_error'))
eq('"a b"\n', eval([[system('cmd /s/c "cmd /s/c "cmd /s/c "echo "a b""""')]]))
eq('"a b"\n', eval([[system('powershell -NoProfile -NoLogo -ExecutionPolicy RemoteSigned -Command echo ''\^"a b\^"''')]]))
end
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')]]))
test_more()
eval([[system('cd "C:\Program Files"')]])
eq(0, eval('v:shell_error'))
test_shell_unquoting()
end)
it('with shell=cmd', function()
command('set shell=cmd')
eq('"a b"\n', eval([[system('echo "a b"')]]))
test_more()
test_shell_unquoting()
end)
it('with shell=$COMSPEC', function()
@ -154,6 +168,8 @@ describe('system()', function()
if comspecshell == 'cmd.exe' then
command('set shell=$COMSPEC')
eq('"a b"\n', eval([[system('echo "a b"')]]))
test_more()
test_shell_unquoting()
else
pending('$COMSPEC is not cmd.exe: ' .. comspecshell)
end
@ -187,7 +203,7 @@ describe('system()', function()
]])
end)
it('`yes` and is interrupted with CTRL-C', function()
it('`yes` interrupted with CTRL-C', function()
feed(':call system("' .. (iswin()
and 'for /L %I in (1,0,2) do @echo y'
or 'yes') .. '")<cr>')
@ -239,6 +255,8 @@ describe('system()', function()
end
end)
it('to backgrounded command does not crash', function()
-- cmd.exe doesn't background a command with &
if iswin() then return end
-- This is indeterminate, just exercise the codepath. May get E5677.
feed_command('call system("echo -n echoed &")')
local v_errnum = string.match(eval("v:errmsg"), "^E%d*:")
@ -254,6 +272,8 @@ describe('system()', function()
eq("input", eval('system("cat -", "input")'))
end)
it('to backgrounded command does not crash', function()
-- cmd.exe doesn't background a command with &
if iswin() then return end
-- This is indeterminate, just exercise the codepath. May get E5677.
feed_command('call system("cat - &", "input")')
local v_errnum = string.match(eval("v:errmsg"), "^E%d*:")
@ -299,7 +319,7 @@ describe('system()', function()
after_each(delete_file(fname))
it('replaces NULs by SOH characters', function()
eq('part1\001part2\001part3\n', eval('system("cat '..fname..'")'))
eq('part1\001part2\001part3\n', eval([[system('"cat" "]]..fname..[["')]]))
end)
end)
@ -366,7 +386,7 @@ describe('systemlist()', function()
end
end)
describe('exectues shell function', function()
describe('executes shell function', function()
local screen
before_each(function()
@ -399,7 +419,7 @@ describe('systemlist()', function()
]])
end)
it('`yes` and is interrupted with CTRL-C', function()
it('`yes` interrupted with CTRL-C', function()
feed(':call systemlist("yes | xargs")<cr>')
screen:expect([[
|
@ -464,7 +484,7 @@ describe('systemlist()', function()
after_each(delete_file(fname))
it('replaces NULs by newline characters', function()
eq({'part1\npart2\npart3'}, eval('systemlist("cat '..fname..'")'))
eq({'part1\npart2\npart3'}, eval([[systemlist('"cat" "]]..fname..[["')]]))
end)
end)

View File

@ -36,6 +36,7 @@ describe(':edit term://*', function()
local scr = get_screen(columns, lines)
local rep = 'a'
meths.set_option('shellcmdflag', 'REP ' .. rep)
command('set shellxquote=') -- win: avoid extra quotes
local rep_size = rep:byte() -- 'a' => 97
local sb = 10
command('autocmd TermOpen * :setlocal scrollback='..tostring(sb)

View File

@ -8,6 +8,7 @@ local funcs = helpers.funcs
local retry = helpers.retry
local ok = helpers.ok
local iswin = helpers.iswin
local command = helpers.command
describe(':terminal', function()
local screen
@ -143,6 +144,7 @@ describe(':terminal (with fake shell)', function()
end)
it('executes a given command through the shell', function()
command('set shellxquote=') -- win: avoid extra quotes
terminal_with_fake_shell('echo hi')
screen:expect([[
^ready $ echo hi |
@ -154,6 +156,7 @@ describe(':terminal (with fake shell)', function()
it("executes a given command through the shell, when 'shell' has arguments", function()
nvim('set_option', 'shell', nvim_dir..'/shell-test -t jeff')
command('set shellxquote=') -- win: avoid extra quotes
terminal_with_fake_shell('echo hi')
screen:expect([[
^jeff $ echo hi |
@ -164,6 +167,7 @@ describe(':terminal (with fake shell)', function()
end)
it('allows quotes and slashes', function()
command('set shellxquote=') -- win: avoid extra quotes
terminal_with_fake_shell([[echo 'hello' \ "world"]])
screen:expect([[
^ready $ echo 'hello' \ "world" |
@ -217,6 +221,7 @@ describe(':terminal (with fake shell)', function()
end)
it('works with gf', function()
command('set shellxquote=') -- win: avoid extra quotes
terminal_with_fake_shell([[echo "scripts/shadacat.py"]])
screen:expect([[
^ready $ echo "scripts/shadacat.py" |