Cmd Batch Tips

This is a technical memorandum of command batch on The famous Ridiculously Complexed Fragile Operating Toy (RCFOT aka Win*), whchi I hope vanished from The Earth as soon as possible.

Table of Contents

The Pitfalls

Variable Value Substitution

SET VAR=2016/04/01_10:09:30_It_is_sunny_day
echo 1:  %VAR%
echo 2:  %VAR:~0,4%    <- 4 chars from 0th
echo 3:  %VAR:~11%     <- from 11th till end
echo 4:  %VAR:~-9%     <- from 9th char from the end till end
echo 5:  %VAR:~-9,5%   <- 5 chars from 9th from the end
echo 6:  %VAR:/=-%     <- replace all / with -
echo 7:  %VAR:/=%      <- remove all /
echo 8:  %VAR:*/=%     <- with wildcard
echo 9:  %VAR:/*=%
 
CALL :sDO %VAR%
goto :EOF
 
:sDO
  echo 10: %1
  echo 11: %1:~/=-
  echo 11: %1:~/=-%
goto :EOF

>output

1:  2016/04/01_10:09:30_It_is_sunny_day
2:  2016
3:  10:09:30_It_is_sunny_day
4:  sunny_day
5:  sunny
6:  2016-04-01_10:09:30_It_is_sunny_day
7:  20160401_10:09:30_It_is_sunny_day
8:  04/01_10:09:30_It is_sunny_day      <- matched shortest
9:  2016/04/01_10:09:30_It_is_sunny_day <- doesn't work
10: 2016/04/01_10:09:30_It_is_sunny_day      <- for posisional parameters..
11: 2016/04/01_10:09:30_It_is_sunny_day:~/=- <- doesn't work
11: 2016/04/01_10:09:30_It_is_sunny_day:~/=- <- doesn't work

This emulates %VAR:/*=% (9: above) which otherwise is impossible. EnableDelayedExpansion is used mostly to mix different signs in one substitution.

SETLOCAL EnableDelayedExpansion
SET VAR=2016/04/01_10:09:30_It_is_sunny_day
SET DELIM=/
CALL :sDO
goto :EOF
 
:sDO
  SET JUNK=!VAR:*%DELIM%=!
  echo JUNK=%JUNK%
  echo !VAR:%DELIM%%JUNK%=!
goto :EOF
2016

Same can be accomplished by this terribly short voodoo (from http://stackoverflow.com/questions/14317034/batch-replace-string-without-a-for-loop);

SET VAR=2016/04/01_10:09:30_It_is_sunny_day
SET "VAR=%VAR:/="&rem %
echo "%VAR%"

Former example 8: is only able to remove a part of string by shortest-match. How can we do longest match removal? Here delimiter is "_" instead of "/" to make this example clearer.

SETLOCAL EnableDelayedExpansion
SET VAR=2016/04/01_10:09:30_It_is_sunny_day
SET DELIM=_
CALL :sDO %VAR%
goto :EOF
 
:sDO
for /F "usebackq tokens=1* delims=%DELIM%" %%I in ('%~1') do (
  if x%%J == x goto :EOF
  SET IN=%%J
  echo [debug]IN=!IN!
  if "!IN!" == "!IN:%DELIM%=!" (
    echo ANS:!IN!
    goto :EOF
  )
  CALL :sDO %%J
)
goto :EOF
ENDLOCAL
[debug]IN=10:09:30_It_is_sunny_day
[debug]IN=It_is_sunny_day
[debug]IN=is_sunny_day
[debug]IN=sunny_day
[debug]IN=day
ANS:day

Positional Parameter Value Substitution

These are valid only for positional parameters or in FOR blocks.

SET VAR="D:\folder\sub\file.txt"
 
FOR %%I in (%VAR%) do (
  echo 1:  %%I
  echo 2:  %%~I        <- strip double quotes
  echo 3:  %%~dI       <- only drive spec
  echo 4:  %%~pI       <- only path spec
  echo 5:  %%~dpI      <- drive + path spec
  echo 6:  %%~nI       <- basename without suffix
  echo 7:  %%~xI       <- only suffix
  echo 8:  %%~nxI      <- basename with suffix
)
 
CALL :sDO %VAR%
goto :EOF
 
:sDO
  echo 9:  %1
  echo 10: %~1
  echo 11: %~d1
  echo 12: %~p1
  echo 13: %~dp1
  echo 14: %~n1
  echo 15: %~x1
  echo 16: %~nx1
goto :EOF
1:  "D:\folder\sub\file.txt"
2:  D:\folder\sub\file.txt
3:  D:
4:  \folder\sub\
5:  D:\folder\sub\
6:  file
7:  .txt
8:  file.txt
9:  "D:\folder\sub\file.txt"
10: D:\folder\sub\file.txt
11: D:
12: \folder\sub\
13: D:\folder\sub\
14: file
15: .txt
16: file.txt

Consideration 1: Do these only work for "\"? What happens if "/" is found. What if delimiters appear repeatedly?

SET VAR="D:/folder\\sub\\//file.txt"

Conclusion 1: They work like nothing unusual. >output

1:  "D:/folder\\sub\\//file.txt"  <- ignore, it's just a string.
2:  D:/folder\\sub\\//file.txt    <-
3:  D:
4:  \folder\sub\                  <- slash converted to backslash, duplication eliminated.
5:  D:\folder\sub\
6:  file
7:  .txt
8:  file.txt
9:  "D:/folder\\sub\\//file.txt"
10: D:/folder\\sub\\//file.txt
11: D:
12: \folder\sub\
13: D:\folder\sub\
14: file
15: .txt
16: file.txt

Arrays (pseudo)

Cmd.exe is stupid enough to have NO array variable functionality at all. They say `VAR[0]', `VAR[1]' ... are arrays, but they are not. These`[' and `]' have no special meaning and `VAR[0]' for example is simply a single variable name. This is proven by the fact that `VAR[0' (no closing square bracket) makes no difference in behavior.

SET VAR[0]=aaa
ECHO %VAR[0]%
-> aaa
SET VAR[0=zzz
ECHO %VAR[0%
-> zzz

Thus, of course there is no built-in function to enumerate "Array" values like `${var[*]}' on Linux. Okey-dokey, let us illustrate a way to list the values of all the `%VAR[x%'. Note I rarely use a closing square bracket for pseudo array names because it just brings inconvenience.

SETLOCAL EnableDelayedExpansion
 
SET MODE[AUTO=auto
SET MODE[DIS=disabled
 
SET SC[AUTO=WSearch
SET SC[DELAY=W32Time wuauserv
SET SC[DEMAND=dot3svc RemoteRegistry BITS
 
for /F "usebackq delims== tokens=2*" %%i in (`SET SC[`) do (
  CALL :sSUB DIS %%i %%j
)
GOTO :EOF
 
:sSUB
  SET MOD=%1
  shift
  :while
    if x%1 == x GOTO endwhile
    ECHO !MODE[%MOD%! %1
    shift
  GOTO while
  :endwhile
GOTO :EOF
 
ENDLOCAL

>outputs

disabled WSearch
disabled W32Time
disabled wuauserv
disabled dot3svc
disabled RemoteRegistry
disabled BITS

The heart of the script is the first highlighted line. It feeds the output of a command `SET SC[', another strange evaluation function of cmd.exe, which means "list variables whose names start with 'SC['". It terribly prints not the values but the variable definitions themselves. God damn!

SET SC[
->
SC[AUTO=WSearch
SC[DELAY=W32Time wuauserv
SC[DEMAND=dot3svc RemoteRegistry BITS

That is why the `FOR /F' requires `delims==' option and tokens is not 1* but 2*. The other highlight illustrates a simple reference of a pseudo array element with "variable in variable name".

If Tests

if [not] <evaluation> (
  <processing>
) else if <evaluation> (
  <processing>
) else (
  <processing>
)

..there is no `&' or `-a', `|' or `-o' things available for evaluation phrase. Just nest them desperately.

Numeric Comparative

A EQU B A equal to B
A NEQ B A not equal to B
A LSS B A less than B
A LEQ B A less than or equal to B
A GTR B A greater than B
A GEQ B A greater than or equal to B

String Comparative

if [/I] [not] %VAR% == this (
  ...
)

"/I" makes it case insensitive. When using /I with negation, be careful with order, "if not /I ..." doesn't work. Note also that if left hand evaluates to NULL, cmd.exe quits immediately with syntax error. To prevent such exception, write like;

if x%VAR% == xthis (
  ...
)

where `x' is a literal x or any dummy character. However, it breaks syntax if %VAR% is a bare string containing spaces, i.e a list. In such case, it is safe to enclose both sides with quotes like `"%VAR%" == "this"' but this fails if actual VAR is already quoted.

Other Tests

if ERRORLEVEL 1 ( ... )

evaluates to true if return code of former command is equal to or greater than 1.

if [not] EXIST <file or dir> ( ... )

evaluates to true if the file or directory exists. As an application, drive existence can be checked by testing nul device.

if EXIST G:\nul ( ... )

Strangely it fails if the path is quoted. Note also that UNC path doesn't show nul device.

Arithmetic

Will illustrate not all of them. With use of /A, variable on the right hand may not be enclosed with %.

SET VAR=0
SET VARB=1
  
SET /A VAR=VAR+VARB
  echo 1: %VAR%
SET /A VAR+=VARB   <- same as above
  echo 2: %VAR%
SET /A VAR-=1
  echo 3: %VAR%
SET /A VAR=VAR*(2+2)
  echo 4: %VAR%
SET /A VAR=VAR/2+VARB
  echo 5: %VAR%
SET /A VAR%%=2    <- remainder. % must be doubled in batch.
  echo 6: %VAR%
SET /A VAR^<^<=3  <- left shift. As VAR is one now, 1*23. `<' must be escaped to prevent it from being interpreted as redirect.
  echo 7: %VAR%
SET /A VAR=0x000A  <- in this manner, hexadecimal value can easily be converted to decimal.
  echo 8: %VAR%
SET /A VAR=010     <- like above, this is octal.
  echo 9: %VAR%

>output

1: 1
2: 2
3: 1
4: 4
5: 3
6: 1
7: 8
8: 10
9: 8

Insanity of FOR

They supplied it with command substitution in W2000 era for the first time ...b..b..by FOR function extention. It is ugly and pain to use. The directors must not be Terrestrials. I don't understand their way of thinking.

Command Substitution (/F)

Example input file:

#SourceDir|DestinationDir|RobocopyOptions
C:\MyWork|D:\Backup\MyWork|/MIR /COPYALL /R:0 /NS /NP
"C:\My Doc"|"D:\Backup\My Doc"|/MIR /COPYDAT /R:1 /NS /NP #comment

test1.bat

Command to feed input is enclosed in backticks. When you use pipe in it, vertical bar must be escaped with a caret (^). Beware %%I , %%J and %%K are case sensitive, although normal variables (e.g. JOBLIST) are case insensitive.

SET JOBLIST=%~dp0list.txt
for /F "usebackq tokens=1,2* delims=| eol=#" %%I in (`type %JOBLIST% ^|findstr /C:Work`) do (
  CALL :sSYNC %%I %%J %%K
)
SET RETVAL=%ERRORLEVEL%
if %RETVAL% geq 16 EXIT %ERRORLEVEL%
EXIT 0
 
:sSYNC
  SET CMDSTR=robocopy %*
  ECHO [debug] %CMDSTR%
EXIT /B %ERRORLEVEL%
[debug] robocopy C:\MyWork D:\Backup\MyWork /MIR /COPYALL /R:0 /NS /NP

test2.bat

This time, input is directly from a file (so this is not command substitution). As "usebackq" option is specified, it is double-quoted. Option "eol=#" tells FOR to ignore lines starting with "#". What a bad naming, isn't it. It never ignores "#comment" at the end of line.

SET JOBLIST=%~dp0list.txt
for /F "usebackq tokens=1,2* delims=| eol=#" %%I in ("%JOBLIST%") do (
<-- rest of code is the same as test1 -->
[debug] robocopy C:\MyWork D:\Backup\MyWork /MIR /COPYALL /R:0 /NS /NP
[debug] robocopy "C:\My Doc" "D:\Backup\My Doc" /MIR /COPYDAT /R:1 /NS /NP #comment <- Stupid! Don't do this.

As we know the first line is a reference header, it can also be written;

SET JOBLIST=%~dp0list.txt
for /F "usebackq tokens=1,2* delims=| skip=1" %%I in ("%JOBLIST%") do (
<-- rest of code is the same -->

Other normal(?) usage

Very normal.

SET VAR=a b c d
for %%I in (%VAR%) do (
  echo %%I
)

>output

a
b
c
d

When it found wildcards (* and ?), it pigheadedly expects %VAR% part represents file names. Suppose in current directory is only list.txt,

SET VAR=*.txt *.jpg list.gif
for %%I in (%VAR%) do (
  echo %%I
)
list.txt
list.gif

Where is *.jpg? Since *.jpg didn't hit any existent file, it was not processed at all, but fixed list.gif was. If you want to process literal *.txt or *.jpg, FOR is not a choice. Use Pseudo while loop instead. Though enclosing the whole string with single quotes tells FOR that it is a literal string,

SET VAR=*.txt *.jpg
for /F "usebackq tokens=1-2" %%I in ('%VAR%') do (
  echo %%I %%J
)

it outputs of cource as below. This rarely be what we want;

*.txt *.jpg

Okey-dokey, then "for each file" operations require no command substitution. Suppose in current directory are list.txt, list.jpg and list(without suffix), this script prints each file name and size;

for %%I in (*.*) do (
  echo %%I %%~zI
)
list.txt 166
list.jpg 305
list 3

"for each directory" can be dealt with by `for /D %%I (*.*)'.

As mentioned above, is single-quoted input with "usebackq" useless? Not at all. The script below emulates common split() function in Perl, PHP etc, which splits one string at specified delimiter into an array. Suppose you have to do with an under-score,

SETLOCAL EnableDelayedExpansion
SET VAR=2016/04/01_10:09:30_It_is_sunny_day
SET DELIM=_
CALL :sSPLIT %VAR%
SET ARRAY=%ARRAY:~1%
echo [debug]ARRAY=%ARRAY%
 
for %%M in (%ARRAY%) do (
  echo %%M
)
goto :EOF
 
:sSPLIT
  for /F "usebackq tokens=1* delims=%DELIM%" %%I in ('%~1') do (
    SET HEAD=%%I
    SET TAIL=%%J
    echo [debug]HEAD=!HEAD! TAIL=!TAIL!
    SET ARRAY=%ARRAY% !HEAD!
    if x%%J == x goto :EOF
    CALL :sSPLIT !TAIL!
  )
goto :EOF
ENDLOCAL
[debug]HEAD=2016/04/01 TAIL=10:09:30_It_is_sunny_day
[debug]HEAD=10:09:30 TAIL=It_is_sunny_day
[debug]HEAD=It TAIL=is_sunny_day
[debug]HEAD=is TAIL=sunny_day
[debug]HEAD=sunny TAIL=day
[debug]HEAD=day TAIL=
[debug]ARRAY=2016/04/01 10:09:30 It is sunny day
2016/04/01
10:09:30
It
is
sunny
day

No. I was too naive. Below is definitely enough for this opportunist interpreter. Eventually it's a one-liner.

SETLOCAL EnableDelayedExpansion
SET VAR=2016/04/01_10:09:30_It_is_sunny_day
SET DELIM=_
SET ARRAY=!VAR:%DELIM%= !
 
<-- do what your want here -->
 
ENDLOCAL

Most scripting language has `for i in (i=0; i<=5; i++)' syntax. In RCFOT, this way. 2nd parameter defines step(interval).

for /L %%I in (0,1,5) do (
  echo %%I
)

Though there are a few more bizarre usage in FOR, I have never wanted to use them yet.

Pseudo while and case

Poor cmd.exe lacks `while' nor `case' structure. This example emulates getopts of Bash builtin on Linux to process batch option arguments. Avoid use of `if ... else' as far as possible. No good things will happen.

:while
  if x%1 == x goto endwhile
  if %1 == -i (
    SET INFILE=%2
    shift
    shift
    goto while
  )
  if %1 == -v (
    SET VERBOSE=1
    shift
    goto while
  )
  shift
  goto while
:endwhile
if "%VERBOSE%" == "" SET VERBOSE=0
 
echo INFILE=%INFILE%  VERBOSE=%VERBOSE%

Reset ERRORLEVEL

Sometimes you may need to reset ERRORLEVEL to zero in the middle of a batch. Consider this case;

SET VAR=%1
CALL :sISEVEN
if ERRORLEVEL 1 (
  echo ODD
) else (
  echo EVEN
)
CALL :sRESETEL
EXIT %ERRORLEVEL%
 
:sRESETEL
  EXIT /B 0
goto :EOF
 
:sISEVEN
  SET /A RET=VAR%%2
  if %RET% equ 0 EXIT /B 0
EXIT /B 1