Ritesh M Nayak
Ritesh M Nayak

Reputation: 8043

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?

Upvotes: 49

Views: 17751

Answers (6)

rein
rein

Reputation: 33455

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..).

Upvotes: 38

dbenham
dbenham

Reputation: 130859

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

Upvotes: 10

jeb
jeb

Reputation: 82327

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 

Upvotes: 7

user895964
user895964

Reputation:

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)

Upvotes: 4

staticsan
staticsan

Reputation: 30555

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.

Upvotes: 9

Binary Worrier
Binary Worrier

Reputation: 51711

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!

Upvotes: 38

Related Questions