K&H home

NT/Win2k scripting

When NT 4.0 came out, it included a bunch of new scripting functionality. This is good. The only problem has been that documentation for this new functionality is light and scattered.*

First, some FAQs:

Commands

Perhaps the handiest of enhancements is in the FOR command. This now provides extended functionality to support things like list processing, loop counting and directory recursion. In addition, FOR variable references (and command line argument references) support an extended syntax for modifying their value. For example, %~pi resolves to the path for this iteration of the i variable in the loop, and %~f2 evaluates to the fully qualified filespec of the second command line argument for the batch. FOR can be used, for example, to manipulate dates:

FOR /f "tokens=2-4 delims=/ " %%a in ('DATE/T') do SET date=%%c-%%a-%%b

My DATE/T output looks like:
Wed 05/27/1998
And the above command sets the corresponding "date" variable to 1998-05-27. (This would override the default behavior of %date%, if that name happens to be magic in your version of Windows.)

(Remember to enter the FOR replaceable parameter in a batch program, as opposed to from the command line, as %%a instead of %a.)

Another useful change appeared in the form of an extension to the SET command. Set can now evaluate arithmetic expressions.
(The following will display the number of files in a directory hierarchy)

FOR /f %a in ('DIR/s ^|FIND " File(s) " /c') do @FOR /f %b in ('SET /a dummy^=%a-1') do @(DIR/s/-c |FIND " File(s) "|MORE /e +%b)

Linebreaks can be used with parentheses to make complex commands more readable.

FOR /f %a in ('DIR/s ^|FIND " File(s) " /c') do @(
FOR /f %b in ('SET /a dummy^=%a-1') do @(
DIR/s/-c |FIND " File(s) "|MORE /e +%b))


IF statements support a form of ELSE clause. Note that, though this was introduced in version 4.0, it doesn't seem to be officially documented anywhere in that version. The feature is fully documented in Win2000 (NT5.0), and so it should be safe in 4.0 scripts, even where forward compatibility is a concern.

IF defined mytest1 (
	rem /* -- rem comments and blank lines work -- */

	set tested=1
) ELSE  IF errorlevel 1 (
	rem /* -- note that an ELSE must be on the same line as the closing paren -- */
	echo There was an error
	set tested=2
) ELSE (
	set tested=0
)


Environment variables can now be evaluated to support better string manipulation:

FOR /F %%a IN ('dir %temp%*^|find "<" ') DO SET aDate=%%a
:: pick out a substring
SET DateSeparator=%aDate:~2,1%

for /f "tokens=*" %%a in ( 'time/t' ) do set time=%%a
:: change spaces to zeros
SET ZeroPaddedTime=%time: =0%

Environment variable that get evaluated within a FOR loop get expanded only at the beginning of the loop. So the following snippet does not produce the expected result:

set count=1
FOR /f "tokens=*" %%a in ('dir /b') do (
  echo %count%: %%a
  set /a count+=1)

To force the variable to get evaluated at the proper iteration, use the new subroutine call mechanism to take the evaluation outside of the loop.
set count=1
FOR /f "tokens=*" %%a in ('dir /b') do (
  call :exec echo %%count%%: %%a
  set /a count+=1)
goto :EOF

:exec
%*
goto :EOF
The above can be replaced with the following equivalent script (But be aware, for when it matters, that the "call call" replacement can be relatively slow. The :exec method is more than seven times faster on the XP pro box I'm currently using).
set count=1
FOR /f "tokens=*" %%a in ('dir /b') do (
  call call echo %%count%%: %%a
  set /a count+=1)
goto :EOF
Newer versions of Windows also support a DELAYEDEXPANSION property to address the variable evaluation issue. I think the syntax is too quirky to be usable, but it may solve a problem for you. See the help associated with SET and SETLOCAL for further info.

Another change worth noting is the new FINDSTR filter utility, which among other capabilities, can do a regular expression search.

The following command will create a text file (CMD_HELP.TXT) that contains usage notes for most of the NT commands that were extended in 4.0

(Help &echo ****** &(FOR %a in (
CMD DEL COLOR CD FOR IF MD PROMPT PUSHD POPD SET SETLOCAL ENDLOCAL CALL SHIFT GOTO START ASSOC FTYPE MORE FINDSTR TITLE AT) do @(
echo * %a
echo ******
(CMD /C%a /?) & echo.&echo ******)))>CMD_HELP.TXT

Miscellany

Redirection

Error output can be redirected.

DIR *.txt > out.txt

sends a directory listing to the file out.txt, while

DIR *.txt 2> out.err

sends any error messages from the command to the file out.err output can be combined using syntax borrowed from unix.

DIR nonexistant > out.all 2>&1

sends the directory listing and any error messages to the file out.all

Conditional Processing

An ampersand on the command line can be used to unconditionally combine commands on a single line. Use conditional symbols (&& and ||) to combine commands in such a way that the result of the preceeding command determines whether the following command gets executed. For example, the "del file2" command below only executes if "dir file1" returns 0

dir file1 >nul && del file2

The above is approximately equivalent to: IF EXIST file1 DEL file2

The "del fileB" below only executes if "dir fileA" returns nonzero.

dir fileA >nul || del fileB

The above is approximately equivalent to: IF NOT EXIST fileA DEL fileB

You can also handle more complex processing:

[editors note: the following example used to contain a ridiculously complex statement littered with the conditional symbols. It turned out to be buggy. Take heed. Don't do a thing just because you can. (Not even to show off...) Do it if the result is cleaner than with another method. Complex conditions will often be best written as explicit IF/ELSE statements, as is now done below.]
::::::::::::::::
@echo off

:: File: MYBAT.bat
:: Description: Demo. Shows how one might iterate over the items in a
:: list (defined in an environment variable) and do
:: some operation on each item. Can optionally specify
:: which items to start and stop on in the list.

setlocal
:nxtarg0
shift
:: any more arguments?
if %0.==. goto parsed

if /i %0==/StartAt goto setstart
if /i %0==/StopAt goto setstop
goto help
:setstart
set StartAt=%1
shift
goto nxtarg0
:setstop
set StopAt=%1
shift
goto nxtarg0

:parsed

if not defined PROJECTLIST set PROJECTLIST=P1;P2;P3;P4;P5;P6
echo on
set PastStart=
if not defined StartAt set PastStart=1
for %%p in (%PROJECTLIST%) do (
	(if not defined PastStart (
		if /I "%StartAt%"=="%%p" (
			set PastStart=1)))
	if defined PastStart call :ShowProject %%p
	if /I "%StopAt%"=="%%p" goto DoneWithList
)

:DoneWithList
goto cleanup

::
::
::

:help
echo.
echo.
echo usage: MYBAT [/StartAt startprj] [/StopAt stopprj]
echo.
echo (e.g., MYBAT /StartAt P2)
echo.
echo purpose: just a demo...
echo.

:cleanup
endlocal
goto :EOF

:ShowProject
echo Project: %1
goto :EOF

Note that the above wouldn't work as expected if any of the PROJECTLIST items contained spaces (e.g., "P 2"). For clues on how this might be remedied, consider the following

::::::::::::::::
call :listtokens "%PROJECTLIST%"
goto :EOF

:listtokens
if not %1.==. (
for /f "tokens=1* delims=;" %%a in (%1) do (
echo %%a & call :listtokens "%%b"))
goto :EOF
::::::::::::::::

The above would list all items in PROJECTLIST, even those with spaces.


The following tests a collection of environment variable and command parameter utility functions.
::::::::::::::::::::::::::::::::::::
@echo off

set test=
echo -[%test%]-
call :testcaseChk blank test

set test=" "
echo -[%test%]-
call :testcaseChk QuotedSpaceTest test

set test=%test:"=%
echo -[%test%]-
call :testcaseChk spaceTest test

set test=x
echo -[%test%]-
call :testcaseChk simple test

echo **[12345678901234567890123]**

set test="  ab c    def 123    "
echo **[%test%]**
call :testcaseChk quoted test

set test=%test:"=%
echo ** unquoted [%test%]
call :testcaseChk unquoted test

call :rtrimvar test
echo ** trimmed [%test%]
call :testcaseChk trimmed test

:: reinit our unquoted test
set test="  ab c    def 123    "
set test=%test:"=%
echo ltrim test -[%test%]-
call :ltrimvar test
echo ** ltrimmed [%test%]
call :testcaseChk ltrimmed test

goto :EOF
::::::::::::::::


::--------------
:: %1 is testcase name
:: %2 is env var name to test
::--------------
:testcaseChk
	call :varLength %2
	echo %1 %2 length is (%varLength%)
	call call :argCt %%%2%%
	echo %2 argCt is %argCt%
	call call :firstArg %%%2%%
	echo %2 firstArg is %firstArg%
	call call :lastArg %%%2%%
	echo %2 lastArg is %lastArg%
    	echo.
    goto :eof

::--------------
:: Trim leading and trailing spaces in an environment variable.
:: 1st (and only) parameter is the name of an evironment variable
:: that holds the unquoted value to be trimmed in place
::--------------
:trimvar
  call :rtrimvar %1
  call :ltrimvar %1
  goto :EOF


::--------------
:: Trim  (in place, from right end) trailing spaces in an environment variable.
:: 1st (and only) parameter is the name of an evironment variable
:: that holds the *unquoted* value to be trimmed in place
::--------------
:rtrimvar
  if not defined %1 goto :eof
  setlocal
  rem deReference
  call set rtv_trimbuf=%%%1%%

  :chkTrim
  if not defined rtv_trimbuf  goto rtrimDone
  if not "%rtv_trimbuf:~-1%"==" " if not "%rtv_trimbuf:~-1%"=="	"  (
  	goto rtrimDone
  	)
  set rtv_trimbuf=%rtv_trimbuf:~0,-1%
  goto chkTrim

  :rtrimDone
  endlocal & set %1=%rtv_trimbuf%
  goto :EOF

::--------------
:: Trim  (in place, from left end) leading spaces in an environment variable.
:: 1st (and only) parameter is the name of an evironment variable
:: that holds the *unquoted* value to be trimmed in place
::--------------
:ltrimvar
  if not defined %1 goto :eof
  setlocal
  rem deReference
  call set rtv_ltrimbuf=%%%1%%

  :chkLTrim
  if not defined rtv_ltrimbuf  goto ltrimDone
  if not "%rtv_ltrimbuf:~0,1%"==" " if not "%rtv_ltrimbuf:~0,1%"=="	"  (
  	goto ltrimDone
  	)
  set rtv_ltrimbuf=%rtv_ltrimbuf:~1%
  goto chkLTrim

  :ltrimDone
  endlocal & set %1=%rtv_ltrimbuf%
  goto :EOF

::--------------
:: argCt(...) =  number of arguments in the given list
::--------------
:argCt
 set argCt=0
 :argCtChk
  if %1.==. goto :eof
  set /a argCt+=1
  shift
  goto argCtChk

::--------------
:: varLength(*var) =  length of vlaue in given env variable
::--------------
:varLength
  setlocal
  set vl_buf=
  set vl_len=0
  if not defined %1 goto lenDone
  call set vl_buf=%%%1%%

  :chkLen
  if not defined vl_buf  goto lenDone
  set vl_buf=%vl_buf:~0,-1%
  set /a vl_len+=1
  goto chkLen

  :lenDone
  endlocal & set varLength=%vl_len%
  goto :EOF

::--------------
:: lastArg(...) =  value of last argument in the given list
::--------------
:lastArg
 set lastArg=
  :nxtLastArg
  if %1.==. goto :eof
  shift
  set lastArg=%0
  goto nxtLastArg

::--------------
:: firstArg(...) =  value of first argument in the given list
::--------------
:firstArg
  set firstArg=%1
  goto :eof
::::::::::::::::::::::::::::::::::::

Lets finish up with some more complete examples.



:: HowConnected.bat
:: tell me the username with which i'm connected to a remote NT computer.
:: (assumes the local scheduler is running, and messaging is active on both machines)
@echo off
setlocal
set remotepc=%1
if not defined remotepc set remotepc=%computername%
call :rsoon cmd.exe "/c for /f \"tokens=1-2\" %%%%a in ('net session') do if /i %%%%a==\\%remotepc% NET SEND %remotepc% %%%%b"
endlocal
goto :EOF

:: soon.exe from the reskit strips out quotes, so we roll our own
:: (hardwired to 50 second bias)

:rsoon
setlocal
:: my time fmt [The current time is:  5:23:42.49]
for /f "tokens=1* delims=:" %%a in ('echo.^|time^|findstr "[0-9]"') do (
    set rawtime=%%b)
:: a stab at portability...
set delim1=%rawtime:~3,1%
set delim2=%rawtime:~9,1%
for /f "tokens=1-3 delims=%delim1%%delim2% " %%a in ('echo %rawtime%') do (
    set hr=%%a
    set min=%%b
    set sec=%%c)
if %sec:~0,1%==0 set sec=%sec:~1%
if %min:~0,1%==0 set min=%min:~1%
if %hr:~0,1%==0 set hr=%hr:~1%

set /a sec+=50
if %sec% GTR 59 (
   set /a sec%%=60
   set /a min+=1
   if %min% GTR 59 (
      set /a min%%=60
      set /a hr+=1
      set /a hr%%=24))
at.exe \\%remotepc%  %hr%:%min%:%sec% %*

endlocal



@echo off
:: here's an nt batch file that works as a simple cgi server
:: blame: steve hardy
:: shardy@differentchairs.com

if %REQUEST_METHOD%.==GET.  goto GET
if %REQUEST_METHOD%.==POST. goto POST
GOTO fini

:: i have to cheat for the POST method, since batch doesn't want to read
:: stdin without newline, which is what the web server provides.
:: I do it with a line of perl, though of course that begs the question - if
:: you have perl, why bother with batch...?

:POST
::for /f %%a in ('getstrn.exe %HTTP_CONTENT_LENGTH%') DO SET QUERY_STRING=%%a
for /f %%a in ('perl -e "my $s; read STDIN,$s,%HTTP_CONTENT_LENGTH%; print $s"') DO SET QUERY_STRING=%%a

:GET
if defined QUERY_STRING call :parseQS "%QUERY_STRING:+= %"

:fini
echo Content-Type: text/html
echo.
echo.
:: caller can define page title...
ifndef title goto body
echo.
echo.
echo ^<head^>
echo ^<TITLE^>%title%^</TITLE^>
echo ^</head^>

:body
echo.
echo ^<body^>
echo ^<PRE^>

:: insert text/functionality here
:: any output shows up on the page.
date/t
time/t

:: uncomment the following for debugging
::set

echo ^</PRE^>
echo ^</body^>
exit

:: put QUERY_STRING format string into environment variable(s)
:: (unescapes left as an exercise...)

:parseQS
if %1.==. goto :EOF
FOR /f "tokens=1* delims=&" %%a in (%*) DO (
	CALL :setval "%%a"
	CALL :parseQS "%%b")
goto :EOF

:setval
FOR /f "tokens=1,2 delims==" %%c in (%1) DO (
	SET %%c=%%d
	IF %%d.==. SET %%c=+)
goto :EOF





 ::*************************************
 ::   Sub: relpath
 ::
 :: descr: Get relative path spec
 :: usage: relpath rtnvar fspec [basedir]
 ::
 ::  e.g.: relpath mypath \a\b\c.txt \a\z
 ::     sets mypath to .\..\b\c.txt
 ::*************************************
 @echo off

 :relpath
 if     %1.==/?. goto rphelp
 if     %2.==.   goto rphelp
 if not %4.==.   goto rphelp

 setlocal
 set basedir=%~f3

 :: basedir defaults to current directory
 if %3.==. for /f "tokens=*" %%a in ('cd') do set basedir=%%a

 set fspec=%~f2

 for %%a in (%fspec%) do if %%~da==\\ goto uncpath

 :localpath
 for /f "delims=\" %%a in ("%basedir%") do (
   for /f "delims=\" %%b in ("%fspec%") do (
     if /i not "%%a"=="%%b" goto diffdrverr
     )
   )
 goto drvok

 :uncpath
 for /f "tokens=1,2 delims=\" %%a in ("%basedir%") do (
   for /f "delims=\" %%y in ("%fspec%") do (
     if /i not "%%a"=="%%y" goto diffdrverr
     if /i not "%%b"=="%%z" goto diffdrverr
     )
   )

 :drvok
 set basedir=%basedir:"=%
 set fspec=%fspec:"=%
 set bkp=.
 set fspec0=%fspec%
 set try0=%basedir%

 :trybase
 set try=%try0%
 call :replstr fspec0 "%try%"
 if not defined fspec0 goto gotbase
 if /i not "%fspec%"=="%fspec0%" goto gotbase
 set bkp=%bkp%\..
 for %%a in ("%try0%") do set try0=%%~dpa
 :: trim trailing backslash, except on x:\
 call :varLength try0
 set /a try0end=varLength-1
 if %varLength% GTR 3 (
   call :exec if "\"=="%%try0:~%try0end%%%" set try0=%%try0:~0,%try0end%%%
   )
 goto trybase

 :gotbase
 set sep=
 if defined fspec0 call :exec if not %fspec0:~0,1%==\ set sep=\
 :: undocumented cleanup trick (exports %1 from local context)...
 endlocal &  set %1=%bkp%%sep%%fspec0%
 goto :EOF

 :replstr
 :: %1->sourcvar  %2->string to replace/del  [%3]->optional string to replace with
 set p2=%2
 if defined p2 set p2=%p2:"=%
 set p3=%3
 if defined p3 set p3=%p3:"=%
 call :exec set %1=%%%1:%p2%=%p3%%%
 goto :EOF

::--------------
:: varLength(*var) =  length of vlaue in given env variable
::--------------
:varLength
  setlocal
  set vl_buf=
  set vl_len=0
  if not defined %1 goto lenDone
  call set vl_buf=%%%1%%

  :chkLen
  if not defined vl_buf  goto lenDone
  set vl_buf=%vl_buf:~0,-1%
  set /a vl_len+=1
  goto chkLen

  :lenDone
  endlocal & set varLength=%vl_len%
  goto :EOF

 :exec
   %*
   goto :EOF

 :diffdrverr
   echo.
   echo fspec and base aren't on same drive. Aborting. >&2
   echo.
   goto :EOF

 :rphelp
   echo usage: relpath rtnvar fspec [basedir] >&2
   goto :EOF



::*************
:: numdir.bat
:: print numbered list of files in current directory
:: (one way to count while iterating)
::*************

@echo off
setlocal
set _i=0
for %%a in (*) do (
  call :exec set /a _i+=1
  call :exec echo %%_i%%: %%a
  )
endlocal
goto :eof

:exec
%*
goto :eof



::**************
:: NT4 batch environment based stack operations
:: blame: steve hardy
:: shardy@differentchairs.com

:: a stack is a (LIFO) list of semicolon (;) delimited
:: strings stored in an environment variable.
:: New strings are added (push'd) on, or removed (pop'd)
:: from, the front of the list.

:: limitation:
:: beware the cmd.exe bug (gpf) that manifests if FOR/f
:: evaluates a command that retuns a line of length greater
:: than 265. (e.g., when our stack exceeds this length)
::
:: reproduce the bug via: for /f %a in ('echo x..x') do ver
:: (where x..x is at least 266 characters)
::
:: The bug is fixed under the NT5/Win2k CMD.exe shell,
:: which can be coaxed to run under NT4 (sp4 and better), and
:: provides additional functionality
::**************


@echo off

:teststack
set mystack=

call :pushtoken mystack "first token=1"
call :pushtoken mystack "2nd token"
call :pushtoken mystack "3rd"

call :printstack mystack

call :poptoken mystack topval
call :poptoken mystack
call :poptoken mystack topval
goto :EOF
::--------------
:pushtoken
:: %1 -> stackvarname %2 -> value to push
if %2.==. goto :EOF
if not defined %1 goto newstack
::everything after the first '='
for /f "tokens=1* delims==" %%x in ('set %1') do (
for /f "delims=;" %%z in (%2) do if /I %%x==%1 (
call :exec if /I "%%x" EQU "%1" set "%1=%%z;%%y"))
goto :EOF
:newstack
for /f "delims=;" %%y in (%2) do (
set %1=%%y)
goto :EOF

::--------------
:poptoken
:: %1 -> stackvarname [%2 varname to receive pop'd val]
if %1.==. goto :EOF
:: set output value, if called for
if not %2.==. (
for /f "tokens=1* delims==" %%w in ('set "%1"') do (
for /f "tokens=1* delims=;" %%y in ("%%x") do (
call :exec if /I %%w EQU %1 set "%2=%%y")))
:: delete top value
for /f "tokens=1* delims==" %%w in ('set "%1"') do (
for /f "tokens=1* delims=;" %%y in ("%%x") do (
call :exec if /I %%w EQU %1 set "%1=%%z" ))
goto :EOF

::--------------
:printstack
:: %1 -> stackvarname
:: dereference the argument, add quotes and pass along
FOR /f "tokens=1* delims==" %%y IN ('set %1') DO (
call :exec if /I "%%y" EQU "%1" call :printtokens "%%z")
goto :EOF

::--------------
:printtokens
:: %1 -> quoted list of tokens
if not %1.==. (
for /f "tokens=1* delims=;" %%y in (%1) do (
echo %%y & call :printtokens "%%z"))
goto :EOF

::--------------
:exec
:: can use this to run commands that FOR won't handle inline
%*
goto :EOF


::**************
:: np.bat
:: here's my single most frequently used batch file.
:: (a wrapper to add wildcard/multi-file support to notepad commandline)
::**************
@echo off
:top
if %1.==. goto done
@for /f "delims=/" %%a in ('dir /b %1') do @start notepad "%~dp1%%a"
shift
goto top

:done

That's about it. Hope you found it helpful.

A good place to go if you have NT batch related questions is the newsgroup alt.msdos.batch.nt (now at Google). And be sure to consider alternate means to solve problems. While staying within batch constraints can be an interesting challenge, for non-trivial problems it will often prove inefficient. (The batch language is inherently crippled in the brave new ActiveX world, because it provides no way to manipulate ActiveX objects. These objects happen to be the main mechanism for getting at most new and interesting functionality in the Windows platform). There are many languages that don't have batch shortcomings, including perl, and anything under the Windows Scripting Host umbrella. Check 'em out.


[* When I first wrote this page back in the stone age, there was not much basic information from Microsoft on their command line interface. This in no longer as true. For example, see this link]

For additional local script examples, see the following

comments to: steve hardy

last updated: 7/06/2006