标题: [原创] 批处理技术内幕:ECHO命令 [打印本页]
作者: Demon 时间: 2012-8-3 16:16 标题: 批处理技术内幕:ECHO命令
标题: 批处理技术内幕:ECHO命令
作者: Demon
链接: http://demon.tw/reverse/cmd-internal-echo.html
版权: 本博客的所有文章,都遵守“署名-非商业性使用-相同方式共享 2.5 中国大陆”协议条款。
echo是批处理中最简单的命令,但是你真的掌握了吗?你知道echo输出空行的十种方法吗?你知道用echo怎么输出on或者off或者/?吗?你知道echo, echo+ echo.哪个效率更高吗?
众所周知,如果echo后面跟一个环境变量,但是该变量却为空时,相当于不加任何参数的echo,即输出当前echo是on还是off。很多文章或者教程给出的解决方案都是在echo后面加一个点号echo.,这样就会输出空行。
@echo off
echo %demon.tw%
:: ECHO is off.
echo.%demon.tw%
pause
据我所知,用echo输出空行至少有十种方法:
@echo off
echo=
echo,
echo;
echo+
echo/
echo[
echo]
echo:
echo.
echo\
pause
这十种方法可以分为三组,每组的效率依次递减。可悲的是,那些被奉为经典的教程给出的却是效率最低那组中的echo.
echo.不仅效率低下,而且还容易引发错误:
@echo off
cd .>echo
echo.
pause
我知道你很难接受,但事实的确如此。
第一组中echo后面的=,;都是批处理中的分隔符,所以CMD可以正确地解析出echo命令,并把=,;作为echo命令的参数。是的,你没有看错,分隔符并不是用来分隔命令与参数,它们通常是参数的一部分。既然是参数,那么为什么不会被输出?那是因为echo命令直接跳过了参数的第一个字符,从第二个字符开始输出,而第二个字符是NUL,所以输出了空行。
你可能又要问,那为什么用空格做分隔符却不能输出空行呢?那是因为在输出之前,CMD要检查echo命令的参数是不是on或者off,或者参数为空:首先跳过所有空白字符,如果跳过之后字符串就结束了,那么就认为没有加参数,输出echo是on还是off;如果字符串没有结束,就调用wcsnicmp函数来判断剩下的字符串是否为on或者off,进而修改echo的状态。
因此加上很多空格也是一样的效果:
@echo off
echo
echo on
echo
pause
而对于第二和第三组,事情就没那么简单了,由于echo后面跟的并不是分隔符,所以解析之后会被当成一个整体,而echo+ echo/等等显然又不是内部命令,CMD会把它们当做外部命令进行搜索。嗯,你知道,搜索是很花时间的,这就是为什么它们的效率低于第一组。
可惜的是,CMD花了很大力气搜索,却仍然找不到这样的外部命令,这时候它会尝试着修复(Fix)命令,看看命令中是否有某些字符(如图):
FindAndFix
可以看到,CMD对:.\的处理跟+[]/不太一样,如果是+[]/,CMD会直接把它们从命令中删除并且添加到原有参数的前面;而如果是:.\并且CMD拓展是开启的话,那么会多调用一次GetFileAttributes函数获取文件属性,多调用一次函数自然会多花一些时间,所以第三组的效率又稍稍比第二组的低些。
GetFileAttributes
再来解释一下为什么echo.有时候会引起错误。文件名中是不能出现:.\的,理论上GetFileAttributes函数都应该返回-1(INVALID_FILE_ATTRIBUTES),然而事实却不是如此,我也不知道这算不算GetFileAttributes函数的BUG:
#include <stdio.h>
#include <windows.h>
int main()
{
FILE *fp = fopen("echo", "wb");
fclose(fp);
printf("0x%x\n", GetFileAttributes("echo:"));
printf("0x%x\n", GetFileAttributes("echo."));
printf("0x%x\n", GetFileAttributes("echo/"));
return 0;
}
如果你测试一下上面的C程序,就会发现echo.那行返回的不是-1。
如果GetFileAttributes函数返回的不是-1(一般表示文件不存在),也不是0×10(表示文件是文件夹),那么命令还是会保持原来的样子,当成外部命令运行。
@echo off
cd .>echo
echo.
pause
‘echo.’ is not recognized as an internal or external command, operable program or batch file.
@echo off
cd .>echo
setlocal disableextensions
echo.
pause
关闭了CMD拓展,没有问题。
@echo off
md echo
echo.
pause
echo是文件夹而不是文件,没有问题。
最后总结一下吧,在大部分情况下,你都应该使用第一组的echo, echo; echo=来进行输出,它们的效率跟echo (空格)是一样的,并且可以用来输出on或者off,在变量为空时还能输出空行。
但是echo, echo; echo=却不能输出以/?开头的行,如果你需要,可以使用第二组的echo+ echo/ echo[ echo],它们的效率低一些,但能保证原样输出。
我不建议你使用第三组的echo: echo. echo\,如果你仍然要像垃圾教程里面那样用,我也没有办法。
由于当时没有分析文件搜索的CALL(太复杂懒得跟踪),错误的认为它们的搜索过程都是一样的,在简单分析了一下分析文件搜索的过程之后,发现有一些观点是错误的,现予以纠正。
CMD在进行外部命令搜索时,如果命令中存在冒号:或者反斜杆\,处理的方法与不存在时是不一样的。另外,在搜索开始之前斜杆/(即Unix路径分隔符)会被替换成反斜杠\,故斜杆和反斜杆效果是一样的。
具体的处理过程比较复杂,就不展开了,具体到echo而言,echo/ echo: echo\都不会进行实际的文件搜索,只是会调用一些无关痛痒的函数,对效率的影响基本是可以忽略的。
而echo+ echo[ echo] echo.会对工作目录与%PATH%中的目录进行搜索,速度自然会比较慢。
所以按照效率高低排列的话,正确的分组应该是:
@echo off
echo=
echo,
echo;
echo/
echo:
echo\
echo+
echo[
echo]
echo.
pause
当然,组员之间可能还有细微的差别。比如拓展开启的话,第二组echo\会比echo/多调用一次GetFileAttributes(上面有谈到);第三组的echo.也许还会比其他组员更慢一点(没有验证,实在懒得分析了),这些几乎是可以忽略不计的。
最后,输出空行其实还有第十一种方法,这似乎的确是通用性最强而且效率也很高的方法。
setlocal enabledelayedexpansion
echo(
echo(/?
echo(on
echo(off
echo(!tmp:\=!
可以用下面的VBS测试效率:
'Author: Demon
'Website: http://demon.tw
Set fso = CreateObject("scripting.filesystemobject")
set WshShell = CreateObject("wscript.Shell")
s = "(=,;/\:+[]."
For i = 1 To Len(s)
c = Mid(s, i, 1)
h = Hex(Asc(c))
With fso.OpenTextFile(h & ".bat", 2, True)
.WriteLine "@echo off"
.WriteLine "set s=%time%"
For j = 1 To 100
.WriteLine "echo" & c '& ">nul"
Next
.WriteLine "set e=%time%"
.Write "echo echo" & c & " %s% %e%>" & h & ".txt"
End With
WshShell.Run h & ".bat", 0, True
With fso.OpenTextFile(h & ".txt")
a = Split(.ReadLine, " ")
End With
WScript.Echo a(0), TimeDiff(a(1), a(2))
fso.DeleteFile h & ".bat"
fso.DeleteFile h & ".txt"
Next
Function TimeDiff(s, e)
t = DateDiff("s", CDate(Left(s, 8)), CDate(Left(e, 8)))
t = t * 1000 + (Right(e, 2) - Right(s, 2)) * 10
TimeDiff = t
End Function
作者: asnahu 时间: 2012-8-3 16:49
常常使用复制代码
因在开启变量延迟输出诸如复制代码
等变量时,不容易出现错误。
作者: CrLf 时间: 2012-8-3 17:48
本帖最后由 CrLf 于 2012-8-3 18:08 编辑
这样的分析有深度,看着受益匪浅,但是看到“垃圾教程”之类的词语,我还是想建议下老兄是否应该斟酌下用词。
那都是N年前的玩意了,其时还在用 dos 系统,set 还没有 /a,for 还没有 /f,小霸王还在流行,我们怎么知道当时的 command.com 和 cmd.exe 是否以同样的方式来解读用户输入呢?
一直很佩服楼主的博学与深入,从博客学了不少 vbs 的知识,也许你这么聪明的人想想就能想到,但是没去想永远也想不到,对吧?我们客观地看问题就好了,不需要以这么尖锐和片面的言辞去数落那些付出过努力的前辈,术业有专攻,也许在造诣更深的人眼里我们的言论也都是垃圾,他们要是直言不讳又会如何呢?
很喜欢这种与人讨论的环境,让我想起了以前 plp 与 qzw 不时出没的岁月,不在乎对错,只是个大家共同参与的游戏,但是说真的,冷嘲热讽并无助于交流,希望这个环境能友好一点,握手。
作者: cjiabing 时间: 2012-8-3 19:52
回复 3# CrLf
我也从楼主手中获得了许多有用的东西,特别是VBS的!~
作者: 冷玉公子 时间: 2012-8-3 22:15
学习了,原来偶就是一直用echo.的人了。
作者: forfiles 时间: 2012-8-3 23:24
学习了,原来偶就是一直用echo,的人了。
作者: qzwqzw 时间: 2012-8-4 10:27
很有见解的分析
顶楼提到的
"再来解释一下为什么echo.有时候会引起错误。文件名中是不能出现:.\的,理论上GetFileAttributes函数都应该返回-1(INVALID_FILE_ATTRIBUTES),然而事实却不是如此,我也不知道这算不算GetFileAttributes函数的BUG:"
首先说明
文件名不能出现:和\ 的
因为这是文件路径中的分隔符
但是句点可以出现的
这个你随便改一个文件名带.就知道了
因为句点原本用作主名和扩展名的分隔符
后来在长文件名LFN设计之时
鉴于句点在文件名中的需求十分迫切
所以微软也做了某种程度的妥协
允许句点出现在文件名之中
但是最后一个句点仍然作为主名和扩展名的分隔符之用
在顶楼的例子中
echo.
实际上就是主名为echo,扩展名为空的文件名的标准写法
所以GetFileAttributes("echo.")的返回值就毫不奇怪了
至于最后提到的“垃圾教程”的问题
我多说几句
楼主站在现在的时空看待这个问题无可厚非
毕竟分析cmd的工作是件挺难熬的事情
能够坚持做下来就已经说明楼主对cmd的热情
顺便对误人子弟的言论发发感慨是很自然的
当然不免有些人躺着也中枪这也是无法避免的
这里仅替这部分前辈做一些解释
我看到的echo.的最早用法来源于NewsGroup的一个讨论组
那是个主要讨论MSDOS和其下的batch的新闻组
伴随着也有一些echo+ echo/ 的用法
但是经过漫长时间的演变
echo.的用法最终成为主流
经过实验证实
MSDOS下echo.确实不会触发文件的匹配搜索
所以至少对于在发现或者发明这个用法前辈来说
在他所处那个的时空和环境中
这个用法是没有任何问题的
至于后来的新人比如我辈不加思索
直接生搬前人可能已经不合时宜的用法
那只能是我辈学习不求甚解的一种表现
而对于那些无责任转发已经失去时效性的教程的传播者而言
也只能感慨人云亦云的惯性真的是太大了
而至于echo.党我也是其中的一员
估计以后还会继续保持下去了
原因无它
相对于它的问题和毛病来说
我的喜好和习惯仍占有绝大的优势
作者: xyqylong 时间: 2012-8-5 20:35
分析的很好,lz花了不少力气吧,不过也希望lz对无偿写出一篇篇bat教程的前辈抱有足够的尊重
作者: plp626 时间: 2012-8-5 21:12
本帖最后由 plp626 于 2012-8-5 21:16 编辑
回复 1# Demon
cho. 我这里测试也是最耗时的,
但某些结论 我的xp测试结果和楼主的有出入, 也许楼主还有些因素未考虑进去
测试结果:- D:\Documents_and_Settings\plp2\desktop
- ->(cff "echo,", "echo;", "echo=", "echo:", "echo/", "echo\", "echo.", "echo+","e
- cho[", "echo]", "echo(")|findstr .
- Microsoft Windows XP [版本 5.1.2600]
- Genuine Intel(R) CPU T1600 @ 1.66GHz
- NUMBER_OF_PROCESSORS=2
- ------------ CMD指令测试 -------------
- "echo," @ 72267.6 Hz
- "echo;" @ 68252.7 Hz
- "echo=" @ 68252.7 Hz
- "echo:" @ 37221.2 Hz
- "echo/" @ 76784.3 Hz
- "echo\" @ 29233.3 Hz
- "echo." @ 29245.2 Hz
- "echo+" @ 74457.5 Hz
- "echo[" @ 79261.2 Hz
- "echo]" @ 76784.3 Hz
- "echo(" @ 66408.1 Hz
复制代码
cff.cmd: http://www.bathome.net/thread-12857-1-1.html
[]/ 貌似比,=; 还快,
作者: Demon 时间: 2012-8-5 21:56
回复 Demon
cho. 我这里测试也是最耗时的,
但某些结论 我的xp测试结果和楼主的有出入, 也许楼 ...
plp626 发表于 2012-8-5 21:12
原文的确有些错误,我在博客更正了,但是没有及时更新帖子,现在更新一下。
但即使更正了错误,[]/也不会比,=;还快,用for测试出来的结果是不正确的,因为在for中的echo不会触发文件搜索。
我使用的测试代码为:- Set fso = CreateObject("scripting.filesystemobject")
- set WshShell = CreateObject("wscript.Shell")
-
- s = "(=,;/\:+[]."
-
- For i = 1 To Len(s)
- c = Mid(s, i, 1)
- h = Hex(Asc(c))
-
- With fso.OpenTextFile(h & ".bat", 2, True)
- .WriteLine "@echo off"
- .WriteLine "set s=%time%"
- For j = 1 To 100
- .WriteLine "echo" & c '& ">nul"
- Next
- .WriteLine "set e=%time%"
- .Write "echo echo" & c & " %s% %e%>" & h & ".txt"
- End With
-
- WshShell.Run h & ".bat", 0, True
-
- With fso.OpenTextFile(h & ".txt")
- a = Split(.ReadLine, " ")
- End With
-
- WScript.Echo a(0), TimeDiff(a(1), a(2))
-
- fso.DeleteFile h & ".bat"
- fso.DeleteFile h & ".txt"
- Next
-
- Function TimeDiff(s, e)
- t = DateDiff("s", CDate(Left(s, 8)), CDate(Left(e, 8)))
- t = t * 1000 + (Right(e, 2) - Right(s, 2)) * 10
- TimeDiff = t
- End Function
复制代码
作者: plp626 时间: 2012-8-5 23:30
回复 10# Demon
确实, 如果不是for 中, [] 是慢于,;= 这大概和某些匹配有关, 不过有一点不解, 如果echo[会引发外表命令匹配,为什么找到了却不去执行?- d:\Documents_and_Settings\plp2\desktop\aaa>dir/b
- echo[.bat
-
- d:\Documents_and_Settings\plp2\desktop\aaa>type echo[.bat
- echo --------
- d:\Documents_and_Settings\plp2\desktop\aaa>echo[
-
-
- d:\Documents_and_Settings\plp2\desktop\aaa>
复制代码
作者: Demon 时间: 2012-8-6 00:21
回复 Demon
确实, 如果不是for 中, [] 是慢于,;= 这大概和某些匹配有关, 不过有一点不解, 如 ...
plp626 发表于 2012-8-5 23:30
命令行模式与批处理脚本的解析过程不尽相同,命令行中的echo[也不会进行文件搜索。- @echo off
- echo,echo hello world^&pause>echo[.bat
- echo[
复制代码
作者: CrLf 时间: 2012-8-6 02:16
回复 10# Demon
原来还有这回事,学习了
测试后发现只要是复合语句,哪怕只是写成 (echo[),都不会触发文件搜索,整体屏蔽输出后,排除IO干扰所得结果不变
作者: guanger 时间: 2012-8-9 21:12
有点深,我得慢慢学。。。
欢迎光临 批处理之家 (http://bbs.bathome.net/) |
Powered by Discuz! 7.2 |