如何将变量内可能包含特殊字符(如 % 或 !)的命令传递给 for /f 循环



我的代码中有一些嵌套循环,在某些时候,它们被对标签的调用所划分,如下所示:

@echo off
chcp 65001
for /r %%a in (*.mkv *.mp4 *.avi *.mov) do (
echo Processing "%%~a"
call :innerloop "%%a" "%%~fa"
)
:: Instead of goto :eof, I chose cmd /k because I want the command prompt to still be open after the script is done, not sure if this is correct though
cmd /k
:innerloop
setlocal EnableExtensions EnableDelayedExpansion
for /f "delims=" %%l in ('mkvmerge.exe -i "%~1"') do (
:: Probably this would be a safer place for setlocal, but I believe that would mean that I wouldn't get to keep a single, different !propeditcmd! per processed file
echo Processing line "%%~l"
for /f "tokens=1,4 delims=: " %%t in ("%%l") do (
:: This section checks for mkv attachments. There are similar checks for chapters and global tags, all of those are handled by mkvpropedit.exe
if /i "%%t" == "Attachment" (
if not defined attachments (
set /a "attachments=1"
) else (
set /a "attachments+=1"
)
if not defined propeditcmd (
set "propeditcmd= --delete-attachment !attachments!"
) else (
set "propeditcmd=!propeditcmd! --delete-attachment !attachments!"
)
)
)
)
:: Since !propeditcmd! (which contains the parameters to be used with the executable) is called after all lines are processed, I figured setlocal must be before the first loop in this label
if defined propeditcmd (
mkvpropedit.exe "%~f1" !propeditcmd!
)
endlocal
goto :eof

该脚本适用于大多数文件,并像这样划分,以允许在达到传递时中断内部循环而不中断外部循环。虽然它适用于大多数文件,但我注意到它无法处理名称中包含括号%的文件名,可能是由于EnableDelayedExtensions

通常,我知道我必须使用插入符号 (^) 转义这些字符,但如果特殊字符位于变量 (%~1内,我不知道该怎么做。

有没有办法做到这一点?

更新:我一直在努力将需要延迟扩展的部分与需要延迟扩展的部分分开,只需在我的代码末尾找到行mkvpropedit.exe "%~f1" !propeditcmd!,由于"%~f1"!propeditcmd!,它都需要关闭和打开它。我认为这意味着没有办法解决这个问题,逃避是必要的。

继续我的研究,这个答案似乎表明这可以通过类似set filename="%~1:!=^^!"来实现。然而,根据SS64,这似乎不是正确的语法。我也不确定这是否会用^!替换所有出现的!,我也担心这种替换可能会创建一个无限循环,如果不是更充分地通过首先用¬替换!来执行此操作^!

虽然我打算尽快进行测试以确定所有这些,但我担心我可能无法涵盖所有内容,因此肯定会有更多的投入。


PS:如果需要更多上下文,此处提供了完整代码(88 行),尽管我会根据要求编辑此问题中的代码段!

编辑:起初我认为这无关紧要,但现在我认为了解什么是mkvmerge.exe -i的标准输出会有所帮助:

File 'test.mkv': container: Matroska
Track ID 0: video (AVC/H.264/MPEG-4p10)
Track ID 1: audio (Opus)
Track ID 2: subtitles (SubRip/SRT)
Attachment ID 1: type 'image/jpeg', size 30184 bytes, file name 'test.jpg'
Attachment ID 2: type 'image/jpeg', size 30184 bytes, file name 'test2.jpg'
Attachment ID 3: type 'image/jpeg', size 30184 bytes, file name 'test3.jpg'
Chapters: 5 entries
Global tags: 3 entries

实际上并不需要子例程。最后需要延迟变量扩展,但可以先将完全限定的文件名分配给环境变量(如FileName),以避免文件名包含感叹号的麻烦。

根据问题中发布的代码重写的代码,并附有一些评论:

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "WindowTitle=%~n0"
rem Find out if the batch file was started with a double click which means
rem with starting cmd.exe with option /C and the batch file name appended
rem as argument. In this case start one more Windows command processor
rem with the option /K and the batch file name to keep the Windows command
rem processor running after finishing the processing of this batch file
rem and exit the current command processor processing this batch file.
rem This code does nothing if the batch file is executed from within a
rem command prompt window or it was restarted with the two options /D /K.
setlocal EnableDelayedExpansion
for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do (
if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" (
endlocal
start %SystemRoot%System32cmd.exe /D /K %0
if not errorlevel 1 exit /B
setlocal EnableDelayedExpansion
)
)
rem Set the console window title to the batch file name.
title !WindowTitle!
endlocal
set "WindowTitle="
rem Get the number of the current code page and change the code page
rem to 65001 (UTF-8). The initial code page is restored at end.
for /F "tokens=*" %%G in ('%SystemRoot%System32chcp.com') do for %%H in (%%G) do set "CodePage=%%~nH"
%SystemRoot%System32chcp.com 65001 >nul 2>&1
for /R %%G in (*.mkv *.mp4 *.avi *.mov) do (
echo(
echo Processing "%%~G"
set "Attachments="
for /F "delims=" %%L in ('mkvmerge.exe -i "%%G"') do (
rem echo Processing line "%%L"
for /F "delims=: " %%I in ("%%L") do if /I "%%I" == "Attachment" set /A Attachments+=1
)
if defined Attachments (
set "FileName=%%G"
setlocal EnableDelayedExpansion
set "propeditcmd=--delete-attachment 1"
for /L %%I in (2,1,!Attachments!) do set "propeditcmd=!propeditcmd! --delete-attachment %%I"
mkvpropedit.exe "!FileName!" !propeditcmd!
endlocal
)
)
rem Restore the initial code page.
%SystemRoot%System32chcp.com %CodePage% >nul
endlocal

为什么使用延迟扩展将窗口标题传递给 TITLE?

如果参数字符串包含动态变量、环境变量、循环变量或批处理文件参数扩展后引用空格或这些字符之一,&()[]{}^=;!'+,`~<>|所有这些字符都应由 Windows 命令处理器cmd.exe逐字解释,则必须用"括起来。出于这个原因,第三行将参数字符串WindowTitle=%~n0括在双引号中,因为%~n0引用没有文件扩展名和没有路径的批处理文件名,例如,可能包含与号,尽管这将是批处理文件非常常见的文件名。

另请参阅:Windows 命令解释器 (CMD.EXE) 如何解析脚本?

命令TITLE类似于关于"的命令ECHO。它始终将双引号解释为文字字符,并且不会从参数字符串中删除它们。因此,使用title "%WindowTitle%"将导致控制台窗口的标题以双引号开头和结尾。那看起来不太好。因此,批处理文件名作为窗口标题应传递给cmd.exe内部命令TITLE,不带双引号。但是,如果批处理文件名包含具有特殊含义的字符,则在执行命令TITLE之前处理命令行cmd.exe则存在问题,例如&。因此,启用延迟变量扩展并在此处引用分配给环境变量的批处理文件名WindowTitle从而可以根据批处理文件名真正设置窗口标题。

为什么在最后确定并恢复当前代码页?

一个好的编写的批处理文件供许多人使用,在执行环境中更改某些内容应始终恢复初始执行环境,除非批处理文件明确设计用于定义批处理文件执行完成后执行的应用程序和脚本的执行环境。

这对批处理文件开发意味着什么?

与启动批处理文件时的属性值相比,完成批处理文件的执行后,执行环境的以下属性应不被修改:

  1. 环境变量及其值的列表;
  2. 命令扩展的状态;
  3. 延迟扩张的状态;
  4. 当前目录;
  5. 命令提示符;
  6. 文本和背景颜色;
  7. 用于字符编码的代码页;
  8. 控制台窗口的行数和列数。

执行环境的前四个属性在批处理文件SETLOCAL顶部使用时未修改,也可以选择在底部使用ENDLOCAL。批处理文件底部的显式 ENDLOCAL 是可选的,因为在退出批处理文件的处理之前,cmd.exe调用它隐式地用于每个SETLOCAL,而没有执行匹配的ENDLOCAL,而与退出批处理文件处理的原因无关。

另请参阅:如何通过引用将环境变量作为参数传递给另一个批处理文件?
它详细解释了每次执行SETLOCALENDLOCAL时会发生什么。

对于每个成功执行的PUSHD,还应执行一个POPD以恢复初始当前目录。

命令提示符仅在使用命令PROMPT进行更改时才需要恢复,大多数批处理文件根本不会这样做。

使用 CHCP 更改代码页应导致在批处理文件末尾再次使用CHCP来还原原始代码页。使用命令COLOR更改文本颜色和背景颜色以及命令MODE更改控制台窗口的行和列时,也应执行相同的操作。

请参阅 DosTips 论坛主题 [信息] 保存当前代码页,尤其是 Compo 撰写的文章,以获取有关将当前代码页号分配给批处理文件末尾用于还原初始代码页的环境变量的说明。

有点难以理解为什么获取当前代码页码是通过两个FOR循环完成的,其中第二个循环使用修饰符%~n尽管chcp.com的输出绝对不是文件名。因此,让我们看看在命令CHCP输出字符串的德语 Windows 上会发生什么:

Aktive Codepage: 850.

不需要输出末尾的点,只需要代码页码,就像在英语 Windows 上输出的那样:

Active code page: 850

请参阅引用的 DosTips 主题,了解其他变体,具体取决于 Windows 的语言。

chcp.com的输出首先完全分配给循环变量,G如果输出带有前导空格/制表符的代码页信息,则chcp.com删除前导法线空格和水平制表符。第二个FOR循环使用普通空格、逗号、分号、等号和 OEM 编码的无中断空格作为单词分隔符来处理此单词列表。

第二个FOR循环使用字符串对德语代码页信息运行命令SET三次:

  1. Aktive
  2. Codepage:
  3. 850.

修饰%~n符的使用现在会导致三次访问文件系统,方法是cmd.exe并在当前目录中搜索一个文件,其中分配给循环变量的字符串H文件名。很可能没有文件Aktive。 末尾带有冒号的Codepage:是无效的文件名,并且很可能在当前目录中找不到由 Windows 文件 IO API 函数删除尾随点的文件850。但是,文件系统条目是否偶然匹配三个字符串之一并不重要,因为%~n导致仅使用从最后一个点开始到字符的字符串。所以命令SET首先用Aktive执行,第二次用Codepage:执行,最后用850第三次。因此,环境变量CodePage最终仅用数字850定义。

处理视频文件的主要 FOR 循环的说明

最外部的FOR将找到的文件的名称始终具有完整路径而不使用包围"分配给指定的循环变量G因为使用了选项/R。因此,只要必须引用完全限定的文件名,就只使用"%%G"而不是"%%~G"以加快文件名的处理速度。

echo(输出空行,请参阅 DosTips 论坛主题 ECHO。无法给出文本或空行 - 改用 ECHO/

如果在SET计算的算术表达式中引用未定义的环境变量(如Attachments),则使用值0,如在命令提示符窗口中运行set /?的用法帮助输出所述。因此,set /A Attachments+=1可用于在首次执行时定义具有1的变量,或者在当前文件的所有进一步执行中将环境变量Attachments的值递增 1。

环境变量Attachments的最终值在处理完mkvmerge输出的所有行后计算。如果有附件,则文件名将分配给环境变量,FileName仍禁用延迟变量扩展,因此!被解释为文字字符。接下来,将根据附件的数量动态创建环境变量propeditcmd

优化整个视频文件处理任务的代码

我既没有安装mkvmerge.exe也没有安装mkvpropedit,但我也查看了引用的完整代码。这是完整代码的重写优化版本,没有任何评论,我无法真正完全测试。

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "WindowTitle=%~n0"
setlocal EnableDelayedExpansion
for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do (
if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" (
endlocal
start %SystemRoot%System32cmd.exe /D /K %0
if not errorlevel 1 exit /B
setlocal EnableDelayedExpansion
)
)
title !WindowTitle!
endlocal
for /F delims^=^=^ eol^= %%G in ('set ^| %SystemRoot%System32findstr.exe /B /I /L /V "ComSpec= PATH= PATHEXT= SystemRoot= TEMP= TMP="') do set "%%G="
if exist "%~dp0mkvmerge.exe" (set "ToolsPath=%~dp0") else if exist mkvmerge.exe (set "ToolsPath=%CD%") else for %%I in (mkvmerge.exe) do set "ToolsPath=%%~dp$PATH:I"
if not defined ToolsPath echo ERROR: Could not find mkvmerge.exe!& exit /B 2
if "%ToolsPath:~-1%" == "" set "ToolsPath=%ToolsPath:~0,-1%"
if not exist "%ToolsPath%mkvpropedit.exe" echo ERROR: Could not find mkvpropedit.exe!& exit /B 2
for /F "tokens=*" %%G in ('%SystemRoot%System32chcp.com') do for %%H in (%%G) do set /A "CodePage=%%H" 2>nul
%SystemRoot%System32chcp.com 65001 >nul 2>&1
del /A /F /Q Errors.txt ExtraTracksList.txt 2>nul
(
set "ToolsPath="
set "CodePage="
for /F "delims=" %%G in ('dir *.mkv /A-D-H /B /S 2^>nul') do (
echo --^> Processing file "%%G" ...
setlocal
set "FullFileName=%%G"
for /F "tokens=1,4 delims=: " %%H in ('^""%ToolsPath%mkvmerge.exe" -i "%%G" --ui-language en^"') do (
if /I "%%I" == "audio" (
set /A AudioTracks+=1
setlocal EnableDelayedExpansion
if !AudioTracks! == 2 echo !FullFileName!>>ExtraTracksList.txt
endlocal
) else if not defined SkipFile if /I "%%I" == "subtitles" (
echo --^> "%%~nxG" has subtitles
"%ToolsPath%mkvmerge.exe" -o "%%~dpnG.nosubs%%~xG" -S -M -T -B --no-global-tags --no-chapters --ui-language en "%%G"
if not errorlevel 1 (
echo --^> Deleting old file ...
del /F "%%G"
echo --^> Renaming new file ...
ren "%%~dpnG.nosubs%%~xG" "%%~nxG"
) else (
echo Warnings/errors generated during remuxing, original file not deleted, check Errors.txt
"%ToolsPath%mkvmerge.exe" -i --ui-language en "%%G">>Errors.txt
del "%%~dpnG.nosubs%%~xG" 2>nul
)
set "SkipFile=1"
) else if /I "%%H" == "Attachment"  (
set /A Attachments+=1
) else if /I "%%H" == "Global" (
set "TagsAll=--tags all:"
) else if /I "%%H" == "Chapters" (
set "Chapters=--chapters """
)
)
if not defined SkipFile (
set "OnlyFileName=%%~nxG"
setlocal EnableDelayedExpansion
if defined Attachments (
set "PropEditOptions= --delete-attachment 1"
for /L %%H in (2,1,!Attachments!) do set "PropEditOptions=!PropEditOptions! --delete-attachment %%H"
)
if defined TagsAll set "PropEditOptions=!PropEditOptions! !TagsAll!"
if defined Chapters set "PropEditOptions=!PropEditOptions! !Chapters!"
if defined PropEditOptions (
echo --^> "!OnlyFileName!" has extras ...
"%ToolsPath%mkvpropedit.exe" "!FullFileName!"!PropEditOptions!
)
endlocal
)
echo(
echo ##########
echo(
endlocal
)
for /F "delims=" %%G in ('dir *.avi *.mp4 *.mov /A-D-H /B /S 2^>nul') do (
echo Processing file "%%G" ...
"%ToolsPath%mkvmerge.exe" -o "%%~dpnG.mkv" -S -M -T -B --no-global-tags --no-chapters --ui-language en "%%G"
if not errorlevel 1 (
echo --^> Deleting old file ...
del /F "%%G"
) else (
echo --^> Warnings/errors generated during remuxing, original file not deleted.
"%ToolsPath%mkvmerge.exe" -i --ui-language en "%%G">>Errors.txt
del "%%~dpnG.mkv" 2>nul
)
echo(
echo ##########
echo(
)
if exist Errors.txt for %%G in (Errors.txt) do if %%~zG == 0 del Errors.txt 2>nul
%SystemRoot%System32chcp.com %CodePage% >nul
)
endlocal

删除局部环境中不需要的环境变量

批处理文件必须使用多个环境变量处理数百甚至数千个文件。

每个MKV文件至少使用一次SETLOCAL和ENDLOCAL创建当前环境变量列表的副本,该副本在完成当前MKV文件的处理后被丢弃

对于每个视频文件,还执行了其他程序,Windows内核库函数CreateProcess还创建了当前进程的当前环境变量列表的副本。

因此,使用本地环境变量列表很有帮助,该列表仅包含视频文件处理期间真正需要的环境变量。

设置窗口标题后的第一个FOR在后台运行cmd.exe,如下所示:

C:WindowsSystem32cmd.exe /c set | C:WindowsSystem32findstr.exe /B /I /L /V "ComSpec= PATH= PATHEXT= SystemRoot= TEMP= TMP="

set的已启动cmd.exe在后台输出与当前处理批处理文件的命令进程相同的环境变量列表及其值。这些行被传递给findstr,搜索不区分大小写(/I)和字面(/L)以查找每行开头的空格分隔字符串(/B),并输出反转结果(/V),这意味着所有行都不以空格分隔的字符串之一开头。因此,输出所有环境变量都用=与其值分隔,除了那些由findstr搜索和找到的环境变量。

捕获的行由FOR处理,使用等号作为字符串分隔符,没有字符作为行尾字符,以处理名称以分号开头并分配给循环变量的环境变量,G仅用于从当前环境变量列表中删除变量的变量名称。

所以只剩下环境变量ComSpecPATHPATHEXTSystemRootTEMPTMP

使用完全限定的文件名以避免不必要的文件系统访问

大多数人在批处理文件中只使用没有文件扩展名和文件路径的可执行文件的文件名,这迫使cmd.exe在当前目录中搜索,然后在环境变量PATH中指定的所有目录中搜索具有环境变量PATHEXT中指定的文件扩展名的文件。这会导致数千个文件系统访问,在一个循环中处理数百个文件,调用每个文件的可执行文件。

通过在批处理文件中指定每个可执行文件及其完全限定的文件名,可以避免所有这些文件系统访问。这并不意味着批处理文件必须已经包含每个可执行文件的完全限定文件名,如上面的代码所示,因为可执行文件的完整文件名也可以在批处理文件的开头确定一次。

批处理文件首先检查mkvmerge.exe是否位于批处理文件的目录中,如果该文件检查为正,则使用完整的批处理文件路径定义环境变量ToolsPath。否则,如果在当前目录中搜索可执行mkvmerge.exe,并且如果存在名为mkvmerge.exe的文件系统条目(希望是文件而不是目录),则将当前目录路径分配给ToolsPath。最后,在环境变量PATH的目录中搜索mkvmerge.exe,如果找到,则将此目录路径分配给ToolsPath

批处理文件输出错误消息,还原初始环境并在可执行mkvmerge.exe或根本找不到mkvpropedit.exe退出。

%~dp0%%~dp$PATH:I扩展为始终以反斜杠结尾的路径字符串。%CD%扩展为不以反斜杠结尾的路径字符串,但当前目录是驱动器的根目录。出于这个原因,带有字符串比较的IF条件用于检查分配给ToolsPath的路径字符串是否以反斜杠结尾,在这种情况下,环境变量将重新定义并删除此反斜杠。在下面的代码中引用ToolsPath的路径字符串时添加了反斜杠。

使用其他方法确定当前代码页

这次使用 Compo 开发的第一个解决方案来确定当前代码页的编号。它类似于使用相同两个 FOR 循环的另一个解决方案,但第二个FOR循环执行的命令SET现在计算一个算术表达式,以便在上次迭代时获取代码页码,而无需将点分配给环境变量CodePage

让我们再看看处理字符串时会发生什么:Aktive Codepage: 850.

首先执行set /A "CodePage=Aktive",这会导致环境变量CodePage定义值0,因为Aktive被解释为环境变量名称,并且没有这样的环境变量。接下来以相同的解释和相同的结果set /A "CodePage=Codepage:"执行0。最后是执行set /A "CodePage=850."这会导致错误消息Missing operator.处理STDERR重定向到设备NUL以抑制它。但是,分配给环境变量CodePage的值会根据需要850

此解决方案的优点是在算术表达式中使用%%H,这不会导致任何文件系统访问。所以在我看来,这个解决方案总体上更好。

如何避免在处理视频文件时访问批处理文件?

我推荐阅读 为什么 GOTO 循环比 FOR 循环慢得多,并且还依赖于电源?

结论:最好将处理数百或数千个文件所需的整个代码放入一个命令块中,Windows 命令处理器只读取和分析一次。

问题在于在大多数情况下,如何处理命令块中值更改的变量,而无需使用所有时间延迟扩展,因为这会影响文件名等字符串的处理。在大多数情况下,这并不容易,但通常是可能的,因为它可以在上面的代码中看到。

环境变量ToolsPathCodePage可以在主代码块的开头立即取消定义,因为命令处理器在执行第一个命令set "ToolsPath="之前已经将所有%ToolsPath%%CodePage%替换为适当的路径和代码页码字符串。因此,执行第一个主FOR循环时的当前环境变量列表仅包含findstr找到的五个环境变量。

Windows 命令处理器在处理完所有视频文件并还原原始代码页之前,不会再访问批处理文件。

有关第二个批处理文件中代码的其他特殊信息

如果文件系统不阻止删除文件,则始终使用命令DEL首先删除在处理视频文件期间收集的信息的两个文本文件。

使用两次for /F而不是for /R作为主FOR循环,首先获取视频文件的所有文件名,以使用加载到 Windows 命令处理器内存中的完整路径进行处理,然后处理视频文件,而不是像for /R那样遍历当前文件系统条目。这对于*.mkv文件的循环处理有很大的不同,特别是在存储在 FAT32 或 exFAT 格式驱动器上的视频文件上,其中文件分配表不仅在处理 MKV 文件时更改,而且在 NTFS 格式驱动器上也不会在文件分配表中以本地字母顺序更新,就像在 NTFS 格式的驱动器上一样。使用for /R可能会导致 FAT32 或 exFAT 格式的驱动器多次处理 MKV 文件,或者由于对 MKV 文件执行mkvmergemkvpropedit而导致的文件分配表更改而跳过意外的一个或多个 MKV 文件。

命令 SETLOCALENDLOCAL用于快速恢复始终在每个MKV文件的主 FOR 循环之外定义的最小环境变量列表,这导致在处理 MKV 文件时始终丢弃对环境变量列表所做的所有更改。

mkvmerge.exe及其带有选项-i的完整路径和当前 MKV 文件的全名执行一个cmd.exe/c开头,指定的命令行有点棘手,考虑到%ToolsPath%%%G也可能包含像&这样的字符,通过cmd.exe处理批处理文件和cmd.exe启动来解释为文字字符在后台。

有必要将整个命令行括起来,以便在后台用双引号cmd.exe执行,以便此cmd.exe实例正确处理。但是,处理批处理文件的cmd.exe实例必须将这两个"解释为文字字符,而不是参数字符串的开头或结尾。否则,开头的""将通过cmd.exe处理批处理文件作为空参数字符串的开头和结尾来解释。因此,工具路径字符串将不再括在双引号中,用于cmd.exe处理批处理文件,这当然在包含&')时存在问题。

出于这个原因,在批处理文件中指定了将整个命令行括在"中的两个双引号,其中包含要转义的插入符号字符^这会导致批处理文件cmd.exe将这两个双引号解释为文字字符,而不是参数字符串的开头/结尾。

结果是,"%ToolsPath%mkvmerge.exe""%%G"被两个cmd.exe解释为双引号参数字符串,因此可以包含解释为文字字符的所有字符,否则这些字符将被解释为具有特殊含义。

有关音轨的信息始终独立于输出有关当前MKV文件的信息数据的顺序mkvmerge.exe。但是,一旦定义了环境变量SkipFile,所有其他信息都不会进一步处理,因为当前的MKV文件具有字幕。

文件Errors.txt在创建时被删除,但最终大小为 0 字节。

所用 Windows 命令的使用帮助

要了解使用的命令及其工作原理,请打开命令提示符窗口,在其中执行以下命令,然后完整而仔细地阅读每个命令的显示帮助页面。

  • call /?
  • chcp /?
  • cmd /?
  • dir /?
  • del /?
  • echo /?
  • endlocal /?
  • exit /?
  • findstr /?
  • for /?
  • if /?
  • rem /?
  • set /?
  • setlocal /?
  • start /?
  • title /?

另请参阅问题 7:使用字母 ADFNPSTXZadfnpstxz 作为循环变量,以及有关初学者在批处理文件编码中提出的一般问题的其他章节。

最新更新