microsoft / WSL

Issues found on WSL
https://docs.microsoft.com/windows/wsl
MIT License
17.18k stars 806 forks source link

problem passing arguments with spaces to a process running under cmd.exe #2835

Open raymod2 opened 6 years ago

raymod2 commented 6 years ago

With the Fall Creators Update I can't figure out a way to pass arguments with spaces to a process running under cmd.exe. Consider the following program compiled to args.exe:

#include <stdio.h>

int main(int argc, char **argv)
{
   int i;

   printf("\nCommand-line arguments:\n");

   for (i = 0; i < argc; i++)
   {
      printf("  argv[%d]   %s\n", i, argv[i]);
   }
}

Here is the output from a Windows shell:

C:\Users\Dan>cmd /c "z:\bin\args.exe "foo bar""

Command-line arguments:
  argv[0]   z:\bin\args.exe
  argv[1]   foo bar

C:\Users\Dan>cmd /c "z:\bin\args.exe \"foo bar\""

Command-line arguments:
  argv[0]   z:\bin\args.exe
  argv[1]   "foo
  argv[2]   bar"

Note that the inner quotes cause foo and bar to be treated as a single argument unless you escape those quotes. Here is from a WSL session:

root@DESKTOP-MVKOR9S:~# cmd.exe /c "z:\bin\args.exe "foo bar""

Command-line arguments:
  argv[0]   z:\bin\args.exe
  argv[1]   foo
  argv[2]   bar
root@DESKTOP-MVKOR9S:~# cmd.exe /c "z:\bin\args.exe \"foo bar\""

Command-line arguments:
  argv[0]   z:\bin\args.exe
  argv[1]   "foo
  argv[2]   bar"

Here foo and bar are treated as separate arguments whether you use quotes or not.

benhillis commented 6 years ago

@raymod2 - What about not wrapping the cmd.exe /c argument in quotes?

root@BENHILL-DELL:~# cmd.exe /c args.exe "foo bar"

Command-line arguments: argv[0] args.exe argv[1] foo bar

raymod2 commented 6 years ago

@benhills - The quotes are needed to prevent bash from parsing the contents of the string.

root@DESKTOP-MVKOR9S:~# cmd /c z:\bin\args.exe "foo bar"
'z:binargs.exe' is not recognized as an internal or external command,
operable program or batch file.

I could get around that here by using forward slashes (cmd /c z:/bin/args.exe "foo bar") but there are situations where the quotes cannot be removed. For instance, if you want to run two commands in sequence in the same cmd.exe process (cmd /c "echo %TMP% && ver").

therealkenc commented 6 years ago

I could get around that here by using forward slashes (cmd /c z:/bin/args.exe "foo bar")

Or just escaping.

cmd /c z:\\bin\\args.exe "foo bar"

But I see what is going on here now. This:

"z:\bin\args.exe \"foo bar\""

...is z:\bin\args.exe " concatenated with foo. That's one argument, z:\bin\args.exe "foo. Followed by a argument space delimiter, followed by bar". That (call it strange) first argument with a space in it gets picked apart because that's how cmd.exe behaves.

Your last example seems okay:

$ cmd.exe /c "echo %TMP% && ver"
C:\Users\ken\AppData\Local\Temp

Microsoft Windows [Version 10.0.17063.1000]

It looks like this is working about right, but yeah I had to double take also.

[edit] I can maybe explain it better. Your head is nesting the quotes, but neither Linux bash nor Windows cmd.exe work that way. They concatenate. "hello""world" is one argument helloworld. "hello "world"" is hello (with a trailing space) concatenated with world concatenated with the empty string.

raymod2 commented 6 years ago

The second example (cmd /c "z:\bin\args.exe \"foo bar\""), which you analyzed, works the same in a Windows shell as a Bash shell. It is the first example (cmd.exe /c "z:\bin\args.exe "foo bar"") that is problematic. It generates different (and undesirable) behavior in Bash. My last example (cmd /c "echo %TMP% && ver") works fine in Bash because there are no arguments with embedded spaces. I used it only to demonstrate that the string passed to cmd.exe must be enclosed in quotes.

The problem remains that I can't send an argument with embedded spaces to a Windows process. Below is yet another example that hopefully fully demonstrates the problem. It works in the Windows shell but not the Bash shell. I can't figure out a way to make it work in Bash. If you can then please share.

root@DESKTOP-MVKOR9S:~# cmd /c "set PATH=Z:\bin; & args.exe "foo bar""

Command-line arguments:
  argv[0]   args.exe
  argv[1]   foo
  argv[2]   bar
therealkenc commented 6 years ago

root@DESKTOP-MVKOR9S:~# cmd /c "set PATH=Z:\bin; & args.exe "foo bar""

Right; follow you now. So you escape the quotes (which you have to), but then foo bar doesn't get treated as one argument.

Your example does work as expected with OpenSSH to win32, so it looks like you have a pretty good case here.

$ ssh ken@localhost "set PATH=C:\bin; & args.exe \"foo bar\""

Command-line arguments:
  argv[0]   args.exe
  argv[1]   foo bar
TSlivede commented 6 years ago

I start with explaining what's going on:

There are 4 translation/quoting steps involved in this scenario:

  1. /bin/bash translates the given command into the executable name and an array of arguments.
    (because on *nix new processes are started with an argument array)
    (used rules: https://www.gnu.org/software/bash/manual/html_node/Quoting.html)
  2. WSL/lxss translates the array of arguments into a single string. (because on Windows, new processes are started with a single argument string)
    (used rules: AFAIK not documented, but it now correctly seems to be inverse to these rules)
  3. cmd.exe (a very old program...) does a weird string transformation, that breaks all usual behavior:
    Quote from Doc:

Processing quotation marks

If you specify /c or /k, cmd processes the remainder of String, and quotation marks are preserved only if all of the following conditions are met:

  • You do not use /s.
  • You use exactly one set of quotation marks.
  • You do not use any special characters within the quotation marks (for example: & < > ( ) @ ^ | ).
  • You use one or more white-space characters within the quotation marks.
  • The String within quotation marks is the name of an executable file.

If the previous conditions are not met, String is processed by examining the first character to verify whether it is an opening quotation mark. If the first character is an opening quotation mark, it is stripped along with the closing quotation mark. Any text following the closing quotation marks is preserved.

The German help-text is actually a little bit more clear: The phrase "closing quotation mark" is not the next quotation mark, but the last quotation mark of the string.

  1. args.exe translates the single argument string into an argument array.
    (because the C-Standard needs an argument array) (Rules depend on the compiler used to compile args.exe, however almost all compilers use these rules)

Some notes:


Currently possible solution for your last example:

User@PC:~$ cmd.exe /c set "PATH=Z:\bin;" "&" "args.exe" "foo bar"

Command-line arguments:
  argv[0]   args.exe
  argv[1]   foo bar

This works, because the first character, that cmd sees after /c is not a quotation mark, because the first argument, that WSL sees after /c does not contain a space.

Translation steps (I represent arrays as bullet-lists):
Original string:
/c set "PATH=Z:\bin;" "&" "args.exe" "foo bar" after step 1.:

after step 2.:
/c set PATH=Z:\bin; & args.exe "foo bar"
because the first char after /c is not " cmd doesn't remove any quotes and it works.


This is of course not a nice solution - but there is no really nice solution for windows executable that don't follow typical rules.

I'd hope that Microsoft provides some wrapper, that just takes one argument, and uses that as complete commandline for Windows, for example:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
   if (argc!=2){
      printf("\nPlease call with exactly one argument.\n");
   }else{
      system(argv[1]);
   }
}

With such a tool it is possible to call programs like this:

/path/to/call.exe 'commandline exactly as you would type in cmd or Win+R-Dialog'

Example output:

User@PC:~$ /mnt/d/call.exe 'cmd /c "z:\bin\args.exe "foo bar""'

Command-line arguments:
  argv[0]   z:\bin\args.exe
  argv[1]   foo bar
User@PC:~$ /mnt/d/call.exe 'cmd /c "set PATH=Z:\bin; & args.exe "foo bar""'

Command-line arguments:
  argv[0]   args.exe
  argv[1]   foo bar
TSlivede commented 6 years ago

In my last comment I provided a possible solution for cmd /c "set PATH=Z:\bin; & args.exe "foo bar"":

User@PC:~$ cmd.exe /c set "PATH=Z:\bin;" "&" "args.exe" "foo bar"

It works, because the first token of the commandline (set) doesn't contain spaces.

If the first token does contain spaces, there is is a simple workaround: Example on Windows without workaround:

C:\Windows>cmd /c ""Z:\path with space\args.exe" "arg 1" "arg 2""

Command-line arguments:
  argv[0]   Z:\path with space\args.exe
  argv[1]   arg 1
  argv[2]   arg 2

From within WSL you can't produce such a strange commandline, with unescaped quotes within a quoted argument - because except for cmd there is no windows executable, that is using such a strange syntax.

Luckily, cmd /c can be persuaded to not remove quotation marks from the commandline, such that a much more "normally" looking commandline (without unescaped quotes within a quoted argument) similar to this can be used:
cmd /c "Z:\path with space\args.exe" "arg 1" "arg 2"
This exact commandline won't work, because cmd will remove the first and last quotation mark. To "persuade" cmd to not remove ", we can simply add a single not-" char that doesn't affect the rest of the command as the first argument after /c: The letter @.
Example on Windows with workaround:

C:\Windows>cmd /c @ "Z:\path with space\args.exe" "arg 1" "arg 2"

Command-line arguments:
  argv[0]   Z:\path with space\args.exe
  argv[1]   arg 1
  argv[2]   arg 2

The same command will now also work from within bash:

User@PC:/mnt/c/Windows$ cmd.exe /c @ "Z:\path with space\args.exe" "arg 1" "arg 2"

Command-line arguments:
  argv[0]   Z:\path with space\args.exe
  argv[1]   arg 1
  argv[2]   arg 2

Because the argument @ contains no spaces, WSL won't add quotation marks and so cmd is "happy".


tl;dr

General way to call cmd /c from bash:

Example: cmd.exe /c @ "Z:\path with space\args.exe" "arg 1" "arg 2"


PS: Why would anyone want to use cmd /c to just call another executable?

raymod2 commented 6 years ago

Thanks, @TSlivede. To answer your question: you will want to use 'cmd /c' when you want to run more than one command in the same environment. In my case I ran into this issue because I wanted to call "dumpbin.exe" from the WSL command line. I had an alias that launched a cmd.exe process, called VCVARS32.BAT, and then called dumpbin.exe with the command line parameters I passed. It broke with the Fall Creator's Update.

TSlivede commented 6 years ago

Ahh, yes environment-setup-batch-files are indeed one of the few places, where cmd is still necessary. This works for me:

alias 'dumpbin=cmd.exe /c @ "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars32.bat" "&" dumpbin'

Ps: Sorry, that you had to wait this long for this sollution, I watched https://github.com/Microsoft/WSL/issues/1625 but this issue was never linked there...

TSlivede commented 6 years ago

@raymod2 Can you close this issue, or are there still problems?

raymod2 commented 6 years ago

I don't think the issue is resolved yet. At the minimum we need documentation on how WSL converts an array of arguments from bash into the string that is passed to the Windows process.

goduck777 commented 5 years ago

I think there are still problems either. I am trying to use the "@" trick to pass parameters to cmd.exe. However, if I use some variables values after cmd.exe, the results are wrong.

This one works, cmd.exe /c @ "dir" "C:\Program Files (x86)"

This one does not work, export tt='"dir" "C:\Program Files (x86)"' cmd.exe /c @ $tt

This one does not work either export tt1='dir' export tt2='C:\Program Files (x86)' cmd.exe /c @ $tt1 $tt2

Is there a way to get around it?

TSlivede commented 5 years ago

This works:

tt1='dir'
tt2='C:\Program Files (x86)'
cmd.exe /c @ "$tt1" "$tt2"

And this also works:

export tt='dir "C:\Program Files (x86)"'
WSLENV="$WSLENV":tt cmd.exe /c @ %tt%
goduck777 commented 5 years ago

@TSlivede Both work. Thank you.

FrenchPie commented 5 years ago

Hi, After reading this with interest, I have been digging further and found a "clean" solution. I leave it here for future reference.

In a batch file, if you ECHO %CMDCmdLine%, you get something that gives you the expected synthax:

C:\WINDOWS\system32\cmd.exe /c ""C:\Folders with spaces\TESTS.bat" "PARAM_TEST_STRING1" "PARAM_TEST_STRING2"" Note the double double-quotes after /C and at the end. This way, you can pass your double-quoted params.

If you run such a command from VBS, for instance, you also have to provide double quotes, which becomes a forest a double quotes since to get a double-quote inside a string (double-quotes limited) you have to double it, eg.:

Set objShellApplication = CreateObject("Shell.Application") strArgs = " ""PARAM_TEST_STRING1"" ""PARAM_TEST_STRING2"" " objShellApplication.ShellExecute "C:\WINDOWS\system32\CMD.exe", "/C """"C:\Folders with spaces\TESTS.bat"" " & strArgs & " """, "", "", 1

Have a nice day ! Have a nice day, Pierre.

odowdbrendan commented 4 years ago

Format like this "\"" + variable + "\""

Shuraken007 commented 1 year ago

I moved mine wsl to other disk. I wanted to run vscode from wsl and pass file to it. So cmd.exe takes 2 params with spaces. First argument must be escaped without quotes. Second argument must use quotes only for parts with spaces, e.g. parts between quotes

example for opening smth on windows from wsl

'/mnt/c/WINDOWS/system32/cmd.exe' /c C:\\Users\\UserName\\AppData\\Local\\Programs\\Microsoft\^ VS\^ Code\\bin\\code.cmd C:\\"Program Files"\\"Windows Defender"

example for opening smth on wsl from wsl

'/mnt/c/WINDOWS/system32/cmd.exe' /c C:\\Users\\UserName\\AppData\\Local\\Programs\\Microsoft\^ VS\^ Code\\bin\\code.cmd //wsl.localhost\\Ubuntu-20.04\\home\\user_name\\p\\Obsidian\\"Obsidian Vault"\\"more spaces"

Also I added some function for escaping all this mess, bash + perl.

command -v wslpath &> /dev/null || return 0

cmd_path='C:\WINDOWS\system32\cmd.exe'
code_path='C:\Users\UserName\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd'

alias code='run_cmd_script $code_path'

# //wsl.localhost\Ubuntu-20.04\home\user\p\Obsidian\Obsidian Vault
# to
# //wsl.localhost\\Ubuntu-20.04\\home\\user\\p\\Obsidian\\"Obsidian Vault"
escape_wsl_path() {
   echo -E $(
      echo -E $1 |\
      perl -pe '
         s#(\\+)([^\\^\s]+\s[^\\]+)#$1"$2"$3#g ;
         s#(\s")$#"#; # fix bug with extra space from first subs
         s#^\\+#//#;
         s#\\#\\\\#g
      ')
}
# C:\Users\user\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd
# to
# C:\\Users\\user\\AppData\\Local\\Programs\\Microsoft\^ VS\^ Code\\bin\\code.cmd
escape_windows_path() {
   echo -E $(
      echo -E "$1" |\
      perl -pe '
         s#\\#\\\\#g;
         s# #\\\^ #g;
      ')
}

run_via_cmd() {
   pushd /mnt/c >/dev/null
   command=$(echo -E $(echo_exe "$cmd_path") /c "$@")
   echo -E "$command"
   eval $command
   popd >/dev/null
}

run_cmd_script() {
   script_path="$(escape_windows_path "$1")"
   script_arg_path=${2-'.'}
   if [[ -e "$script_arg_path" ]]; then
      script_arg_path=$(wslpath -w "$script_arg_path")
      script_arg_path=$(escape_wsl_path "$script_arg_path")
   fi

   run_via_cmd "$script_path" "$script_arg_path" "${@:3}"
}
Shuraken007 commented 11 months ago

One more example with lot of arguments. I want to run exa, which installed on windows from WSL. Exa is ls replacement. If I run exa without cmd and try to check windows files - I got error - no access.

/mnt/c/Users/UserName/dotfiles_w/scripts$
/mnt/c/WINDOWS/system32/cmd.exe /c C:\\tools\\exa\\target\\release\\exa.exe '-I' '.git^|.vscode' '--icons' '--group-directories-first' '--color=always' '--tree' '-L2' '--all' 'C:\Users\UserName\dotfiles_w\scripts'

So, I add bash script win_exa

#!/bin/bash
set -e

win_path_to_linux() {
   printf "%s" "$(wslpath -u "$1" | perl -pe 's# #\\ #g;')"
}

escape_windows_path() {
   echo -E "$1" |\
   perl -pe '
      s#\\#\\\\#g;
      s# #\\\^ #g;
   '
}

CMD_PATH='C:\WINDOWS\system32\cmd.exe'
cmd_exe=$(win_path_to_linux "$CMD_PATH");
EXA_WIN_PATH='C:\tools\exa\target\release\exa.exe'
exa_exe=$(escape_windows_path "$EXA_WIN_PATH");

run_cmd_script() {
   local -a args=();

   for e in "$@"; do
      e="${e//'|'/'^|'}"
      e="${e//'&'/'^|'}"
      e="${e//'('/'^('}"
      e="${e//')'/'^)'}"
      if [ -e "$e"  ]; then
         e=$(wslpath -w "$e");
      fi
      args+=("'$e'");
   done;

   pushd /mnt/c >/dev/null
   eval "$cmd_exe" "/c" "$exa_exe" "${args[@]}"
   popd >/dev/null
}

run_cmd_script "$@"

And now it's easy to use exa as usual win_exa -I '.git|.vscode' --icons --group-directories-first --color=always --tree -L2 --all .