10

After using batch files for many years I was surprised to discover that the equals sign '=' is considered an argument separator.

Given this test script:

echo arg1: %1
echo arg2: %2
echo arg3: %3

and an invocation:

test.bat a=b c

the output is:

arg1: a
arg2: b
arg3: c

Why is that and how can it be avoided? I don't want the user of the script to account for this quirk and quote "a=b", which is counter-intuitive.

This batch script was run on Windows 7.

===== EDIT =====

A little more background: I encountered this problem when writing a bat file to start a Java application. I wanted to consume some args in the bat file and then pass the rest to the java application. So my first attempt was to do shift and then rebuild the args list (since %* is not affected by shift). It looked something like this, and that's when I discovered the issue:

rem Rebuild the args, %* does not work after shift
:args
if not "%1" == "" (
  set ARGS=!ARGS! %1
  shift
  goto args
)

The next step was to not use shift anymore, but rather implement shift by hand by removing one character at a time from %* until a space is encountered:

rem Remove the 1st arg if it was the profile
set ARGS=%*
if not "%FIRST_ARG%" == "%KNOA_PROFILE%" goto remove_first_done
:remove_first
if not defined ARGS goto remove_first_done
if "%ARGS:~0,1%" == " " goto remove_first_done
set ARGS=%ARGS:~1%
goto remove_first
:remove_first_done

But this is ugly and might still fail in some cases I haven't considered. So finally I decided to write a Java program to deal with the argument parsing! In my case this is fine, since I am launching a server and the penalty of an extra java invocation is minimal. It's mind-boggling what you end up doing sometimes.

You might wonder why didn't I take care of the args in the Java application itself? The answer is that I want to be able to pass JVM options like -Xmx which must be processed before invoking java.

Bogdan Calmac
  • 7,993
  • 6
  • 51
  • 64
  • 1
    Comma, semicolon and equal-sign are separators of command parameters and FOR sets. The only way to pass a separator is enclosing it in quotes: test "a=b" c – Aacini Sep 01 '11 at 20:16
  • With `enabledelayedexpansion` and the assumption that the first argument is still in %1 a rather clean solution might look like: `setlocal enabledelayedexpansion & set args=%* & if "%1" == "%KNOA_PROFILE%" ( set args=!args:%1 =! )` _split lines on `&`, I'm unable to enter multi line comments_ – eremmel Aug 23 '16 at 10:27

5 Answers5

6

I'm guessing it does this so that /param=data is the same as /param data

I don't know any tricks to fix that stupid (probably by design) parsing issue but I was able to come up with a super ugly workaround:

@echo off
setlocal ENABLEEXTENSIONS
set param=1
:fixnextparam
set p=
((echo "%~1"|find " ")>nul)||(
    call :fixparam %param% "%~1" "%~2" %* 2>nul
)
if "%p%"=="" (set "p%param%=%1") else shift
shift&set /A param=%param% + 1
if not "%~1"=="" goto fixnextparam

echo.1=%p1%
echo.2=%p2%
echo.3=%p3%
echo.4=%p4%
echo.5=%p5%
goto:EOF


:fixparam
set p%1=
for /F "tokens=4" %%A in ("%*") do (
    if "%%~A"=="%~2=%~3" set p=!&set "p%1=%%A"
)
goto:EOF

When I execute test.cmd foo=bar baz "fizz buzz" w00t I get:

1=foo=bar
2=baz
3="fizz buzz"
4=w00t
5=

The problem with this is of course that you cannot do %~dp1 style variable expansion. It is not possible to do call :mylabel %* and then use %1 either because call :batchlabel has the same parameter parsing problem!

If you really need %~dp1 handling you could use the WSH/batch hybrid hack:

@if (1==1) @if(1==0) @ELSE
@echo off
@SETLOCAL ENABLEEXTENSIONS
if "%SPECIALPARSE%"=="*%~f0" (
    echo.1=%~1
    echo.2=%~2
    echo.3=%~3
    echo.4=%~4
    echo.5=%~5
) else (
    set "SPECIALPARSE=*%~f0"
    cscript //E:JScript //nologo "%~f0" %*
)
@goto :EOF
@end @ELSE
w=WScript,wa=w.Arguments,al=wa.length,Sh=w.CreateObject("WScript.Shell"),p="";
for(i=0;i<al;++i)p+="\""+wa.Item(i)+"\" ";
function PipeStream(i,o){for(;!i.AtEndOfStream;)o.Write(i.Read(1))}
function Exec(cmd,e){
    try{
        e=Sh.Exec(cmd);
        while(e.Status==0){
            w.Sleep(99);
            PipeStream(e.StdOut,w.StdOut);
            PipeStream(e.StdErr,w.StdErr);
        }
        return e.ExitCode;
    }catch(e){return e.number;}
}
w.Quit(Exec("\""+WScript.ScriptFullName+"\" "+p));
@end
Community
  • 1
  • 1
Anders
  • 97,548
  • 12
  • 110
  • 164
2

I am only answering this: >> Why is that and how can it be avoided?

My suggestion: To use a better language. No i am not joking. batch has too many quirks/nuances such as this plus a lot of other limitations, its just not worth the time coming up with ugly/inefficient workarounds. If you are running Windows 7 and higher, why not try using vbscript or even powershell. These tools/language will greatly help in your daily programming/admin tasks. As an example of how vbscript can propely take care of such an issue:

For i=0 To WScript.Arguments.Count-1
    WScript.Echo WScript.Arguments(i)
Next

Output:

C:\test>cscript //nologo myscript.vbs a=b c
a=b
c

Note that it properly takes care of the arguments.

ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • vbscripts (WSH) work out of the box on Win98+ (or install IE5 on Win95). The problem is of course that you have to prefix all commands with "cscript //nologo ". – Anders Aug 31 '11 at 03:05
  • not really a problem, calling "cscript //nologo" is like calling any other command line tools. example, `ping -c 1 127.0.0.1` ... Further, all the tasks that needs to be done can be written as one whole vbscript program if OP desired. There is no need to call multiiple cscript (of course, not in the case when you need to call many vbs files one after another ) – ghostdog74 Aug 31 '11 at 03:34
2
@echo off
setlocal enabledelayedexpansion

if %1 neq ~ (
 set "n=%*"
 for %%a in ("!n: =" "!") do (
 set "s=%%a"
 if !s:~0^,2!==^"^" (
   set "s=!s:" "= !"
   set "s=!s:""="!"
)
 set "m=!m! !s!"
 )
 %0 ~ !m!
)
endlocal
shift

echo arg1: %~1
echo arg1: %~2
echo arg1: %~dp3
exit /b

c:> untested.bat a=b c "%USERPROFILE%"
walid2mi
  • 2,704
  • 15
  • 15
1

@jeb: here another method

untested.cmd

@echo off
setlocal enabledelayedexpansion

set "param=%*"
set param="%param: =" "%"

for /f "delims=" %%a in ('echo %param: =^&echo.%') do (
  set "str=%%a"
  if !str:~0^,2!==^"^" (
     set "str=!str:^&echo.=!"
     set "str=!str:"^"= !"
     set str="!str:~1,-1!"
  )
  set "m=!m! !str!"
)

Call :sub %m%
endlocal & goto :eof

:sub
 echo arg1: %~1
 echo arg2: %~2
 echo arg3: %~3
 echo arg4: %~4
goto :eof
walid2mi
  • 2,704
  • 15
  • 15
  • +1, :-) also nice but fails now with a simple `"one=1" two=2` -- arg1=`one 1` (the = is missing) arg2 and arg3 are empty arg4=`two=2`. I suppose something similiar to [Pretty print, how to split..](http://stackoverflow.com/questions/5471556/pretty-print-windows-path-variable-how-to-split-on-in-cmd-shell/5472168#5472168) could work. Using the parser to separate between unquoted and quoted text – jeb Sep 05 '11 at 20:13
0

Again, I can't explain the unexpected behavior, but there is a very simple solution. Just change your argument references, then quote the argument when invoking as such:

test.bat script:

echo arg1: %~1
echo arg2: %~2
echo arg3: %~3

Invoke:

test.bat "a=b" c

Output:

arg1: a=b
arg2: c
arg3:

Note that referencing the arguments with a tilde (~) removes the leading/trailing quotes from the argument.

tahoar
  • 1,788
  • 3
  • 20
  • 36
  • Exactly this is not the solution. From the OP 'I don't want the user of the script to account for this quirk and quote "a=b", which is counter-intuitive.' – jeb May 22 '15 at 07:38
  • I missed that part, but not all intuitions are equal. To me, reading instructions is "intuitive" (RTFM i.e. cmd.exe /?). It explains how cmd.exe parses command line quotes but no "=" sign explanation. This question helped me identify this issue when troubleshooting command strings created in Perl. Complicated, non-intuitive parser code is a batch file solution, but it does little to teach the fundamental reality of cmd.exe's underlying design. Once you understand that, the simple and proper quote solution applies to all scripting shells that call cmd.exe including Batch, Perl, Python, etc. – tahoar Jun 04 '15 at 00:26