script home

Command Tail

Consider the following batch:
@echo off

for /f "tokens=1*" %%a in  ('echo "abc 123" xyz') do echo FOR split:  [%%a]---[%%b]
call :shArg "abc 123" xyz
goto :eof

echo CALL split: [%1]---[%2]
goto :eof
on my box, the output looks like:
FOR split:  ["abc]---[123" xyz]
CALL split: ["abc 123"]---[xyz]

This shows the FOR command tokenizing quoted strings differently from how the command shell separates out command parameters. That can present a problem when your batch file wants to be able to recognize a variable number of parameters, some of which could contain spaces. In the specific case I ran into recently, an existing batch file, B1, served as an argument preprocessor for another batch program, B2. In general, B1 stepped through the given command line parameters, acting on each one that it recognized. When and if it comes to an unrecognized parameter, preprocessing is complete and all remaining unprocessed parameters are forwarded to B2. And there's the rub. How to get at the remaining parameters? If you can count on there being no quoted parameters (with embedded spaces) in the first or second position, and you want to pass all parameters after the second, you could probably get away with
FOR /f "tokens=2*" %%a in ("%*") DO call B2 %%b
But that wouldn't give the desired result if, say, B1 had been invoked as
B1 -d "c:\my notes" readme.txt -A
The quotes confuse the FOR parse.

We could do some shifts and then pass up to 10 arguments explicitly...

call B2 %0 %1 %2 %3 %4 %5 %6 %7 %8 %9
but that wouldn't work right if the command tail had more than 10 parameters.

The best quick and dirty solution is probably to build up the desired command tail stepwise.

set cmdTail=%3
  if %4.==. goto callB2
  set cmdTail=%cmdTail% %4
goto top

call B2 %cmdTail%

If that's too easy, here's one that's sure to qualify as the Hard Way by any measure...

@echo off
:: file: detach.cmd
:: Example: detach "123 456" abc  xyz
::                ["123 456"][abc  xyz]
:: Example: detach 123 " 456 abc" xyz
::                [123][" 456 abc" xyz]

set argList=%*
call :detachArg1 argList
echo [%detachArg1%][%argList%]
goto done

:: if no quotes, this is much simpler:
::     FOR /f "tokens=1*" %%a IN ("%*") DO echo [%%a][%%b]

:: detachArg1 arglistName
:: (split first arg from given argList)
::  detachArg1  = firstArg(*arglistName)
::  *arglistName = argTail(*arglistName)

  set _detachArg1=
  if %1.==. goto :eof
  call call :firstArg %%%1%%
  call set %%^^%0%%=%firstArg%
  call call :argTail %%%1%%
  set %1=%argTail%
  goto :eof

  call set %%^^%0%%=%1
  goto :eof

:: return argument list, without first.
  set argList=%*
  set arg1=%1
  if not defined arg1 goto :eof
  call :varlength arg1
  call call :trimlist %%argList:~%varlength%%%
  call set %%^^%0%%=%trimlist%
  goto :eof

  call set %%^^%0%%=%*
  goto :eof

 :: get length of environment variable (via brute force)
 :: %1 name of var to check
   for /f "tokens=1* delims==" %%y in ('set %1^|findstr /b /i "%1=" ') do (
     set vl_val=%%z)
   set vl_vlen=0
   if not defined vl_val goto lenrpdone
   :: trap quoted string
   if "".==%vl_val:~0,1%%vl_val:~-1%. (
	set vl_vlen=2
	set vl_val=%vl_val:~1,-1%
   ) else (
	set vl_vlen=0
     if "%vl_val%"=="" goto lenrpdone
     set vl_val=%vl_val:~1%
     set /a vl_vlen+=1
     goto chklenrpdone
   call set %%^^%0%%=%vl_vlen%
   set vl_vlen=
   set vl_val=
   goto :EOF

Note a couple of conventions above: the 'return' value of a called subroutine is assigned to an environment variable with the name of the subroutine. So for example in the :firstArg subroutine, the line
  call set %%^^%0%%=%1
has the effect of setting an environment variable named firstArg to the first passed parameter. Aside from being exqisitely ugly, that's undocumented usage, as far as I know. So it's not guarenteed to continue to work. But it's a handy idiom, and it seems to work everywhere I care about for now. (w2k-pro .. w2k3-srv), so I may decide to use it somewhere, sometime. Generally though, I'll probably stick with a decidedly less questionable means to accomplish the desired result. That is, simply setting the target variable explicitly
  call set firstArg=%1
  goto :eof