This site has begun just as my own memo. Refer to the specific OFFICAL documentations or other resources and utilize this at your own risk. ABSOLUTELY NO WARRANTY. |
Cmd Batch Tips | ||
---|---|---|
HOME |
This is a technical memorandum of command batch on The famous Ridiculously Complexed Fragile Operating Toy (RCFOT aka Win*), which I hope vanished from The Earth as soon as possible.
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
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
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 [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.
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 |
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.
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.
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
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.
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 -->
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.
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%
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