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. security reasons.
*'shellcmdflag'* *'shcf'* *'shellcmdflag'* *'shcf'*
'shellcmdflag' 'shcf' string (default: "-c"; Windows: "/c") 'shellcmdflag' 'shcf' string (default: "-c"; Windows: "/s /c")
global global
Flag passed to the shell to execute "!" and ":!" commands; e.g., 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 systems, the default is set according to the value of 'shell', to
reduce the need to set this option by the user. reduce the need to set this option by the user.
On Unix it can have more than one flag. Each white space separated 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. to execute most external commands with cmd.exe.
*'shellxquote'* *'sxq'* *'shellxquote'* *'sxq'*
'shellxquote' 'sxq' string (default: "") 'shellxquote' 'sxq' string (default: "", Windows: "\"")
global global
Quoting character(s), put around the command passed to the shell, for Quoting character(s), put around the command passed to the shell, for
the "!" and ":!" commands. Includes the redirection. See the "!" and ":!" commands. Includes the redirection. See

View File

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

View File

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

View File

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

View File

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