for-loopbatch-fileinifindandmodify

Modify a specific setting in an INI file on multiple PCs using Windows Batch


My goal is to modify a single setting in an INI file over the network on multiple PC's using an external list of PCs, and output the results to a log file.

So far, I have found and modified a script that will edit a file locally, but have been unable to get it to work with a FOR/DO loop to process that action for each PC in the list - OR - add the logging output

@Echo Off
SetLocal EnableDelayedExpansion

Set _PathtoFile=C:\Test\Sample.ini
Set _OldLine=Reboot=
Set _NewLine=Reboot=1

Call :_Parse "%_PathtoFile%"
Set _Len=0
Set _Str=%_OldLine%
Set _Str=%_Str:"=.%987654321

:_Loop
If NOT "%_Str:~18%"=="" Set _Str=%_Str:~9%& Set /A _Len+=9& Goto _Loop
Set _Num=%_Str:~9,1%
Set /A _Len=_Len+_Num
PushD %_FilePath%
If Exist %_FileName%.new Del %_FileName%.new
If Exist %_FileName%.old Del %_FileName%.old
Set _LineNo=0
For /F "Tokens=* Eol=" %%I In (%_FileName%%_FileExt%) Do (
    Set _tmp=%%I
    Set /A _LineNo+=1
    If /I "!_tmp:~0,%_Len%!"=="%_OldLine%" (
        >>%_FileName%.new Echo %_NewLine%
    ) Else (
        If !_LineNo! GTR 1 If "!_tmp:~0,1!"=="[" Echo.>>%_FileName%.new
        SetLocal DisableDelayedExpansion
        >>%_FileName%.new Echo %%I
        EndLocal
    ))
Ren %_FileName%%_FileExt% %_FileName%.old
Ren %_FileName%.new %_FileName%.ini
PopD
Goto :EOF

:_Parse
Set _FilePath=%~dp1
Set _FileName=%~n1
Set _FileExt=%~x1
Goto :EOF

Here is the sample files: Settings.ini

[SAMPLE SETTINGS]
SERVER=MYPC
Reboot=0
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567

PCList.txt

MY-PC
YOUR_PC
NETSERVER
192.168.10.100

Still trying to wrap my head around everything that this script is doing - this was the only information provided in the tech's answer (source of the initial script) It preserves the original file by renaming it with a .old extension It will remove all blank lines, but will insert a blank line before any line that starts with [ (unless it's the first line in the file)

I get the length of the specified search line in case the old line in the file has trailing spaces.
If more than one line starts with the old line text, it will be changed as well. Example, if the file has these lines: test=line1 test=line1 again and you set _OldLine to test=line1, both lines will be changed.
If that might be a problem,
change this line:
If /I "!_tmp:~0,%_Len%!"=="%_OldLine%" (
to this:
If /I "!_tmp!"=="%_OldLine%" (
Just keep in mind that with this, if the old line in the file has trailing spaces, it won't be changed unless you include them in the _OldLine variable

The main thing I need at this point is getting this action to take place using the above script... Or something similar, on a list of PC's listed in an external TXT file - over the network. I'm open to alternative approaches provided they are windows batch scripting, and do not include calling an external application.

Wish list, (by no means required at this time):


Solution

  • Batch file code for the task

    I suggest to use the following code:

    @echo off
    setlocal EnableExtensions DisableDelayedExpansion
    set "SectionName=[SAMPLE SETTINGS]"
    set "EntryName=Reboot"
    set "EntryValue=1"
    set "LogFile=%~dpn0.log"
    set "TempFile=%TEMP%\%~n0.tmp"
    set "ListFile=%~dp0PCList.txt"
    
    if not exist "%ListFile%" (
        echo ERROR: List file %ListFile%" not found.>"%LogFile%"
        goto EndBatch
    )
    
    del /A /F "%LogFile%" 2>nul
    for /F "usebackq delims=" %%I in ("%ListFile%") do call :UpateIniFile "\\%%I\C$\Test\Sample.ini"
    goto EndBatch
    
    :UpateIniFile
    if not exist %1 (
        echo File not found:   %1>>"%LogFile%"
        goto :EOF
    )
    set "EmptyLines="
    set "EntryUpdate="
    set "CopyLines="
    (for /F delims^=^ eol^= %%I in ('%SystemRoot%\System32\findstr.exe /N "^" %1 2^>nul') do (
        set "Line=%%I"
        setlocal EnableDelayedExpansion
        if defined CopyLines (
            echo(!Line:*:=!
            endlocal
        ) else if not defined EntryUpdate (
            echo(!Line:*:=!
            if /I "!Line:*:=!" == "!SectionName!" (
                endlocal
                set "EntryUpdate=1"
            )
        ) else (
            if /I "!Line:*:=!" == "!EntryName!=!EntryValue!" (
                endlocal
                goto ValueExists
            )
            if "!Line:*:=!" == "" (
                endlocal
                set /A EmptyLines+=1
            ) else (
                set "Line=!Line:*:=!"
                if "!Line:~0,1!!Line:~-1!" == "[]" (
                    echo !EntryName!=!EntryValue!
                    if defined EmptyLines for /L %%J in (1,1,!EmptyLines!) do echo(
                    echo !Line!
                    endlocal
                    set "EntryUpdate=3"
                    set "CopyLines=1"
                ) else (
                    if defined EmptyLines for /L %%L in (1,1,!EmptyLines!) do echo(
                    for /F delims^=^=^ eol^= %%J in ("!Line!") do (
                        if /I not "%%~J" == "!EntryName!" (
                            echo !Line!
                            endlocal
                        ) else (
                            echo !EntryName!=!EntryValue!
                            endlocal
                            set "EntryUpdate=2"
                            set "CopyLines=1"
                        )
                    )
                    set "EmptyLines="
                )
            )
        )
    ))>"%TempFile%"
    
    if not defined EntryUpdate (
        >>"%TempFile%" echo %SectionName%
        >>"%TempFile%" echo %EntryName%=%EntryValue%
        set EntryUpdate=4
    )
    if %EntryUpdate% == 1 (
        >>"%TempFile%" echo %EntryName%=%EntryValue%
        set "EntryUpdate=3"
    )
    
    move /Y "%TempFile%" %1 2>nul
    if errorlevel 1 (
        echo Failed to update: %1>>"%LogFile%"
        del "%TempFile%"
        goto :EOF
    )
    
    if %EntryUpdate% == 2 (
        echo Value updated in: %1>>"%LogFile%"
        goto :EOF
    )
    if %EntryUpdate% == 3 (
        echo Entry added to:   %1>>"%LogFile%"
        goto :EOF
    )
    if %EntryUpdate% == 4 (
        echo Section+entry to: %1>>"%LogFile%"
        goto :EOF
    )
    
    :ValueExists
    echo Value existed in: %1>>"%LogFile%"
    del "%TempFile%"
    goto :EOF
    
    :EndBatch
    endlocal
    

    File related variables defined at top

    The log file is created in directory of the batch file with batch file name, but with file extension .log.

    The temporary file is created on local machine in directory for temporary files with batch file name, but with file extension .tmp.

    The list file containing the computer names or IP addresses must be PCList.txt in directory of the batch file.

    Explanation of main code

    For each line in the list file not starting with ; the first FOR loop calls a subroutine with the full file name of the INI file to update with appropriate UNC path. A semicolon at beginning of a line can be used to comment out a computer name or IP address.

    Requirements for the INI file update routine

    The subroutine is written to fulfill following requirements:

    1. The subroutine should not modify the INI file if it contains already the entry to update with the correct value in correct section. I don't like it setting archive attribute and modifying the last modification date of a file if the file contents is not really modified at all.
    2. It should replace the value of the entry in the correct section on being different to the value defined at top of the batch file.
    3. It should add the entry with the wanted value to the section if the section is already existing, but does not yet contain the entry. The entry should be added after last non-empty line of the section.
    4. It should add the section and the entry with the wanted value at end of the file if the file does not contain the section at all.

    The subroutine is written to keep all empty lines in file, except empty lines at end of the file if just the entry or the section and the entry must be appended at end of the file. It does not add empty lines. It could be enhanced to additionally reformat an INI file with making sure that there is always exactly one empty line above a section except at top of the file.

    Sample set of files

    There were six files used in current directory for testing with using as first FOR loop the following command line instead of the command line in posted batch file code:

    for %%I in (*.ini) do call :UpateIniFile "%%I"
    

    File1.ini with having the section and the entry, but with wrong value:

    [SAMPLE SETTINGS]
    SERVER=MYPC
    Reboot=0
    
    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    

    File2.ini with having the section and the entry with the wanted value:

    [SAMPLE SETTINGS]
    SERVER=MYPC
    Reboot=1
    [SECTION2]
    SETTINGX=1234
    [SECTION3]
    SETTINGX=4567
    

    File3.ini with having the section at top, but not containing the entry:

    [SAMPLE SETTINGS]
    SERVER=MYPC
    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    

    File4.ini with missing the section completely:

    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    

    File5.ini with having the section at end, but not containing the entry:

    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    
    [SAMPLE SETTINGS]
    
    
    SERVER=MYPC
    

    File6.ini is like File1.ini, but read-only attribute is set for this file.

    Results for sample set of files

    The batch file writes to the log file for this sample set:

    Value updated in: "File1.ini"
    Value existed in: "File2.ini"
    Entry added to:   "File3.ini"
    Section+entry to: "File4.ini"
    Entry added to:   "File5.ini"
    Failed to update: "File6.ini"
    

    File2.ini and File6.ini are not updated at all.

    The other four files have the following lines after batch file execution:

    File1.ini:

    [SAMPLE SETTINGS]
    SERVER=MYPC
    Reboot=1
    
    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    

    File3.ini:

    [SAMPLE SETTINGS]
    SERVER=MYPC
    Reboot=1
    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    

    File4.ini:

    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    [SAMPLE SETTINGS]
    Reboot=1
    

    File5.ini:

    [SECTION2]
    SETTINGX=1234
    
    [SECTION3]
    SETTINGX=4567
    
    [SAMPLE SETTINGS]
    
    
    SERVER=MYPC
    Reboot=1
    

    So the batch file makes a good job for the sample set of files.

    Explanation of INI file update routine

    There is first checked if the file to update is found at all and an appropriate information is appended to the log file if the file cannot be found.

    The subroutine undefines next three environment variables used in the code below.

    The FOR loop used in subroutine UpateIniFile is explained in full detail by my answer on
    How to read and print contents of text file line by line?

    Every line in the INI file output by FINDSTR with line number and colon at beginning is assigned first to environment variable Line with delayed expansion not enabled to prevent line corruption on containing exclamation marks.

    Next delayed expansion is enabled which results in creating also a copy of the existing environment variables. Please read lower half of this answer for details about the commands SETLOCAL and ENDLOCAL. It is really important to understand what these two commands do to understand the rest of the code.

    The first IF condition is true if the entry of which value should be updated is already in the temporary file with the wanted value and so all remaining lines in the file can be copied to temporary file without any further processing. In this case the line read from file is output with removing the line number and the colon added by FINDSTR to the line.

    The second IF condition is true if the section defined at top of the batch file was not found up to current line. In this case the line read from file is also output with line number and colon, but an additional case-insensitive string comparison is done to find out if this line contains the section of interest. If the section of interest is found, the environment variable EntryUpdate is defined with value 1 in the environment defined on entering the subroutine.

    Otherwise the current line is below the section of interest.

    If this line without line number and colon is case-insensitive equal the entry to update with wanted value, the processing of the lines can be stopped as the file contains the entry already with the wanted value. Command GOTO is used to exit the FOR loop and continue batch file processing below the batch label ValueExists where the temporary file is deleted and the information is written into the log file that the value exists already in the current file before leaving the subroutine.

    If the line in section of interest without line number and colon is an empty line, the environment variable EmptyLines is incremented by one in previous environment as defined on entering the subroutine. The value 0 is used on EmptyLines not defined all as explained by help of command SET. There is nothing else done on line in section of interest is an empty line.

    For a non-empty line in section of interest the environment variable Line is first redefined with removal of line number and colon for easier processing this line further.

    The next condition compares case-sensitive the first and the last character of the line extra enclosed in double quotes with the string "[]" to find out if this line is the beginning of a new section. If this condition is true, the section of interest does not contain the entry at all. Therefore the entry with the wanted value is output now and next all empty lines found perhaps before the new section not yet output. Then the line with the new section is output and in environment defined on entering the subroutine the environment variable EntryUpdate is redefined with string value 3 and environment variable CopyLines is defined as all other lines in file can now be just copied to the temporary file.

    But if the current line is not a new section, it is a non-empty line in section of interest. It could be the line with the entry of which value is not the wanted value or a different entry. Therefore first all empty lines are output first which are perhaps above the current entry line in section of interest.

    Next one more command FOR is used to split up the current line and get assigned to the loop variable J the string left to first equal sign (after zero or more equal signs at beginning of the line which hopefully no INI file contains ever).

    If the current line in section of interest is case-insensitive not the entry of which value should be updated, the entry line is simply output. Otherwise the entry line to update is found and so the line is output with the entry name and the wanted value before the environment variable EntryUpdate is redefined with string value 2 and environment variable CopyLines is defined to copy all other lines simply to the temporary file.

    Everything output inside the FOR loop processing the lines of the INI file is redirected into the temporary file.

    It could be that current INI file does not contain the section of interest at all which can be found out after processing all lines by checking if the environment variable EntryUpdate is still not defined. In this case the section and the entry with the wanted value is appended to the temporary file with ignoring environment variable EmptyLines to ignore all empty lines at bottom of the INI file.

    It is also possible that the last section in the INI file is the section of interest, but the entry with the value to update is not found up to the end of the file. So one more IF condition is used to check if it is necessary to append at end of the file with ignoring all empty lines at end of the file the entry line with the wanted value.

    Now the temporary file contains the lines which the just processed INI file should contain after finishing batch file execution. So the temporary file is moved to INI file directory with replacing the INI file if it is not read-only or write-protected by file permissions or write-protected by file access permissions because of being currently opened by the application using this INI file.

    If the current INI file cannot be replaced with the wanted modification, the temporary file is deleted and this error condition is recorded in the log file. Otherwise the current INI file is successfully updated and that is recorded in the log file with the information how the update was done by the batch file.

    Additional information

    It should be clear now that the Windows command processor is not designed for file contents modification because it is designed for running commands and executables. There are lots of script interpreters more suitable for this task than cmd.exe.

    To understand the commands used and how they work, open a command prompt window, execute there the following commands, and read the displayed help pages for each command, entirely and carefully.

    See also: Where does GOTO :EOF return to?