标题: 批处理技术内幕:ECHO命令
做者: Demon
连接: http://demon.tw/reverse/cmd-internal-echo.html
版权: 本博客的全部文章,都遵照“署名-非商业性使用-相同方式共享 2.5 中国大陆”协议条款。javascript
echo是批处理中最简单的命令,可是你真的掌握了吗?你知道echo输出空行的十种方法吗?你知道用echo怎么输出on或者off或者/?吗?你知道echo, echo+ echo.哪一个效率更高吗?php
众所周知,若是echo后面跟一个环境变量,可是该变量却为空时,至关于不加任何参数的echo,即输出当前echo是on仍是off。不少文章或者教程给出的解决方案都是在echo后面加一个点号echo.,这样就会输出空行。html
@echo off echo %demon.tw% :: ECHO is off. echo.%demon.tw% pause
据我所知,用echo输出空行至少有十种方法:java
@echo off echo= echo, echo; echo+ echo/ echo[ echo] echo: echo. echo\ pause
这十种方法能够分为三组,每组的效率依次递减。可悲的是,那些被奉为经典的教程给出的倒是效率最低那组中的echo.nginx
echo.不只效率低下,并且还容易引起错误:windows
@echo off cd .>echo echo. pause
我知道你很难接受,但事实的确如此。dom
第一组中echo后面的=,;都是批处理中的分隔符,因此CMD能够正确地解析出echo命令,并把=,;做为echo命令的参数。是的,你没有看错,分隔符并非用来分隔命令与参数,它们一般是参数的一部分。既然是参数,那么为何不会被输出?那是由于echo命令直接跳过了参数的第一个字符,从第二个字符开始输出,而第二个字符是NUL,因此输出了空行。ide
你可能又要问,那为何用空格作分隔符却不能输出空行呢?那是由于在输出以前,CMD要检查echo命令的参数是否是on或者off,或者参数为空:首先跳过全部空白字符,若是跳过以后字符串就结束了,那么就认为没有加参数,输出echo是on仍是off;若是字符串没有结束,就调用wcsnicmp函数来判断剩下的字符串是否为on或者off,进而修改echo的状态。函数
所以加上不少空格也是同样的效果:post
@echo off echo echo on echo pause
而对于第二和第三组,事情就没那么简单了,因为echo后面跟的并非分隔符,因此解析以后会被当成一个总体,而echo+ echo/等等显然又不是内部命令,CMD会把它们当作外部命令进行搜索。嗯,你知道,搜索是很花时间的,这就是为何它们的效率低于第一组。
惋惜的是,CMD花了很大力气搜索,却仍然找不到这样的外部命令,这时候它会尝试着修复(Fix)命令,看看命令中是否有某些字符(如图):
能够看到,CMD对:.\的处理跟+[]/不太同样,若是是+[]/,CMD会直接把它们从命令中删除而且添加到原有参数的前面;而若是是:.\而且CMD拓展是开启的话,那么会多调用一次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(通常表示文件不存在),也不是0x10(表示文件是文件夹),那么命令仍是会保持原来的样子,当成外部命令运行。
@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 With fso.OpenTextFile("echoLog.txt",8,true) .WriteLine a(0) & " " & TimeDiff(a(1), a(2)) 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
拓展阅读:http://bbs.bathome.net/viewthread.php?tid=18350
随机文章: