.bat fails - where is the problem - windows

What could cause running a batch file to terminate in the middle of running?

An exit statement will cause a batch file to terminate early.

There exists some immediatly stop errors.
But in your special case, the cause is unknown, like the code.
rem error1 - Invalid % Expansion
rem %~
rem error2 - call REM /?
set "switch=/?"
call rem %%switch%%
rem error3 - stackoverflow
:stack
call :stack
rem error4 - call caret crash only with Vista
set "caret=^"
call echo %%caret%%
rem error5 - Long expansion
rem shows that expansion of % in a line is before the rem ignores the rest of the line
rem The expansion results in a very long line with 10000 characters, the batch stops immediatly
setlocal EnableDelayedExpansion
set "longStr=."
for /L %%c in (1,1,13) DO set longStr=!longStr:~0,2500!!longStr:~0,2500!
rem Now we heave a string with 5000 characters, and build a rem line with 2 times 5000 = 10000 characters
rem The next line crashes the batch file
rem tokenOne %longStr% %longStr%
(
ENDLOCAL
goto :eof
)
rem error 6 - %%var<LF> error, crashes only sometimes
set critical_content=hello%%~^
echo $
for %%a in (1 ) do (
for %%x in (4) do (
rem #%critical_content%#
)
)

As others mentioned, this is too open-ended to answer effectively.
Nevertheless, you may want to see this question about needing the CALL command to call another batch script and have it return to the call site.

Related

Windows batch script to collate log files into master log under condition

I’m new to batch scripting and thanks to stackoverflow, I’m able to put together a script with no time
I’m working on a batch script which triggers other batch scriptlets in parallel (with combination of start + cmd) generating individual logs, wait for them to complete and collate them into a master log.
The condition i’m using is that every log file ends with a keyword “Elapsed” the master scripts checks if each log ends with the keyword and moves the log to the masterlog, else moves to a timeout state and repeats the process again. It work fine for the first attempts but fails read the last line of rest of the files (ch2.log ch3.log and ch4.log) and copies them without checking. Could you please let me know what I am missing?
Here is the part of the script which has the logic
for /f %%i in ('dir /b ch*.log') do (
REM display the list of logs (in this case it's ch1.log ch2.log ch3.log ch4.log)
set %fname% =%%i
:ctout
timeout 20>nul
REM wait until the timer runs out
for /f delims ^=^ eol^=%%l in (%fname%) do set lastline=%%l
REM check for the last line of the file and set the last line of the log as 'lastline'
echo %lastline% | findstr /i "\<Elapsed\>" >null && set var=elapsed
REM check if the lastline has the word "Elapsed", which marks the end of file and assign a dummy variable
if not "%var%"="elapsed" goto :ctout
REM check if the variable is "elapsed" else goto ctout
type %fname% >> masterlog.txt
REM if the condition satisfies the contents of ch1.log is moved to masterlog.txt
del /s %fname% >nul 2>nul
REM deletes the logs from the list and moves to the next log file
)
for /f %%i in ('dir /b ch*.log') do (
REM display the list of logs (in this case it's ch1.log ch2.log ch3.log ch4.log)
call :wait "%%i"
)
rem ... any remaining instructions after concatenating logs here
goto :eof
rem Jumps over the internal subroutine ":wait"
:wait
timeout 20>nul
REM wait until the timer runs out
for /f "usebackq" %%l in (%1) do set lastline=%%l
REM check for the last line of the file and set the last line of the log as 'lastline'
set "var="
echo %lastline% | findstr /i "\<Elapsed\>" >nul && set var=elapsed
REM check if the lastline has the word "Elapsed", which marks the end of file and assign a dummy variable
if not "%var%"=="elapsed" goto wait
REM check if the variable is "elapsed" else goto wait
>> masterlog.txt type %1
REM if the condition satisfies the contents of ch?.log is moved to masterlog.txt
del /s %1 >nul 2>nul
REM deletes the logs from the list and moves to the next log file
goto :eof
Note: Since the filename is being supplied to the :wait routine as a quoted string, %1 will be quoted hence requirement for usebackq.
eol and delims are not required as the default delims includes Space
set var to nothing before the test as its value will persist. the first file will set var to not-empty, so it needs t be cleared before the second file is tested. The syntax SET "var=value" (where value may be empty) is used to ensure that any stray trailing spaces are NOT included in the value assigned.
the destination for findstr redirection should be nul, not null - you will find that a file named null was created.
The comparison operator in an if is == not =.

How to extract certain portion of a file that starts with "HDR" followed by a search keyword?

How to extract portion of file that starts with HDR followed by search keyword using a batch file and Windows command interpreter?
Only certain HDR should be copied to another file with name GoodHDR.txt.
HDRs not included in searches should be copied also to another file with name BadHDR.txt.
For example, I have HeaderList.txt below and need to get HEADER0001 and HEADER0003 portions.
HDRHEADER0001 X004010850P
BEG00SAD202659801032017021699CANE
HDRHEADER0002 X004010850P
BEG00SAD202611701012017021499CANW
DTM01020170214
N1ST 92 0642397236
N315829 RUE BELLERIVE
N4MONTREAL QCH1A5A6 CANADA
HDRHEADER0003 X004010850P
BEG00SAP521006901012017021399CANOUT B16885
DTM01020170213
N1STCEGEP SAINT LAURENT 92 0642385892
Expected outcome:
GoodHDR.txt only contains HEADER0001 and HEADER0003.
HDRHEADER0001 X004010850P
BEG00SAD202659801032017021699CANE
HDRHEADER0003 X004010850P
BEG00SAP521006901012017021399CANOUT B16885
DTM01020170213
N1STCEGEP SAINT LAURENT 92 0642385892
BadHDR.txt contains HEADER0002:
HDRHEADER0002 X004010850P
BEG00SAD202611701012017021499CANW
DTM01020170214
N1ST 92 0642397236
N315829 RUE BELLERIVE
N4MONTREAL
The batch code below expects to be started with the parameters 0001 0003 to produce the two output files from source file as posted in question.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "SourceFile=HeaderList.txt"
set "FoundFile=GoodHDR.txt"
set "IgnoreFile=BadHDR.txt"
if "%~1" == "" goto ShowHelp
if "%~1" == "/?" goto ShowHelp
if not exist "%SourceFile%" goto NoHeaderList
del "%IgnoreFile%" 2>nul
del "%FoundFile%" 2>nul
rem Assign the headers passed as arguments to environment variables with
rem name HDR%~1X, HDR%~2X, HDR%~3X, etc. used later for quickly searching
rem for number of current header within the list of specified numbers.
rem All parameter strings not existing of exactly 4 digits are ignored.
set HeadersCount=0
:SetHeaders
set "HeaderNumber=%~1"
if "%HeaderNumber:~3,1%" == "" goto NextArgument
if not "%HeaderNumber:~4,1%" == "" goto NextArgument
for /F "delims=0123456789" %%I in ("%HeaderNumber%") do goto NextArgument
set "HDR%HeaderNumber%X=%HeaderNumber%"
set /A HeadersCount+=1
:NextArgument
shift /1
if not "%~1" == "" goto SetHeaders
if %HeadersCount% == 0 goto ShowHelp
rem Proces the header blocks in the source file.
set "OutputFile=%IgnoreFile%"
for /F "usebackq delims=" %%L in ("%SourceFile%") do call :ProcessLine "%%L"
rem Output a summary information of header block separation process.
if "%HeadersCount%" == "-1" set "HeadersCount="
if not defined HeadersCount (
echo All header blocks found and written to file "%FoundFile%".
goto EndBatch
)
set "SingularPlural= was"
if not %HeadersCount% == 1 set "SingularPlural=s were"
echo Following header block%SingularPlural% not found:
echo/
for /F "tokens=2 delims==" %%V in ('set HDR') do echo %%V
goto EndBatch
rem ProcessLine is a subroutine called from main FOR loop with
rem a line read from source file as first and only parameter.
rem It compares the beginning of the line with HDRHEADER. The line is
rem written to active output file if it does not start with that string.
rem Otherwise the string after HDRHEADER is extracted from the
rem line and searched in list of HDR environment variables.
rem Is the header in list of environment variables, this line and all
rem following lines up to next header line or end of source file are
rem written to file with found header blocks.
rem Otherwise the current header line and all following lines up to
rem next header line or end of source file are written to file with
rem header blocks to ignore.
rem Once all header blocks to find are indeed found and written completely
rem to the file for found header blocks, all remaining lines of source file
rem are written to the ignore file without further evaluation.
:ProcessLine
if not defined HeadersCount (
>>"%OutputFile%" echo %~1
goto :EOF
)
set "Line=%~1"
if not "%Line:~0,9%" == "HDRHEADER" (
>>"%OutputFile%" echo %~1
goto :EOF
)
set "HeaderLine=%Line:~9%"
for /F %%N in ("%HeaderLine%") do set "HeaderNumber=%%N"
set "OutputFile=%IgnoreFile%"
for /F %%N in ('set HDR%HeaderNumber%X 2^>nul') do (
set "HDR%HeaderNumber%X="
set /A HeadersCount-=1
set "OutputFile=%FoundFile%"
)
>>"%OutputFile%" echo %~1
if %HeadersCount% == 0 (
set "HeadersCount=-1"
) else if %HeadersCount% == -1 (
set "HeadersCount="
)
goto :EOF
:NoHeaderList
echo Error: The file "%SourceFile%" could not be not found in directory:
echo/
echo %CD%
goto EndBatch
:ShowHelp
echo Searches for specified headers in "%SourceFile%" and writes the
echo found header blocks to file "%FoundFile%" and all other to file
echo "%IgnoreFile%" and outputs the header blocks not found in file.
echo/
echo %~n0 XXXX [YYYY] [ZZZZ] [...]
echo/
echo %~nx0 must be called with at least one header number.
echo Only numbers with 4 digits are accepted as parameters.
:EndBatch
echo/
endlocal
pause
The redirection operator >> and the current name of the output file is specified at beginning of all lines which print with command ECHO the current line to avoid appending a trailing space on each line written to an output file and get the line printing nevertheless working if a line ends with 1, 2, 3, ...
Some additional notes about limitations on usage of this code:
The batch code is written with avoiding the usage of delayed expansion to be able to easily process also lines containing an exclamation mark. The disadvantage of not using delayed expansion is that lines containing characters in a line with a special meaning on command line like &, >, <, |, etc. result in wrong output and can even produce additional, unwanted files in current directory.It would be of course possible to extend the batch code to work also for lines in source file containing any ANSI character, but this is not necessary according to source file example which does not contain any "poison" character.
FOR ignores empty lines on reading lines from a text file. So the code as is produces 1 or 2 output files with no empty lines copied from source file.
The main FOR loop reading the lines from source file skips all lines starting with a semicolon. If this could be a problem, specify on FOR command line reading the lines from source file before delims= the parameter eol= with a character which definitely never exists at beginning of a line in source file. See help of command FOR displayed on running in a command prompt window for /? for details on parameters of set /F like eol=, delims= and tokens=.
The length of a string assigned to an environment variable plus equal sign plus name of environment variable is limited to 8192 characters. For that reason this batch code can't be used for a source file with lines longer than 8187 characters.
The length of a command line is also limited. The maximum length depends on version of Windows. So this batch file can't be used with a very large number of header numbers.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /?
del /?
echo /?
endlocal /?
for /?
goto /?
if /?
pause /?
rem /?
set /?
setlocal /?
shift /?
Read also the Microsoft article about Using Command Redirection Operators for details about >> and 2>nul and 2^>nul with redirection operator > being escaped with caret character ^ for being interpreted as literal character on parsing FOR command line, but as redirection operator later on execution of command SET by command FOR.

execute batch files in parallel and get exit code from each

How can I execute set of batch files from single batch file in parallel and get the exit code from each. When I use start it executes the batch file in parallel (new cmd window) but don't return the exit code from each. And while using call, I can get the exit code but the batch file execution happens sequentially. I have following code:
ECHO ON
setlocal EnableDelayedExpansion
sqlcmd -S server_name -E -i select_code.sql >\path\output.txt
for /f "skip=2 tokens=1-3 delims= " %%a in ('findstr /v /c:"-" \path\output.txt') do (
echo $$src=%%a>\path1\%%c.prm
echo $$trg=%%b>>\path1\%%c.prm
set param_name=%%c
start cmd \k \path\exec_pmcmd_ctrm.bat workflow_name %%param_name%%
ping 1.1.1.1 -n 1 -w 5000 > nul
set exitcode="%V_EXITCODE%"
echo %exitcode%>>\path\exitcode.txt
)
This executes the exec_pmcmd_ctrm.bat 3 times with different variable in parallel , but I am unable to get the exit code from each execution. I tried using call but then I miss the parallel execution of bat file. Any help in this regard?
First of all, the "exit code" from another Batch file (that ends with exit /B value command) is taken via %ERRORLEVEL% variable (not %V_EXITCODE%). Also, if such a value changes inside a FOR loop, it must be taken via Delayed Expansion instead: !ERRORLEVEL! (and EnableDelayedExpansion at beginning of your program). However, these points don't solve your problem because there is a misconception here...
When START command is used (without the /WAIT switch), a parallel cmd.exe process start execution. This means that there is not a direct way that the first Batch file could know in which moment the parallel Batch ends in order to get its ERRORLEVEL at that point! There is not a "wait for a started Batch file" command, but even if it would exist, it don't solve the problem of have several concurrent Batch files. In other words, your problem can not be solved via direct commands, so a work around is necessary.
The simplest solution is that the parallel Batch files store their ERRORLEVEL values in a file that could be later read by the original Batch file. Doing that imply a synchronization problem in order to avoid simultaneous write access to the same file, but that is another story...
Here is a pure batch-file solution. Basically, this script executes all batch files located in the same directory simultaneously, where the exit code (ErrorLevel) of each one is written to an individual log file (with the same name as the batch file and extension .log); these files are checked for existence; as soon as such a log file is found, the stored exit code is read and copied into a summary log file, together with the respective batch file name; as soon as all log files have been processed, this script is terminated; the exit code of this script is zero only if all the exit codes of the executed batch files are zero too. So here is the code -- see all the explanatory rem remarks:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Collect available scripts in an array:
set /A "INDEX=0"
for %%J in ("%~dp0*.bat" "%~dp0*.cmd") do (
if /I not "%%~nxJ"=="%~nx0" (
set "ITEM=%%~fJ"
call set "$BATCH[%%INDEX%%]=%%ITEM%%"
set /A "INDEX+=1"
)
)
rem // Execute scripts simultaneously, write exit codes to individual log files:
for /F "tokens=1,* delims==" %%I in ('set $BATCH[') do (
start "" /MIN cmd /C rem/ ^& "%%~fJ" ^& ^> "%%~dpnJ.log" call echo %%^^ErrorLevel%%
)
rem // Deplete summary log file:
> "%~dpn0.log" rem/
rem // Polling loop to check whether individual log files are available:
:POLLING
rem // Give processor some idle time:
> nul timeout /T 1 /NOBREAK
rem /* Loop through all available array elements; for every accomplished script,
rem so its log file is availabe, the related array element becomes deleted,
rem so finally, there should not be any more array elements defined: */
for /F "tokens=1,* delims==" %%I in ('set $BATCH[') do (
rem // Suppress error message in case log file is not yet available:
2> nul (
rem // Read exid code from log file:
set "ERRLEV=" & set "FILE="
< "%%~dpnJ.log" set /P ERRLEV=""
if defined ERRLEV (
rem // Copy the read exit code to the summary log file:
set "NAME=%%~nxJ"
>> "%~dpn0.log" call echo(%%ERRLEV%% "%%NAME%%"
rem // Undefine related array element:
set "%%I="
rem // Store log file path for later deletion:
set "FILE=%%~dpnJ.log"
)
rem // Delete individual log file finally:
if defined FILE call del "%%FILE%%"
)
)
rem // Jump to polling loop in case there are still array elements:
> nul 2>&1 set $BATCH[ && goto :POLLING
rem // Check individual exit codes and return first non-zero value, if any:
set "ERRALL="
for /F "usebackq" %%I in ("%~dpn0.log") do (
if not defined ERRALL if %%I neq 0 set "ERRALL=%%I"
)
if not defined ERRALL set "ERRALL=0"
endlocal & exit /B %ERRALL%
This approach is quite similar to the one I used in the following Improving Batch File for loop with start subcommand, but there the outputs of simultaneously executed commands are collected.

Batch script read a file that's continously written

I have a file that's being continuously written with information like this
HU/gen/tcfg/target/jacinto/starter/starter_b1.cfg
32.77 34% 141.59kB/s 0:00:00
94.64 100% 405.35kB/s 0:00:00 (xfer#60, to-check=1002/1097)
It is the output of rsync tool, that copies a folder to another path
I'm trying to write a batch script that reads this file and calculates the total amount of data being copied, focusing on this line
94.64 100% 405.35kB/s 0:00:00 (xfer#60, to-check=1002/1097)
the number 94.64 being the file size in bytes, so I'm guessing I should extract whatever is before "100%" from the line and add it
But I don't know how to continuously read the file while it's being written at the same time
Can somebody help?
Thanks
Here is an example that shows how to read a file while it is being written by another process. This is very similar to what jeb shows, except I have added a generic test to see if the process is complete. It assumes the process maintains a lock on the file throughout the entire execution, and it also assumes 20 consecutive empty lines indicates either the end of file, or else we are waiting for more output from the process. The 20 empty line threshold could be set to any number that works for you.
This solution may not work properly if the process writes partial lines to the file. I believe it is reliable if the the process always writes each line in its entirety in a single operation. Also, the lines must be less than or equal to 1021 bytes long, and they must be terminated by carriage return, linefeed.
#echo off
setlocal enableDelayedExpansion
set "file=test.txt"
set emptyCount=0
del "%file%
:: For this test, I will start an asynchronous process in a new window that
:: writes a directory listing to an output file 3 times, with a 5 second pause
:: between each listing.
start "" cmd /c "(for /l %%N in (1 1 3) do #dir & ping localhost -n 6 >nul) >"%file%""
:wait for the output file to be created and locked
if not exist "%file%" goto :wait
:: Now read and process the file, as it is being written.
call :read <"%file%"
exit /b
:read
set "ln="
set /p "ln="
if defined ln (
REM Process non-empty line here - test if correct line, extract size, and add
REM I will simply echo the line instead
echo(!ln!
REM Reset emptyCount for end of file test and read next line
set /a emptyCount=0
goto :read
) else ( REM End of file test
REM Count how many consecutive empty lines have been read.
set /a emptyCount+=1
REM Assume that 20 consecutive empty lines signifies either end of file
REM or else waiting for output. If haven't reached 20 consectutive
REM empty lines, then read next line.
if !emptyCount! lss 20 goto :read
REM Test if output file is still locked by attempting to redirect an unused
REM stream to the file in append mode. If the redirect fails, then the file
REM is still locked, meaning we are waiting for the process to finish and have
REM not reached the end. So wait 1 sec so as not to consume 100% of CPU, then
REM reset count and try to read the next line again.
REM If the file is not locked, then we are finished, so simply fall through
REM and exit.
(echo off 9>>"%file%")2>nul || (
set emptyCount=0
ping localhost -n 2 >nul
goto :read
)
)
exit /b
Like dbenham said, it can be done, but batch isn't the first choice.
Attention, the sample will run in an endless loop.
You need to add a break condition, depending of the file content.
I add the ping ... command to avoid, that the process consumes 100% CPU-time
#echo off
setlocal disabledelayedexpansion
setlocal enableDelayedExpansion
< rsyncOutput.tmp (
call :readData
)
echo ready
exit /b
:readDataX
set "line="
set /p line=
if defined line (
for /f "tokens=1,2 delims= " %%a in ("!line!") do (
if "%%b"=="100%%" echo Bytes %%a
)
)
ping localhost -n 2 > nul
goto :readDataX
exit /b

Changing a batch file when its running

I am running a long running batch file. I now realize that I have to add some more commands at the end of the batch file (no changes to exisiting content, just some extra commands). Is it possible to do this, given that most batch files are read incrementally and executed one by one? Or does the system read the entire contents of the file and then runs the job?
I just tried it, and against my intuition, it picked up the new commands at the end (on Windows XP)
I created a batch file containing
echo Hello
pause
echo world
I ran the file, and while it was paused, added
echo Salute
Saved it and pressed enter to contine the pause, all three prompts were echoed to the console.
So, go for it!
The command interpreter remembers the line position byte offset it's at in the batch file. You will be fine as long as you modify the batch file after the current executing line position byte offset at the end of the most recently parsed line of code.
If you modify it before then it will start doing strange things (repeating commands etc..).
jeb's example is a lot of fun, but it is very dependent on the length of the text that is added or deleted. I think the counter-intuitive results are what rein meant when he said "If you modify it before then it will start doing strange things (repeating commands etc..)".
I've modified jeb's code to show how dynamic code of varying length can be freely modified at the beginning of an executing batch file as long as appropriate padding is in place. The entire dynamic section is completely replaced with each iteration. Each dynamic line is prefixed with a non interfering ;. This conveniently allows FOR /F to strip the dynamic code because of the implicit EOL=; option.
Instead of looking for a particular line number, I look for a specific comment to locate where the dynamic code begins. This is easier to maintain.
I use lines of equal signs to harmlessly pad the code to allow for expansion and contraction. Any combination of the following characters could be used: comma, semicolon, equal, space, tab and/or newline. (Of course the padding cannot begin with a semicolon.) The equal signs within the parentheses allow for code expansion. The equal signs after the parentheses allow for code contraction.
Note that FOR /F strips empty lines. This limitation could be overcome by using FINDSTR to prefix each line with the line number and then strip out the prefix within the loop. But the extra code slows things down, so it's not worth doing unless the code is dependent on blank lines.
#echo off
setlocal DisableDelayedExpansion
echo The starting filesize is %~z0
:loop
echo ----------------------
::*** Start of dynamic code ***
;set value=1
::*** End of dynamic code ***
echo The current value=%value%
::
::The 2 lines of equal signs amount to 164 bytes, including end of line chars.
::Putting the lines both within and after the parentheses allows for expansion
::or contraction by up to 164 bytes within the dynamic section of code.
(
call :changeBatch
==============================================================================
==============================================================================
)
================================================================================
================================================================================
set /p "quit=Enter Q to quit, anything else to continue: "
if /i "%quit%"=="Q" exit /b
goto :loop
:changeBatch
(
for /f "usebackq delims=" %%a in ("%~f0") do (
echo %%a
if "%%a"=="::*** Start of dynamic code ***" (
setlocal enableDelayedExpansion
set /a newValue=value+1, extra=!random!%%9
echo ;set value=!newValue!
for /l %%n in (1 1 !extra!) do echo ;echo extra line %%n
endlocal
)
)
) >"%~f0.tmp"
::
::The 2 lines of equal signs amount to 164 bytes, including end of line chars.
::Putting the lines both within and after the parentheses allows for expansion
::or contraction by up to 164 bytes within the dynamic section of code.
(
move /y "%~f0.tmp" "%~f0" > nul
==============================================================================
==============================================================================
)
================================================================================
================================================================================
echo The new filesize is %~z0
exit /b
The above works, but things are much easier if the dynamic code is moved to a subroutine at the end of the file. The code can expand and contract without limitation, and without the need for padding. FINDSTR is much faster than FOR /F at removing the dynamic portion. Dynamic lines can be safely be prefixed with a semicolon (including labels!). Then the FINDSTR /V option is used to exclude lines that begin with a semicolon and the new dynamic code can simply be appended.
#echo off
setlocal DisableDelayedExpansion
echo The starting filesize is %~z0
:loop
echo ----------------------
call :changeBatch
call :dynamicCode1
call :dynamicCode2
echo The current value=%value%
set /p "quit=Enter Q to quit, anything else to continue: "
if /i "%quit%"=="Q" exit /b
goto :loop
:changeBatch
(
findstr /v "^;" "%~f0"
setlocal enableDelayedExpansion
set /a newValue=value+1, extra=!random!%%9
echo ;:dynamicCode1
echo ;set value=!newValue!
echo ;exit /b
echo ;
echo ;:dynamicCode2
for /l %%n in (1 1 !extra!) do echo ;echo extra line %%n
echo ;exit /b
endlocal
) >"%~f0.tmp"
move /y "%~f0.tmp" "%~f0" > nul
echo The new filesize is %~z0
exit /b
;:dynamicCode1
;set value=33
;exit /b
;
;:dynamicCode2
;echo extra line 1
;exit /b
Short answer: yes, batch files can modify themselves whilst running. As others have already confirmed.
Years and years ago, back before Windows 3, the place I worked had an inhouse menu system in MS-DOS. The way it ran things was quite elegant: it actually ran from a batch file that the main program (written in C) modified in order to run scripts. This trick meant that the menu program itself was not taking up memory space whilst selections were running. And this included things like the LAN Mail program and the 3270 terminal program.
But running from a self-modifying batch file meant its scripts could also do things like load TSR programs and in fact could do pretty much anything you could put in a batch file. Which made it very powerful. Only the GOTO command didn't work, until the author eventually figured out how to make the batch file restart itself for each command.
Nearly like rein said, cmd.exe remember the file position (not only the line position) it's currently is, and also for each call it push the file position on an invisble stack.
That means, you can edit your file while it's running behind and before the actual file position, you only need to know what you do ...
A small sample of an self modifying batch
It changes the line set value=1000 continuously
#echo off
setlocal DisableDelayedExpansion
:loop
REM **** the next line will be changed
set value=1000
rem ***
echo ----------------------
echo The current value=%value%
<nul set /p ".=Press a key"
pause > nul
echo(
(
call :changeBatch
rem This should be here and it should be long
)
rem ** It is neccessary, that this is also here!
goto :loop
rem ...
:changeBatch
set /a n=0
set /a newValue=value+1
set /a toggle=value %% 2
set "theNewLine=set value=%newValue%"
if %toggle%==0 (
set "theNewLine=%theNewLine% & rem This adds 50 byte to the filesize.........."
)
del "%~f0.tmp" 2> nul
for /F "usebackq delims=" %%a in ("%~f0") DO (
set /a n+=1
set "line=%%a"
setlocal EnableDelayedExpansion
if !n!==5 (
(echo !theNewLine!)
) ELSE (
(echo !line!)
)
endlocal
) >> "%~f0.tmp"
(
rem the copy should be done in a parenthesis block
copy "%~f0.tmp" "%~f0" > nul
if Armageddon==TheEndOfDays (
echo This can't never be true, or is it?
)
)
echo The first line after the replace action....
echo The second line comes always after the first line?
echo The current filesize is now %~z0
goto :eof
The command interpreter appears to remember the byte offset within each command file it is reading, but the file itself is not locked, so it is possible to make changes, say with a text editor, whilst it is running.
If a change is made to the file after this remembered location, the interpreter should happily continue to execute the now modified script. However if the change is made before that point, and that modification changes the length of the text at that point (for example you've inserted or removed some text), that remembered location is now no longer referring to the start of that next command. When the interpreter tries to read the next 'line' it will instead pick up a different line, or possibly part of a line depending on how much text was inserted or removed. If you're lucky, it will probably not be able to process whatever word it happen to land on, give an error and continue to execute from the next line - but still probably not what you want.
However, with understanding of what's going on, you can structure your scripts to reduce the risk. I have scripts that implement a simply menu system, by displaying a menu, accepting input from the user using the choice command and then processing the selection. The trick is to ensure that the point where the script waits for input is near the top of the file, so that any edits you might wish to make will occur after that point and so have no nasty impacts.
Example:
:top
call :displayMenu
:prompt
REM The script will spend most of its time waiting here.
choice /C:1234 /N "Enter selection: "
if ERRORLEVEL == 4 goto DoOption4
if ERRORLEVEL == 3 goto DoOption3
if ERRORLEVEL == 2 goto DoOption2
goto DoOption1
:displayMenu
(many lines to display menu)
goto prompt
:DoOption1
(many lines to do Option 1)
goto top
:DoOption2
(many lines to do Option 2)
goto top
(etc)

Resources