Vim's system('string') interface is not always straightforward in Windows,
its behaviour can be downright unexpected involves an intricate set of options. Its primary purpose its to execute
a command in the shell (see
&shell) in simple terms we can think that calling
system('some command') is equivalent
to spawning a process with the following arguments
[&shell, &shellcmdflag, 'some command']
and sometimes this might be true but it also might not. Consider the following examples (I'm running in gVim 7.4/Windows 8)
set shell=powershell shellquote=\" shellpipe=\| shellredir=> set shellcmdflag=-Command let &shellxquote=' ' " this works echo system('echo a') " this is a valid powershell expression and should print the line " a b " instead it prints them on separate lines echo system('echo "a b"')
To help figure out what is going on I've grabbed the printargs-test.exe binary from the Neovim functional tests, it prints the command line arguments it gets, separated by
" this a test program used by neovim to debug command arguments set shell=c:\msys64\home\dummy\neovim\build\bin\printargs-test.exe " this one prints arg1:-Command;arg2:echo;arg3:a; echo system('echo a') " this one prints arg1:-Command;arg2:echo;arg3:a b; echo system('echo "a b"')
The first command works but it does not produce the expected command arguments it is called as
[&shell, -Command, echo, a]
The second case is even more surprising
[&shell, -Command, echo, a b]
and notice that the double quotes
"a b" were lost, those were a meaningful part of the powershell expression. That is why the output does not match what we expect.
To understand what happened you first need to know that in windows when spawning a process, arguments are represented as a string. This makes it dificult to know where one argument stops and the other begins, but there a convention for this documented here, I assume most modern programs follow it by using this function but the cmd.exe shell does not.
" this is what we wanted arg1:-Command;arg2:echo "a b" echo system('"echo \"a b\""')
[&shell, -Command, echo "a b"]
Actually my initial examples were a bit disonest, since they went straight for powershell. To understand why Vim does it this way you need to understand that UNIX systems have ways to handle argv correctly, but since in Vim in Windows works mostly around cmd.exe, then it makes sense that
system() does not immediately work with other shells. The specific problem in Windows is that system() not only needs to build a valid shell command it also needs to build a valid command according to whatever convention your shell uses, and cmd.exe is different from other windows programs.
Another way to put it is that in windows, shell command construction gets mixed up with process argument construction. There are several historical reasons for this, for example Vim redirects process output to temporary files when calling system().
Windows argument quoting according to convention
Vim has several options related to shell invocation
These help you define how system calls the shell process. For example as a test, I've tried to follow up the previous examples with this
" this is one step closer to correct quoting, but it only works in Neovim " in Vim it fails to open the redirect tmp file set shellxquote=\" echo system('echo \"a b\"')
I don't think we can implement proper quoting through the Vim options alone, AFAIK the only option that influences the contents of the string passed to
shellxescape but this option is only used if
shellxquote=( which we don't really want for this case.
Here is an attempt to write a function to build process arguments in vimscript. It definitely DOES NOT work for cmd.exe, it is meant for conformant shells in windows. It basically does in vimscript what libuv did for Neovim prior to #6359.
" ported from libuv/quote_cmd_arg - quotes a single argument for calling a " process. This works well for quoting arguments for windows shells that " follow the expected argument convention (cmd.exe DOES NOT), at least " assuming that you want to pass your entire command as single argument to " your shell e.g. powershell -Command <...Shell cmd> " " Usage: " QuoteW32Arg(str) " QuoteW32Arg(str, 0) " " If the optional second argument is not 1 it disables wrapping in double quotes. " " NOTE: its unclear to me if this clashes with &shellquote=" when used as " system(QuoteW32Arg('...)) function! QuoteW32Arg(arg, ...) let wrap = (a:0 >= 1) ? a:1 : 1 if strlen(a:arg) == 0 " empty arguments use double quotes return (wrap == 1) ? '""' : '' endif if a:arg !~ '"' && a:arg !~ "\t" && a:arg !~ ' ' " no quotation needed return a:arg endif if a:arg !~ '\' && a:arg !~ '"' " no inner double quotes or backslashes, wrap in double quotes return (wrap == 1) ? '"'.a:arg.'"' : a:arg endif let revcmd = reverse(split(a:arg, '.\zs')) let target = '' let quote_hit = 1 for c in revcmd let target = target . c if quote_hit == 1 && c == '\' " double backslash let target = target . '\' elseif c == '"' let quote_hit = 1 " double quote let target = target . '\' else let quote_hit = 0 endif endfor let result = join(reverse(split(target, '.\zs')), '') return (wrap == 1) ? '"'.result.'"' : result endfunction let v:errors =  call assert_equal('"hello\"world"', QuoteW32Arg('hello"world')) call assert_equal('hello\"world', QuoteW32Arg('hello"world', 0)) call assert_equal('"hello\"\"world"', QuoteW32Arg('hello""world')) call assert_equal('hello\world', QuoteW32Arg('hello\world')) call assert_equal('hello\\world', QuoteW32Arg('hello\\world')) call assert_equal('"hello\\\"world"', QuoteW32Arg('hello\"world')) call assert_equal('"hello\\\\\"world"', QuoteW32Arg('hello\\"world')) call assert_equal('"hello world\\"', QuoteW32Arg('hello world\')) call assert_equal('""', QuoteW32Arg('')) call assert_equal('', QuoteW32Arg('', 0)) call assert_equal('"hello world"', QuoteW32Arg('hello world')) call assert_equal('hello world', QuoteW32Arg('hello world', 0)) for err in v:errors echoerr err endfor