for-loop - 使用 Windows Batch 在多台 PC 上修改 INI 文件中的特定设置
问题描述
我的目标是使用外部 PC 列表通过网络在多台 PC 上修改 INI 文件中的单个设置,并将结果输出到日志文件。
- 需要是一个windows批处理文件。网络被锁定,无法运行 PS 等脚本。
- 安全性不是问题,因为在我的级别上对所有目标 PC 都开放了读\写访问权限。
- 要修改的 INI 文件的内容是 PC 独有的,因此不能选择全面覆盖/复制。
- 在 INI 中删除或添加空行不是问题。
- 该文件
PCList.txt
可以包含 1-100 个 PC 名称或 IP,每行一个。 - 输出到日志文件
PCNAME: 'Success' or 'Fail'
到目前为止,我已经找到并修改了一个脚本,该脚本将在本地编辑文件,但无法使其与 FOR/DO 循环一起处理列表中每台 PC 的操作 - 或者 - 添加日志输出
@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
以下是示例文件: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
仍在尝试围绕该脚本正在执行的所有操作-这是技术答案中提供的唯一信息(初始脚本的来源)它通过使用 .old 扩展名重命名来保留原始文件它将删除所有空白行, 但会在任何以 [ 开头的行之前插入一个空行(除非它是文件中的第一行)
如果文件中的旧行有尾随空格,我会得到指定搜索行的长度。
如果多行以旧行文本开头,它也会被更改。例如,如果文件有这些行: test=line1 test=line1 并且您将 _OldLine 设置为 test=line1,则这两行都将被更改。
如果这可能是一个问题,请
更改此行:
If /I "!_tmp:~0,%_Len%!"=="%_OldLine%" (
改为:
If /I "!_tmp!"=="%_OldLine %" (
请记住,如果文件中的旧行有尾随空格,除非您将它们包含在 _OldLine 变量中,否则它不会被更改
在这一点上,我需要做的主要事情是使用上面的脚本来执行这个操作......或者类似的东西,在外部 TXT 文件中列出的 PC 列表上 - 通过网络。我对替代方法持开放态度,只要它们是 Windows 批处理脚本,并且不包括调用外部应用程序。
愿望清单,(目前不需要):
- 能够
[SECTION]
在找到正在修改的设置的 INI 中指定或设置,在同一 INI 内的多个部分中找到相同设置的一些可能性(不是我最初需要的情况,但我可以看到它是这种情况在将来)
解决方案
任务的批处理文件代码
我建议使用以下代码:
@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
顶部定义的文件相关变量
日志文件在批处理文件的目录中创建,批处理文件名为,但文件扩展名为.log
.
临时文件是在本地计算机上为具有批处理文件名但具有文件扩展名的临时文件的目录中创建的.tmp
。
包含计算机名称或 IP 地址的列表文件必须PCList.txt
在批处理文件的目录中。
主要代码说明
对于列表文件中不以;
第一个FOR循环开头的每一行,调用具有 INI 文件完整文件名的子例程,以使用适当的 UNC 路径进行更新。行首的分号可用于注释掉计算机名称或 IP 地址。
INI 文件更新例程的要求
编写子程序以满足以下要求:
- 如果 INI 文件已包含要在正确部分中使用正确值更新的条目,则子例程不应修改它。如果文件内容根本没有真正修改,我不喜欢它设置存档属性并修改文件的最后修改日期。
- 它应该替换正确部分中条目的值,因为它与批处理文件顶部定义的值不同。
- 如果该部分已经存在但尚未包含该条目,则它应该将具有所需值的条目添加到该部分。该条目应添加在该部分的最后一个非空行之后。
- 如果文件根本不包含该部分,它应该在文件末尾添加该部分和具有所需值的条目。
编写子例程以保留文件中的所有空行,如果只有条目或节和条目必须附加在文件末尾,则文件末尾的空行除外。它不会添加空行。可以增强它以另外重新格式化 INI 文件,以确保除了文件顶部之外的部分上方始终有一个空行。
样本文件集
当前目录中有六个文件用于测试,第一个FOR循环使用以下命令行而不是发布的批处理文件代码中的命令行:
for %%I in (*.ini) do call :UpateIniFile "%%I"
File1.ini具有部分和条目,但值错误:
[SAMPLE SETTINGS]
SERVER=MYPC
Reboot=0
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
File2.ini具有所需值的部分和条目:
[SAMPLE SETTINGS]
SERVER=MYPC
Reboot=1
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
File3.ini的部分位于顶部,但不包含条目:
[SAMPLE SETTINGS]
SERVER=MYPC
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
File4.ini完全缺少该部分:
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
File5.ini以该部分结尾,但不包含条目:
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
[SAMPLE SETTINGS]
SERVER=MYPC
File6.ini类似于File1.ini,但为该文件设置了只读属性。
样本文件集的结果
批处理文件写入此样本集的日志文件:
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和File6.ini根本没有更新。
其他四个文件在批处理文件执行后有以下几行:
文件 1.ini:
[SAMPLE SETTINGS]
SERVER=MYPC
Reboot=1
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
文件 3.ini:
[SAMPLE SETTINGS]
SERVER=MYPC
Reboot=1
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
文件4.ini:
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
[SAMPLE SETTINGS]
Reboot=1
文件 5.ini:
[SECTION2]
SETTINGX=1234
[SECTION3]
SETTINGX=4567
[SAMPLE SETTINGS]
SERVER=MYPC
Reboot=1
因此,批处理文件可以很好地处理示例文件集。
INI 文件更新例程说明
首先检查是否完全找到要更新的文件,如果找不到文件,则将适当的信息附加到日志文件中。
该子例程取消定义下面代码中使用的接下来的三个环境变量。
我对如何逐行读取和打印文本文件内容的回答详细解释了子例程中使用的FOR循环?UpateIniFile
FINDSTR输出的 INI 文件中的每一行(行号和冒号开头)首先分配给环境变量Line
,延迟扩展未启用,以防止包含感叹号的行损坏。
启用下一个延迟扩展,这会导致创建现有环境变量的副本。有关命令SETLOCAL和ENDLOCAL的详细信息,请阅读此答案的下半部分。理解这两个命令对理解其余代码的作用非常重要。
如果应该更新哪个值的条目已经在具有所需值的临时文件中,则第一个IF条件为真,因此文件中的所有剩余行都可以复制到临时文件中而无需任何进一步处理。在这种情况下,从文件中读取的行被输出,其中删除了行号和由FINDSTR添加到该行的冒号。
如果在当前行之前未找到批处理文件顶部定义的部分,则第二个IF条件为真。在这种情况下,从文件中读取的行也会输出行号和冒号,但会进行额外的不区分大小写的字符串比较,以确定该行是否包含感兴趣的部分。如果找到感兴趣的部分,则使用在进入子例程时定义的环境中的EntryUpdate
值来定义 环境变量。1
否则,当前行低于感兴趣的部分。
如果该行没有行号和冒号是不区分大小写的,等于要使用所需值更新的条目,则可以停止对行的处理,因为文件包含已具有所需值的条目。命令GOTO用于退出FOR循环,继续批处理标签下的批处理文件ValueExists
,其中临时文件被删除,并在离开子程序之前将值已存在于当前文件中的信息写入日志文件。
如果感兴趣的部分中没有行号和冒号的行是空行,则环境变量EmptyLines
在进入子例程时定义的先前环境中增加一。如命令SET的帮助所解释的,该值0
用于EmptyLines
未定义的全部。感兴趣的部分是空行,在线上没有其他任何操作。
对于感兴趣的部分中的非空行,Line
首先重新定义环境变量,删除行号和冒号,以便进一步处理该行。
下一个条件比较区分大小写的行的第一个和最后一个字符,额外括在双引号中,"[]"
以确定该行是否是新部分的开头。如果此条件为真,则感兴趣的部分根本不包含该条目。因此,现在输出具有所需值的条目,接下来可能在新部分尚未输出之前找到的所有空行。然后输出带有新部分的行,并且在进入子例程时定义的环境EntryUpdate
中,使用字符串值重新定义环境变量,3
并且将环境变量CopyLines
定义为文件中的所有其他行现在可以复制到临时文件中。
但是如果当前行不是一个新的部分,它是感兴趣部分中的非空行。它可能是其条目的值不是所需值或不同条目的行。因此,首先输出所有空行,这些空行可能高于感兴趣部分的当前输入行。
下一个命令FOR用于拆分当前行并将J
左侧的字符串分配给循环变量第一个等号(在行开头的零个或多个等号之后,希望没有 INI 文件包含)。
如果感兴趣部分中的当前行不区分大小写而不是应该更新哪个值的条目,则只需输出该条目行。EntryUpdate
否则,将找到要更新的条目行,因此在使用字符串值重新定义2
环境变量和定义环境变量CopyLines
以将所有其他行简单地复制到临时文件之前,该行将输出条目名称和所需值。
FOR循环内处理INI文件行的所有输出都被重定向到临时文件中。
可能是当前的 INI 文件根本不包含感兴趣的部分,在处理完所有行后,通过检查环境变量EntryUpdate
是否仍未定义,可以找到该部分。在这种情况下,带有所需值的部分和条目将附加到临时文件中,并忽略环境变量EmptyLines
以忽略 INI 文件底部的所有空行。
也有可能 INI 文件中的最后一个部分是感兴趣的部分,但是直到文件末尾都找不到具有要更新值的条目。因此,再使用一个IF条件来检查是否有必要在文件末尾附加带有所需值的条目行,而忽略文件末尾的所有空行。
现在临时文件包含刚刚处理的 INI 文件在完成批处理文件执行后应该包含的行。因此,如果由于应用程序当前正在使用此 INI 文件打开临时文件,则临时文件将移动到 INI 文件目录并替换 INI 文件,如果它不是只读的或受文件权限写保护或受文件访问权限写保护的。
如果当前的 INI 文件不能被想要的修改替换,则删除临时文件,并将此错误情况记录在日志文件中。否则,当前的 INI 文件将成功更新,并记录在日志文件中,其中包含批处理文件如何完成更新的信息。
附加信息
现在应该清楚的是,Windows 命令处理器不是为修改文件内容而设计的,因为它是为运行命令和可执行文件而设计的。有很多脚本解释器比cmd.exe
.
要了解使用的命令及其工作原理,请打开命令提示符窗口,在其中执行以下命令,并仔细阅读每个命令显示的所有帮助页面。
call /?
del /?
echo /?
endlocal /?
findstr /?
for /?
goto /?
if /?
move /?
set /?
setlocal /?
另请参阅:GOTO :EOF 返回到哪里?
推荐阅读
- java - 等待容器端口打开超时(本地主机端口:[32773] 应该正在监听)
- jenkins - 如何在扩展选择 Jenkins 插件中使用 Groovy 变量?
- c++ - 调试时访问冲突写入位置异常
- python - 如果无法加载可选库,则创建模拟库——一种可接受的做法?
- php - PHP CRUD 和 MySQL 在两个文件上
- react-native - 从一个堆栈导航到另一个堆栈
- javascript - React – 如何在另一个已安装的组件中渲染附加组件?
- php - PHP 根据页面 ID 显示/隐藏幻灯片
- bash - 使用 wget 发送 urlencoded POST 请求
- excel - 根据另一个工作表中的多个单元格值隐藏多个工作表