[新手上路]批处理新手入门导读[视频教程]批处理基础视频教程[视频教程]VBS基础视频教程[批处理精品]批处理版照片整理器
[批处理精品]纯批处理备份&还原驱动[批处理精品]CMD命令50条不能说的秘密[在线下载]第三方命令行工具[在线帮助]VBScript / JScript 在线参考
返回列表 发帖

[原创] 批处理技术内幕: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
2

评分人数

    • cjiabing: 谢谢分享!技术 + 1
    • CrLf: 不错,有深度技术 + 1

回复  Demon


    cho. 我这里测试也是最耗时的,
但某些结论 我的xp测试结果和楼主的有出入, 也许楼 ...
plp626 发表于 2012-8-5 21:12


原文的确有些错误,我在博客更正了,但是没有及时更新帖子,现在更新一下。

但即使更正了错误,[]/也不会比,=;还快,用for测试出来的结果是不正确的,因为在for中的echo不会触发文件搜索。

我使用的测试代码为:
  1. Set fso = CreateObject("scripting.filesystemobject")
  2. set WshShell = CreateObject("wscript.Shell")
  3. s = "(=,;/\:+[]."
  4. For i = 1 To Len(s)
  5.     c = Mid(s, i, 1)
  6.     h = Hex(Asc(c))
  7.    
  8.     With fso.OpenTextFile(h & ".bat", 2, True)
  9.         .WriteLine "@echo off"
  10.         .WriteLine "set s=%time%"
  11.         For j = 1 To 100
  12.             .WriteLine "echo" & c '& ">nul"
  13.         Next
  14.         .WriteLine "set e=%time%"
  15.         .Write "echo echo" & c & " %s% %e%>" & h & ".txt"
  16.     End With
  17.    
  18.     WshShell.Run h & ".bat", 0, True
  19.    
  20.     With fso.OpenTextFile(h & ".txt")
  21.         a = Split(.ReadLine, " ")
  22.     End With
  23.    
  24.     WScript.Echo a(0), TimeDiff(a(1), a(2))
  25.    
  26.     fso.DeleteFile h & ".bat"
  27.     fso.DeleteFile h & ".txt"
  28. Next
  29. Function TimeDiff(s, e)
  30.     t = DateDiff("s", CDate(Left(s, 8)), CDate(Left(e, 8)))
  31.     t = t * 1000 + (Right(e, 2) - Right(s, 2)) * 10
  32.     TimeDiff = t
  33. End Function
复制代码

TOP

回复  Demon


    确实, 如果不是for 中, [] 是慢于,;= 这大概和某些匹配有关, 不过有一点不解,  如 ...
plp626 发表于 2012-8-5 23:30


命令行模式与批处理脚本的解析过程不尽相同,命令行中的echo[也不会进行文件搜索。
  1. @echo off
  2. echo,echo hello world^&pause>echo[.bat
  3. echo[
复制代码
1

评分人数

TOP

返回列表