Hi
I always had a strange fascination about batch files in Windows. I like automation, but for some reason I don't like using an elephant gun (Perl, Powershell) to shoot a mosquito. Plus I like the automation stuff to work on any PC, without having to install anything new. The Windows shell commands look totally crippled, but there is more to it than meets the eye.
The idea for it got started by a post I read that suggested that you could implement a DELAY command by pinging the local host with the -w parameter. The idea suggested was something like:
ping 127.0.0.1 -n 10 -w 1000
-n determines the number of pings, -w the timeout in msec for the pings.
However -w will determine the timeout only when there is no response to the ping. If there is a response, then the ping command does not waste time and move to the next repetition.
So clearly this is not an accurate way to create a delay and you can test this with a trivial batch file as I will explain below.
What the batch file does is not that important (you can write a delay command in Win32 in about 3 lines of code), but the reason for this post is mainly to showcase several batch file techniques.
Specifically:
- How to extract a substring from the value of an environment variable.
- How to do math in a batch file.
- How to invoke a subroutine inside a batch file
Here is how these tricks work:
- %envvar:~X,Y% retrieves a substring of length Y from %envvar% starting from position X (zero-based).
- "set /A" can do integer math in the shell.
For example if you type "set /A 201*3" the shell will output 603. A quick integer replacement for your beloved calc.exe.
"set /A envvar=math_expression" will do the math and store the result in the specified environment variable. Don't forget that environment variables appearing in the expression have to be included inside %%. - A batch file "subroutine" starts with a label declaration and "returns" with a GOTO :EOF statement. You call the subroutine by typing CALL :MYROUTINE
Executing GOTO :EOF in the "main" execution flow is just a cool way to exit the batch file.
Given the above goodies you can easily implement for/while loops in a batch file.
You should keep in mind one particularly nasty habit of the windows shell that has to do with environment variable expansion. The shell will expand an environment variable when it READS the command, NOT when it executes it.
This means that in the following block:
set VAR=before
if "%VAR%" == "before" (
set VAR=after
if "%VAR%" == "after" @echo Sorry, but echo will not be executed
)
the highlighted comparison will FAIL, because the whole if-block is read in one move and expanded before it gets executed, so the comparison will actually look like if "before" == "after".
You can start CMD.EXE with the /V switch to enable delayed expansion, but I didn't want my DELAY batch file to depend on switches.
So, in my batch file I will also use PING in order to introduce a small delay and not go into a tight loop.
Here is what the code looks like:
@echo off
setlocal
if "%1" == "" goto :EOF
SET lastrecordedtime=%time:~6,2%%time:~9,2%
SET elapsedtime=0
:LOOP
rem Generate a small delay so we don't busy LOOP
ping 127.10.20.30 -n 1 >nul
rem Pickup the current time
SET currenttime=%time:~6,2%%time:~9,2%
rem This happens sometimes and the calculations go wrong
if "%currenttime%" EQU "%lastrecordedtime%" goto :LOOP
rem Use GOTOs so as not to have trouble with delayed environment expansion
rem Since the strings are zero-prefixed the string comparisons are equivalent to numeric comparisons
if "%currenttime%" GTR "%lastrecordedtime%" (
CALL :TIMEINCREASED
) ELSE (
CALL :TIMEWRAPPED
)
rem Elapsed time might be greater than the requested so remaining time becomes negative
SET /a remainingtime=%1*100 - %elapsedtime%
if "%remainingtime%" == "0" goto :EOF
if "%remainingtime:~0,1%" == "-" goto :EOF
CALL :UNPATCHOCTAL
SET lastrecordedtime=%currenttime%
goto LOOP
:TIMEINCREASED
CALL :PATCHOCTAL
SET /a elapsedtime=%elapsedtime% + %currenttime% - %lastrecordedtime%
goto :EOF
:TIMEWRAPPED
CALL :PATCHOCTAL
SET /a elapsedtime=%elapsedtime% + %currenttime% + 6000 - %lastrecordedtime%
goto :EOF
:PATCHOCTAL
rem Remove leading zero so as to avoid feeding invalid octals to set /a
rem There might be as much as three "disturbing" zeros.
set saved_currenttime=%currenttime%
set saved_lastrecordedtime=%lastrecordedtime%
if "%currenttime:~0,1%" == "0" set currenttime=%currenttime:~1,3%
if "%currenttime:~0,1%" == "0" set currenttime=%currenttime:~1,3%
if "%currenttime:~0,1%" == "0" set currenttime=%currenttime:~1,3%
if "%lastrecordedtime:~0,1%" == "0" set lastrecordedtime=%lastrecordedtime:~1,3%
if "%lastrecordedtime:~0,1%" == "0" set lastrecordedtime=%lastrecordedtime:~1,3%
if "%lastrecordedtime:~0,1%" == "0" set lastrecordedtime=%lastrecordedtime:~1,3%
goto :EOF
:UNPATCHOCTAL
rem Return the original string values so that string comparisons work ok
set currenttime=%saved_currenttime%
set lastrecordedtime=%saved_lastrecordedtime%
goto :EOF
I extracted both the seconds value and the hundreds of a second value, because my initial version would not be very accurate for small delay values (1-5 sec). Since both strings are zero prefixed I can directly concatenate them to generate the effect of (sec*100 + hundreds_of_sec).
The batch file takes the number of seconds as a parameter, but it is trivial to change it so that it accepts the hundreds of a second.
Unfortunately this BATCH file takes a little CPU time (3-5% on my PC) so it might not be convenient for some purposes. Maybe someone will come up with a better way to introduced a guaranteed-small delay, instead of pinging the local host.
Oh, and in case you didn't know, the whole 127.x.x.x range is considered a loopback address, so you don't have to type 127.0.0.1 every time. Type something else and look cool to your astounded friends :-P
Finally I should say that I tested the batch file (and it works of course) on Vista and XP.
If you want to test the accuracy of the batch file, then type another batch file as shown below and check things out for yourself:
@echo off
echo Delaying for %1 seconds
echo %time:~6,2%:%time:~9,2%
call delay.bat %1
echo %time:~6,2%:%time:~9,2%
Have Fun!
Dimitris Staikos
Stunning solution.
I usually settle for just pinging 100.100.100.100 , which is a preposterous address on any well-designed network :)
Posted by: LongTimeReaderFirsttimeCaller | October 12, 2009 at 03:19 PM