Open raymod2 opened 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
@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"
).
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.
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
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
I start with explaining what's going on:
There are 4 translation/quoting steps involved in this scenario:
/bin/bash
translates the given command into the executable name and an array of arguments.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.
args.exe
translates the single argument string into an argument array.args.exe
, however almost all compilers use these rules)Some notes:
cmd /c
in between, then step 2. and step 4. cancel each other out and the main
function of the new process gets exactly the arguments, that it should get. This was previously not always the case if "
or \
were contained in some arguments.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.:
/c
set
PATH=Z:\bin;
&
args.exe
foo bar
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
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:
/c
@
after /c
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?
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.
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...
@raymod2 Can you close this issue, or are there still problems?
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.
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?
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%
@TSlivede Both work. Thank you.
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.
Format like this "\"" + variable + "\""
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}"
}
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 .
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:
Here is the output from a Windows shell:
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:
Here foo and bar are treated as separate arguments whether you use quotes or not.