The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何強迫 Azure CLI 一定要以 UTF-8 輸出以避免亂碼問題

今天又解決了一個卡了我幾個月的問題,這次是 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 程式的語言」設定!

「控制台」中「地區」設定的「系統管理」的「非 Unicode 程式的語言」設定

基本上我都是強烈建議不要隨變更動這裡的設定,因為出意外的機率非常高:

目前的系統地區設定

我只能跟你說,這裡的 繁體中文 (台灣) 選項,所代表的就是 CP950 這個頁碼(Code Page),也就是 Big5 編碼,代表著目前作業系統遇到無法識別的文字時,或是遇到「不支援 Unicode 的應用程式」時,預設就會用這個編碼來處理文字,其中也包含了輸入、輸出與管道(Pipe)傳輸的文字編碼。

你可能會想說:「還有應用程式不支援 Unicode 嗎?」你如果有這個疑惑,那就建議你先勾選上圖的 Beta: 使用 Unicode UTF-8 提供全球語言支援 選項,我相信你看到「亂碼」的機率應該會高出許多!😅

哪些應用程式比較容易出問題?

如果系統地區設定不能改,那是不是意味著所有應用程式都要改用 Big5CP950 編碼了呢?也不是,確實有許多應用程式已經都支援 Unicode 了,所以這些應用程式在處理 Unicode 的時候,是完全沒問題的,甚至能自動判斷編碼,不太容易出錯。但是,若有一些早期的文字檔案,包含原始碼、INI、... 等等各種純文字格式的檔案,如果當時使用了 Big5 編碼,這些檔案就不一定能夠順利的判斷其編碼,這時就會依賴作業系統所設定的「預設編碼」。如果你預設編碼設定錯誤,或是選擇 UTF-8 的話,很容易就會在文字編輯器中顯示「亂碼」,或是看到應用程式的選單出現無法閱讀的亂碼文字,有時候甚至會導致原始碼無法編譯等狀況。這些問題當時是出現在文字檔內容包含「中文」的情況,如果只有英文是沒問題的。

另一個常見的狀況,就是本文的重點所在:命令列程式 (Command Line Programs)!

命令列程式是一種非常特殊的應用程式,他沒有傳統的 UI 介面,一個命令列程式的輸出可以傳給另一個命令列程式當作輸入,這種特性讓命令列程式成為一個非常強大的工具,尤其是在自動化工作上。但是,命令列程式輸出編碼如果跟另一個命令列程式輸入編碼不一致,基本上接收到的文字就會是亂碼。但如果大家都用 Unicode 就皆大歡喜了,可惜有許多命令列程式並沒有特別針對 Unicode 進行處理,當程式不知道該如何處理編碼時,預設就會遵照「作業系統」的地區設定,但也有許多命令列程式只會回應 Unicode 編碼。不過,大家的預設值如果不一樣,這就是一個超大問題了!🔥

本文要提到的這套 Azure CLI 命令列程式,預設會遵照「作業系統」的地區設定來選用預設編碼,而這樣的設定其實在大部分的情況下都是沒問題的。

另一個非常關鍵的程式,就是我們常用的命令列執行環境,首先,我們來看看 Windows 有哪些命令列執行環境:

  1. Command Prompt (cmd.exe) (命令提示字元)

    預設會遵照「作業系統」的地區設定來選用預設編碼,但你可以透過 chcp 命令來變更編碼。

    例如: chcp 65001 就可以將編碼切換到 UTF-8 編碼。

  2. Windows PowerShell (powershell.exe)

    預設會遵照「作業系統」的地區設定來選用預設編碼,但你可以透過設定 [Console]::OutputEncoding 來變更輸出編碼。

  3. PowerShell (pwsh.exe)

    預設會遵照「作業系統」的地區設定來選用預設編碼,但你可以透過設定 [Console]::OutputEncoding 來變更輸出編碼。

  4. Windows Terminal (wt.exe)

    預設僅支援 UTF-8 編碼,不支援變更編碼。

    你可以把 Windows Terminal 當成一種「終端機」,然後在裡面執行 cmd.exepowershell.exepwsh.exe,所以只要裡面執行的程式最終輸出的編碼為 UTF-8 的話,在 Windows Terminal 是可以正常顯示文字的。

    不過你要知道,無論是 cmd.exepowershell.exepwsh.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

你可以看到我這段程式讓整個狀況變的有點棘手:

  1. 終端機採用 Windows Terminal

    僅支援 UTF-8 編碼,不支援變更編碼

  2. 啟動 PowerShell v7.4.1

    預設採用 UTF-8 編碼,可以變更編碼。

  3. 執行 Azure CLI 命令列工具

    預設採用 Big5 編碼 (系統地區設定),可以變更編碼。

  4. 執行 jq 命令列工具

    預設採用 Big5 編碼 (系統地區設定),可以變更編碼。

關於 Windows 預設編碼的問題,可以閱讀 分享幾個在 Windows 與 Linux 常見的編碼問題與解決方案 文章,補充一些重要知識。

基本上,跨程式的資料傳輸,就是一種不斷在編碼轉換的過程!

先說結果,上述命令在不同的環境下有不同結果:

  1. 在 Windows PowerShell (powershell.exe) 執行

    預設設定下執行的結果正常

  2. 在 Windows PowerShell (pwsh.exe) 執行

    預設設定下執行的結果正常

  3. 在 Windows Terminal + Windows PowerShell (powershell.exe) 執行

    預設設定下執行的結果正常

  4. 在 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)

這次終於看見錯誤了:

Azure CLI 的亂碼問題

這裡的 $() 其實跟 | 有異曲同工之妙,因為都需要先讓 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')

oh-my-posh

沒錯,我的 PowerShell 環境中那個華麗的提示字元,就是需要 UTF-8 編碼才能正常顯示特殊字型,但 pwsh.exe 強制使用 UTF-8 編碼之後,我的 Azure CLI 就會開始輸出亂碼了。(詳見: 如何打造一個華麗又實用的 PowerShell 命令輸入環境)

雖然華麗的提示字元變成亂碼了,但是我的命令執行成功了,這就是目前可行的應變措施 (Workaround)!

解決方案

太棒了,問題都可以穩定重現,也找到了問題的根源,接下來解決問題就不是太困難了!(才怪)

我們總結一下,我現在的狀況是這樣:

  1. 我想用 Windows Terminal (wt.exe)
  2. 我想用 PowerShell (而非傳統的 Windows PowerShell 5.1)
  3. 我想用 Oh My Posh 提供的華麗提示字元介面,所以一定要用 UTF-8 編碼

在我的刁鑽要求下,只有一種解決方案了:

  1. 強迫 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 編碼輸出了!

az.cmd

相關連結

留言評論