###### tags: `DOS` `batch file` # DOS 指令與批次檔 ## 參考資料 :::info 以下文件可供參考: - [官方語法參考](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands) - [非官方語法參考](https://ss64.com/nt/) - [線上教學 1](https://www.tutorialspoint.com/batch_script/index.htm) ::: 雖然現在已經是 Windows 10 的時代, 不過批次檔還是簡單好用。 ## 變數 ### 使用數值型態的變數 如果要以數值型態使用變數, 需要在設定時加上 [`/a`](https://ss64.com/nt/set.html#expressions) 選項: ```bash >set /a a=20 20 >set /a a+=5 25 >echo %a% 25 ``` 這樣就可以利用各種運算計算數值, 而且也可以在 `IF` 中[比較數值](https://ss64.com/nt/equ.html), 例如: ```bash >IF %a% EQU 25 echo "==25" "==25" >IF %a% GTR 25 echo ">25" >IF %a% LEQ 25 echo "<=25" "<=25" ``` ### 取得使用者輸入資料 ```bash= >set /p port="請輸入連接埠:" 請輸入連接埠:com3 >echo %port% com3 ``` 執行時會顯示『輸入連接埠:』然後等待輸入。輸入的內容會存到 port 變數, 後續在就可以用 %port% 取用變數內容。 ### 環境變數的字串取代與子字串 環境變數可以使用 %變數名稱:要被取代的字串=要置換的新內容% 這樣的語法進行[取代](https://ss64.com/nt/syntax-replace.html), 例如: ```bash >set a=hello >set a=%a:ll=p% >echo %a% hepo ``` 如果要取出環境變數的[子字串](https://ss64.com/nt/syntax-substring.html), 則可以用 %變數名稱:~起始位置 (從 0 起算), 字數% 的格式, 例如: ```bash >set a=hello >echo %a:~2,2% ll ``` ## IF 比較 ### ELSE 要和小括號寫在同一行 使用 IF 常會利用小括號執行多道命令, 但是若要加上 ELSE 分支, 請記得要跟小括號們寫在同一行, 例如以下的批次檔內容: ```bash= @echo off set m=DEBUG if %m%==DEBUG ( echo debugging mode. ) else ( echo normal mode. ) ``` 執行結果如下: ```shell >test.bat debugging mode. 'else' 不是內部或外部命令、可執行的程式或批次檔。 normal mode. > ``` 之所以會出現錯誤訊息, 是因為解譯器只是單純一行一行解譯, 因此在遇到第一個右小括號時就以為 IF 已經結束了, 下一行單獨出現的 ELSE 就會被當成不認識的指令, 並且把再下一行的 echo 當然新的一行指令執行了。 只要改寫如下, 就可以正常運作: ```bash >test.bat debugging mode. ``` 由於右小括號之後立即接著 ELSE, 所以解譯器知道 IF 還沒結束, 就會讀取接續的內容了。 ### 利用 errorlevel 變數判斷執行結果 ```bash= @echo off set /p port="輸入連接埠:" :start .\esptool.exe -vv -cd nodemcu -cb 115200 -cp %port% -ca 0x00000 -cf wifi.bin if errorlevel 1 ( set /p dummy="!!!燒錄失敗, 要再試一次請直接按 Enter..." ) else ( set /p dummy="燒錄完成, 請換下一片後按 Enter 繼續..." ) goto start ``` 第 5 行的 if errorlevel 會在最後執行的程式傳回的 errorlevel 值**大於或等於**指定值的時候成立, 請特別留意是**大於或等於**, 而**不是等於**喔, 否則流程就會錯掉。 ### 字串比較的陷阱 if 除了可以判斷執行結果, 也可以比較字串, 例如: ```shell= @echo off :next set ans= set /p ans="按 Enter 繼續 (q 結束)" if %ans%==q (goto end) else (goto next) :end echo exit ``` 不過執行時卻有點怪, 如果按 <kbd>q</kbd> 的確會正常結束: ```shell >if_str.bat 按 Enter 繼續 (q 結束)q exit ``` 但是如果直接按 <kbd>enter</kbd> 卻會跳出錯誤: ```bash >if_str.bat 按 Enter 繼續 (q 結束) 這個時候不應有 (goto。 ``` 問題就在直接按 <kbd>enter</kbd> 時, %ans% 是空的, 所以第 5 行就會變成: ```bash= if ==q (goto end) else (goto next) ``` 讓 if 的敘述不完整, == 前面沒有要比對的對象, 這時可以幫 == 前後的比較對象都加上額外的字元, 讓 ans 即使是空的, 也不會讓 if 敘述不完整。建議的作法是加上象徵字串的雙引號, 比較容易理解: ```bash= @echo off :next set ans= set /p ans="按 Enter 繼續 (q 結束)" if "%ans%"=="q" (goto end) else (goto next) :end echo exit ``` 執行結果就正確了, 按 <kbd>enter</kbd> 會回頭再來, 按 <kbd>q</kbd> 就會跳到 :end 結束: ```bash >if_str.bat 按 Enter 繼續 (q 結束) 按 Enter 繼續 (q 結束) 按 Enter 繼續 (q 結束)q exit ``` ## FOR 迴圈進階 ### 使用 for 迴圈將指令執行結果儲存到環境變數中 for 迴圈有幾種用法, 其中一種比較常見的是循序取得檔案名稱, 例如在這個資料夾中有 2 個 .py 檔案: ```bash >dir 磁碟區 D 中的磁碟是 Data 磁碟區序號: 9446-A1B0 D:\temp\code\test_js 的目錄 2021/05/03 下午 03:32 <DIR> . 2021/06/26 上午 12:08 <DIR> .. 2021/05/04 下午 02:58 1,828 get_adv.py 2021/05/03 下午 01:57 893 Lab14.py ``` 以下 for 指令就可以一一取得個別的檔案名稱: ```bash= @echo off for %%f in (*.py) do (echo %%f) ``` 執行如下: ```bash >..\batch\for1 get_adv.py Lab14.py ``` 要注意的是, 這裡 f 環境變數要用 2 個百分比符號, 如果不是在批次檔中, 而是直接在指令行中執行, 則要用 1 個百分比符號, 例如: ```bash >for %f in (*.py) do echo %f >echo get_adv.py get_adv.py >echo Lab14.py Lab14.py ``` 這個指令可以加上 /f 選項, 它會針對指定的檔案內容進行字符切割, 預設會使用空白字元, 並且傳回第一個字符, 不過這個功能只能用在指定檔案內容、字串、指令執行結果, 例如: ```bash= @echo off for /f %%l in (for1.bat) do (echo %%l) ``` 若 for1.bat 就是上述批次檔, 執行結果如下: ```bash >for1 @echo for ``` 由於切割並傳回第一個字符, 所以輸出結果裡只有每一行的第一個字符。你也可以用雙引號指定額外的剖析關鍵字 (parsing keywords) 來指定要傳回哪些字符, 例如: ```bash= @echo off for /f "tokens=1,2" %%l in (for1.bat) do (echo %%l %%m) ``` 表示要傳回前兩個字符, 注意到雖然在 in 前面只有指定變數 l, 但是在 do 後面可以使用自動依字母序產生的變數 m, 他們分別會接收分割出來的第 1、2 個字符, 執行結果如下: ```bash= >for1 @echo off for / ``` 你也可以用 * 表示剩餘沒有傳回的其他內容, 例如: ```bash= @echo off for /f "tokens=1,2*" %%l in (for1.bat) do (echo %%l %%m %%n) ``` 執行結果就是: ```bash >for1 @echo off for /f "tokens=1,2*" %%l in (for1.bat) do (echo %%l %%m %%n) ``` 你會看到除了前 2 個字符外, 剩餘的內容就會設定給第 3 個自動建立的變數 n。 你也可以利用 delims 指定切割字符的字元, 例如: ```bash= @echo off for /f "delims=f" %%l in (for1.bat) do (echo %%l) ``` 執行結果如下: ```bash >for1 @echo o or / ``` 如果把 in 之後括號內用單引號指定指令, 就會變成將指令執行結果一行一行切割字符後送給變數。例如在我的機器上 vol 指令的執行結果如下: ```bash >vol 磁碟區 D 中的磁碟是 Data 磁碟區序號: 9446-A1B0 ``` 若執行以下的批次檔: ```bash= @echo off for /f "tokens=4" %%l in ('vol') do (echo %%l) ``` 執行結果如下: ```bash >for1 Data ``` 這是因為 vol 的執行結果中, 第 1 行依據空格切割, 依序是 "磁碟區"、"D"、"中的磁碟是"、"Data", 所以第 4 個字符是 Data。第 2 行的執行結果切割後依序是 "磁碟區序號:"、"9446-A1B0", 只有 2 個字符, 並沒有第 4 個字符, 所以沒有輸出內容。 ### for 迴圈或是括號群組指令的陷阱--延後置換變數內容 由於批次檔是源於古早的 DOS 時代, 那個年代電腦記憶體小, 因此批次檔的處理有些現在無法想像的作法, 例如每一個指令會在讀取內容的時候就進行變數取代, 以底下的批次檔為例: ```bash= @echo off set /a count=0 for /L %%i in (1, 1, 5) do ( set /a count=count+1 echo count:%count%, i:%%i ) echo %count% ``` 你覺得他的輸出結果是什麼呢?以下可能會讓你覺得莫名其妙: ```bash >var count:0, i:1 count:0, i:2 count:0, i:3 count:0, i:4 count:0, i:5 5 ``` 批次檔中第 4 行我們建立了一個從 1 到 5, 每次遞增 1 的迴圈, 並在迴圈中將變數 count 的值每次加 1, 第 9 行顯示的最後 count 值的確是 5 沒錯, 可是在迴圈中顯示的 count 值怎麼會都是 0 呢? 這個問題就出在當批次檔執行到第 4 行時, 他把 4~7 行看成完整的一個指令, 因此讀取這一整個指令時就進行了變數置換, 這時的 count 是 0, 所以這個指令中的 %count% 就被換成 0, 接著才執行 for 的內容。因此, 雖然我們有變更 count 的值, 但是 echo 出來的卻是之前就已經被置換的 0 了。 如果你把批次檔中第 5 行也改用 %count%, 就可以更清楚看出問題: ```bash= @echo off set /a count=0 for /L %%i in (1, 1, 5) do ( set /a count=%count%+1 echo count:%count%, i:%%i ) echo %count% ``` 執行結果如下: ```bash >var count:0, i:1 count:0, i:2 count:0, i:3 count:0, i:4 count:0, i:5 1 ``` 這次連加總都不對了, 因為第 5 行的 %count% 在讀取 for 指令內容時就被置換成 0, 所以不管加給次 count 的值都會是 0+1。 為了解決這個問題, 批次檔加上了[延遲置換](https://ss64.com/nt/delayedexpansion.html)的功能, 讓你可以將變數的置換延後到真正執行的時候。我們可以將上述批次檔改寫如下: ```bash= @echo off setlocal EnableDelayedExpansion set /a count=0 for /L %%i in (1, 1, 5) do ( set /a count=count+1 echo count:!count!, i:%%i ) echo %count ``` 這裡第 2 行我們執行了 setlocal 指令, 這個指令會讓以下的環境變數只在這個批次檔中有效, 不會影響到外部的環境變數, 而後面的 EnableDelayExpandion 選項就是啟動延後置換的功能。為了搭配延後置換, 你必須改用 `!變數名稱!` 的形式來取得當下變數的真正內容, 所以在第 7 行就改用這個格式顯示變數內容。我們來看看執行結果: ```bash >var_delay.bat count:1, i:1 count:2, i:2 count:3, i:3 count:4, i:4 count:5, i:5 5 ``` 即使我們在第 6 行使用 !count! 也沒有問題: ```bash= @echo off setlocal EnableDelayedExpansion set /a count=0 for /L %%i in (1, 1, 5) do ( set /a count=!count!+1 echo count:!count!, i:%%i ) echo %count% ``` 如同剛剛提到, setlocal 會讓環境變數只在批次檔中有效, 如果現在直接顯示變數值, 你會發現並不是剛剛批次檔中最後的 5: ``` >echo %count% 1 ``` 這個 1 是前面我們尚未啟用延後置換功能時設定的值。 上述問題不只會出現在指令內容會重複執行的 for 中, 任何你會用 () 將多個指令群組在一起的地方都需要注意同樣的問題, 例如: ```bash= @echo off set /a n=10 if %n%==10 ( set /a n=0 echo %n% ) ``` 你應該可以猜到執行結果了: ``` >if_nodelay.bat 10 ``` 改成啟用延後執行就可以了: ```bash= @echo off setlocal EnableDelayedExpansion set /a n=10 if %n%==10 ( set /a n=0 echo !n! ) ``` 這樣結果就對了: ``` >if_delay.bat 0 ``` 同樣的方法也可以用在使用 && 等結合指令的情況下, 例如: ```bash= @echo off set /a n=10 if %n%==10 set /a n=0 && echo %n% ``` 執行結果一樣是錯誤的 10: ``` >if_compound.bat 10 ``` 啟用延後置換即可得到真正的值: ```bash= @echo off setlocal EnableDelayedExpansion set /a n=10 if %n%==10 set /a n=0 && echo !n! ``` 這樣結果就對了: ``` >if_compound_delay.bat 0 ``` 建議多使用延後置換的功能, 並且一律以 `!變數名稱!` 的方式取用變數, 以免遇到莫名其妙的結果。 ## 重新導向 ### 同時多個重新導向 有些指令會把正常的訊息顯示到標準輸出, 而將錯誤訊息顯示在標準錯誤, 如果你希望把這兩種訊到都隱藏起來, 那麼光是將標準輸出[重新導向](https://ss64.com/nt/syntax-redirection.html)到 nul 是不夠的, 以下我們以這個簡單的批次檔為例: ```bash @echo off echo hello echo error >&2 ``` 它會輸出 "hello" 到標準輸出, 並將 "error" 輸出到標準錯誤, 其中 `&2` 表示代碼為 2 的檔案, 也就是標準錯誤。如果只是單純將標準輸出導向到 nul, 結果如下: ```shell >test > nul error ``` 你會發現輸出到標準錯誤的訊息還是會出現。其實重新導向允許[多重導向](https://www.rushis.com/windows-command-prompt-redirecting-stdoutstderr/), 例如: ```shell >test > nul 2>nul > ``` 就是把標準輸出以及代碼 2 的標準錯誤都導向到 nul, 因此所有的輸出都看不到了。寫在後面的重新導向可以使用前面已經生效的結果, 例如剛剛的指令也可以寫成這樣: ```shell >test > nul 2>&1 > ``` 第二個重新導向雖然把標準錯誤導向到代碼 1 號的檔案, 不過因為前面的重新導向已經將標準輸出導向到 nul, 所以這也等於把標準錯誤重新導向到 nul。 ## 常用指令 ### 使用 mode 指令監控序列埠等裝置 #### 使用 mode 查看所有裝置 ```shell= ❯ mode 裝置 COM4 的狀態: ------------ 傳輸速率: 19200 同位檢查: None 資料位元: 8 停止位元: 1 逾時: OFF XON/XOFF: OFF CTS 交握: OFF DSR 交握: OFF DSR 敏感度: OFF DTR 電路: ON RTS 電路: ON 裝置 CON 的狀態: ----------- 行: 47 欄: 114 鍵盤速率: 31 鍵盤延遲: 1 字碼頁: 950 ``` #### 使用 mode 查看特定裝置狀態 如果序列埠沒有接上實體裝置, 檢查狀態會出錯, 傳回 -1: ```shell= >mode com4 /status 裝置名稱不合法 - COM4 >echo %ERRORLEVEL% -1 ``` 裝置有接上的話, 就會顯示該序列埠的目前設定, 並傳回 0: ```shell= >mode com4 /status 裝置 COM4 的狀態: ------------ 傳輸速率: 19200 同位檢查: None 資料位元: 8 停止位元: 1 逾時: OFF XON/XOFF: OFF CTS 交握: OFF DSR 交握: OFF DSR 敏感度: OFF DTR 電路: ON RTS 電路: ON >echo %ERRORLEVEL% 0 ``` #### 設定控制台的語言頁碼 原始命令提示字元的編碼使用 BIG5, 語言頁碼是 950, 若是要執行輸出 UTF-8 的程式, 要將頁碼設為 65001。 ```shell mode con codepage select=65001 ``` #### 取得所有磁碟機的資訊 ```bash= >wmic logicaldisk get name, volumename Name VolumeName C: OS D: Data ``` #### 網路磁碟 ```bash= net use Y: \\172.23.85.73\danny /user:danny 1234 ``` ```bash= net use Y: /delete ``` ### PowerShell 中查看序列埠名稱 ``` ❯ [System.IO.Ports.SerialPort]::getportnames() COM4 ```