foolproof counting of arguments

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

foolproof counting of arguments

#1 Post by Liviu » 22 Jan 2012 12:27

Is there a way to reliably get the count of arguments a batch file was called with, safe against odd inputs, and preferably without invoking external programs or temp files?

Some test cases which fail most naive counting approaches would be...
  • " --- expected count 1
  • "" a --- 2
  • a " b --- 2
...and others involving the usual poison characters.

Thanks,
Liviu

Squashman
Expert
Posts: 4106
Joined: 23 Dec 2011 13:59

Re: foolproof counting of arguments

#2 Post by Squashman » 22 Jan 2012 13:57

Don't think there is much you could do about a string not being closed quoted correctly. If you just have a set of empty quotes then you can check to see if it is empty.
So given your example of "" a ---
This code would correctly count it as 2.

Code: Select all

@echo off
set count=0
for %%I in (%*) do IF NOT "%%~I"=="" set /a count+=1
echo %count%
Last edited by Squashman on 22 Jan 2012 14:09, edited 1 time in total.

aGerman
Expert
Posts: 3643
Joined: 22 Jan 2010 18:01
Location: Germany

Re: foolproof counting of arguments

#3 Post by aGerman » 22 Jan 2012 14:08

Liviu wrote:Is there a way to reliably get the count of arguments a batch file was called with, safe against odd inputs, and preferably without invoking external programs or temp files?

I'm afraid the answer is NO.
REM is the only command that is able to catch the arguments %* as is.

Regards
aGerman

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: foolproof counting of arguments

#4 Post by Liviu » 22 Jan 2012 15:36

Squashman wrote:Don't think there is much you could do about a string not being closed quoted correctly. If you just have a set of empty quotes then you can check to see if it is empty.
So given your example of "" a ---

I should have made it more clear that the "---" was part of my comment, not part of the arguments. So for example I expect "" a --- to be counted as 3 arguments, and "" a as just 2. Sorry for the confusion.

aGerman wrote:I'm afraid the answer is NO.
REM is the only command that is able to catch the arguments %* as is.

I am aware of the REM redirection trick to get %*. In fact, I was wondering if I could get the count in advance, so that I could then get the individual %1, %2 etc arguments as well in one shot, as opposed to (re)using a temp file for each.

Guess my original question can be reduced to how to check whether %1 is present or not, in a way safe against odd input like single ", ^", ^& etc. Since that's a lighter requirement than literally retrieving %1 itself, I was hoping there might be some easier workaround.

Thanks,
Liviu

aGerman
Expert
Posts: 3643
Joined: 22 Jan 2010 18:01
Location: Germany

Re: foolproof counting of arguments

#5 Post by aGerman » 22 Jan 2012 16:11

I'm not sure whether this would work in each case:

Code: Select all

set "x=%*#"

setlocal EnableDelayedExpansion
if "!x:~1,1!"=="" (endlocal &set "x=0") else (endlocal &set x=1)

echo %x%

Regards
aGerman

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: foolproof counting of arguments

#6 Post by Liviu » 22 Jan 2012 16:50

aGerman wrote:I'm not sure whether this would work in each case:

Nice! and appears to be doing just what I was looking for, thank you.

Side comment: it's even kind of obvious in hindsight now, isn't it ;-) Guess I didn't think at it becaused of the reflex don't set "x=%1" when %1 may contain quotes or unquoted &<>|etc since it's going to be a pain to use later. Of course, just checking that 'x' is empty or not happens to be one case when those special characters don't matter.

Variation on the same theme...

Code: Select all

set "x=%1"
setlocal EnableDelayedExpansion
if defined x (endlocal &set x=1) else (endlocal &set x=0)
echo %x%

The above looks at %1 instead of %*. Off topic somewhat, but it is possible that %1 is not present, but %* is still not empty, for example when passing a separators-only command line such as ;,=.

Thanks,
Liviu

aGerman
Expert
Posts: 3643
Joined: 22 Jan 2010 18:01
Location: Germany

Re: foolproof counting of arguments

#7 Post by aGerman » 22 Jan 2012 17:26

Liviu wrote:The above looks at %1 instead of %*. Off topic somewhat, but it is possible that %1 is not present, but %* is still not empty, for example when passing a separators-only command line such as ;,=.

Yeah, that's exactly the reason why I used %* instead. (You told that you need to look at all odd "poison" characters :wink:)
I'm not sure whether you need the hash character. I used it to make sure that a caret would not escape the quotation mark but it was useless in my tests...

Regards
aGerman

aGerman
Expert
Posts: 3643
Joined: 22 Jan 2010 18:01
Location: Germany

Re: foolproof counting of arguments

#8 Post by aGerman » 22 Jan 2012 18:20

Edit:
It's still not exactly foolproof :(

Example argument (jeb):
one^&two"&three

simplified:
"& or "&"

It however detects the character(s) in front of &. For that reason it would probably be sufficient to redirect the error message of the SET line. But it is potentially dangerous since the command in argument
"& command &"
would be executed :!:

Regards
aGerman

dbenham
Expert
Posts: 2261
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: foolproof counting of arguments

#9 Post by dbenham » 22 Jan 2012 20:02

Not only is it susceptible to command injection, it can also completely miss the parameter.

"one&rem " (quotes included) will be reported as no argument, and it executes the REM command. The one is stripped because it occurs after the last quote, and the rem command could just as easily be something destructive.

I really don't think it is possible to safely test for 1 or more arguments without using the REM echoed to a file trick.

Here is a clean looking script that safely loads all of the arguments into an arg "array" using the REM trick. There is a space at the end of the temp file that protects against stripping trailing control characters from the argument. I think the only limitation is each argument must be <=1015 bytes long.

Code: Select all

@echo off
setlocal enableDelayedExpansion
set argCnt=1
:getArgs
>"%temp%\getArg.txt" <"%temp%\getArg.txt" (
  setlocal disableExtensions
  set prompt=#
  echo on
  for %%a in (%%a) do rem . %1.
  echo off
  endlocal
  set /p "arg%argCnt%="
  set /p "arg%argCnt%="
  set "arg%argCnt%=!arg%argCnt%:~7,-2!"
  if defined arg%argCnt% (
    set /a argCnt+=1
    shift /1
    goto :getArgs
  ) else set /a argCnt-=1
)
del "%temp%\getArg.txt"
set arg


Edited to guard against following corner cases:
/?
^ at end (entered as ^^)
%a
%~a %~dpa %~path:a etc.


Dave Benham
Last edited by dbenham on 27 Jan 2012 13:35, edited 6 times in total.

aGerman
Expert
Posts: 3643
Joined: 22 Jan 2010 18:01
Location: Germany

Re: foolproof counting of arguments

#10 Post by aGerman » 22 Jan 2012 20:12

I agree with Dave.

That's my attempt:

Code: Select all

setlocal DisableDelayedExpansion &prompt $
>"%temp%\x.txt" (
  echo on &for %%i in (1) do rem %*#
)
@prompt &endlocal &setlocal EnableDelayedExpansion &echo off
<"%temp%\x.txt" (for %%i in (1 2) do set /p "x=")
del "%temp%\x.txt"
if "!x:~4,-2!"=="" (endlocal &set x=0) else (endlocal &set x=1)

echo %x%

The hash character is to protect you from the multi line effect with ^.

Regards
aGerman

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: foolproof counting of arguments

#11 Post by Liviu » 22 Jan 2012 20:36

aGerman wrote:It's still not exactly foolproof :(

Example argument (jeb):
one^&two"&three

simplified:
"& or "&"

Thanks for that. Somehow thought it couldn't be that easy ;-) And it takes down any following parameters as well.

Now, I don't think that'd be much of a problem in my particular use-case, though it's surely disappointing in a more academical sense. What I am actually after is basically "tokenize" the _beginning_ of the %* line in order to simulate an %* "shift" (since the built-in one only shifts the individual %1, %2, etc arguments, but not %* itself). Suppose I wanted to write a simple loggedrun.cmd which would save one line per run to a designated logfile, to be invoked like

loggedrun «logfile» «program-to-run» «program-arguments»

In a different better world, loggedrun.cmd could contain just

@setlocal
@set "logfile=%~1"
@rem _but_ 'shift' doesn't affect %*
@shift
@echo >>"%logfile%" %date% %time% starting: %*
@%*
@echo >>"%logfile%" %date% %time% done: %*
@endlocal

But of course this wouldn't work as such in the cmd world. Firstly, shift doesn't update %*, and then cmd has its own parsing rules which are different from everybody else's. In my example, what comes before «program-to-run» is obviously intended for the batch file, and would follow the cmd rules. However the «program-arguments» part would be under the control of the target program command-line-parsing logic, and it would need to be literally passed as such by the batch file.

Hope here was that combining the REM redirection trick for %*/%1/etc with some time-saving trick to extract the first "cmd-compliant" arguments would then allow me to remove those parts from %* and pass the rest thorugh unchanged.

Since my particular case only involves the first parameters which are largely under my/user's control, I could rule out "pathological" values for those parameters, and make it work under that assumption. That said, it would obviously be more "academically satisfying" if there was a general solution to that, more efficient than one temp file per %1/%2/etc argument.

Thanks again for all responses,
Liviu

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: foolproof counting of arguments

#12 Post by Liviu » 22 Jan 2012 22:18

dbenham wrote:Here is a clean looking script that safely loads all of the arguments into an arg "array" using the REM trick.

Not sure I fully follow the part about exiting and reentrying the doubly redirected () block, yet, but it looks as if it's internally equivalent to individual per-argument redirections. It's working otherwise, thank you, only nitpick is that "rem %1" should probably be "rem [ %1 ]" or such (and the ~5,-1 offsets adjusted accordingly) in order to handle oddities like /?, ^^.

Thanks,
Liviu

dbenham
Expert
Posts: 2261
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: foolproof counting of arguments

#13 Post by dbenham » 22 Jan 2012 23:09

Now I understand why jeb had that # in his REM statement, to protect against /?

I've edited my previous post to compensate.

Thanks

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: foolproof counting of arguments

#14 Post by Liviu » 24 Jan 2012 00:42

dbenham wrote:Now I understand why jeb had that # in his REM statement, to protect against /?

I've edited my previous post to compensate.

Pretty sure I've seen this noted before, though I can't find a reference handy now... One other corner case is that the 'for' loop would save the incorrect %1 when passed %a or similar as an argument, presumably because of some loop variable name clash. Didn't look into it much yet, but for whatever reason replacing it with

Code: Select all

for %%a in (%%a) do rem . %1.

appears to work better under xp.sp3 at least. Again, not fully understood or tested on my part.

Liviu

dbenham
Expert
Posts: 2261
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: foolproof counting of arguments

#15 Post by dbenham » 24 Jan 2012 07:55

Thanks

You were correct - an argument of %a was being reported as 1.

Code has been edited

Dave Benham

Post Reply