Using CMD update batch script file by modifying the JVM command lines and inserting additional JVM switches

35 Views Asked by At

I managed to write the following script (with the help of experts from Stack Overflow) to read the source batch script line by line, find the line with java.exe (first part) and a specific substring which is expected to be at the end of the line (last part), if found, update the line and insert the additional JVM switches before the last part. Then write the updated line to the output. If there is no match, the line is written to the output as is.

Below is the script which is working successfully:

@echo off

setlocal enabledelayedexpansion
set sourcefile=my_source_script.bat

:: Create the destination file
type NUL > new_%sourcefile%

:: Define the part to be inserted in the JVM Command Line
:: The switches to be inserted are defined in config.ini file
call :load_config
echo insert switches value = %INSET_JVM_SWITCHES%

REM Define the first and last part which will be used to identify the lines to be modified in the batch script
set "firstpart=java.exe"
set "lastpart=com.main.java.class.of.the.application -auto"
REM Read source line by line and prefix with line number to pick empty lines
FOR /f "delims=" %%l IN ('findstr /N "^" ".\%sourcefile%"') DO (
  set "line=%%l"
  REM Remove the line number until the column
  set "line=!line:*:=!"
  REM Initialize left and right parts. This is needed to identify the lines to be modified
  set "left=x"
  if not "!line!"=="" for %%b in ("!lastpart!") do set "left=x!line:%%~b=!"
  if not "!line!"=="" for %%b in ("!firstpart!") do set "left=x!line:%%~b=!"
  set "right=x!line!"
  REM If firstpart and lastpart are found in line ...
  if not "!line!"=="" if not !left!==!right! (
    REM Update the line by replacing the last part with the addition plus the last part
    for %%b in ("!lastpart!") do set "line=!line:%%~b=%INSET_JVM_SWITCHES% %lastpart%!"
  )
  REM Write the line to the new batch script
  echo(!line!>>new_%sourcefile%
)


goto :eof

::**************************************************************************************
:: Function: Load Config
:: Purpose: Parse the text file config.ini
:load_config
echo.
echo Loading config data from config.ini file ...
echo.
SET "configfile=%currentpath%config.ini"
if not exist %configfile% (
    echo Error: The following config file was not found:
    echo    %configfile%
    exit /b 1
)
set "uniqueList="
:: remove variables starting count_
FOR  /F "delims==" %%e In ('set count_ 2^>Nul') DO SET "%%e="

FOR /f "usebackq tokens=1* delims==" %%b IN ("%configfile%") DO (
 IF "%%c" neq "" (
  set keyname=%%b
  IF DEFINED count_!keyname! (SET /a count_%%b+=1) ELSE (SET /a count_!keyname!=0 & set uniqueList=!uniqueList!,!keyname!)
  if "!keyname:~-4!"=="_arr" (
    SET "!keyname![!count_%%b!]=%%c"
  ) else (
    SET "!keyname!=%%c"
  )
 )
)
:: Remove first char if it is a coma ,
if "!uniqueList!" neq "" if "!uniqueList:~0,1!" equ "," set uniqueList=!uniqueList:~1!
echo Completed parsing the config file.
echo.
goto :eof
::****************************************************************

In the end, I need to replace the source with the new file only after performing a check something along the following logic:

  • The size of the new file is the same as the source file plus the number of inserted characters.
  • The percentage of the difference between the source and the destination is small.

If the above criteria are matched, delete the source file and replace it with the new one.

How could this be done?

1

There are 1 best solutions below

1
Mofi On BEST ANSWER

There is not defined the environment variable currentpath as referenced in subroutine load_config. The lines of the file config.ini are not posted too. I inserted for that reason after the first three lines of the subroutine load_config the two lines:

set "INSET_JVM_SWITCHES=hello world"
goto :EOF

The lines of the file my_source_script.bat are also not posted in the question. I used for testing:

Line 1 is with nothing special. Next line 2 is an empty line.

;Line 3 with a semicolon at beginning.
   Line 4 has leading spaces.
    Line 5 has a leading horizontal tab.
Line 6 is with nothing special. Next line 7 has just a tab and four spaces if used internet browser does not remove them.
        
Line 8 is ! with exclamation marks ! in line!
? Line 9 starts with a question mark.
: Line 10 starts with a colon.
] Line 11 starts with a closing square bracket.
"C:\Program Files\Oracle\bin\java.exe" arg1 "argument 2" com.main.java.class.of.the.application -auto
"C:\Program Files\Oracle\bin\java.exe" arg1 "argument 2" com.main.java.class.of.the.application

That is of course no valid batch file. But it contains lines which are hard to handle correct using just Windows commands for making the file modification for which the Windows Command Processor is not designed at all. The usage of JScript or PowerShell would make the search and replace much easier and faster.

The last but one line of the example contents of my_source_script.bat should be modified and that search and replace should be done only once even on user runs the batch file below multiple times.

@echo off
setlocal EnableExtensions DisableDelayedExpansion
for %%I in ("my_source_script.bat") do set "SourceFile=%%~fI"
if not exist "%SourceFile%" echo ERROR: File "%SourceFile%" not found!& exit /B 3

:: Define the part to be inserted in the JVM command line.
:: The switches to be inserted are defined in config.ini file.
call :load_config

REM Define the first and last part which will be used to
REM identify the lines to be modified in the batch script.
set "FirstPart=java.exe"
set "LastPart=com.main.java.class.of.the.application -auto"
rem Do not modify the file on containing already the JVM switches to insert.
%SystemRoot%\System32\findstr.exe /I /L /C:"%INSET_JVM_SWITCHES% %LastPart%" "%SourceFile%" >nul && exit /B

echo Insert switches value = %INSET_JVM_SWITCHES%
del /A /F "%SourceFile%.tmp" 2>nul

REM Read source line by line and prefix with line number to pick empty lines.
REM If a line read from the source file contains the two strings as defined
REM above, the value of INSET_JVM_SWITCHES is inserted left to last part.
(for /F delims^=^ eol^= %%I in ('%SystemRoot%\System32\findstr.exe /N "^" "%SourceFile%" 2^>nul') do (
    set "Line=%%I"
    setlocal EnableDelayedExpansion
    if not "!Line:%FirstPart%=!" == "!Line!" if not "!Line:%LastPart%=!" == "!Line!" set "Line=!Line:%LastPart%=%INSET_JVM_SWITCHES% %LastPart%!"
    echo(!Line:*:=!
    endlocal
))>"%SourceFile%.tmp"
if not exist "%SourceFile%.tmp" echo ERROR: Failed to create temporary file: "%SourceFile%.tmp"& exit /B 2

:: This code was taken from: https://www.dostips.com/?t=Function.strLen
(   setlocal EnableDelayedExpansion
    set "str=A!INSET_JVM_SWITCHES!"
    set "len=0"
    for /L %%B in (12,-1,0) do (
        set /A "len|=1<<%%B"
        for %%C in (!len!) do if "!str:~%%C,1!"=="" set /A "len&=~1<<%%B"
    )
    rem There is additionally inserted a space considered here.
    set /A len+=1
)
endlocal & set "ExpectedDiff=%len%"

for %%I in ("%SourceFile%") do set "FileSizeSource=%%~zI"
for %%I in ("%SourceFile%.tmp") do set "FileSizeNew=%%~zI"
set /A FileSizeDiff=FileSizeNew-FileSizeSource

if %FileSizeDiff% == %ExpectedDiff% (
    %SystemRoot%\System32\attrib.exe -R "%SourceFile%"
    move /Y "%SourceFile%.tmp" "%SourceFile%" >nul
)

if not exist "%SourceFile%.tmp" exit /B 0
del "%SourceFile%.tmp"
echo ERROR: Failed to update file "%SourceFile%"!
exit /B 1

::**************************************************************************************
:: Function: Load Config
:: Purpose: Parse the text file config.ini
:load_config
echo(
echo Loading config data from config.ini file ...
echo(
SET "configfile=%currentpath%config.ini"
if not exist "%configfile%" (
    echo Error: The following config file was not found:
    echo    %configfile%
    exit /b 1
)
set "uniqueList="
:: remove variables starting count_
FOR /F "delims==" %%e IN ('set count_ 2^>Nul') DO SET "%%e="

FOR /F "usebackq tokens=1* delims==" %%b IN ("%configfile%") DO (
 IF NOT "%%c" == "" (
  set keyname=%%b
  IF DEFINED count_!keyname! (SET /a count_%%b+=1) ELSE (SET /a count_!keyname!=0 & set uniqueList=!uniqueList!,!keyname!)
  if "!keyname:~-4!" == "_arr" (
    SET "!keyname![!count_%%b!]=%%c"
  ) else (
    SET "!keyname!=%%c"
  )
 )
)
:: Remove first char if it is a coma ,
if not "!uniqueList!" == "" if "!uniqueList:~0,1!" == "," set uniqueList=!uniqueList:~1!
echo Completed parsing the config file.
echo(
goto :EOF
::****************************************************************

The batch file references the file my_source_script.bat and most likely also config.ini with a path relative to current working directory. This can be any directory as the process starting cmd.exe for processing the batch file defines the current working directory or cmd.exe changes itself in case of the parent process defines a directory on a network resource using a UNC path as current working directory.

Three examples for current working directory not being the directory containing the batch file:

The batch file user should at least get an error message output with fully qualified file name to see in which directory the batch file tried to update the batch file. That is the reason for using as third line:

for %%I in ("my_source_script.bat") do set "SourceFile=%%~fI"

There are added several additional conditions to inform the user of the batch file about various errors which could occur during the execution of the batch file preventing this batch file to update the other batch file. The user can see the error message on running the batch file from within a command prompt window, but not on double clicking the batch file as in this case cmd.exe is started by explorer.exe with the option /c and the batch file name appended resulting in an immediate close of cmd.exe once the batch file processing finished for whatever reason. There could be added pause & left to every exit /B to halt the batch file execution until a key is pressed giving the user also the chance to see the error message on having started the batch file execution with a double click on the batch file in Windows File Explorer.

The first added condition is:

if not exist "%SourceFile%" echo ERROR: File "%SourceFile%" not found!& exit /B 3

The condition should be self-explaining.

The second added check is:

%SystemRoot%\System32\findstr.exe /I /L /C:"%INSET_JVM_SWITCHES% %LastPart%" "%SourceFile%" >nul && exit /B

This command line prevents multiple updates of the same line if the user runs the batch file more than once for whatever reason.

The following line makes sure that the temporary file in the directory of the batch file to update does not already exist by chance:

del /A /F "%SourceFile%.tmp" 2>nul

It is of course possible that the batch file user does not have the permissions to modify any file in the directory of the batch file to update or there is very unlikely a directory with name of the temporary file.

The following condition handles at least the first unusual use case with write-protected directory:

if not exist "%SourceFile%.tmp" echo ERROR: Failed to create temporary file: "%SourceFile%.tmp"& exit /B 2

The loop for creating the temporary file with updating the single line is rewritten for working also for a batch file containing one or more ! or a line beginning with ;. For more details see: How to read and print contents of text file line by line? The loop is also with less command lines as before.

There is determined next the length of the string with the JVM switches to insert to know the number of characters (=bytes) of which the updated batch file should grow in file size.

The file size of the source file and of the created temporary file with the updated line is determined and the file size difference is calculated using a 32-bit signed integer arithmetic. The batch file is surely less than 2 GiB (less than 2147483648 bytes) even after the insert of the JVM switches.

The source file is replaced by the temporary file with the updated line if the file size difference is equal the length of the inserted string (with the additional space character). The batch file processing is exited with exit code 0 on this file replacement works as expected.

Otherwise the batch file deletes the temporarily created file on existing at all and informs the user about the failed attempt to update the batch file before exiting with exit code 1 indicating like 2 and 3 an error to the parent process.

Read following pages for the reasons of the modifications in subroutine load_config:

  • DosTips forum topic: ECHO. FAILS to give text or blank line - Instead use ECHO/
    The usage of echo. for printing an empty line results always in searching by cmd.exe for a file with name echo in the current working directory and in execution of this file on really existing. echo/ and echo( results in the output of an empty line without accessing the file system at all.
  • Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files
    The comparison operators EQU and NEQ designed primary for comparing two integer values should not be used on comparing two strings enclosed in " although it is possible. The referenced answer explains the reasons.

There is used if %FileSizeDiff% == %ExpectedDiff% instead of if "%FileSizeDiff%" == "%ExpectedDiff%" or if %FileSizeDiff% EQU %ExpectedDiff% because of FileSizeDiff and ExpectedDiff are always defined with the provided code with integer values as strings because of using an arithmetic expression for the final definition of each environment variable and a case-sensitive string comparison can be done by the CPU with less instructions to execute than an integer comparison with converting first both integer string values internally in cmd.exe to 32-bit signed integers before doing the integer comparison.