呼叫 PowerShell 模組函式或跑 .ps1 程式難免會遇到程式跑完但結果不如預期的狀況,要找出哪裡出錯,加 Log 是最直接有效的做法。而 PowerShell 有一件好用武器,允許事先在程式偷偷埋入偵錯資訊,平時不顯示,使用者遇到問題時可打開開關再跑一次,就能藉由顯示的偵錯訊息推測發生什麼事?

這種貼心設計,能讓你的 PowerShell 模組或小工具專業形象瞬間上升,是專業 PowerShell 開發者的必備技巧之一。

事實上,幾乎所有 PowerShell 官方函式,都內建這層貼心設計。例如:我遇到一個狀況,Repository 的 PS 模組已更新到 1.1.0 版,跑完 Update-Module 有看到下載進度,但用 Get-Module 模組卻還仍是 1.0.0。於是,我加上 -Verbose 參數再重新執行一次,這回會看到比較多的資訊,包含它有找到 Repository 目錄 X:\PrivNuGet,也找到 1.1.0 版,而且因為已經安裝過了所以略過 (Skipping Installed module):(這是初學開發模組常遇到的狀況,先賣個關子,未來再另開一篇方便需要的人 Google 查詢)

要提供這種隱藏版偵錯訊息很簡單 - PowerShell 有個 Write-Verbose Cmdlet,使用方法 Write-Host、Write-Output 差不多,差別在可以透過 $VerbosePreference 控制是否顯示,$VerbosePreference 的預設值是 SilentlyContinue - 不顯示且繼續執行;修改成 Continue 就會變成顯示後繼續執行。當然,在程式裡寫死 $VerbosePreference = "Continue" 就沒意思了,PowerShell 有所謂的 CommonParameters,提供所有 Cmdlet 都通用的參數,如 -Verbose、-Information、-Debug、-ErrorAction,可在執行階段設定 $$VerbosePreference,而舉一反三,除了 Wirte-Verbose、也有 Write-Debug (-Debug 參數在互動模式會切成 Inquire 一直問你要不要繼續,有點煩)、Write-Information (彈性最大但比較複雜)。

而要接收 CommonParameters,函式或 .ps1 參數宣告要加上 [CmdletBinding()],以前幾天介紹的 Merge-ModuleScripts.ps1 為例,我加上 [CmdletBinding()] 及 Write-Verbose() 在必要時提供更多偵錯訊息:

[CmdletBinding()]
param (
    [switch][bool]
    $publish,
    [switch][bool]
    $clear,
    [string]
    $repository = "",
    [string]
    $nugetApiKey = "NoKey"
)
$ErrorActionPreference = "STOP"
if ($publish -and [string]::IsNullOrWhiteSpace($repository)) {
    Write-Host "Repository parameter missing" -ForegroundColor Red
    Exit
}
$moduleName = [System.IO.Path]::GetFileName($PSScriptRoot.TrimEnd('\'))
$psm1Name = "$moduleName.psm1"
$outputPath = "$PSScriptRoot\$moduleName";
if ($clear) { # clear temp folder
    if (Test-Path -Path $outputPath) {
        Remove-item $outputPath -Recurse
        Write-Host "$outputPath deleted"
    }
    Exit
}
# prepare temp folder
[System.IO.Directory]::CreateDirectory($moduleName) | Out-Null
Write-Verbose "Temp folder [$moduleName] ready"
$functionsToExport = @()
"# Module $moduleName" | Out-File "$outputPath\$psm1Name" -Encoding utf8
# merge all .ps1 under scripts folder to create a single module_name.psm1
Get-ChildItem -Path "$PSScriptRoot\src" -Filter *.ps1 -ErrorAction SilentlyContinue | 
Sort-Object { $_.Name } | # order by ps1 filename
ForEach-Object {
    Write-Verbose "Merging $($_.FullName)..."
    Get-Content $_.FullName | Select-String -Pattern "##MOD_EXEC## Export-ModuleMember -Function ([-_A-Za-z0-9]+)" -AllMatches |
    ForEach-Object {
        $funcName = $_.Matches.Groups[1].Value
        $functionsToExport += $funcName
        Write-Verbose " * Function <$funcName> found"
    }
    $scriptContent = Get-Content $_.FullName -Raw -Encoding utf8
    $scriptContent = $scriptContent.Replace("##MOD_EXEC## ", "")
    $scriptContent | Out-File "$outputPath\$psm1Name" -Append  -Encoding utf8
}
# copy all non-.ps1 files
Get-ChildItem -Path "$PSScriptRoot\src" | Where-Object { !$_.Name.EndsWith('.ps1') } | ForEach-Object { 
    Copy-Item -Path $_.FullName -Destination $outputPath 
    Write-Verbose " * File $($_.FullName) copied"
}
$psd1Path = "$moduleName.psd1"
if (!(Test-Path $psd1Path -PathType Leaf)) {
    Write-Verbose "Creating psd1..."
    New-ModuleManifest -Path $psd1Path -RootModule $psm1Name -Author (Read-Host "Author of module") -ModuleVersion "1.0.0" -Description (Read-Host "Description of module")
}
$psd1 = Get-Content "$moduleName.psd1" -Raw -Encoding utf8
[System.Text.RegularExpressions.Regex]::Replace($psd1, "FunctionsToExport = ([-@()A-Za-z0-9 ,`"'*]+)", 'FunctionsToExport = @("' + ($functionsToExport -join '","') + '")') | 
Out-File "$outputPath\$moduleName.psd1" -Encoding utf8
if ($publish) {
    Publish-Module -Path $outputPath -Repository $repository -NuGetApiKey $nugetApiKey
    Write-Host "$moduleName published"
    Remove-Item $outputPath -Recurse -Force
    Write-Verbose "Temp folder deleted"
}

如此,加上 -Verbose 後,可以看到系統合併哪些 .ps1、發現哪些公開函式,以及暫存目錄的建立與刪除,有利於排除問題。

大家可以善用這個小技巧,讓開發的小工具或 PowerShell 模組更貼心更順手。

Tips of adding Write-Verbose() log in PowerShell cmdlet to make it easy for trouble-shooting.


Comments

Be the first to post a comment

Post a comment