今天又解決了一個卡了我幾個月的問題,這次是 Azure CLI 的輸出編碼問題,只要在一種特殊的條件組合下,就會遇到的問題,我一直都知道怎樣避開這個問題,但卻一直無法理解原因。今天這篇文章我就來說說這個特殊的狀況,幫助大家未來可以順利避開這個問題。
認識非 Unicode 程式的語言 (Language for non-Unicode programs)
我手邊有許多 PowerShell 自動化腳本,尤其是 Microsoft 365、Azure 或 Azure DevOps 這類微軟服務,基本上對 PowerShell 的支援度都很高,所以要實現自動化相對容易許多。但是還是有不少自動化的程式是用「批次檔」寫的 (*.bat
或 *.cmd
),當我在 PowerShell 腳本中使用 Azure CLI 時,總是會遇到一個問題,就是 Azure CLI 遇到中文總是「有機會」會輸出亂碼,這讓我很困擾,因為我並不知道確切發生問題的原因,這種不知道問題根源狀況就有點困擾著我。
其實作業系統的預設文字編碼一直都是一個很棘手的問題,尤其是在 Windows 作業系統上,面對老舊的地區設定 (legacy system locale),像是台灣數十年來常用的 Big5
編碼,一直到現在都還擺脫不掉,連我都不敢直接改變系統的預設編碼,因為這樣會影響到許多應用程式的運作,幾乎可以說一定會出事。
我所說的「老舊的地區設定」(legacy system locale),講的其實就是「控制台」中「地區」設定的「系統管理」的「非 Unicode 程式的語言」設定!
基本上我都是強烈建議不要隨變更動這裡的設定,因為出意外的機率非常高:
我只能跟你說,這裡的 繁體中文 (台灣) 選項,所代表的就是 CP950
這個頁碼(Code Page),也就是 Big5
編碼,代表著目前作業系統遇到無法識別的文字時,或是遇到「不支援 Unicode 的應用程式」時,預設就會用這個編碼來處理文字,其中也包含了輸入、輸出與管道(Pipe)傳輸的文字編碼。
你可能會想說:「還有應用程式不支援 Unicode 嗎?」你如果有這個疑惑,那就建議你先勾選上圖的 Beta: 使用 Unicode UTF-8 提供全球語言支援
選項,我相信你看到「亂碼」的機率應該會高出許多!😅
哪些應用程式比較容易出問題?
如果系統地區設定不能改,那是不是意味著所有應用程式都要改用 Big5
或 CP950
編碼了呢?也不是,確實有許多應用程式已經都支援 Unicode 了,所以這些應用程式在處理 Unicode 的時候,是完全沒問題的,甚至能自動判斷編碼,不太容易出錯。但是,若有一些早期的文字檔案,包含原始碼、INI、... 等等各種純文字格式的檔案,如果當時使用了 Big5
編碼,這些檔案就不一定能夠順利的判斷其編碼,這時就會依賴作業系統所設定的「預設編碼」。如果你預設編碼設定錯誤,或是選擇 UTF-8 的話,很容易就會在文字編輯器中顯示「亂碼」,或是看到應用程式的選單出現無法閱讀的亂碼文字,有時候甚至會導致原始碼無法編譯等狀況。這些問題當時是出現在文字檔內容包含「中文」的情況,如果只有英文是沒問題的。
另一個常見的狀況,就是本文的重點所在:命令列程式 (Command Line Programs)!
命令列程式是一種非常特殊的應用程式,他沒有傳統的 UI 介面,一個命令列程式的輸出可以傳給另一個命令列程式當作輸入,這種特性讓命令列程式成為一個非常強大的工具,尤其是在自動化工作上。但是,命令列程式的輸出編碼如果跟另一個命令列程式的輸入編碼不一致,基本上接收到的文字就會是亂碼。但如果大家都用 Unicode 就皆大歡喜了,可惜有許多命令列程式並沒有特別針對 Unicode 進行處理,當程式不知道該如何處理編碼時,預設就會遵照「作業系統」的地區設定,但也有許多命令列程式只會回應 Unicode 編碼。不過,大家的預設值如果不一樣,這就是一個超大問題了!🔥
本文要提到的這套 Azure CLI 命令列程式,預設會遵照「作業系統」的地區設定來選用預設編碼,而這樣的設定其實在大部分的情況下都是沒問題的。
另一個非常關鍵的程式,就是我們常用的命令列執行環境,首先,我們來看看 Windows 有哪些命令列執行環境:
-
Command Prompt (cmd.exe
) (命令提示字元)
預設會遵照「作業系統」的地區設定來選用預設編碼,但你可以透過 chcp
命令來變更編碼。
例如: chcp 65001
就可以將編碼切換到 UTF-8 編碼。
-
Windows PowerShell (powershell.exe
)
預設會遵照「作業系統」的地區設定來選用預設編碼,但你可以透過設定 [Console]::OutputEncoding
來變更輸出編碼。
-
PowerShell (pwsh.exe
)
預設會遵照「作業系統」的地區設定來選用預設編碼,但你可以透過設定 [Console]::OutputEncoding
來變更輸出編碼。
-
Windows Terminal (wt.exe
)
預設僅支援 UTF-8
編碼,不支援變更編碼。
你可以把 Windows Terminal 當成一種「終端機」,然後在裡面執行 cmd.exe
、powershell.exe
或 pwsh.exe
,所以只要裡面執行的程式最終輸出的編碼為 UTF-8
的話,在 Windows Terminal 是可以正常顯示文字的。
不過你要知道,無論是 cmd.exe
、powershell.exe
或 pwsh.exe
預設都是透過「作業系統」的地區設定來選用預設編碼,因此若你在 Windows Terminal 中執行 "非 PowerShell" 的程式時,就有機會遇到亂碼的狀況。不過,大部分的情況下,這樣的狀況都是可以避免的,只有在特殊的條件下容易發生亂碼的狀況。
問題重現
我想在 PowerShell 中執行 Azure CLI 時,你可以用以下命令找出執行所在位置:
(Get-Command -Name az.cmd).Source
結果為:
C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd
所以 az.cmd
其實他是一個批次檔(Batch file),預設所有的批次檔都是透過 cmd.exe
來執行的,所以當你在 PowerShell 中執行 az.cmd
時,其實是透過 cmd.exe
來執行的。
我手邊剛好有個命令想要查出 Azure DevOps Service 上面某個使用者可以存取哪些專案:
az devops user show --user $UserPrincipalName -o json | jq ".projectEntitlements[].projectRef.name" -r
你可以看到我這段程式讓整個狀況變的有點棘手:
-
終端機採用 Windows Terminal
僅支援 UTF-8
編碼,不支援變更編碼。
-
啟動 PowerShell v7.4.1
預設採用 UTF-8
編碼,可以變更編碼。
-
執行 Azure CLI 命令列工具
預設採用 Big5
編碼 (系統地區設定),可以變更編碼。
-
執行 jq 命令列工具
預設採用 Big5
編碼 (系統地區設定),可以變更編碼。
關於 Windows 預設編碼的問題,可以閱讀 分享幾個在 Windows 與 Linux 常見的編碼問題與解決方案 文章,補充一些重要知識。
基本上,跨程式的資料傳輸,就是一種不斷在編碼轉換的過程!
先說結果,上述命令在不同的環境下有不同結果:
-
在 Windows PowerShell (powershell.exe
) 執行
預設設定下執行的結果正常
-
在 Windows PowerShell (pwsh.exe
) 執行
預設設定下執行的結果正常
-
在 Windows Terminal + Windows PowerShell (powershell.exe
) 執行
預設設定下執行的結果正常
-
在 Windows Terminal + PowerShell (pwsh.exe
) 執行
預設設定下執行的結果正常
對,全部都正常!
但是,當我在 Windows Terminal + PowerShell (pwsh.exe
) 執行時,同時載入 oh-my-posh 之後,問題就出現了:
Import-Module oh-my-posh
az devops user show --user $UserPrincipalName -o json | jq ".projectEntitlements[].projectRef.name" -r
錯誤訊息如下,基本上是 JSON 無法解析的錯誤:
jq: parse error: Invalid escape at line 63, column 32
重點是,我重新執行一次 az devops user show --user $UserPrincipalName -o json
命令,其實看不到任何錯誤,所有中文字都有正常顯示,是串接 jq
的時候才出錯的。所以我每次遇到「編碼」的問題,都十分刁鑽,非常難偵錯!😒
不過,這次我嘗試用這個命令執行:
$(az devops user show --user $UserPrincipalName -o json)
這次終於看見錯誤了:
這裡的 $()
其實跟 |
有異曲同工之妙,因為都需要先讓 az
命令輸出,先做好編碼轉換後,才傳給下一個命令或程式處理,所以這種寫法會讓我看到亂碼!
出問題的當下,我的 [Console]::OutputEncoding
輸出如下:
Preamble :
BodyName : utf-8
EncodingName : Unicode (UTF-8)
HeaderName : utf-8
WebName : utf-8
WindowsCodePage : 1200
IsBrowserDisplay : True
IsBrowserSave : True
IsMailNewsDisplay : True
IsMailNewsSave : True
IsSingleByte : False
EncoderFallback : System.Text.EncoderReplacementFallback
DecoderFallback : System.Text.DecoderReplacementFallback
IsReadOnly : False
CodePage : 65001
我的環境被強制改成 UTF-8
編碼了,但這是有原因了,你看看我強制把 [Console]::OutputEncoding
改回 big5
的結果:
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding('big5')
沒錯,我的 PowerShell 環境中那個華麗的提示字元,就是需要 UTF-8
編碼才能正常顯示特殊字型,但 pwsh.exe
強制使用 UTF-8
編碼之後,我的 Azure CLI 就會開始輸出亂碼了。(詳見: 如何打造一個華麗又實用的 PowerShell 命令輸入環境)
雖然華麗的提示字元變成亂碼了,但是我的命令執行成功了,這就是目前可行的應變措施 (Workaround)!
解決方案
太棒了,問題都可以穩定重現,也找到了問題的根源,接下來解決問題就不是太困難了!(才怪)
我們總結一下,我現在的狀況是這樣:
- 我想用 Windows Terminal (
wt.exe
)
- 我想用 PowerShell (而非傳統的 Windows PowerShell 5.1)
- 我想用 Oh My Posh 提供的華麗提示字元介面,所以一定要用
UTF-8
編碼
在我的刁鑽要求下,只有一種解決方案了:
- 強迫 Azure CLI 一定要以
UTF-8
編碼輸出即可!
可惜 Azure CLI 並沒有提供變更輸出編碼的選項,所以我把問題發到 StackOverflow 上面,希望有人能夠幫我解答,結果還真的給我遇到了個大神,他徹底解決了我的難題。看解答: https://stackoverflow.com/a/78023334/910074
簡單說,你只要在 $PROFILE
定義一個 az
的函式如下,未來執行 az
就可以強制以 UTF-8
編碼輸出了!
# Custom az CLI entry point that requests UTF-8 output and decodes it as such.
# Based on the az.cmd batch file that comes with v2.57.0
# (but this batch file's content rarely changes).
function az {
# Determine the full az.cmd path and derive the location of the
# bundled Python executable.
$azCliPath = (Get-Command -ErrorAction Ignore az.cmd).Path
if (-not $azCliPath) { throw "'az.cmd' cannot be located via the system's path." }
$bundledPythonExe = Convert-Path -ErrorAction Ignore -LiteralPath "$azCliPath\..\..\python.exe"
if (-not $bundledPythonExe) { throw "Failed to load Python executable." }
# Prepare the environment and temporarily instruct
# PowerShell to decode external-program output as UTF-8.
$prevValue = $env:AZ_INSTALLER; $env:AZ_INSTALLER = 'MSI'
$prevEncoding = [Console]::OutputEncoding; [Console]::OutputEncoding = [Text.UTF8Encoding]::new()
# Call the actual CLI via the bundled Python, requesting UTF-8 output (-X utf8),
# and passing all arguments as well as the output through.
& $bundledPythonExe -X utf8 -IBm azure.cli @args
# Restore previous settings.
[Console]::OutputEncoding = $prevEncoding
$env:AZ_INSTALLER = $prevValue
}
這段 PowerShell 函式的亮點就在於,他把 C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd
批次檔的內容,改用 PowerShell 來執行。而 Azure CLI 是基於 Python 開發的,所以只要在命令列啟動的地方加上 -X utf8
參數,就可以強制以 UTF-8
編碼輸出!
我最後也有實驗出,其實 Azure CLI 完全以 UTF-8
輸出,完全不會有問題,所有程式都可以自動的識別 UTF-8
編碼。所以我的最終結果就是,直接去修改 C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd
批次檔,手動加上 -X utf8
參數,如此一來日後 Azure CLI 就可以強制以 UTF-8
編碼輸出了!
相關連結
- 相關文章
- StackOverflow
- GitHub