Windows 批处理脚本学习教程

Collected by Clove

第六章:常用实例 上

6.1 批量修改文件名

在第4.2.2节中,我们已经会使用循环命令对大量文件改名进行批量处理。但总结一下,该批处理并不是很健壮。判断一个程序的好坏,往往不是站在程序员的角度,而从用户的角度出发。比如:在用户使用它的时候,如果输入了不正确的路径格式怎么办?如果输入了含有非法符号的前缀怎么办?输入的扩展名也有问题怎么办?改完名后看不到是否执行成功的反馈信息,等等。带着这些想法,我们将原程序再次修改一下。

:::::::批量修改文件名.bat:::::::
@echo off
title 批量修改文件名
setlocal EnableDelayedExpansion
:: 启用延迟变量扩充

:GetPath
set zpath=%CD%
:: 对变量进行初始化,防止用户不输入而直接跳过。其中%CD%表示当前路径
set /p zpath=请输入目标文件所在的路径:
if %zpath:~0,1%%zpath:~-1%=="" set zpath=%zpath:~1,-1%
:: 检查变量 zpath 的第一个和最后一个字符是否为 "" ,是的话就去掉
if not exist "%zpath%" goto :GetPath
:: 如果 zpath 值的路径不存在,就得跳转回去,要求重新输入

:GetPrefix
set prefix=未命名
set /p prefix=请输入文件名前缀(不能包含以下字符\/:*?"<>|):
for /f "delims=\/:*?<>| tokens=2" %%i in ("z%prefix%z") do goto :GetPrefix
:: 这里对变量 perfix 进行检查,发现有非法符号便跳转到 :GetPrefix
:: 事实上,这里并没有对双引号 " 进行检测,因为双引号无法在此被转义为可用的分隔符
:: 即使是在这个程序里,不正确地使用双引号也会引起程序异常而退出。
:: 因此,想把它做的非常人性化并不是一件容易的事情

:GetExt
set ext=.*
set /p ext=请输入文件的扩展名(不输入则表示所有类型):
if not "%ext:~0,1%"=="." set ext=.%ext%
:: 检查变量 ext 的第一个是否为句点 . ,不是的话就加上
:: 建议这里对变量 ext 也检查一下,发现有除*外的非法符号便跳转到 :GetExt

set answer=N
echo.
echo 您试图将 %zpath%\ 里的所有 %ext% 类型的文件以 %prefix% 为前缀名进行批量改名,是否继续?
set /p answer=继续请输入 Y ,输入其它键放弃...
if "%answer%"=="Y" goto :ReadyToRename
if "%answer%"=="y" goto :ReadyToRename

echo 放弃文件改名,按任意键退出... & goto :PauseThenQuit

:ReadyToRename

set /a num=0
echo.

if "%ext%"==".*" (
for %%i in ("%zpath%\*%ext%") do (
set /a num+=1
ren "%%i" "%prefix%!num!%%~xi" || echo 文件 %%i 改名失败 && set /a num-=1
)
) else (
for %%i in ("%zpath%\*%ext%") do (
set /a num+=1
ren "%%i" "%prefix%!num!%ext%" || echo 文件 %%i 改名失败 && set /a num-=1
)
)

if %num%==0 echo %zpath%\ 里未发现任何文件。按任意键退出... & goto :PauseThenQuit

echo 文件改名完成,按任意键退出...

:PauseThenQuit
pause>nul
::::::::::::::::::::::::::::::::


相对第4.2.2节里的批量修改文件的批处理来说,已经全面多了。不过这仍然有许多地方需要进一步完善,比如,输出的文件名编号可以用001、002、003这样的方式来表达,以便浏览器在以文件名排列文件时能按我们或需要的顺序进行排列。

========================================朴实的分割线========================================

6.2 批量备份进程映像列表以及注册表自启动项

什么是备份;为什么要备份;怎么去备份;备份些什么,说起来我们可能会更关心这些问题。
什么是备份:将某事物复制出额外一份完全一样的事物,并妥善保存起来的过程;
为啥要备份:当原事物出现异常或无法使用的时候,取出复制品并代替原来无法使用的事物;
怎么去备份:复制,与原事物完全一样地去复制,然后保存到安全的地方;
备份些什么:系统、文档、数据库、工程、记录、进度等等。
不过,这些都不是本文的主旨。

本节的写作起因是从《利用Windows系统自带命令手工搞定病毒》这篇文章开始的。这篇文章的主旨是:用 tasklist 备份好进程列表→通过 fc 比较文件找出病毒→用 netstat 判断进程→用 ntsd 终止进程→搜索找出病毒以及同伙文件并删除→用 reg 命令修复注册表。
不过,这些也不是本文的主旨。

在前面提到的《手工杀毒》一文中,第二步提到:如果感觉机器异常,可以将现有的进程与以前机器正常时的进程加以对比,看看是否多出了可疑的进程。这就意味着您必须得记忆或保留住以前正常时的进程映像名,即备份。备份时正如《手工杀毒》中所说的,在系统正常且刚进入Windows的时候就做一个进程映像名列表的备份。查看机器当前的进程可以通过组合键 Ctrl+Alt+Del 召唤出"Windows 任务管理器",然后切换到"进程"页即可。对于如何记录下当前的这些进程,您可以用 PrintScreen 键抓图(Alt+PrintScreen 可以仅抓取当前窗口的图)并以图片的形式保存,当然,您甚至可以简单地将这些映像名抄到一张纸上。本文所提供的方法要比抓图或是手抄的方法更先进一些,至少是更严肃了一些。

除了 Windows 任务管理器 以外,使用命令 tasklist 也可以看到(详细信息请参阅 tasklist/?)当前进程。tasklist>进程.txt 即可将进程以表格的形式输出到一个文本文件中。在输出结果中除了进程映像名以外,还有 PID 、会话、内存使用等信息,但后面的这些信息并不总是固定的,因此我们也并不需要这些。我们所关心的只是一个按照字母顺序排列的进程映像名清单,以便今后进行比较。

::::::备份进程和自启动.bat::::::
@echo off

set TempFolder=临时文件夹
:: 设置临时文件夹的名称
if not exist %TempFolder% md %TempFolder%
:: 创建临时文件夹
echo.>%TempFolder%\temp.tmp
:: 在临时文件夹里创建一个临时文件 temp.tmp ,并清空其内容

set ExportImageName=%cd%\进程映像列表%date:~0,10%.txt
:: 设置默认导出文件的路径以及文件名
set /p ExportImageName=所要导出的进程映像名称列表备份文件名:
:: 用户自定义输入的路径文件名

for /f "delims=," %%i in ('tasklist /nh /fo csv') do echo %%~i>>%TempFolder%\temp.tmp
:: 将 tasklist 中的进程映像名输出到临时文件 temp.tmp 中 [注1]
sort %TempFolder%\temp.tmp>"%ExportImageName%"
:: 将进程映像名按字母顺序排列
del %TempFolder%\temp.tmp
:: 删除临时文件夹里的文件 temp.tmp
echo 当前进程中的所有映像名已导出到以下文件:%ExportImageName%
pause
:::::::: 以上完成了进程备份,下面开始备份自启动的注册表信息

set ExportRegRunName=%cd%\注册表自启动%date:~0,10%.txt
:: 设置默认导出文件的路径以及文件名
set /p ExportRegRunName=所要导出的注册表自启动备份文件名:
:: 用户自定义输入的路径文件名

reg export HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run %TempFolder%\HKLMrun.reg
reg export HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run %TempFolder%\HKLMexp.reg
reg export HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run %TempFolder%\HKCUrun.reg
reg export HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run %TempFolder%\HKCUexp.reg
:: 将相关的注册表信息导出到各个临时的注册表文件中 [注2]

copy %TempFolder%\*.reg "%ExportRegRunName%">nul
:: 将临时文件夹里所有的注册表文件合并复制到一个文件中
del %TempFolder%\*.reg
:: 删除临时文件夹里所有的临时注册表文件
if exist %TempFolder% rd %TempFolder%
:: 删除临时文件夹
echo 当前自启动的注册表信息已导出到以下文件:%ExportRegRunName%
pause
::::::::::::::::::::::::::::::::

注1. tasklist /nh /fo csv 中参数 /nh 表示不输出栏标头,参数 /fo csv 表示以双引号包含字符串,并用逗号分隔各个字符串。for /f "delims=," %%i in ('tasklist /nh /fo csv') do echo %%~i>>a.txt 表示在无栏标头且以 csv 格式输出的 tasklist 结果中,以逗号为分隔符,将每行的首字符串依次输出到文本文件 a.txt 中。

注2. reg export 能将注册表指定项的所有子项和值导出到指定的文件中(请参阅 reg/? 与 reg export /?)。当然,以上4个只是常见的自启动键值,病毒或木马所在注册表中添加或修改的项不仅此而已,更多需要备份的项可用类似的方法自行添加。

对于查找可以进程的方法,与前文 5.2.1 小节中所提到的对比系统文件夹找差异的过程类似,使用命令 fc 对比两个不同时期的进程映像名列表,找出多余的可疑进程(如果您无法确定某个进程是干什么用的或者是否存在安全风险,可以先到网上搜索并了解一下,比如:进程知识库)。至于是用 taskkill 还是 ntsd 来终止可以的进程,怎样用属性加时间来查找并锁定可以的文件,以及 reg import 注册表导入恢复的用法,这些仍然不是本文的重点。[创作日期:2008-01-08]

========================================朴实的分割线========================================

6.3 批量查看同一子网络下的所有IP在线情况[编辑中]

本小节的批处理可以让您知道自己所在局域网的同一网段下都有哪些IP被使用了。不得不承认,我在创造这个批处理的时候想法很奇怪,甚至有些愚蠢。

:::::::查看所有子网IP.bat:::::::
@echo off
title 查看所有子网IP

set /a Online=0
set /a Offline=0
set /a Total=256
set ExportFile=子网IP在线统计.txt
:: 初始化在线IP与不在线IP的个数为零,共扫描256个IP,结果输出的文件名

set StartTime=%time%
:: 记录程序的开始时间

for /f "delims=: tokens=2" %%i in ('ipconfig /all ^| find /i "IP Address"') do set IP=%%i
:: 获得本机IP [注1]

if "%IP%"=="" echo 未连接到网络 & pause & goto :EOF
if "%IP%"==" 0.0.0.0" echo 未连接到网络 & pause & goto :EOF
:: 当IP为空或 0.0.0.0 时,提示未连接并退出该程序

for /f "delims=. tokens=1,2,3,4" %%i in ("%IP%") do (
set /a IP1=%%i
set /a IP2=%%j
set /a IP3=%%k
set /a IP4=%%l
)
:: 以句点为分隔符,分别将IP的四个十进制数赋给四个变量

set /a IP4=0
echo 在线的IP:>%ExportFile%
:: 初始化IP的第四个数值为零,并创建结果输出文件

:RETRY
ping %IP1%.%IP2%.%IP3%.%IP4% -n 1 -w 200 -l 16>nul && set /a Online+=1 && echo %IP1%.%IP2%.%IP3%.%IP4%>>%ExportFile% || set /a Offline+=1
:: ping 目标IP [注2]

set /p =[将本文底部评论4中的退格符替换到此处]<nul
set /a Scanned=%Online%+%Offline%
set /a Progress=(%Online%+%Offline%)*100/%Total%
set /p =正在扫描:%Scanned%/%Total% 扫描进度:%Progress%%%<nul
:: 删除当前行的内容,并重新显示进度信息 [注3]

set /a IP4+=1
if %IP4% lss %Total% goto :RETRY
:: 当IP的第四个数值小于总数时,跳转回 :RETRY 处,重复执行直到全部 ping 完为止

echo.
echo.

set EndTime=%time%
:: 记录程序的结束时间

set /a Seconds = %EndTime:~6,2% - %StartTime:~6,2%
set /a Minutes = %EndTime:~3,2% - %StartTime:~3,2%
if %Seconds% lss 0 set /a Seconds += 60 & set /a Minutes -= 1
if %Minutes% lss 0 set /a Minutes += 60
:: 计算时间差

set /a Percent=%Online%*100/(%Online%+%Offline%)
:: 计算在线百分比

echo 在线IP个数: %Online%
echo 不在线IP个数: %Offline%
echo 在线百分比: %Percent%%%
echo 统计耗时: %Minutes%分%Seconds%秒
echo 统计日期: %date% %time:~0,-3%
echo.>>%ExportFile%
echo 在线IP个数: %Online%>>%ExportFile%
echo 不在线IP个数: %Offline%>>%ExportFile%
echo 在线百分比: %Percent%%%>>%ExportFile%
echo 统计耗时: %Minutes%分%Seconds%秒>>%ExportFile%
echo 统计日期: %date% %time:~0,-3%>>%ExportFile%
echo 记录已保存到文件"%ExportFile%"中
::显示结果并将结果保存到文件中
pause
::::::::::::::::::::::::::::::::

注1. ipconfig 是内置于 Windows 的 TCP/IP 应用程序,用于显示本地计算机网络适配器的物理地址和IP地址等配制信息,这些信息一般用来检验手动配置的 TCP/IP 设置是否正确。当在网络中使用 DHCP 服务时, ipconfig 可以检测到计算机中分配到了什么IP地址,是否配置正确,并且可以释放,重新获取IP地址。这些信息对于网络测试和故障排除都有重要的作用。[3]
更详细的说明请参阅 ipconfig/? 。ipconfig /all ,参数 /all 表示查看详细的网络配置。命令 ipconfig /all ^| find /i "IP Address" 表示在 'ipconfig /all 的结果中,以 "IP Address" 为查找对象,进行搜索(其结果类似于:IP Address. . . . . . . . . . . . : 10.30.11.51 )。
而整条命令中的 for 语句,则表示在上述结果中,以冒号为间隔(delims=:),查找第2个字串(tokens=2)。很明显,所找到的结果就是自己电脑当前的IP地址了(如果您只有一快网卡或是只启用了一个网卡的话。显然,对于多个网卡会显示出多个IP的情况,我并没有考虑的太全面)。[关于 for 更详细请参阅 4.2.4 小节]
另外,注意到在 ipconfig /all ^| find /i "IP Address" 中有一个转义字符 ^ ,它的作用是让后面的管道命令 | 生效,而不是让程序把 | 误解为 for 语句里参数的一部分。

注2. ping 其实才是本批处理的核心部分。命令 ping 的主要作用是通过发送数据包并接收应答信息来检测两台计算机之间的网络是否连通。比如我可以输入 ping 10.30.11.35 以便查看我是否能与我所在的局域网中IP为 10.30.11.35 的机器连通。如果我不懂批处理的话,也许我就得从 IP 10.30.11.1 开始,挨个地 ping 到 IP 10.30.11.255 ,才能达到我在本小节的最初目的。
在批处理中 ping 的3个参数 -n 1 -w 200 -l 16 分别表示:仅 ping 一遍[-n 1],等待200毫秒后按超时考虑[-w 200],发送16字节的数据[-l 16]。
另外,此命令行中同时用到了两个 && 和一个 || 的组合命令,我不得不承认这种复杂的逻辑关系会给您带来阅读上的困难。

注3. 这里使用了 set /p =显示内容<nul 来显示一些内容,是因为这中方式显示出内容后并不换行,而 echo 却不行。还有向上数4行的那一堆奇怪的符号,表示的是退格符号,能删除掉当前行中以显示的内容。


图6-3 查看所有子网IP.bat 的运行结果
本小节的使用程度并不大,却很有趣,至少并没有想象中的那么愚蠢。[创作日期:2008-01-10]

========================================朴实的分割线========================================

6.4 令人大囧的WGA,华丽地卸载之

WGA 全称 Windows Genuine Advantage ,是微软制造的反盗版软件,因其自动潜入系统而备受争议。当用户访问 Windows Update 服务以及微软网站的下载页面时,该计划的附属检测软件就会被请求安装。很多用户都是在不知情的情况下被安装上了WGA。事实上,安装了WGA后,正版的用户并没见得有什么优势,但盗版用户的劣势绝对会被发挥的淋漓尽致。如果您是盗版的受害者(尽管您更坚持认为自己是WGA的受害者),那么建议您仔细阅读一下本小节。首先,卸载方法在微软的帮助与支持中有详细英文说明,中文版的在百度知道里也有介绍。不过,这些依然不是本文的主旨,我们更关心的还是批处理。

::::::::::WGA卸载器.bat:::::::::
@echo off
title WGA卸载器
setlocal EnableDelayedExpansion

set /a step=1
if exist WGA卸载记录.txt (
for /f "tokens=2" %%i in (WGA卸载记录.txt) do set /a step=%%i+1 & goto :Step!step!
)
:: 查看记录进行到第几步了

:Step1
:RenameFiles
if exist %Windir%\system32\wgalogon.old del %Windir%\system32\wgalogon.old
if exist %Windir%\system32\wgatray.old del %Windir%\system32\wgatray.old
:: 检查相关的临时文件是否已存在,若存在则删之
Ren %Windir%\system32\WgaLogon.dll WgaLogon.old
if %errorlevel% neq 0 goto :Abort
Ren %Windir%\system32\WgaTray.exe WgaTray.old
if %errorlevel% neq 0 goto :Abort
:: 将相关文件改名,如果操作失败则中断程序
call :WriteUninstallStatus
:: 将卸载情况记录到文件中
echo 步骤一完成。已重命名文件 WgaLogon.dll 和 WgaTray.exe。
ping -n 2 127.0>nul
goto :RebootPrompt
:: 需要重启计算机

:Step2
:UnregisterServer
Regsvr32 %Windir%\system32\LegitCheckControl.dll /u
:: 将相关的服务注册撤消
call :WriteUninstallStatus
:: 将卸载情况记录到文件中
echo 步骤二完成。已撤消服务 LegitCheckControl.dll。
ping -n 2 127.0>nul
goto :RebootPrompt
:: 需要重启计算机

:Step3
:DeleteFiles
Del %Windir%\system32\wgalogon.old
Del %Windir%\system32\WgaTray.old
Del %Windir%\system32\LegitCheckControl.dll
:: 将相关的文件删除
call :WriteUninstallStatus
:: 将卸载情况记录到文件中
echo 步骤三完成。已删除文件 wgalogon.old WgaTray.old 和 LegitCheckControl.dll。
ping -n 2 127.0>nul

:Step4
:DeleteRegistrySubkeys
set /a step=4
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify\WgaLogon" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\WgaNotify" /f
:: 将相关的子项从注册表中删除
call :WriteUninstallStatus
echo 步骤四完成。已删除注册表中相关的子项。
:: 将卸载情况记录到文件中
ping -n 2 127.0>nul

:Step5
:UninstallDoneSuccessfully
echo.
echo 已完成对WGA的卸载!
del WGA卸载记录.txt
pause>nul
exit

::::以下为程序所调用到的函数::::

:WriteUninstallStatus
:: 该函数用于将当前卸载情况记录到文本文件中
echo WGA卸载已完成第 %step% 步>WGA卸载记录.txt
set /p =更改文件名称:<nul >>WGA卸载记录.txt
call :IsCompleted %step%
set /p =撤消服务注册:<nul >>WGA卸载记录.txt
call :IsCompleted %step%
set /p =删除相关文件:<nul >>WGA卸载记录.txt
call :IsCompleted %step%
set /p =清除注册信息:<nul >>WGA卸载记录.txt
call :IsCompleted %step%
goto :EOF

:IsCompleted
:: 该函数用于判断各个卸载步骤的完成情况
if %1 gtr 0 (
echo 已完成>>WGA卸载记录.txt
set /a step-=1
) else (
echo 未完成>>WGA卸载记录.txt
)
goto :EOF

:RebootPrompt
set /a TimerTick=9
echo.
echo 需要重启计算机才能继续下一步。
echo 请在计算机重启后再运行该批处理,以便继续完成对WGA的卸载。
set /p choice=是否现在就重启? [Y/N]
if "%choice%"=="y" shutdown -r -t 15 -c 因卸载WGA的需要而重启& exit
if "%choice%"=="Y" shutdown -r -t 15 -c 因卸载WGA的需要而重启& exit
echo 稍后请手动重启计算机。
pause>nul
exit

:Abort
echo 程序中止。
echo.
echo 您确信您已经安装了WGA?
pause>nul
exit
::::::::::::::::::::::::::::::::

Collected by Clove--Mail to Me

Windows 批处理脚本学习教程