Windows batch scripting simplified
Summary: A few tips for Windows batch script developers.
One of my recent projects called me to write a couple of batch scripts (something I haven't done in a while). This post is mostly a reference for myself, but I hope it helps some unfortunate soul stuck in the last century's technology.If you find yourself working on a batch script, chances are you will need to implement command-line parsing. You can obviously use simple position-based parameters (e.g. %1, %2, etc), but if you need anything more sophisticated, you may find the _mtplib.bat library more rewarding.
"What is the the _mtplib.bat library?" you may ask. It's a batch file implementing a few general-purpose functions (such as command-line parsing), which you can call from your batch script. [In case you wonder: yes, your batch script can call code from another batch script.] The _mtplib.bat library [mtp stands for MacMillan Technical Publishing] was originally released by Tim Hill, the author of Windows NT® Shell Scripting, a book I cannot overpraise.
Unfortunately, the link to the source code in the book does not work. After extensive search, I managed to find a version of _mtplib.bat at CodeProject, but some of the characters in the source code appear to be corrupted (I tried them in different browsers and using different character sets to no avail).
I cleaned up the file and made a couple of minor changes (e.g. added a helper function to imitate a delay). I also, cleaned up another sample published by Tim Hill: skeleton.bat (an outline that can be used to create a batch script). I made one change to the skeleton file: all of the calls to the procedures implemented in _mtplib.bat from skeleton.bat invoke them via full path (instead of just the name of the file). This change assumes that the library is located in the same folder (this way you do not need to put _mtplib.bat in your path or working directory when you execute your scripts). And I extended skeleton.bat to a more illustrative sample.bat which shows how to parse command line, call procedures, handle errors, etc. You can download the source code from: You will find the listings of the source code at the end of this post, but before, let me recommend a few excellent resources for Windows batch script developers:
- DosTips - The DOS Batch Guide
Lots of resources including function library, script snippets, function tutorial, support forum (available at the bottom of every page), and much more. - Top 10 DOS Batch tips (Yes, DOS Batch...)
Some helpful tips for beginners (but not only) from Jon Galloway. - Batch files for DOS, OS/2, Windows 95/98, NT 4, 2000 and XP
Rob Vanderwoude offers a few hints. - Dos Patterns
Not a comprehensive list, but a couple of ideas are worth noting.
@echo OFF @if not "%ECHO%"=="" echo %ECHO% @if not "%OS%"=="Windows_NT" goto DOSEXIT rem $Workfile: _mtplib.bat $ rem $Revision: 2 $ rem $Date: 12/04/97 9:51a $ rem $Archive: .../_mtplib.bat $ rem UPDATED BY ALEK DAVIS on 11/02/2011 rem If no arguments, show version information and exit if "%1"=="" ( (echo Script MTP Script Library [%0] $Revision: 2 $) (goto :EOF) ) rem At least one argument, so dispatch to procedure set _PROC=%1 shift /1 goto %_PROC% rem ------------------------------------------------------------------ rem INIT procedure rem Must be called in local state before other procs are used rem :INIT if defined TRACE %TRACE% [proc %0 %*] goto :EOF rem ------------------------------------------------------------------ rem VARDEL procedure rem Delete multiple variables by prefix rem rem Arguments: rem %1=variable name prefix rem :VARDEL if defined TRACE %TRACE% [proc %0 %*] for /f "tokens=1 delims==" %%I in ('set %1 2^>nul') do set %%I= goto :EOF rem ------------------------------------------------------------------ rem PARSECMDLINE procedure rem Parse a command line into switches and args rem rem Arguments: rem CMDLINE=command text to parse rem %1=0 for new parse (def) or 1 to append to existing rem rem Returns: rem CMDARG_n=arguments, CMDSW_n=switches rem CMDARGCOUNT=arg count, CMDSWCOUNT=switch count rem RET=total number of args processed rem :PARSECMDLINE if defined TRACE %TRACE% [proc %0 %*] if not {%1}=={1} ( (call :VARDEL CMDARG_) (call :VARDEL CMDSW_) (set /a CMDARGCOUNT=0) (set /a CMDSWCOUNT=0) ) set /a RET=0 call :PARSECMDLINE1 %CMDLINE% set _MTPLIB_T1= goto :EOF :PARSECMDLINE1 if {%1}=={} goto :EOF set _MTPLIB_T1=%1 set _MTPLIB_T1=%_MTPLIB_T1:"=% set /a RET+=1 shift /1 if "%_MTPLIB_T1:~0,1%"=="/" goto :PARSECMDLINESW if "%_MTPLIB_T1:~0,1%"=="-" goto :PARSECMDLINESW set /a CMDARGCOUNT+=1 set CMDARG_%CMDARGCOUNT%=%_MTPLIB_T1% goto :PARSECMDLINE1 :PARSECMDLINESW set /a CMDSWCOUNT+=1 set CMDSW_%CMDSWCOUNT%=%_MTPLIB_T1% goto :PARSECMDLINE1 goto :EOF rem ------------------------------------------------------------------ rem GETARG procedure rem Get a parsed argument by index rem rem Arguments: rem %1=argument index (1st arg has index 1) rem rem Returns: rem RET=argument text or empty if no argument rem :GETARG if defined TRACE %TRACE% [proc %0 %*] set RET= if %1 GTR %CMDARGCOUNT% goto :EOF if %1 EQU 0 goto :EOF if not defined CMDARG_%1 goto :EOF set RET=%%CMDARG_%1%% call :RESOLVE goto :EOF rem ------------------------------------------------------------------ rem GETSWITCH procedure rem Get a switch argument by index rem rem Arguments: rem %1=switch index (1st switch has index 1) rem rem Returns: rem RET=switch text or empty if none rem RETV=switch value (after colon char) or empty rem :GETSWITCH if defined TRACE %TRACE% [proc %0 %*] (set RET=) & (set RETV=) if %1 GTR %CMDSWCOUNT% goto :EOF if %1 EQU 0 goto :EOF if not defined CMDSW_%1 goto :EOF set RET=%%CMDSW_%1%% call :RESOLVE for /f "tokens=1* delims=:" %%I in ("%RET%") do (set RET=%%I) & (set RETV=%%J) goto :EOF rem ------------------------------------------------------------------ rem FINDSWITCH procedure rem Finds the index of the named switch rem rem Arguments: rem %1=switch name rem %2=search start index (def: 1) rem rem Returns: rem RET=index (0 if not found) rem RETV=switch value (text after colon) rem :FINDSWITCH if defined TRACE %TRACE% [proc %0 %*] if {%2}=={} (set /a _MTPLIB_T4=1) else (set /a _MTPLIB_T4=%2) :FINDSWITCHLOOP call :GETSWITCH %_MTPLIB_T4% if "%RET%"=="" (set RET=0) & (goto :FINDSWITCHEND) if /i "%RET%"=="%1" (set RET=%_MTPLIB_T4%) & (goto :FINDSWITCHEND) set /a _MTPLIB_T4+=1 goto :FINDSWITCHLOOP :FINDSWITCHEND set _MTPLIB_T4= goto :EOF rem ------------------------------------------------------------------ rem REGSETM and REGSETU procedures rem Set registry values from variables rem rem Arguments: rem %1=reg context (usually script name) rem %2=variable to save (or prefix to save set of vars) rem :REGSETM if defined TRACE %TRACE% [proc %0 %*] for /f "tokens=1* delims==" %%I in ("set %2 2^>nul") do call :REGSET1 HKLM %1 %%I "%%J" goto :EOF :REGSETU if defined TRACE %TRACE% [proc %0 %*] for /f "tokens=1* delims==" %%I in ("set %2 2^>nul") do call :REGSET1 HKCU %1 %%I "%%J" goto :EOF :REGSET1 set _MTPLIB_T10=%4 set _MTPLIB_T10=%_MTPLIB_T10:\=\\% reg add %1\Software\MTPScriptContexts\%2\%3=%_MTPLIB_T10% >nul reg update %1\Software\MTPScriptContexts\%2\%3=%_MTPLIB_T10% >nul goto :EOF rem ------------------------------------------------------------------ rem REGGETM and REGGETU procedures rem Get registry value or values to variables rem rem Arguments: rem %1=reg context (usually script name) rem %2=variable to restore (def: restore entire context) rem rem Returns: rem RET=value of last variable loaded rem rem WARNING: rem The "delims" value in the FOR commands below is a TAB rem character, followed by a space. If this file is edited by rem an editor which converts tabs to spaces, this procedure rem will break!!! rem :REGGETM if defined TRACE %TRACE% [proc %0 %*] for /f "delims= tokens=2*" %%I in ("reg query HKLM\Software\MTPScriptContexts\%1\%2 ^|find "REG_SZ"") do call :REGGETM1 %%I "%%J" goto :EOF :REGGETU if defined TRACE %TRACE% [proc %0 %*] for /f "delims= tokens=2*" %%I in ("reg query HKCU\Software\MTPScriptContexts\%1\%2 ^|find "REG_SZ"") do call :REGGETM1 %%I "%%J" goto :EOF :REGGETM1 set _MTPLIB_T10=%2 set _MTPLIB_T10=%_MTPLIB_T10:\\=\% set _MTPLIB_T10=%_MTPLIB_T10:"=% set %1=%_MTPLIB_T10% set RET=%_MTPLIB_T10% goto :EOF rem ------------------------------------------------------------------ rem REGDELM and REGDELU procedures rem Delete registry values rem rem Arguments: rem %1=reg context (usually script name) rem %2=variable to delete (def: delete entire context) rem :REGDELM if defined TRACE %TRACE% [proc %0 %*] call :GETTEMPNAME echo y >%RET% reg delete HKLM\Software\MTPScriptContexts\%1\%2 <%RET% >nul del %RET% goto :EOF :REGDELU if defined TRACE %TRACE% [proc %0 %*] call :GETTEMPNAME echo y >%RET% reg delete HKCU\Software\MTPScriptContexts\%1\%2 <%RET% >nul del %RET% goto :EOF rem ------------------------------------------------------------------ rem SRAND procedure rem Seed the random number generator rem rem Arguments: rem %1=new seed value rem :SRAND if defined TRACE %TRACE% [proc %0 %*] set /a _MTPLIB_NEXTRAND=%1 goto :EOF rem ------------------------------------------------------------------ rem RAND procedure rem Get next random number (0 to 32767) rem rem Returns: rem RET=next random number rem :RAND if defined TRACE %TRACE% [proc %0 %*] if not defined _MTPLIB_NEXTRAND set /a _MTPLIB_NEXTRAND=1 set /a _MTPLIB_NEXTRAND=_MTPLIB_NEXTRAND * 214013 + 2531011 set /a RET=_MTPLIB_NEXTRAND ^>^> 16 ^& 0x7FFF goto :EOF rem ------------------------------------------------------------------ rem RESOLVE procedure rem Fully resolve all indirect variable references in RET variable rem rem Arguments: rem RET=value to resolve rem rem Returns: rem RET=as passed in, with references resolved rem :RESOLVE if defined TRACE %TRACE% [proc %0 %*] :RESOLVELOOP if "%RET%"=="" goto :EOF set RET1=%RET% for /f "tokens=*" %%I in ('echo %RET%') do set RET=%%I if not "%RET%"=="%RET1%" goto :RESOLVELOOP goto :EOF rem ------------------------------------------------------------------ rem GETINPUTLINE procedure rem Get a single line of keyboard input rem rem Returns: rem RET=Entered line rem :GETINPUTLINE if defined TRACE %TRACE% [proc %0 %*] call :GETTEMPNAME set _MTPLIB_T1=%RET% copy con "%_MTPLIB_T1%" >nul for /f "tokens=*" %%I in ("type '%_MTPLIB_T1%'") do set RET=%%I if exist "%_MTPLIB_T1%" del "%_MTPLIB_T1%" set _MTPLIB_T1= goto :EOF rem ------------------------------------------------------------------ rem GETSYNCFILE procedure rem Get a sync file name (file will not exist) rem rem Returns: rem RET=Name of sync file to use rem :GETSYNCFILE if defined TRACE %TRACE% [proc %0 %*] call :GETTEMPNAME goto :EOF rem ------------------------------------------------------------------ rem SETSYNCFILE procedure rem Flag sync event (creates the file) rem rem Arguments: rem %1=sync filename to flag rem :SETSYNCFILE if defined TRACE %TRACE% [proc %0 %*] echo . >%1 goto :EOF rem ------------------------------------------------------------------ rem DELSYNCFILE procedure rem Delete sync file rem rem Arguments: rem %1=sync filename rem :DELSYNCFILE if defined TRACE %TRACE% [proc %0 %*] if exist %1 del %1 goto :EOF rem ------------------------------------------------------------------ rem WAITSYNCFILE procedure rem Wait for sync file to flag rem rem Arguments: rem %1=sync filename rem %2=timeout in seconds (def: 60) rem rem Returns: rem RET=Timeout remaining, or 0 if timeout rem :WAITSYNCFILE if defined TRACE %TRACE% [proc %0 %*] if {%2}=={} (set /a RET=60) else (set /a RET=%2) if exist %1 goto :EOF :WAITSYNCFILELOOP sleep 1 set /a RET-=1 if %RET% GTR 0 if not exist %1 goto :WAITSYNCFILELOOP goto :EOF rem ------------------------------------------------------------------ rem GETTEMPNAME procedure rem Create a temporary file name rem rem Returns: rem RET=Temporary file name rem :GETTEMPNAME if defined TRACE %TRACE% [proc %0 %*] if not defined _MTPLIB_NEXTTEMP set /a _MTPLIB_NEXTTEMP=1 if defined TEMP ( (set RET=%TEMP%) ) else if defined TMP ( (set RET=%TMP%) ) else (set RET=%SystemRoot%) :GETTEMPNAMELOOP set /a _MTPLIB_NEXTTEMP=_MTPLIB_NEXTTEMP * 214013 + 2531011 set /a _MTPLIB_T1=_MTPLIB_NEXTTEMP ^>^> 16 ^& 0x7FFF set RET=%RET%\~SH%_MTPLIB_T1%.tmp if exist "%RET%" goto :GETTEMPNAMELOOP set _MTPLIB_T1= goto :EOF rem ------------------------------------------------------------------ rem DELAY procedure rem Sleep for the specified number of seconds rem rem PARAMETERS rem %1=Number of seconds to sleep :DELAY if defined TRACE %TRACE% [proc %0 %*] if {%1}=={} goto :EOF @ping 127.0.0.1 -n 2 -w 1000 >nul @ping 127.0.0.1 -n %1% -w 1000 >nul goto :EOF rem These must be the FINAL LINES in the script :DOSEXIT echo This script requires Windows NT rem ------------------------------------------------------------------
@echo OFF @if not "%ECHO%"=="" echo %ECHO% @if not "%OS%"=="Windows_NT" goto DOSEXIT rem $Workfile: skeleton.bat $ rem $Revision: 2 $ rem $Date: 12/04/97 9:51a $ rem $Archive: .../skeleton.bat $ rem UPDATED BY ALEK DAVIS rem Set local scope and call MAIN procedure setlocal & pushd & set RET= set SCRIPTNAME=%~n0 set SCRIPTPATH=%~f0 set SCRIPTDIR=%~dp0 if "%DEBUG%=="1" (set TRACE=echo) else (set TRACE=rem) call %SCRIPTDIR%_mtplib :INIT %SCRIPTPATH% if /i {%1}=={/help} (call :HELP %2) & (goto :HELPEXIT) if /i {%1}=={/?} (call :HELP %2) & (goto :HELPEXIT) if /i {%1}=={/h} (call :HELP %2) & (goto :HELPEXIT) call :MAIN %* :HELPEXIT popd & endlocal & set RET=%RET% goto :EOF rem ------------------------------------------------------------------ rem HELP procedure rem Display usage information rem :HELP if defined TRACE %TRACE% [proc %0 %*] rem Put help message here goto :EOF rem ------------------------------------------------------------------ rem MAIN procedure rem :MAIN if defined TRACE %TRACE% [proc %0 %*] rem Put main script code here goto :EOF rem ------------------------------------------------------------------ rem Additional procedure go here rem These must be the FINAL LINES in the script :DOSEXIT echo This script requires Windows NT rem ------------------------------------------------------------------
@echo OFF rem Sample batch script rem Requires _mtplib.bat to be in the same folder. @if not "%ECHO%"=="" echo %ECHO% @if not "%OS%"=="Windows_NT" goto DOSEXIT rem Set local scope and call MAIN procedure setlocal & pushd & set RET= set SCRIPTNAME=%~n0 set SCRIPTPATH=%~f0 set SCRIPTDIR=%~dp0 set SCRIPTEXT=%~x0 if "%DEBUG%"=="1" (set TRACE=echo) else (set TRACE=rem) call %SCRIPTDIR%_mtplib :INIT %SCRIPTPATH% if /i {%1}=={/help} (call :HELP %2) & (goto :HELPEXIT) if /i {%1}=={/?} (call :HELP %2) & (goto :HELPEXIT) if /i {%1}=={/h} (call :HELP %2) & (goto :HELPEXIT) call :MAIN %* :HELPEXIT popd & endlocal & set RET=%RET% goto :EOF rem ------------------------------------------------------------------ rem HELP procedure rem Display usage information rem rem ------------------------------------------------------------------ rem HELP procedure rem Display usage information rem :HELP if defined TRACE %TRACE% [proc %0 %*] echo DESCRIPTION echo. echo Does this or that on the specified server. echo. echo SYNTAX echo. echo %ScriptName% [this^|that] [switches] echo. echo ARGUMENTS echo. echo this echo Does this. echo. echo that echo Does that. echo. echo /s:server echo. echo Name of server affected by this or that. echo If not specified, local server will be used. echo. echo /u:user echo. echo Name of the user performing this or that. echo If not specified, active Windows user will be used. echo. echo /p:password echo. echo Password of the the user identified by switch /u. echo. echo EXAMPLES echo. echo %ScriptName% this /s:XYZ /u:BillieJean /p:IsNotMyLover echo. echo Does this on server XYZ as user BillieJean. echo. echo %ScriptName% that /s:XYZ echo. echo Does that on server XYZ as active Windows user. goto :EOF rem ------------------------------------------------------------------ rem MAIN procedure rem :MAIN if defined TRACE %TRACE% [proc %0 %*] rem If no arguments, display help if /i {%1}=={/h} (call :HELP %2) & (goto :HELPEXIT) if /i {%1}=={} (call :HELP %2) & (goto :HELPEXIT) rem Initialize variables set SERVER= set USER= set PASSWORD= rem Process command line and set up variables set CMDLINE=%* call %SCRIPTDIR%_mtplib :PARSECMDLINE 0 rem Process positional arguments if %CMDARGCOUNT% LSS 1 (call :HELP) & (goto :EOF) call %SCRIPTDIR%_mtplib :GETARG 1 set OPERATION=%RET% if /i "%OPERATION%"=="this" set OPERATION=this if /i "%OPERATION%"=="that" set OPERATION=that rem Process command-line switches set /a IX=1 :GETSWITCHLOOP call %SCRIPTDIR%_mtplib :GETSWITCH %IX% if "%RET%"=="" goto :GETSWITCHLOOPEND set /a IX+=1 if /i "%RET%"=="/s" set SERVER=%RETV% if /i "%RET%"=="/u" set USER=%RETV% if /i "%RET%"=="/p" set PASSWORD=%RETV% goto :GETSWITCHLOOP :GETSWITCHLOOPEND rem Use defaults for non-specified variables if "%SERVER%"=="" set SERVER=%COMPUTERNAME% if "%USER%"=="" set USER=%USERDOMAIN%\%USERNAME% rem Check the operation type and perform this or that if /i "%OPERATION%"=="this" ( call :DOTHIS ) else if "%OPERATION%"=="that" ( call :DOTHAT ) else ( goto :HELP ) goto :EOF rem ------------------------------------------------------------------ rem DOTHIS procedure rem Print parameter values and sleep for 5 seconds. rem :DOTHIS if defined TRACE %TRACE% [proc %0 %*] echo Started this on %DATE% at %TIME% call :PRINTPARAMS echo Please wait 5 seconds... call %SCRIPTDIR%_mtplib :DELAY 5 if %ERRORLEVEL% neq 0 goto :EOF echo Ended this on %DATE% at %TIME% goto :EOF rem ------------------------------------------------------------------ rem DOTHAT procedure rem Print parameter values and sleep for 10 seconds. rem :DOTHAT if defined TRACE %TRACE% [proc %0 %*] echo Started that on %DATE% at %TIME% call :PRINTPARAMS echo Please wait 10 seconds... call %SCRIPTDIR%_mtplib :DELAY 10 if %ERRORLEVEL% neq 0 goto :EOF echo Ended that on %DATE% at %TIME% goto :EOF :PRINTPARAMS if defined TRACE %TRACE% [proc %0 %*] echo Server =%SERVER% echo User =%USER% echo Password=%PASSWORD% goto :EOF rem These must be the FINAL LINES in the script :DOSEXIT echo This script requires Windows NT rem ------------------------------------------------------------------
See also:
DOS - Script Snippets (library of various DOS snippets)
Using parameter qualifiers in Windows shell scripts
Thanks Alek. Lots of good scripting stuff in here. Now off to find a used version of this book on Amazon ;-)
ReplyDeleteAnother book I have considered my scripting bible is Windows 2000 Scripting Guide.
Thanks for the reference. I haven't heard of this one. Will check it out.
ReplyDelete