iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0

對應 30天挑戰精通 PowerShell 該書第 19 章。

昨天介紹了基本的指令碼編寫,今天接續著 19 章後面的內容繼續給它看下去。


19.5 為你的指令碼撰寫說明

我已經讀完了整本書的一半以上,正如書中最初章節所提醒的,Get-Help 已成為我最常用的 cmdlet 之一。當我們為團隊編寫可重複使用的腳本時,按照規範在腳本中撰寫說明,讓使用者能透過 Get-Help 清楚了解程式的描述、目的、用法以及支援的參數,這確實是一件利人利己的事情。
PowerShell 支援基於註解的幫助(Comment-Based Help)。我們可以在腳本的開頭添加特殊格式的註解,PowerShell 會自動解析這些註解,並在使用 Get-Help 命令時顯示相應的信息。

備註格式

<#
.SYNOPSIS
    這是一個範例腳本的簡要說明。

.DESCRIPTION
    這裡是腳本的詳細描述,解釋腳本的功能和工作原理。

.PARAMETER InputFile
    輸入文件的路徑。

.PARAMETER OutputFile
    輸出文件的路徑。

.EXAMPLE
    .\MyScript.ps1 -InputFile "data.txt" -OutputFile "result.txt"
    這是如何使用該腳本的範例。

.NOTES
    作者:你的名字
    日期:2023-10-01
#>

param(
    [Parameter(Mandatory)]
    [string]$InputFile,

    [Parameter(Mandatory)]
    [string]$OutputFile
)

# 腳本主體代碼

範例

<#
.SYNOPSIS
    獲取遠端或本地計算機上邏輯磁碟的大小及可用空間資訊。

.DESCRIPTION
    此腳本透過 `Get-CimInstance` cmdlet 獲取指定計算機上的邏輯磁碟資訊,並篩選出硬碟驅動器 (DriveType=3)。
    它將結果依據設備ID排序,並以表格格式顯示每個磁碟的設備ID、可用空間 (以MB為單位)、總大小 (以GB為單位) 以及可用空間的百分比。
    腳本默認為 "server01" 作為計算機名稱,但可以更改為其他計算機。

.PARAMETER computername
    目標計算機的名稱或IP地址。默認值為 "server01"。

.EXAMPLE
    .\Get-DiskInfo.ps1 -computername "server01"
    此命令將顯示 "server01" 計算機上的邏輯磁碟資訊。

    .\Get-DiskInfo.ps1 -computername "localhost"
    此命令將顯示本地計算機上的邏輯磁碟資訊。

.NOTES
    作者:你的名字
    日期:2023-10-07
#>

param (
    $computername = 'server01'
)

Get-CimInstance -class Win32_LogicalDisk -computername $computername `
-Filter "DriveType=3" | Sort-Object -Property DeviceID |
Format-Table -Property DeviceID ,
@{Label='FreeSpace(MB)'; expression={$_.FreeSpace / 1MB -as [int]}},
@{Label='Size(GB)'; expression={$_.Size / 1GB -as [int]}},
@{Label='%Free'; expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

遇到亂碼了?

編寫完指令碼後,嘗試用 Get-Help 查看時,卻發現中文變成亂碼了,後來發現主要是因為文件的編碼是 UTF-8 ,但是需要將其編碼改成 UTF-8 with BOM ,以下是我透過 chatGPT 查詢後,針對兩者差異的描述及整理:
UTF-8UTF-8 with BOM(Byte Order Mark,位元組順序標記)之間的區別在於是否包含一個特殊的字節標記(BOM)在文件的開頭。

什麼是 BOM

BOM(Byte Order Mark)是一個 Unicode 字符,用來標記文本的編碼方式。對於不同的編碼,BOM 的字節序會有所不同:

  • UTF-8 BOM: 0xEF 0xBB 0xBF
  • UTF-16 LE (Little Endian) BOM: 0xFF 0xFE
  • UTF-16 BE (Big Endian) BOM: 0xFE 0xFF

BOM 的主要目的是在某些需要區分字節順序的編碼(如 UTF-16 和 UTF-32)中,用來區分不同的字節順序(大端或小端),但在 UTF-8 中,因為它是字節無關的,所以 BOM 並不是必要的。

為什麼需要 BOM

主要與 Windows 系統的兼容性有關係,Windows 在處理不同編碼時經常依賴 BOM 來區分不同的 Unicode 編碼格式。而如果文件沒有 BOM,系統可能會默認使用 ANSI 或其他本地語系編碼,導致無法正確顯示中文字符。通過添加 UTF-8 with BOM 編碼,這些應用可以正確識別編碼並顯示中文,而避免亂碼。

透過 PowerShell 判別文件編碼 1

function Get-FileEncoding {
    param (
        [string]$FilePath
    )

    $stream = [System.IO.StreamReader]::new($FilePath, $true)
    $encoding = $stream.CurrentEncoding
    $stream.Close()
    
    return $encoding
}
# 用法範例
$encoding = Get-FileEncoding -FilePath "C:\path\to\your\file.txt"
$encoding

https://ithelp.ithome.com.tw/upload/images/20241007/20168708BkT42mOeb9.png
可是透過上述的 function 並不能顯示出該編碼是否有包含 BOM,所以需要透過下面的 funtion 去確認。

透過 PowerShell 判別文件編碼 2

function Get-FileEncoding {
    param (
        [string]$FilePath
    )

    $bom = Get-Content -Path $FilePath -Encoding Byte -TotalCount 3
    $bomHex = ($bom | ForEach-Object { '{0:X2}' -f $_ }) -join ' '

    switch ($bomHex) {
        "EF BB BF" { "UTF-8 with BOM" }
        "FF FE" { "UTF-16 LE" }
        "FE FF" { "UTF-16 BE" }
        default {
            $stream = [System.IO.StreamReader]::new($FilePath, $true)
            $encoding = $stream.CurrentEncoding.EncodingName
            $stream.Close()
            $encoding
        }
    }
}
# 用法範例
$encoding = Get-FileEncoding -FilePath "C:\path\to\your\file.txt"
$encoding

https://ithelp.ithome.com.tw/upload/images/20241007/20168708z4ls8r8jtm.png
這樣就可以知道目前文件的編碼並未包含 BOM,因此,可以透過下面的 Function 去將既有的 script 編碼加入 BOM。

透過 PowerShell 修改文件編碼 ( UTF-8 to UTF-8 with BOM )

function ConvertToUtf8WithBom {
    param (
        [string]$FilePath
    )

    # 读取文件的内容
    $content = Get-Content -Path $FilePath -Raw

    # 使用 UTF-8 with BOM 编码将内容写回到文件
    $utf8WithBom = New-Object System.Text.UTF8Encoding $true
    [System.IO.File]::WriteAllText($FilePath, $content, $utf8WithBom)

    Write-Host "文件 $FilePath 已成功转换为 UTF-8 with BOM 编码。"
}

# 用法示例
ConvertToUtf8WithBom -FilePath "C:\path\to\your\file.ps1"

https://ithelp.ithome.com.tw/upload/images/20241007/20168708JRRfgIZxE1.png
依據截圖,已經看到目前 script 的編碼已經轉為 UTF-8 with BOM
當然你也可以透過 notepad++ 等文字編輯器將編碼修改成 UTF-8 with BOM

透過 Notepad++ 修改編碼

  1. 在 Notepad++ 中:
  2. 點擊菜單欄的 “編碼”。
  3. 選擇 “轉換為 UTF-8 編碼” 或 “轉換為 UTF-8 BOM 編碼”,或者 “轉換為 UTF-16 LE 編碼”。
  4. 保存文件。

19.6 一份指令碼,一條管線

透過 script 運行多個 cmdlet 時

書中透過了一張圖表現出當我們將多行 cmdlet 放在 script 裡去執行時的邏輯圖,我參考後重新整理了相關的邏輯,重新繪製如下:
https://ithelp.ithome.com.tw/upload/images/20241007/20168708jy2faGkVNz.png
圖示中顯示了 Get-Uptime 和 Get-Process 的執行流程。從流程圖來看,它展示了 PowerShell 如何將不同 Cmdlet 的輸出物件(Timespan 和 Process)傳送到管線,最後經由 Out-Default、Out-Host 輸出到主機應用。
透過這樣的 script 我們最終能看到的 output 為清單:
https://ithelp.ithome.com.tw/upload/images/20241007/20168708uhNRdxGC4o.png
所以當 script 的功能是要透過執行後呈現想要的格式時,這部分要特別小心。

透過主機應用程式運行多個 cmdlet 時

先看圖:
https://ithelp.ithome.com.tw/upload/images/20241007/201687082DEs0vHPeq.png
上圖展示了 PowerShell 中兩個不同 Cmdlet ( Get-Uptime 和 Get-Process ) 的執行順序和輸出處理過程,並將每個 Cmdlet 的輸出物件如何經過 PowerShell 的格式化系統( Out-Default 和 Out-Host )再輸出到主機應用程式(通常是 PowerShell 控制台或其他輸出介面),且這兩個 Cmdlet 的輸出不共享同一個管線,因此不會有因為物件類型不同而影響格式化的問題。


19.7 快速了解作用域

作用域是 PowerShell 某些元素類型的一種容器( container )形式,主要包含別名、變數和 Function。
Shell 本身是最上層的作用域,被稱爲「全域作用域」( global scope )。當你執行一個指令碼時,會爲該指令碼建立一個新的作用域,稱為「指令碼作用域」( script scope )。指分碼作用域是全域作用域的分支,或稱爲「子作用域」( child )。而函式也同樣擁有屬於它們自己的「私有作用域」( private scope )。
Reference - P.311
我的理解是:**作用域 **是指變數、函數、別名等項目的可見性和生存週期的範圍。簡單來說,就是在什麼地方可以存取到這些項目,以及它們存在多久。

作用域的種類

全域作用域(Global Scope)

這是整個 PowerShell 會話的最外層作用域。任何在全域作用域中定義的變數或函數,整個會話期間都可用。

區域作用域(Local Scope)

這是你當前正在執行的作用域,例如一個函數或腳本區塊內部。在區域作用域中定義的變數,僅在該作用域內可見。

腳本作用域(Script Scope)

當你執行一個腳本時,該腳本有自己的作用域,稱為腳本作用域。腳本內定義的變數和函數不會影響到全域作用域。

私有作用域(Private Scope)

使用 Private 關鍵字可以將變數或函數的可見性限制在當前作用域,防止它們被子作用域訪問。

作用域的重要性

  • 避免名稱衝突:不同作用域中的同名變數互不影響。
  • 控制變數生命週期:可以管理變數何時被創建和銷毀。
  • 提高腳本安全性:通過限制變數的可見性,防止未經授權的訪問。

範例一:全域作用域

Script

$var = "全域作用域"

function Test-LocalScope {
    $var = "區域作用域"
    Write-Host "函數內:$var"
}

Test-LocalScope
Write-Host "函數外:$var"

Result

https://ithelp.ithome.com.tw/upload/images/20241007/20168708uYMqd5AXso.png

Explanation

在全域作用域(紅框)中,我們將 全域作用域 的字串賦值給 變數 var,接著我們建立一個 function - Test-LocalScope,並在 function 內( 黃框 )再次建立一個 var 並將 區域作用域 的字串賦值給 變數var,當第 8 行呼叫 Test-LocalScope 時,Write-Host 會顯示區域作用域裡面的值( 即 區域作用域 ),但黃框內的變數僅適用於黃框內,因此,當第 9 行 Write-Host 顯示 var 變數內的值時,其值為 全域作用域。

範例二:腳本作用域

Script

$scriptVar = "腳本作用域"

function Show-ScriptVar {
    Write-Host "腳本內:$scriptVar"
}

Show-ScriptVar

Result 1

https://ithelp.ithome.com.tw/upload/images/20241007/20168708Ob0jNVUQMc.png

Explain Result 1

  • $scriptVar 定義在腳本作用域中,僅在 script.ps1 中可見。
  • 在控制台(全域作用域)中,無法存取 $scriptVar

Result 2

https://ithelp.ithome.com.tw/upload/images/20241007/20168708XbquZtPLkZ.png

Explain Result 2

因為執行該 script 的方式是透過點來源( **Dot Sourcing **),該方式會將腳本中的所有內容加載到當前作用域(通常是全域作用域)中。
Reference - Script scope and dot sourcing

範例三:使用作用域修飾符

Script

$counter = 0

function IncrementCounter {
    $Global:counter += 1
}

IncrementCounter
Write-Host "計數器:$counter"

Result

https://ithelp.ithome.com.tw/upload/images/20241007/20168708Tt0mY0jglw.png

Explanation

  • 使用 $Global: 修飾符,直接修改全域作用域中的變數 counter。
  • 函數內的變更影響到了全域作用域。

Reference - Scope modifiers


明日主題

Day 24 - 改善你的參數化指令


上一篇
Day 22 - 指令碼編寫 Part 1
下一篇
Day 24 - 改善你的參數化指令
系列文
《30天挑戰精通 PowerShell:從 Windows Server 到 Azure DevOps 自動化之旅》30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言