標題: 批處理技術內幕: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
隨機文章: