[tip] ulimited args & subargs with spaces & any order in bat

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
Voodooman
Posts: 12
Joined: 25 Sep 2011 07:45

[tip] ulimited args & subargs with spaces & any order in bat

#1 Post by Voodooman » 15 Aug 2012 20:04

Was experimenting with arguments for a long time, reed things about shift, but everything was useless for my task.
All methods i reed about have these limitations (not all of them at once, mind you):
*Order of arguments fixed, put them in random order and arguments ruined
*no subarguments (not sure how this called officialy but call that extra parameters of specific arguments like that)
*no way to avoid spaces to be considered as delimeters
*very messy, unreadable and hard to understand code
*very limited total number of arguments
*random or invalid arguments could cause mess

So after many personal expirements i managed to bypass all of these limitations and create my technique that works like charm and even kids can understand it.

Example of exact part code from one of my scripts, that i made for DXHR to use with command line dll injector:

Code: Select all

@echo off
@title Loading
@setlocal enabledelayedexpansion

:Init
rem processing arguments
for %%A in (%*) do (
   set "TmpArg=%%A"
   echo TmpArg=!TmpArg!
   if /i "!TmpArg:~0,6!"=="/AppId" (
      set "AppId=!TmpArg:~7,-1!"
      echo Appid=!appid!
      echo ----
   )
   if /i "!TmpArg:~0,8!"=="/AppName" (
      if /i "!TmpArg:~8,1!"=="[" (
         if /i not "!TmpArg:~-1,1!"=="]" (
            set "AppName=!TmpArg:~9!"   
            echo AppName=!appname!
            echo ----
            set BrokenAppNameMode=1
         ) else (
            set "AppName=!TmpArg:~9,-1!"
            echo AppName=!appname!
            echo ----
            set BrokenAppNameMode=0
         )
      )
   )
   if /i !BrokenAppNameMode!==1 (
      if /i not "!TmpArg:~0,1!"=="/" (
         if /i not "!TmpArg:~-1,1!"=="]" (
            set "AppName=!AppName! !TmpArg!"
            echo AppName=!appname!
            echo ----
            set BrokenAppNameMode=1
         )
      )
      if /i not "!TmpArg:~0,1!"=="/" (
         if /i "!TmpArg:~-1,1!"=="]" (
            set "AppName=!AppName! !TmpArg:~0,-1!"
            echo AppName=!appname!
            echo ----
            set BrokenAppNameMode=0
         )
      )
   )
   if /i "!TmpArg:~0,8!"=="/ExeName" (
      if /i "!TmpArg:~8,1!"=="[" (
         if /i not "!TmpArg:~-1,1!"=="]" (
            set "ExeName=!TmpArg:~9!"   
            echo ExeName=!ExeName!
            echo ----
            set BrokenExeNameMode=1
         ) else (
            set "ExeName=!TmpArg:~9,-1!"
            echo ExeName=!ExeName!
            echo ----
            set BrokenExeNameMode=0
         )
      )
   )
   if /i !BrokenExeNameMode!==1 (
      if /i not "!TmpArg:~0,1!"=="/" (
         if /i not "!TmpArg:~-1,1!"=="]" (
            set "ExeName=!ExeName! !TmpArg!"
            echo Name=!ExeName!
            echo ----
            set BrokenExeNameMode=1
         )
      )
      if /i not "!TmpArg:~0,1!"=="/" (
         if /i "!TmpArg:~-1,1!"=="]" (
            set "ExeName=!ExeName! !TmpArg:~0,-1!"
            echo Name=!ExeName!
            echo ----
            set BrokenExeNameMode=0
         )
      )
   )
   if /i "!TmpArg:~0,8!"=="/ApiName" (
      set "ApiName=!TmpArg:~9,-1!"
      echo ApiName=!apiname!
      echo ----
   )
   if /i "!TmpArg!"=="/test" (
      set TestMode=1
      echo TestMode=!TestMode!
      echo ----
   )
   if /i "!TmpArg!"=="/test2" (
      set Test2Mode=1
      echo Test2Mode=!Test2Mode!
      echo ----
   )
   rem cleaning temp argument, not neccessary now, but it was in prev version of script
   set "TmpArg="
)
title !appname! launcher [!appid!] [!exename!] [!apiname!]

rem debug info
echo ----
echo CD = %CD%
echo ----
echo SELF = %0
echo ----
echo CMD = %cmdcmdline%
echo ----
echo RawArgs = %*
echo ----
echo Id = !appid!
echo ----
echo Name = !appname!
echo ----
echo Exe = !exename!
echo ----
echo Api = !apiname!
echo ----
pause
rem rest of code remove from this example
call :MainFunction
exit /b 0


Lets run this bat, this is what we have:

----
CD = C:\Users\home\Documents\SGL
----
SELF = "C:\Users\home\Documents\SGL\testargs.bat"
----
CMD = cmd /c ""C:\Users\home\Documents\SGL\testargs.bat" "
----
RawArgs =
----
Id =
----
Name =
----
Exe =
----
Api =
----
Для продолжения нажмите любую клавишу . . .


Not much, because we have no arguments.

Lets start it with some random mumbo-jumbo args from another bat

Code: Select all

call testargs.bat  agsasga klhkbibo awibawbf /WMafawb ibwbdiaw sfasvcnkl 151252hin dfa[pk  agas,weeh twwta. asg...   fasgasg_fsg cakeislie agasga agsasga


Code: Select all

TmpArg=agsasga
TmpArg=klhkbibo
TmpArg=awibawbf
TmpArg=/WMafawb
TmpArg=ibwbdiaw
TmpArg=sfasvcnkl
TmpArg=151252hin
TmpArg=dfa[pk
TmpArg=agas
TmpArg=weeh
TmpArg=twwta.
TmpArg=asg...
TmpArg=fasgasg_fsg
TmpArg=cakeislie
TmpArg=agasga
TmpArg=agsasga
----
CD = C:\Users\home\Documents\SGL
----
SELF = testargs.bat
----
CMD = cmd /c ""C:\Users\home\Documents\SGL\testargs_launch.bat" "
----
RawArgs = agsasga klhkbibo awibawbf /WMafawb ibwbdiaw sfasvcnkl 151252hin dfa[pk
  agas,weeh twwta. asg...   fasgasg_fsg cakeislie agasga agsasga
----
Id =
----
Name =
----
Exe =
----
Api =
----
Для продолжения нажмите любую клавишу . . .


Now we can see every piece of cake delimited by space as TmpArg, but they are all ignored, as they dont match any of if conditions we expect to see.

And lets add proper args in a middle of this jumbo and see what we can get:

Code: Select all

TmpArg=agasga
TmpArg=agsasga
TmpArg=/exename[dxhr.exe]
ExeName=dxhr.exe
----
----
CD = C:\Users\home\Documents\SGL
----
SELF = testargs.bat
----
CMD = cmd /c ""C:\Users\home\Documents\SGL\testargs_launch.bat" "
----
RawArgs = agsasga klhkbibo /appname[Deus Ex: Human Revolution] awibawbf /WMafawb
 ibwbdiaw /appid[28050] sfasvcnkl  /apiname[d3d9] 151252hin dfa[pk  agas,weeh tw
wta. asg...   fasgasg_fsg cakeislie agasga agsasga /exename[dxhr.exe]
----
Id = 28050
----
Name = Deus Ex: Human Revolution
----
Exe = dxhr.exe
----
Api = d3d9
----
Для продолжения нажмите любую клавишу . . .


Oh this simple code managed to find proper arguments in middle of random code and take subarguments even with spaces. Isnt it great?

And now lets get only proper args in different order:

Code: Select all

TmpArg=/apiname[d3d9]
ApiName=d3d9
----
TmpArg=/appid[28050]
Appid=28050
----
TmpArg=/exename[dxhr.exe]
ExeName=dxhr.exe
----
TmpArg=/appname[Deus
AppName=Deus
----
TmpArg=Ex:
AppName=Deus Ex:
----
TmpArg=Human
AppName=Deus Ex: Human
----
TmpArg=Revolution]
AppName=Deus Ex: Human Revolution
----
TmpArg=/test
TestMode=1
----
TmpArg=/blah
TmpArg=/dummy
----
CD = C:\Users\home\Documents\SGL
----
SELF = testargs.bat
----
CMD = cmd /c ""C:\Users\home\Documents\SGL\testargs_launch.bat" "
----
RawArgs = /apiname[d3d9] /appid[28050] /exename[dxhr.exe] /appname[Deus Ex: Huma
n Revolution] /test /blah /dummy
----
Id = 28050
----
Name = Deus Ex: Human Revolution
----
Exe = dxhr.exe
----
Api = d3d9
----
Для продолжения нажмите любую клавишу . . .


Everything just as expected, notticed how name composed of few TmpArgs?
Thats the best part of code!
Advanced users on this forums probably no need extra explaination know most of i wrote (however i have found here any method how to get arg with spaces and glue pieces of args together, so maybe its something really new), but rest of users needs some explaination:

1) We need @setlocal enabledelayedexpansion , period.
2) We need to ignore 9 default arguments separated by windows and shift and separate entire line of raw arguments %* with simple FOR cycle.
This will allow us to use unlimited number of args in any order, as we dont need to use indexed args.
FOR cycle separate raw single line or arguments into non indexed chunks delimited by space and by "," , we should write this chunk into Temporary arg variable just for this cycle !TmpArg! its importan to dp that, as %%A like vars of FOR loop dont allow to extract substings that we would need later.
3) We need to process temp arg and check if its match any desired condition, in case if match we should set flag like 0 or 1 to use later in main function or we could read subargument and write its value as variable.
Syntax matters.
4) So, to check args that have subargs, we need to check if fixed number of symbols from tmparg matches expected arg name.
For example /AppId - Expected name have 6 symbols , but our actual argument is quite longer as its /AppId[2805], so wee need to compare substring of TmpArg which will extract only 1st 6 symbols, do it we need to use !TmpArg:~0,6! instead of !TmpArg!.
Then it matches, we need to set subarg as variable, we know that 1st 6 symbols just a fixed name, symbol 7 and last symbol are just our custome delimiters, for we need to extract substring of same variable by skipping 1st 7 symbols and 1st from end, and set it as variable !TmpArg:~7;-1".
Same logic applies to other arguments with subargs, unti we met one that could possible have some spaces.

5) Understanding logic described above, we need to deal somehow with unavoidable separation of subargument into chunks by space that considered as delimiter.
Unfortunatelly simple FOR do not allow to set custom delimiters and processin SPACE as delimiter.
For you may think that usage of FOR /F could be good idea, as it process entire line and allows to set custom delims= ?
Wrong! Syntax of /f could be a mess, and number of tokens is very limited, and they are all indexed, and we symply dont know what to expect from exact indexed token so we should forget about for /f and custom tokens!
My idea is to let FOR to separate subargs into chunks using space as delimiter and to compose these chunks back together in a few loops. To do this we need to detect first chunk, middle chunk and last chunks and melt them together.
But how? Well i managed to create pretty simple and effect solution: after we detected name of argument, we need to make sure that there is an opening braket right after, with if condition like this:

Code: Select all

 if /i "!TmpArg:~8,1!"=="[" (

this will give us idea that we dealing with 1st chunk of separated subargument, but we must emidatle check if its really separate of its full, so inside that condition we need to put this one:

Code: Select all

if /i not "!TmpArg:~-1,1!"=="]" (

we need to make sure that its not ending by closing braket.
ANd it its edning by it (else) we should consider subargument as full. But it doesnt end by ] we must set a flag for next cycle to let it know that we just started composing full argument from chunks by this

Code: Select all

set BrokenAppNameMode=1 

and in opposite case if name already full we need to set this

Code: Select all

set BrokenAppNameMode=0

Now we need to write 1st chunk in our !AppName! variable, yet again by extracting substing to clear argname and opening braket by this

Code: Select all

set "AppName=!TmpArg:~9!"   

soon after should be condition that checks if we are in this "broken name mode"

Code: Select all

if /i !BrokenAppNameMode!==1 (

and obviosly it will be true as we just set this value to 1, but we dont to avoid using anything inside this condition now, in 1st chunk of name, to avoid merge of dublicate parts of string.
So we should put this inside

Code: Select all

if /i not "!TmpArg:~0,1!"=="/" (

this will check if we currently have arg name (which indicats its a 1st chunk) and we know its starts with / so we need to make sure that nothing starts with /, this will give us 1st clue that we dealing with middle part and will come handy in next cycle, where TmpArg will have that middle part. Also i must not that 1st part condition will be ignored in next cycle as middle parts will never have /appname as 1st 8 symbols, so we dont need to worry about this.
What we should worry about is that 2nd clue that makes us 100% we dealing with middle part, and we could do that by making sure we do not have ] symbol in the end:

Code: Select all

if /i not "!TmpArg:~-1,1!"=="]" (

now when we know this is middle part, we need to merge it with 1st part, this is simple

Code: Select all

set "AppName=!AppName! !TmpArg!"
(note the space between vars, and cunstruction that similar to typical N=N+1, for which we need delayed expansions )
this will also work for unlimited number of middle parts, as they all will match same conditions and will definetly goe in proper order, one after another, so there is no need to worry about order or number of separated chunks.
But we should yet again set BrokenAppNameMode=1, to make next cycle to be aware that we aint yet finished to merge subargument value.
And no its time to check if we have last chunk, but keep in mind that this condition should be insaide same condition where check if broken mode is 1 or anything else.
To get idea that we dealing with last chunk, we need yet again to deal with 2 clues, again we should check it doesnt start with / symbol by this

Code: Select all

if /i not "!TmpArg:~0,1!"=="/" (

and check for closing braket in the end by this

Code: Select all

if /i "!TmpArg:~-1,1!"=="]" (

and this time when we set name, we should remov one last symbol

Code: Select all

set "AppName=!AppName! !TmpArg:~0,-1!"

so now we done, name is merged, and we need to let next cycle to know that and make it skip entire check for middle and last chunks, so we just set BrokenAppNameMode=0.
And thats it, we have full name again.
My current syntax is this i consider its the most intuitive and safe
/arg[sub arg] /arg
but you can change it to this
-arg(sub arg) -arg
if you like,
you can even make a even deeper layer of subsubarg like this
/arg[subarg(subsubarg)]
or this
-arg[subarg:subsubarg]
just make sure you have unique symbols that will help you to logically separate arguments and will let you have good idea whe is 1st, middle and last part. Oh yeah, i do not suggest you play with "" as thye have equal symbols and you can tell which one opens and closes something, but you can put them insde if you need [].
Also there are probles with "," as they also considered as delims by default, and i dont know the way to restore , in right place during composition of chunks, or i wasnt just thinking too much about it, actually now im thinking i could put entire %* into single variable and replace , with some pseudovar like [$c$] and then restore it as , after full name will be recomposed, but i need to test this idea, because what works in theory usually doesnt in bat)))

Also there is a space for improvement, like checking 1st if 1st symbol is / and then making other check, storing previoustmparg as backup variable to use in next cycle after tmparg changed. And few more things to avoid somethng i have no encountered and forgot to predict.

Hope it will come handy for you guys.

Ed Dyreen
Expert
Posts: 1569
Joined: 16 May 2011 08:21
Location: Flanders(Belgium)
Contact:

Re: [tip] ulimited args & subargs with spaces & any order in

#2 Post by Ed Dyreen » 15 Aug 2012 22:10

'
Hi Voodooman,

Nice work, but your parser is incapable of taking special characters like the caret and exclamation mark.
This can be solved by disabling delayedExpansion, but this requires you to push the variables over endlocal.

Tweak it a little, and you could pump out a general reusable parser that can be included in all your batches :idea:

Code: Select all

file.CMD /apiname: d3d9 /appid: 28050 /exename: dxhr.exe /appname: "Deus Ex: Human Revolution" /test
I build a generic command-line parser, it's function name is toParams_ and is included in doskit ( Unfortunately it is designed for XP dutch release only ).
The subargs effect doesn't seem efficient, my function achieves the same, simply by calling it twice, feeding the previous output as input.

Code: Select all

file.CMD /arg0: "this ^works!" /arg1: this_^works! /arg2 not_recognized
file.CMD

Code: Select all

setlocal disableDelayedExpansion
set "$par0=%*" &( %toParams_% !$par0! )

Code: Select all

 » toParams_
 « toParams_ [ok:loaded]

 toParams_( /arg0: "this ^works!" /arg1: this_^works! /arg2 not_recognized )
  $par: '6'
  $par0: '/arg0:,"this ^works!",/arg1:,this_^works!,/arg2,not_recognized'
  $par1: '/arg0:'
  $arg0: '1'
  $par2: '"this ^works!"'
  $arg0: 'this ^works!'
  $par3: '/arg1:'
  $arg1: '1'
  $par4: 'this_^works!'
  $arg1: 'this_^works!'
  $par5: '/arg2'
  $arg2: '1'
  $par6: 'not_recognized'
  $p: '"this ^works!",this_^works!,1,not_recognized'
 [ok:0]

 toParams_( )
  $par: ''
  $par0: ''
  $p: ''
 [ok:0]
 endoftest Druk op een toets om door te gaan. . .
By now the following variables and some extra will be defined $arg0, $arg1, $arg2.

  • $par0: is a reconstructed paramRAW.
  • $par: is the number of individual arguments.
  • Arguments are only valid if they start with a slash and are combined if they end with a colon.
    All other cases are ignored and added raw to $p.
  • If the argument is valid If the argument is combined (
    . the part before the colon is the variable name and the part behind the colon is the value.
    ) else the argument is considered boolean with value true ( Microsoft programs parse command-line arguments similarly ).
  • $p: is the raw data string.
  • Order of arguments is random.
  • The default delimiters apply unless they are enclosed in double quotes.
  • Double quotes that are part of the argument and unevenly distributed need to be escaped.

Regards,
ED

Post Reply