Board logo

标题: [原创] [分享]批处理调试手段与常见错误浅析 [打印本页]

作者: CrLf    时间: 2012-3-19 06:15     标题: [分享]批处理调试手段与常见错误浅析

==========================望闻问切==========================
常用调试手段简介
简单介绍批处理脚本调试过程中的防秒退、下断点、看回显等常用手段

==========================对症下药==========================
1.点击运行时,窗口一闪而过

2.脚本无限循环

3.提示“不是内部或外部命令,也不是可运行的程序或批处理文件。”

4.所有的 pause、set /p 都自动跳过,或所有本应输出到屏幕的内容都无显示

5.执行脚本时提示文件被占用

6.if 进行数值比较时经常得出错误的结果

7.提示“以零为除数的错误。”或“无效数字。”

8.变量赋值失败或丢失部分内容

==========================医疗典籍==========================
批处理常用符号详解
作者:namejm
简介:对符号功能的简要概括。我们知道大部分命令都有自带帮助信息,但是符号的解释该去哪里查呢?这是一本关于批处理字符的字典。

批处理for语句从入门到精通
作者:namejm
简介:for 入门学习必备,经典之作。

批处理for命令的参数和扩展特性
作者:plp626
简介:初窥 for 的特性,你知道下面这个代码为什么不能全盘搜索 qq.exe 吗?看完帖子也许你就明白了
  1. for %%a in (c d e f g) do (
  2.    for /r %%a:\ %%b in (qq.exe) do echo 在盘符 %%a: 中找到 %%b
  3. )
复制代码
Windows 代码页与字符顺序
作者:hanyeguxing
简介:揭秘代码页的真相,人机交流中的翻译家。

《批处理技术内幕》系列:
批处理技术内幕:序
批处理技术内幕:批处理与Unicode
批处理技术内幕:ECHO命令
批处理技术内幕:IF命令
作者:demon
简介:作者通过对 cmd 反编译来解读批处理的工作流程,阅之有益。

批处理变量表机制的猜测及测试
作者:caruko
简介:讨论 cmd 变量环境的本质,变量究竟是如何被读写的。

不能说的秘密-CMD命令奇诡语法特性汇集
作者:qzwqzw
简介:疑难杂症集。转义符引发的血案、call 的二次扩展特性、findstr 的 bug、奇异的 if 比较机制...
作者: CrLf    时间: 2012-3-19 06:22

本帖最后由 CrLf 于 2012-8-14 03:33 编辑

常用调试手段简介:

1、通过 call 或 cmd /k 运行脚本,一般可以在脚本头部加上一行 %1 @cmd /k %0 : 来观察错误退出时的提示。
错例:
  1. if !date:~,4!==2012 echo 末日年
  2. rem 一闪而过
复制代码
调试:
  1. %1 cmd /k %0 :
  2. if !date:~,4!==2012 echo 末日年
  3. rem 可观察到错误信息为“此时不应有 4!==2012。”
  4. rem 分析错误原因,if 解析语法时 !date:~,4! 尚未被解释,符号 , 错被当成 if 的分隔符产生语法错误
复制代码
修正:
  1. if %date:~,4%==2012 echo 末日年
  2. rem 使用在解析语法之前被解释的 %str% 形式的变量
  3. if !date:~^,4!==2012 echo 末日年
  4. rem 将 if 中的默认分隔符转义
  5. if "!date:~,4!"=="2012" echo 末日年
  6. rem 用双引号转义也可
复制代码
2、替换 @echo off 和 >nul 为空(如果有 2>nul 的话要先去除 2>nul),运行观察提示信息。
如果要求纠错后能方便地还原,可以将 echo off 替换为 echo on,将所有 >nul 暂时替换为 >"con"。

错例:
  1. @echo off
  2. set n=Test123
  3. echo %n%>1.txt
  4. rem 为什么文件为空?
复制代码
调试:
  1. echo on
  2. set n=
  3. Test123
  4. echo %n%>1.txt
  5. pause
  6. rem 可以看到预处理的结果是 echo Test123 3>1.txt
  7. rem 很明显,这里的 3 被理解成重定向的句柄号了,而 echo Test123 运行时的句柄3 压根没有输出,定向到文件自然为空。
  8. rem 当脚本很长时,可以只在关键代码前加上 echo on 进行目的性明确的分析,而避免过多的干扰。
复制代码
修正:
  1. @echo off&setlocal enabledelayedexpansion
  2. set n=Test123
  3. echo !n!>1.txt
  4. rem 变量延迟的特性是等语法解析进行完后,再对 !n! 的变量形式进行解释
  5. @echo off
  6. set n=Test123
  7. echo>1.txt %n%
  8. rem 避免重定向符号与数字接触
  9. @echo off
  10. set n=Test123
  11. >1.txt echo %n%
  12. rem 同理
复制代码
3、下断点,常用 pause(建议写成 pause<con>con 增强抗干扰能力),这是最常见的手段,往往配合其他命令查看断点位置的进度和环境。有时也会视情况改用 cmd 进行更深入的调试。
错例:
  1. type 1.txt>1.txt
  2. rem 为何 1.txt 为空呢?
复制代码
调试:
  1. (pause>con
  2. type 1.txt)>1.txt
  3. rem 逐步观察 1.txt 的变化,发现执行到暂停处 1.txt 为空了
  4. rem 分析可知,重定向发生于语法解释之后、命令执行之前,所以 (...)>1.txt 这个重定向在 type 读取 1.txt 之前就先把 1.txt 清空了,type 当然得不到输入
复制代码
修正:
  1. type 1.txt>tmp.txt
  2. move /y tmp.txt 1.txt
  3. rem 使用临时文件,再覆盖回来
复制代码
4、在关键命令前加个 echo 观察其真实的输入参数,如果确认命令运行了却看不到输出,可能是 echo 的输出被重定向,可在命令后加个 >con 再试。注意要对原命令中的特殊字符 (、)、&、|、<、> 等进行转义,所以经常是写成 echo "原命令"
错例:
  1. for /f "delims=" %%a in ('dir /b /od *.txt') do if defined old set old=%%a
  2. echo 最老的文件是:
  3. %old%
  4. rem 我希望获得修改日期最早的文件名称(即 dir /b /od *.txt 的第一条输出),可为什么取不到?
复制代码
调试:
  1. setlocal enabledelayedexpansion
  2. for /f "delims=" %%a in ('dir /b /od *.txt') do echo if defined old[!old!] set old=%%a
  3. rem 因为 if defined 是直接读 old 变量的,所以我们要把这里的变量名加上 ! 成看看 old 变量的值究竟是什么
  4. echo 最老的文件是:
  5. %old%
  6. rem 观察到 !old! 一直为空
  7. rem 原来是 if defined old 这里逻辑有误,漏了个 not
复制代码
修正:
  1. setlocal enabledelayedexpansion
  2. for /f "delims=" %%a in ('dir /b /od *.txt') do echo if not defined old set old=%%a
  3. echo 最老的文件是:
  4. %old%
复制代码
5、使用 "if errorlevel" 或逻辑连接符来判断命令的结果
错例:
  1. for /l %%a in (1 1 100) do setlocal
  2. rem 提示“已经达到最大的 setlocal 递归层。”
  3. rem 那如何知道何时出错的呢?
复制代码
调试:
  1. for /l %%a in (1 1 100) do setlocal||echo 在 %%a 层时出错&&pause
  2. rem 这里的逻辑关系是:当 setlocal 失败或未执行时执行 echo,当 echo 成功时执行 pause
  3. rem 显示“在 33 层时出错”
  4. rem 原来 setlocal 最大递归层只有 32,第33层 setlocal 将出错
复制代码
修正:
  1. for /l %%a in (1 1 100) do setlocal&endlocal
  2. rem 及时 endlocal 销毁 setlocal 创建的局部变量表
  3. for /l %%a in (1 1 100) do call :test
  4. pause&exit
  5. :test
  6. setlocal
  7. rem 每一次 call 都可独享 32 次的 setlocal(甚至可以突破 cmd 的64 兆内存上限),所以当 setlocal 实在不够用时(尽量避免,极端情况才考虑),可以考虑用 call 弥补
复制代码
6、用 debug 精确查看命令的输出内容,以便发现一些隐蔽的错误
错例:
  1. for /f "skip=1 delims=" %%a in ('wmic logicaldisk get name') do echo 盘符 %%a
  2. rem 单独运行 wmic 似乎很正常,可为何用 for 输出总是多出一行?
复制代码
调试:
  1. wmic logicaldisk get name|more>1.txt
  2. rem wmic 的输出是 uniocode 编码的,所以建议用 more 命令转换为 ansi
  3. debug 1.txt
  4. rem 注意 debug 不支持长路径,路径中若含有超过 8 字节宽度的文件夹名或文件名建议使用路径短名
  5. rem 以下为 debug 中的输入及输出
  6. -d100
  7. 0B4E:0100  4E 61 6D 65 20 20 0D 0D-0A 43 3A 20 20 20 20 0D   Name  ...C:    .
  8. 0B4E:0110  0D 0A 44 3A 20 20 20 20-0D 0D 0A 45 3A 20 20 20   ..D:    ...E:
  9. 0B4E:0120  20 0D 0D 0A 46 3A 20 20-20 20 0D 0D 0A 47 3A 20    ...F:    ...G:
  10. 0B4E:0130  20 20 20 0D 0D 0A 48 3A-20 20 20 20 0D 0D 0A 49      ...H:    ...I
  11. 0B4E:0140  3A 20 20 20 20 0D 0D 0A-4A 3A 20 20 20 20 0D 0D   :    ...J:    ..
  12. 0B4E:0150  0A 4B 3A 20 20 20 20 0D-0D 0A 4C 3A 20 20 20 20   .K:    ...L:
  13. 0B4E:0160  0D 0D 0A 4D 3A 20 20 20-20 0D 0D 0A 0D 0D 0A 0D   ...M:    .......
  14. 0B4E:0170  0A 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
  15. rem 观察到 wmic 的输出都以 0D 结尾,而 wmic 的输出中多了一行只有 0D 的内容,因此引发了 for /f 的一次多余的循环
复制代码
修正:
  1. for /f "skip=1 delims=" %%a in ('wmic logicaldisk get name') do if exist %%a\nul echo 盘符 %%a
  2. rem 判断盘符是否存在
  3. for /f "skip=1 delims=" %%a in ('wmic logicaldisk get name') do if %%a geq a: echo 盘符 %%a
  4. rem 判断结果是否大等于 a:
  5. for /f "skip=1 delims=" %%a in ('wmic logicaldisk get name') do call :e %%a
  6. pause&exit
  7. :e
  8. echo 盘符 %1
  9. rem 用 call 排除 0D
复制代码

作者: CrLf    时间: 2012-3-19 06:23

本帖最后由 CrLf 于 2012-8-11 03:06 编辑

症状:
点击运行时,窗口一闪而过
分析:
如果不是执行了 exit 或 goto :eof,那么这种情况往往是语法问题造成的直接退出,主要用 1、3 手段分析错误出于何处
凡是错误提示为“此时不应有”的往往是 for 或 if 的语法错误(详见用药介绍),也有可能是 &、|、<、> 处有误(比如写成 &&&、|||、&<、<> 等)
如果提示“命令语法不正确”则可能有多种原因,但此类错误都发生在预处理阶段,比如未转义的 & 或 | 前没有任何命令,比如 > 或 < 后没有指定目标设备,再比如 for 或 if 的语法不正确...
而若无任何提示,则很可能是括号未匹配,检查括号否成对、参数中的括号是否经过转义处理
用药:
如果是逻辑问题造成错误执行了不该执行的 exit,自行修改脚本逻辑
如果是因为括号不匹配,检查丢失了哪一个反括号,并检查是否有复合语句中某个命令参数里的括号未转义导致错误划分
如果问题出在 if 或 for 上,其内容比较长的话先简化掉一部分看看是否正常,如果恢复正常则缩小简化的范围,逐步逼近错误的源头,但如果语句很短,可以检查几个关键点:
将一条 for 拆成几个部分,用 [] 括起来的是比较容易导致一闪而过的:
  1. for
  2. 参数[是否有误或使用变量延迟]
  3. %%a[是否漏写或写成 %a]
  4. in[是否漏写或连写]
  5. (
  6. 参数[是否含有未转义的 &、|、<、> 等特殊符号,以及字符串中是否含有未转义的默认分隔符]
  7. )
  8. do[是否漏写]
  9. 附属命令
复制代码
将一个 if 拆成几个部分:
  1. if
  2. 开关[是否使用变量延迟]
  3. 参数1[是否含有未转义的特殊字符,含连接符、分隔符、重定向符等]
  4. 条件式[是否使用变量延迟,是否写错,比如将 == 写成 =]
  5. 参数2[同参数1]
  6. (
  7. 附属命令1
  8. ) else [是否使用变量延迟来指定 "else"] (
  9. 附属命令2
  10. )
复制代码
如此检查出具体错因后修正即可
作者: CrLf    时间: 2012-3-19 06:24

本帖最后由 CrLf 于 2012-3-21 06:47 编辑

症状:
脚本无限循环
分析:
一方面有可能是错误逻辑导致产生预料之外的代码顺序,另一方面也有可能是脚本与内部调用的外部命令重名导致反复调用脚本自身(典型案例:ping.bat、find.bat 等)
用药:
若为原因一,需要综合运用前文多种调试手段进行纠错
如果是原因二,可将脚本改名,也可为外部命令指定后缀名或完整路径
作者: CrLf    时间: 2012-3-19 06:25

本帖最后由 CrLf 于 2012-3-21 06:15 编辑

症状:
提示“不是内部或外部命令,也不是可运行的程序或批处理文件。”
分析:
表示用户输入命令不属于内部命令、在当前目录下找不到同名文件,并且无法再 path 路径下找到以此为名的可执行文件。
用药:
检查命令名称或文件路径是否含分隔符,比如常见的错误为:
  1. %programfiles%\winrar\rar.exe /?
  2. rem 假如 %programfiles% 含空格将导致出错
  3. rem 此处假设为默认路径因为 programfiles=C:\Program Files,则 cmd 认为 C:\Program 是命令名称或文件名,而 Files\winrar\rar.exe /? 是其参数
  4. rem 解决方法是使用短路径,或先 cd 再调用,或者将路径加入 path,再或者将其中的分隔符转义(推荐):
  5. "%programfiles%\winrar\rar.exe" /?
复制代码
而在调用外部可执行文件时报错的情况下,如果该文件所在路径不在 path 中,请判断命令执行时 cmd 的工作路径是否为其所在目录,否则请检查 path 变量是否被修改(正因为 path 变量直接影响外部命令的调用,所以非必要情况,尽量不要对 path 变量操作)
作者: CrLf    时间: 2012-3-19 06:26

症状:
所有的 pause、set /p 都自动跳过,或所有本应输出到屏幕的内容都无显示
最有代表性的就是 cmd<1.txt 时
分析:
可能是输入输出被重定向了,为命令再次重定向试试(如 pause<con>con)。
用药:
如果是命令所在复合语句被整个重定向到其他设备(也可能有其他情况,如外部调用时被重定向、句柄备份),那么其内的命令若想显示到屏幕,应该再次指定为输出到相应设备,输入设备同理
作者: CrLf    时间: 2012-3-19 06:26

症状:
执行脚本时提示文件被占用
分析:
往往是因为命令本身存在句柄冲突,或是未关闭的进程中有某个进程打开了该文件的句柄
用药:
检查是否存在 >1.txt 2>1.txt 之类多个句柄重定向到同一文件的情况,如有,则改写成 >1.txt 2<&1
检查是否存在 (for /f %%a in (1.txt) do echo 1.txt)>>1.txt 这样的情况,如果存在,可以用 in ('type 1.txt') do 来读取文本,或者写成 do echo 1.txt>>1.txt 先将 1.txt 载入内存再开启句柄。但这两种办法都会增加额外的耗时,建议先输出到临时文件再合并,或复制 1.txt 为临时文件再操作。
作者: CrLf    时间: 2012-3-19 06:36

本帖最后由 CrLf 于 2012-3-19 18:11 编辑

症状:
if 进行数值比较时经常得出错误的结果
分析
基本可以确定是数值比较时因为某一参数不是合法数字而触发了字符串比较
用药:
数值比较时,两边的数字必须是合法的纯数字,不能带引号或其他非数字字符(负号和十六进制数允许的字符除外),也不能是以 0 开头却不是合法八进制数,同样也不能以 0x 开头却不是合法十六进制数,否则将改为字符串比较,比如以下几例都是如此:
  1. if "12" gtr "%n%" echo 12 gtr n
  2. if @52 gtr @%n% echo 52 gtr n
  3. if 08 gtr %n% echo 08 gtr n
  4. if 0xx gtr %n% echo 0xx gtr n
复制代码

作者: CrLf    时间: 2012-3-19 06:40

本帖最后由 CrLf 于 2012-3-19 19:39 编辑

症状:
提示“以零为除数的错误。”或“无效数字。”
分析:
基本可以把原因锁定在 set /a 上
当算式中分母为 0 的模运算或除法运算时将提示“以零为除数的错误。”
当算式中某一数字或变量名以 0 开头却不为合法的八进制数或十六进制数时将提示“无效数字。”,如果由 set /a 引用的变量名以数字为开头也将被理解为非法数字
用药:
如果是第一种情况,确保分母不为 0,如果分母是通过变量引用,则检查运算前变量是否已经赋值
第二种情况下,避免在 set /a 中使用以数字开头或含有运算符的变量名,检查是否存在 08、0xfg 等非法数字。
此类错误常见于时间日期计算时忽略了 08 09 的情况
  1. set /a s=%time:~,2%*3600+%time:~3,2%*60+%time:6,2%
  2. echo 离凌晨过了 %s% 秒
复制代码

作者: CrLf    时间: 2012-3-19 06:40

本帖最后由 CrLf 于 2012-8-11 03:45 编辑

症状:
变量赋值失败或丢失部分内容
分析:
1、最常见的新手问题是在复合语句中没有使用变量延迟,其实成功赋值了,但是用 %str% 来观察时误以为赋值失败。
2、也有可能是超出变量长度(变量名+等号+变量值+变量分隔符\0 的字符长度相加必须小等于 8192),当长度相加超过上限时是无法成功赋值的。
3、若值中含有 % 或 ! 可能导致部分内容被理解为变量。
4、若字符串中含有 " 号,则可能丢失最后一个引号后的内容。
5、若使用 call set 进行赋值,则可能丢失 &、| 等特殊字符后的内容。
用药:
原因1
基础问题,搜索变量延迟就明白了
原因2
确保变量赋值时长度不会超过限制,过长的内容建议分别保存到多个变量
原因3
脚本中的字符串若含有 %,应改写为 %% 进行自转义。
而 ! 的问题出现得更为广泛,如下例:
  1. setlocal enabledelayedexpansion
  2. for /f %%a in (1.txt) do (
  3.    set /a n+=1
  4.    echo 第 !n! 行:%%a
  5. )
  6. rem 会将 1.txt 中含 ! 的字符串作为变量解释
复制代码
这是因为 cmd 在解释 %%a 后才对延迟变量进行解读,解决办法是不使用变量延迟...当然有时候必须要使用变量延迟,那么可以参考下例的技巧:
  1. for /f %%a in (1.txt) do (
  2.    set /a n+=1
  3.    set "str=%%a"
  4.    setlocal enabledelayedexpansion
  5.    echo 第 !n! 行:!str!
  6.    endlocal
  7. )
  8. rem 通过适时开闭变量延迟,避免了对 %%a 中的感叹号进行解释
复制代码
原因4
建议养成在 set 中使用双引号的习惯,如下例
  1. set "变量名=变量值"
复制代码
原因5
尽量避免使用 call set 来进行赋值,因为它弊大于利。
作者: CrLf    时间: 2012-3-19 06:40

占楼待编辑
作者: QIAOXINGXING    时间: 2012-3-19 10:00

很强大,对我们新手很有用啊!
作者: cjiabing    时间: 2012-3-19 12:10

其他有关帖子:
http://bbs.bathome.net/thread-445-1-11.html
http://www.bathome.net/thread-7495-1-1.html
http://www.bathome.net/thread-13616-1-1.html
作者: cjiabing    时间: 2012-3-19 18:12

老大,看这个:
【挑战】批处理论坛专家系统——容错处理
http://www.bathome.net/thread-15943-1-1.html
作者: lowprofile    时间: 2012-3-26 10:15

新手來學習了
作者: tangqingfu    时间: 2013-5-28 21:29

学习,谢谢分享!
作者: hnzz110    时间: 2013-5-28 22:36

学习一下,谢谢
作者: zhanglei1371    时间: 2014-2-2 00:09

回复 1# CrLf


    实在是太强大了!
作者: cobat    时间: 2015-3-6 14:07

看的有点晕
%1 cmd /k %0 :是什么意思?
第二个例子有点问题
作者: CrLf    时间: 2015-3-6 14:28

回复 19# cobat


    %1 cmd /k %0 : 可以这样理解
第一次执行时 %1 为空,相当于 cmd /k %0 : 用 cmd /k 执行自身
第二次运行时 %1 为 :,相当于 : cmd /k %0 :,此句不执行

    哪个例子有问题?能否详细说下,我改改
作者: MCRGZN    时间: 2015-8-14 00:04

说得很有道理支持!!
作者: 7746351    时间: 2016-8-18 18:37

学习学习没回家都将
作者: 7746351    时间: 2016-8-18 18:38

学习,学习还是不错的
作者: ai20110304    时间: 2018-6-13 23:46

受益多多。谢谢了




欢迎光临 批处理之家 (http://bbs.bathome.net/) Powered by Discuz! 7.2