How to make SHIFT work with %* in batch files
Asked Answered
R

8

48

In my batch file on Windows XP, I want to use %* to expand to all parameters except the first.
Test file (foo.bat):

@echo off
echo %*
shift
echo %*

Call:

C:\> foo a b c d e f

Actual result:

a b c d e f
a b c d e f

Desired result:

a b c d e f
b c d e f

How can I achieve the desired result? Thanks!!

Rossiya answered 20/2, 2012 at 14:52 Comment(0)
C
22

Wouldn't it be wonderful if CMD.EXE worked that way! Unfortunately there is not a good syntax that will do what you want. The best you can do is parse the command line yourself and build a new argument list.

Something like this can work.

@echo off
setlocal
echo %*
shift
set "args="
:parse
if "%~1" neq "" (
  set args=%args% %1
  shift
  goto :parse
)
if defined args set args=%args:~1%
echo(%args%

But the above has problems if an argument contains special characters like ^, &, >, <, | that were escaped instead of quoted.

Argument handling is one of many weak aspects of Windows batch programming. For just about every solution, there exists an exception that causes problems.

Carlinecarling answered 20/2, 2012 at 15:49 Comment(3)
I write a simple one, can process any ascii characters. Including \t \r \n ^ Back Slash and quote. github.com/zhanhb/kcptun-sip003-wrapper/blob/v0.1/src/…Spanjian
Given the typical bash and powershell script use of incoming arguments shift is focused on separating out first argument from the remaining ones, and fact that with cmd scripts there are only 1 - 9 argument index pointers, why not just write one liner that does set arg1=%1 & set argsRemainder=%2 %3 %4 %5 %6 %7 %8 %9. At which point the most command use case is straight forward, e.g. echo executing some command that needs arg1 = %arg1% and argRemainder = %argsRemainder% references ?Reprieve
@Reprieve - Sure, your proposal works for some situations, but it is far from a general purpose solution, and I strive for solutions with minimal constraints. First off, I don't agree that separating the 1st argument from the others solves most use cases. My solution is easily extended by storing 1 through N, and then doing N shifts before entering the loop to populate ARGS. Second - batch is not limited to 9 arguments, but rather it only allows access to up to 9 at one time (actually 10 with %0). SHIFT is used to access arguments beyond %9.Carlinecarling
A
6

That´s easy:

setlocal ENABLEDELAYEDEXPANSION
  set "_args=%*"
  set "_args=!_args:*%1 =!"

  echo/%_args%
endlocal

Same thing with comments:

:: Enable use of ! operator for variables (! works as % after % has been processed)
setlocal ENABLEDELAYEDEXPANSION
  set "_args=%*"
  :: Remove %1 from %*
  set "_args=!_args:*%1 =!"
  :: The %_args% must be used here, before 'endlocal', as it is a local variable
  echo/%_args%
endlocal

Example:

lets say %* is "1 2 3 4":

setlocal ENABLEDELAYEDEXPANSION
  set "_args=%*"             --> _args=1 2 3 4
  set "_args=!_args:*%1 =!"  --> _args=2 3 4

  echo/%_args%
endlocal

Remarks:

  • Does not work if any argument contains the ! or & char
  • Any extra spaces in between arguments will NOT be removed
  • %_args% must be used before endlocal, because it is a local variable
  • If no arguments entered, %_args% returns * =
  • Does not shift if only 1 argument entered
Aridatha answered 26/5, 2015 at 2:40 Comment(6)
You need to remove the %1 argument, not the %0 (which is the name of the script)Demarcation
Indeed, you are correct. I have edited %0 to %1 now. Thanks.Aridatha
As written, this is dangerously wrong, because the value for the first arg could be repeated in a subsequent arg, and your code will remove both. That could be fixed by using * to remove everything through the first instance: set _args=!_args:*%1=!. Other problems that cannot be fixed is it will fail if %1 contains ! or =.Carlinecarling
Thank you dbenham, I have reviewed the code based on your good comment.Aridatha
Just nitpicking here, @cyberponk, but that's not that easy... Great job!Taste
I think a higher quality approach would be to :len the argument and then remove the number of characters from args, repeat for as many argument as you want. Also, what happens in the current formulation if there are quoted spaces inside an argument (which often happens with filepaths)Catherine
A
3

Don't think there's a simple way to do so. You could try playing with the following workaround instead:

@ECHO OFF
>tmp ECHO(%*
SET /P t=<tmp
SETLOCAL EnableDelayedExpansion
IF DEFINED t SET "t=!t:%1 =!"
ECHO(!t!

Example:

test.bat 1 2 3=4

Output:

2 3=4
Abettor answered 20/2, 2012 at 15:50 Comment(6)
I don't see the benefit of the temp file - set t=%* would work just as well. This answer will fail if %1 contains = or starts with * or ~. Also will have problems if args are delimited with , or ; instead of spaces. Better to only remove %1 and leave the delimiter(s) in place.Carlinecarling
Killer problem - this answer will give wrong answer if args are A A B. Could be improved with set t=!t:*%1=!Carlinecarling
Using set "t=!t:*%1=! modification, this answer will still fail if %1 contains =, but starting with * or ~ is ok.Carlinecarling
@dbenham: Thanks for the feedback! I think with set /p I was just being overcautious about something, not sure now about what exactly, and so you may well be right about set t=%* being no worse than using a temp file. And I agree with you on your other points. Basically, it turns out that despite batch scripting being already weak in argument handling, I managed to add even more restraint with my suggestion. :) There's a couple of (minor) advantages of my script over yours, though: =s and , are preserved if they are not part of the first argument: 1 2,3 -> 2,3 and 1 2=3 -> 2=3.Abettor
Thank you for your expert advice and discussion of the pros and cons. I appreciate it! Both answers are great, but I may only accept one. I will go with the other solution because it seems to me to be more easily understandable to less proficient readers, and ease of maintenance is very important in my case.Rossiya
Preserving 2=3 is kind of pointless if it is passed on to another batch script since batch will parse it as 2 arguments 2 and 3. The batch token delimiters are ,, ;, =, <space>, <tab>. If you pass on 2=3 to something other than batch, then yes it could be important.Carlinecarling
C
2

I had to do this recently and came up with this:

setlocal EnableDelayedExpansion

rem Number of arguments to skip
set skip=1

for %%a in (%*) do (
  if not !position! lss !skip! (
    echo Argument: '%%a'
  ) else (
    set /a "position=!position!+1"
  )
)

endlocal

It uses loop to skip over N first arguments. Can be used to execute some command per argument or build new argument list:

setlocal EnableDelayedExpansion

rem Number of arguments to skip
set skip=1

for %%a in (%*) do (
  if not !position! lss !skip! (
    set args=!args! %%a
  ) else (
    set /a "position=!position!+1"
  )
)

echo %args%

endlocal

Please note that the code above will add leading space to the new arguments. It can be removed like this:

Carrot answered 10/3, 2017 at 16:22 Comment(0)
S
2

UPDATE: The implementation is replaced by more reliable one. Use the link below to directly download the scripts.

Pros:

  • Can handle almost all control characters.
  • Can call builtin commands.
  • Does restore previous ERRORLEVEL variable before call a command.
  • Can skip first N used arguments from the %* variable including additional command line arguments.
  • Can avoid spaces trim in the shifted command line.

Cons:

  • The control characters like & and | still must be escaped before call in a user script (side issue).
  • Does write to a temporary file to save the command line as is.
  • The delayed expansion feature must be disabled before this script call: setlocal DISABLEDELAYEDEXPANSION, otherwise the ! character will be expanded.
  • A batch script command line and an executable command line has different encoders.

You can shift arguments before call to another command.

CAUTION:

Seems the stackoverflow incorrectly handles tabulation characters (and loses other characters like \x01 or \x04) in the copy-pasted code, so the below code might not work if you copy it directly by CTRL+C. Use the link below to directly download the scripts.

Implementation: https://github.com/andry81/contools/tree/HEAD/Scripts/Tools/std/callshift.bat

@echo off

rem USAGE:
rem   callshift.bat [-no_trim] [-skip <skip-num>] <shift> <command> [<cmdline>...]

rem Description:
rem   Script calls second argument and passes to it all arguments beginning
rem   from %3 plus index from %1. Script can skip first N arguments after %2
rem   before shift the rest.

rem -no_trim
rem   Avoids spaces trim in the shifted command line.

rem -skip <skip-num>
rem   Additional number of skip arguments after %2 argument.
rem   If not defined, then 0.

rem <shift>:
rem   Number of arguments in <cmdline> to skip and shift.
rem   If >=0, then only shifts <cmdline> after %2 argument plus <skip-num>.
rem   If < 0, then skips first <shift> arguments after %2 argument plus
rem   <skip-num> and shifts the rest <cmdline>.

rem <command>:
rem   Command to call with skipped and shifted arguments from <cmdline>.

rem Examples:
rem   1. >callshift.bat 0 echo "1 2" ! ^^? ^^* ^& ^| , ; = ^= "=" 3
rem      "1 2" ! ? * & | , ; "=" 3
rem   2. >callshift.bat 2 echo."1 2" 3 4 5
rem      "1 2" 5
rem   3. >callshift.bat . set | sort
rem   4. >errlvl.bat 123
rem      >callshift.bat
rem      >callshift.bat 0 echo.
rem      >callshift.bat 0 echo 1 2 3
rem      >echo ERRORLEVEL=%ERRORLEVEL%
rem      ERRORLEVEL=123
rem   5. >callshift.bat -3 echo 1 2 3 4 5 6 7
rem      1 2 3 7
rem      rem in a script
rem      >call callshift.bat -3 command %%3 %%2 %%1 %%*
rem   6. >callshift.bat -skip 2 -3 echo a b 1 2 3 4 5 6 7
rem      a b 1 2 3 7
rem      rem in a script
rem      >call callshift.bat -skip 2 -3 command param0 param1 %%3 %%2 %%1 %%*
rem   7. >callshift.bat 0 exit /b 123
rem      >echo ERRORLEVEL=%ERRORLEVEL%
rem      ERRORLEVEL=123
rem   8. >errlvl.bat 123
rem      >callshift.bat 0 call errlvl.bat 321
rem      >echo ERRORLEVEL=%ERRORLEVEL%
rem      ERRORLEVEL=321
rem   9. >callshift.bat -no_trim 1 echo  a  b  c  d
rem       b  c  d

rem Pros:
rem
rem   * Can handle `!`, `?`, `*`, `&`, `|`, `,`, `;`, `=` characters.
rem   * Can call builtin commands.
rem   * Does restore previous ERRORLEVEL variable before call a command.
rem   * Does not leak variables outside.
rem   * Can skip first N used arguments from the `%*` variable including
rem     additional command line arguments.
rem   * Can avoid spaces trim in the shifted command line.
rem
rem Cons:
rem
rem   * The control characters like `&` and `|` still must be escaped.
rem   * To handle `?` and `*` characters you must prefix them additionally to escape: `^?`, `^*`.
rem   * Can not handle `=` character without quotes.
rem   * Does write to a temporary file to save the command line as is.

rem with save of previous error level
setlocal & set LAST_ERROR=%ERRORLEVEL%

rem drop last error level
call;

set "CMDLINE_TEMP_FILE=%TEMP%\callshift.%RANDOM%-%RANDOM%.txt"

rem redirect command line into temporary file to print it correcly
for %%i in (1) do (
  set "PROMPT=$_"
  echo on
  for %%b in (1) do rem %*
  @echo off
) > "%CMDLINE_TEMP_FILE%"

for /F "usebackq eol= tokens=* delims=" %%i in ("%CMDLINE_TEMP_FILE%") do set "LINE=%%i"

del /F /Q "%CMDLINE_TEMP_FILE%" >nul 2>nul

rem script flags
set FLAG_SHIFT=0
set FLAG_SKIP=0
set FLAG_NO_TRIM=0

rem flags always at first
set "FLAG=%~1"

if defined FLAG ^
if not "%FLAG:~0,1%" == "-" set "FLAG="

if defined FLAG (
  if "%FLAG%" == "-no_trim" (
    set FLAG_NO_TRIM=1
    shift
    set /A FLAG_SHIFT+=1
  )
)

if defined FLAG (
  if "%FLAG%" == "-skip" (
    set "FLAG_SKIP=%~2"
    shift
    shift
    set /A FLAG_SHIFT+=2
  )
)

set "SHIFT=%~1"
set "COMMAND="
set "CMDLINE="

rem cast to integer
set /A FLAG_SKIP+=0
set /A SHIFT+=0

set /A COMMAND_INDEX=FLAG_SHIFT+1
set /A ARG0_INDEX=FLAG_SHIFT+2

set /A SKIP=FLAG_SHIFT+2+FLAG_SKIP

if %SHIFT% GEQ 0 (
  set /A SHIFT+=FLAG_SHIFT+2+FLAG_SKIP
) else (
  set /A SKIP+=-SHIFT
  set /A SHIFT=FLAG_SHIFT+2+FLAG_SKIP-SHIFT*2
)

rem Escape specific separator characters by sequence of `$NN` characters:
rem  1. `?` and `*` - globbing characters in the `for %%i in (...)` expression
rem  2. `,`, `;`, <space> - separator characters in the `for %%i in (...)` expression
setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:$=$00!") do endlocal & set "LINE=%%i"
setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:^*=$01!") do endlocal & set "LINE=%%i"
setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:^?=$02!") do endlocal & set "LINE=%%i"
setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:,=$03!") do endlocal & set "LINE=%%i"
setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:;=$04!") do endlocal & set "LINE=%%i"
if %FLAG_NO_TRIM% NEQ 0 (
  setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:  = $05!") do endlocal & set "LINE=%%i"
  setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%i in ("!LINE:$05 =$05$05!") do endlocal & set "LINE=%%i"
)

set INDEX=-1

setlocal ENABLEDELAYEDEXPANSION
for /F "eol= tokens=* delims=" %%i in ("!LINE!") do endlocal & for %%j in (%%i) do (
  setlocal ENABLEDELAYEDEXPANSION
  if !INDEX! GEQ !ARG0_INDEX! (
    if !INDEX! LSS !SKIP! (
      if defined CMDLINE (
        for /F "eol= tokens=* delims=" %%v in ("!CMDLINE!") do endlocal & set "CMDLINE=%%v %%j"
      ) else endlocal & set "CMDLINE=%%j"
    ) else if !INDEX! GEQ !SHIFT! (
      if defined CMDLINE (
        for /F "eol= tokens=* delims=" %%v in ("!CMDLINE!") do endlocal & set "CMDLINE=%%v %%j"
      ) else endlocal & set "CMDLINE=%%j"
    ) else endlocal
  ) else if !INDEX! EQU !COMMAND_INDEX! (
    endlocal & set "COMMAND=%%j"
  ) else endlocal
  set /A INDEX+=1
)

if defined COMMAND (
  setlocal ENABLEDELAYEDEXPANSION
  for /F "eol= tokens=* delims=" %%i in ("!COMMAND!") do (
    if defined CMDLINE (
      for /F "eol= tokens=* delims=" %%v in ("!CMDLINE:$04=;!") do endlocal & set "CMDLINE=%%v"
      if %FLAG_NO_TRIM% NEQ 0 setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%v in ("!CMDLINE:$05= !") do endlocal & set "CMDLINE=%%v"
      setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%v in ("!CMDLINE:$03=,!") do endlocal & set "CMDLINE=%%v"
      setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%v in ("!CMDLINE:$02=?!") do endlocal & set "CMDLINE=%%v"
      setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%v in ("!CMDLINE:$01=*!") do endlocal & set "CMDLINE=%%v"
      setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%v in ("!CMDLINE:$00=$!") do endlocal & set "CMDLINE=%%v"
      setlocal ENABLEDELAYEDEXPANSION & for /F "eol= tokens=* delims=" %%v in ("!CMDLINE!") do endlocal & endlocal & call :SETERRORLEVEL %LAST_ERROR% & %%i %%v
    ) else endlocal & endlocal & call :SETERRORLEVEL %LAST_ERROR% & %%i
    exit /b
  )
  exit /b %LAST_ERROR%
)

(
  endlocal
  exit /b %LAST_ERROR%
)

:SETERRORLEVEL
exit /b %*

Examples:

>callshift.bat 0 echo "1 2" ! ^^? ^^* ^& ^| , ; = ^= "=" 3
"1 2" ! ? * & | , ; "=" 3

>callshift.bat 2 echo."1 2" 3 4 5
"1 2" 5

>callshift.bat . set | sort

>errlvl.bat 123
>callshift.bat
>callshift.bat 0 echo.
>callshift.bat 0 echo 1 2 3
>echo ERRORLEVEL=%ERRORLEVEL%
ERRORLEVEL=123

>callshift.bat -3 echo 1 2 3 4 5 6 7
1 2 3 7
rem in a script
>call callshift.bat -3 command %%3 %%2 %%1 %%*

>callshift.bat -skip 2 -3 echo a b 1 2 3 4 5 6 7
a b 1 2 3 7
rem in a script
>call callshift.bat -skip 2 -3 command param0 param1 %%3 %%2 %%1 %%*

>callshift.bat -no_trim 1 echo  a  b  c  d
 b  c  d

Pros:

  • Can handle !, ?, *, &, |, ,, ;, = characters.
  • Can call builtin commands.
  • Does restore previous ERRORLEVEL variable before call a command.
  • Does not leak variables outside.
  • Can skip first N used arguments from the %* variable including additional command line arguments.
  • Can avoid spaces trim in the shifted command line.

Cons:

  • The control characters like & and | still must be escaped.
  • To handle ? and * characters you must prefix them additionally to escape: ^?, ^*.
  • Can not handle = character without quotes.
  • Does write to a temporary file to save the command line as is.
Shakitashako answered 2/1 at 3:22 Comment(1)
That's a nice collection of tools Andry!Luzluzader
A
1

Another easy way of doing this is:

set "_args=%*"
set "_args=%_args:* =%"

echo/%_args%

Remarks:

  • Does not work if first argument (%1) is 'quoted' or "double quoted"
  • Does not work if any argument contains the & char
  • Any extra spaces in between arguments will NOT be removed
Aridatha answered 11/5, 2016 at 17:17 Comment(0)
J
1

Yet another obnoxious shortcoming of DOS/Windows batch programming...

Not sure if this is actually better than some of the other answers here but thought I'd share something that seems to be working for me. This solution uses FOR loops rather than goto, and is contained in a reusable batch script.

Separate batch script, "shiftn.bat":

@echo off
setlocal EnableDelayedExpansion
set SHIFTN=%1
FOR %%i IN (%*) DO IF !SHIFTN! GEQ 0 ( set /a SHIFTN=!SHIFTN! - 1 ) ELSE ( set SHIFTEDARGS=!SHIFTEDARGS! %%i ) 
IF "%SHIFTEDARGS%" NEQ "" echo %SHIFTEDARGS:~1%

How to use shiftn.bat in another batch script; in this example getting all arguments following the first (skipped) arg:

FOR /f "usebackq delims=" %%i IN (`call shiftn.bat 1 %*`) DO set SHIFTEDARGS=%%i 

Perhaps someone else can make use of some aspects of this solution (or offer suggestions for improvement).

Jackleg answered 11/10, 2019 at 22:15 Comment(0)
S
0

Resume of all and fix all problems:

set Args=%1
:Parse
shift
set First=%1
if not defined First goto :EndParse
  set Args=%Args% %First%
  goto :Parse
:EndParse

Unsupport spaces between arguments: 1 2 3 4 will be 1 2 3 4

Soidisant answered 25/4, 2018 at 3:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.