PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化
· 小村 豪 · PowerShell, Windows, 自動化, 日誌調查, 維運改善, 既有資產活用
1. 首先要掌握的事
上一篇整理了 PowerShell 的基本命令、管線、CSV 輸出、JSON、.ps1 腳本,以及用 -WhatIf 進行安全確認。本文接續前一篇,做一個實務上更好用、稍微大一點的腳本。題材是下面這種維運作業:
- 調查日誌
- 把錯誤行整理成 CSV
- 把舊日誌列成封存候選清單
- 必要時搬移舊日誌
- 把執行結果留存為紀錄
PowerShell 應用的重點不是把命令寫得又長又複雜,而是建立以下這種流程:
- 把設定分開管理
- 先做讀取處理
- 留下輸出
- 把變更處理拆成函式
- 用
-WhatIf先做預演 - 最後再考慮自動執行
自動化不是「省掉人的確認」。而是把該確認的地方固定下來,讓每次都留下同樣的紀錄。
另外,本文出現的程式碼,已整理成可以直接執行的範例一套(完成版腳本、設定檔、建立測試用假日誌環境的腳本,以及驗證調查、預演、搬移的 Pester 測試)公開在 GitHub 上。
powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)
2. 本文要做的東西
本文要做一個叫 Invoke-LogMaintenance.ps1 的腳本。
主要功能如下:
| 功能 | 內容 |
|---|---|
| 日誌搜尋 | 搜尋指定資料夾以下的 .log |
| 指定期間 | 只調查最近 N 天內有更新的日誌 |
| 錯誤擷取 | 擷取 ERROR、WARN、FATAL 等行 |
| CSV 輸出 | 把搜尋結果存成 log-hits.csv |
| 舊日誌清單 | 把比 N 天更舊的日誌存成 archive-targets.csv |
| 封存 | 把舊日誌搬移到另一個資料夾 |
| 預演執行 | 指定 -Preview 時不搬移,只顯示預定動作 |
| 執行紀錄 | 保存 transcript、summary JSON、結果 CSV |
另外,本文不做刪除。第一篇應用篇只做到比刪除更安全的「搬移」為止。
3. 資料夾結構
以下面這種結構為例:
C:\Ops
Invoke-LogMaintenance.ps1
log-maintenance.json
C:\App\Logs
app.log
batch.log
old
app-202401.log
C:\App\Reports
20260602-030000
log-hits.csv
archive-targets.csv
archive-result.csv
summary.json
transcript.txt
C:\App\Archive
20260602-030000
old
app-202401.log
把腳本本體和設定檔分開,換環境時替換起來會比較輕鬆。可以做成開發環境用 C:\Test\Logs、正式環境用 D:\App\Logs,只需要改路徑的運作方式。
4. 建立設定檔
先建立 log-maintenance.json。
{
"LogPath": "C:\\App\\Logs",
"OutputPath": "C:\\App\\Reports",
"Days": 7,
"Patterns": [
"ERROR",
"WARN",
"FATAL"
],
"ArchiveDays": 90,
"ArchivePath": "C:\\App\\Archive"
}
各項目的意義如下:
| 項目 | 意義 |
|---|---|
LogPath |
要調查的日誌資料夾 |
OutputPath |
報表輸出位置 |
Days |
要調查最近幾天的日誌 |
Patterns |
要搜尋的字串/模式 |
ArchiveDays |
比幾天更舊的日誌要列為封存對象 |
ArchivePath |
封存目的地資料夾 |
寫成 JSON 之後,就能在不修改腳本本體的情況下變更條件。
PowerShell 可以用 ConvertFrom-Json 把 JSON 當成物件來處理;反過來,要把處理結果留存為 JSON 時則使用 ConvertTo-Json。ConvertTo-Json 是把物件轉換成 JSON 字串的 cmdlet,處理較深的層級時,指定 -Depth 就很重要。
5. 先只做讀取處理
不要一開始就寫搬移處理,先只做找日誌、輸出成 CSV 這件事。
$config = Get-Content .\log-maintenance.json -Raw -Encoding UTF8 | ConvertFrom-Json
$since = (Get-Date).AddDays(-[int]$config.Days)
$files = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
Where-Object { $_.LastWriteTime -ge $since }
$files |
Select-Object FullName, Length, LastWriteTime
接著搜尋日誌的內容。
$patterns = [string[]]$config.Patterns
Select-String -LiteralPath ($files | Select-Object -ExpandProperty FullName) -Pattern $patterns |
Select-Object Path, LineNumber, Pattern, Line |
Export-Csv .\log-hits.csv -NoTypeInformation -Encoding UTF8
到這裡都只是讀取。即使在正式環境的資料夾中測試,也先在這個階段停下來。
6. 把舊日誌列表化
接著,把封存對象列表化。
$limit = (Get-Date).AddDays(-[int]$config.ArchiveDays)
$targets = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
Where-Object { $_.LastWriteTime -lt $limit } |
Sort-Object LastWriteTime
$targets |
Select-Object FullName, Length, LastWriteTime |
Export-Csv .\archive-targets.csv -NoTypeInformation -Encoding UTF8
這個階段也還不搬移。要看看 archive-targets.csv,確認對象數量是不是太多,資料夾是不是選錯了。
7. 把變更處理拆成函式
像搬移這種變更處理,要和讀取處理分開。
在 PowerShell 中,只要在函式加上 SupportsShouldProcess,就能使用 -WhatIf 與 -Confirm。-WhatIf 會不實際執行、只顯示「預定要變更什麼」,-Confirm 則是在執行前顯示確認的機制。詳細內容可以參考 Microsoft Learn 的 about_Functions_CmdletBindingAttribute。
function Move-OldLogFile {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo[]]$File,
[Parameter(Mandatory)]
[string]$SourceRoot,
[Parameter(Mandatory)]
[string]$ArchiveRoot
)
foreach ($item in $File) {
$relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
$destination = Join-Path $ArchiveRoot $relativePath
$destinationDirectory = Split-Path -Path $destination -Parent
if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
if (-not [System.IO.Directory]::Exists($destinationDirectory)) {
[System.IO.Directory]::CreateDirectory($destinationDirectory) | Out-Null
}
Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Moved"
Message = ""
}
}
else {
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Preview"
Message = ""
}
}
}
}
重點是把 $PSCmdlet.ShouldProcess() 放在 Move-Item 的正前方。要不要變更的判斷,不是放在函式外面,而是放在真正變更之前。
8. 完成版腳本
以下是把上面內容整理起來的完成版。檔名設為 Invoke-LogMaintenance.ps1。
# Invoke-LogMaintenance.ps1
#Requires -Version 7.0
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string]$ConfigPath = ".\log-maintenance.json",
[switch]$Preview,
[switch]$SkipArchive,
[switch]$SkipTranscript
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Ensure-Directory {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not [System.IO.Directory]::Exists($Path)) {
[System.IO.Directory]::CreateDirectory($Path) | Out-Null
}
}
function Import-LogMaintenanceConfig {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "Config file not found: $Path"
}
$config = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($name in @("LogPath", "OutputPath", "Days", "Patterns", "ArchiveDays", "ArchivePath")) {
if (-not ($config.PSObject.Properties.Name -contains $name)) {
throw "Config value missing: $name"
}
}
if ([string]::IsNullOrWhiteSpace([string]$config.LogPath)) {
throw "LogPath is empty."
}
if (-not (Test-Path -LiteralPath $config.LogPath)) {
throw "LogPath not found: $($config.LogPath)"
}
if ([string]::IsNullOrWhiteSpace([string]$config.OutputPath)) {
throw "OutputPath is empty."
}
if ([string]::IsNullOrWhiteSpace([string]$config.ArchivePath)) {
throw "ArchivePath is empty."
}
if (@($config.Patterns).Count -eq 0) {
throw "Patterns is empty."
}
if ([int]$config.Days -lt 1) {
throw "Days must be 1 or greater."
}
if ([int]$config.ArchiveDays -lt 1) {
throw "ArchiveDays must be 1 or greater."
}
return $config
}
function Export-CsvWithHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[object[]]$InputObject,
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string[]]$Header
)
if ($InputObject.Count -gt 0) {
$InputObject |
Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
}
else {
($Header -join ",") |
Set-Content -LiteralPath $Path -Encoding UTF8
}
}
function Get-LogHit {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$LogPath,
[Parameter(Mandatory)]
[ValidateRange(1, 3650)]
[int]$Days,
[Parameter(Mandatory)]
[string[]]$Pattern
)
$since = (Get-Date).AddDays(-$Days)
$files = @(
Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
Where-Object { $_.LastWriteTime -ge $since }
)
Write-Verbose "Recent log files: $($files.Count)"
if ($files.Count -eq 0) {
return @()
}
$paths = $files | Select-Object -ExpandProperty FullName
Select-String -LiteralPath $paths -Pattern $Pattern -ErrorAction Stop |
ForEach-Object {
[pscustomobject]@{
Path = $_.Path
LineNumber = $_.LineNumber
Pattern = $_.Pattern
Line = $_.Line.Trim()
}
}
}
function Get-OldLogFile {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$LogPath,
[Parameter(Mandatory)]
[ValidateRange(1, 3650)]
[int]$ArchiveDays
)
$limit = (Get-Date).AddDays(-$ArchiveDays)
Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
Where-Object { $_.LastWriteTime -lt $limit } |
Sort-Object LastWriteTime
}
function Move-OldLogFile {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo[]]$File,
[Parameter(Mandatory)]
[string]$SourceRoot,
[Parameter(Mandatory)]
[string]$ArchiveRoot
)
foreach ($item in $File) {
$relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
$destination = Join-Path $ArchiveRoot $relativePath
$destinationDirectory = Split-Path -Path $destination -Parent
if (Test-Path -LiteralPath $destination) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($item.Name)
$ext = $item.Extension
$destination = Join-Path $destinationDirectory ("{0}_{1:yyyyMMddHHmmss}{2}" -f $name, $item.LastWriteTime, $ext)
}
if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
Ensure-Directory -Path $destinationDirectory
Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Moved"
Message = ""
}
}
else {
[pscustomobject]@{
Source = $item.FullName
Destination = $destination
Status = "Preview"
Message = ""
}
}
}
}
$config = Import-LogMaintenanceConfig -Path $ConfigPath
$runStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportDir = Join-Path ([string]$config.OutputPath) $runStamp
Ensure-Directory -Path $reportDir
$transcriptStarted = $false
$transcriptPath = Join-Path $reportDir "transcript.txt"
try {
if (-not $SkipTranscript) {
Start-Transcript -Path $transcriptPath -Force | Out-Null
$transcriptStarted = $true
}
Write-Host "Report directory: $reportDir"
$hits = @(
Get-LogHit `
-LogPath ([string]$config.LogPath) `
-Days ([int]$config.Days) `
-Pattern ([string[]]$config.Patterns)
)
$hitCsv = Join-Path $reportDir "log-hits.csv"
Export-CsvWithHeader `
-InputObject $hits `
-Path $hitCsv `
-Header @("Path", "LineNumber", "Pattern", "Line")
$oldFiles = @(
Get-OldLogFile `
-LogPath ([string]$config.LogPath) `
-ArchiveDays ([int]$config.ArchiveDays)
)
$archiveTargets = @(
$oldFiles |
Select-Object FullName, Length, LastWriteTime
)
$archiveTargetCsv = Join-Path $reportDir "archive-targets.csv"
Export-CsvWithHeader `
-InputObject $archiveTargets `
-Path $archiveTargetCsv `
-Header @("FullName", "Length", "LastWriteTime")
$moveResults = @()
if ($SkipArchive) {
Write-Host "Archive skipped."
}
elseif ($oldFiles.Count -eq 0) {
Write-Host "No archive targets."
}
else {
$archiveRunRoot = Join-Path ([string]$config.ArchivePath) $runStamp
$moveResults = @(
Move-OldLogFile `
-File $oldFiles `
-SourceRoot ([string]$config.LogPath) `
-ArchiveRoot $archiveRunRoot `
-WhatIf:$Preview
)
}
$archiveResultCsv = Join-Path $reportDir "archive-result.csv"
Export-CsvWithHeader `
-InputObject $moveResults `
-Path $archiveResultCsv `
-Header @("Source", "Destination", "Status", "Message")
$summary = [pscustomobject]@{
CheckedAt = (Get-Date).ToString("s")
ComputerName = $env:COMPUTERNAME
LogPath = [string]$config.LogPath
ReportDirectory = $reportDir
HitCount = $hits.Count
ArchiveTargetCount = $oldFiles.Count
ArchiveResultCount = $moveResults.Count
Preview = [bool]$Preview
SkipArchive = [bool]$SkipArchive
}
$summaryPath = Join-Path $reportDir "summary.json"
$summary |
ConvertTo-Json -Depth 5 |
Set-Content -LiteralPath $summaryPath -Encoding UTF8
Write-Host "Finished."
Write-Host "Hits: $($hits.Count)"
Write-Host "Archive targets: $($oldFiles.Count)"
}
catch {
$errorPath = Join-Path $reportDir "error.txt"
$_ | Out-String | Set-Content -LiteralPath $errorPath -Encoding UTF8
Write-Error "Failed: $($_.Exception.Message)"
exit 1
}
finally {
if ($transcriptStarted) {
Stop-Transcript | Out-Null
}
}
9. 執行範例
先在不執行封存的情況下,只做日誌調查。
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -SkipArchive
接著,確認封存的預定動作。
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview
加上 -Preview 之後,舊日誌的搬移處理會被當成 -WhatIf 處理。
這個時間點要確認的檔案有下面 3 個:
log-hits.csvarchive-targets.csvarchive-result.csv
沒問題之後,就把 -Preview 拿掉再執行。
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json
想看詳細內容時可以加上 -Verbose。
.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview -Verbose
10. 會輸出的檔案
執行之後,會在 OutputPath 底下建立像 C:\App\Reports\20260602-030000 這種帶日期時間的資料夾。
裡面會輸出下列檔案:
| 檔案 | 內容 |
|---|---|
log-hits.csv |
被判定為錯誤/警告的行 |
archive-targets.csv |
被列為封存對象的舊日誌 |
archive-result.csv |
搬移結果,或 Preview 結果 |
summary.json |
件數與執行條件的摘要 |
transcript.txt |
PowerShell 工作階段的紀錄 |
error.txt |
發生錯誤時的詳細內容 |
Start-Transcript 是把 PowerShell 工作階段的命令與主控台輸出記錄成文字檔的 cmdlet。在維運腳本中,之後要確認「何時、在什麼條件下、輸出了什麼」會比較容易。
11. 用工作排程器定期執行
手動執行沒有問題之後,就可以用工作排程器定期執行。
以下是每天凌晨 3 點執行的範例。
$scriptPath = "C:\Ops\Invoke-LogMaintenance.ps1"
$configPath = "C:\Ops\log-maintenance.json"
$action = New-ScheduledTaskAction `
-Execute "pwsh.exe" `
-Argument "-NoProfile -File `"$scriptPath`" -ConfigPath `"$configPath`"" `
-WorkingDirectory "C:\Ops"
$trigger = New-ScheduledTaskTrigger -Daily -At 3:00
Register-ScheduledTask `
-TaskName "AppLogMaintenance" `
-Action $action `
-Trigger $trigger `
-Description "Collect app log errors and archive old logs"
New-ScheduledTaskAction 會建立代表工作要執行的命令的物件,New-ScheduledTaskTrigger 則建立每天、每週、登入時等啟動條件。最後用 Register-ScheduledTask 把工作註冊到本機電腦。
在正式環境中,還要確認下面這些項目:
- 執行使用者有日誌資料夾的讀取權限
- 有封存目的地的寫入權限
pwsh.exe的路徑已設定好- 符合腳本的執行原則與簽章規則
- 手動執行與工作排程器執行的結果一致
- 失敗時可以查看
error.txt或工作排程器的歷程紀錄
12. 常見的卡關情況
| 症狀 | 原因 | 對策 |
|---|---|---|
| 找不到日誌 | LogPath 設錯了 |
用 Test-Path 與 Get-ChildItem 確認 |
| CSV 是空的 | 對象期間內沒有符合的日誌 | 把 Days 放寬後再確認 |
| 文字出現亂碼 | 日誌的文字編碼和預期不同 | 確認輸入輸出的文字編碼 |
| 在工作排程器裡跑不動 | 執行使用者或工作目錄不同 | 確認 WorkingDirectory 與權限 |
| 封存對象太多 | ArchiveDays 設太短 |
看 archive-targets.csv 再調整 |
| 搬移目的地和預期不同 | 沒理解相對路徑的保留規則 | 用 -Preview 確認 Destination |
| 只有在正式環境失敗 | 權限、原則或被鎖定檔案的差異 | 確認 error.txt 與 transcript.txt |
特別是用工作排程器執行時,「自己執行時」和「工作排程器實際執行的使用者」可能不一樣。如果手動可以動、但工作排程器會失敗,請先懷疑權限與工作目錄。
13. 改造時的思考方式
這個腳本的前提是,與其直接原封不動使用,更適合依現場情況慢慢改造。以下列出常見的改造範例。
| 想做的事 | 改造位置 |
|---|---|
也想把 .txt 納入對象 |
修改 Get-ChildItem -Filter *.log |
也想看 ERROR 前後幾行 |
以 Select-String 的結果為基礎,用 Get-Content 取得周圍的行 |
| 想先壓縮再封存 | 在 Move-OldLogFile 之前加上 Compress-Archive |
| 想做電子郵件通知 | 以 summary.json 為基礎加上通知處理 |
| 想依應用程式分開設定 | 準備多份 JSON,把工作分開 |
| 想連刪除都自動化 | 先用搬移運作一段時間再評估 |
不過,一開始就把所有功能都塞進去並不安全。維運腳本重要的不是功能多,而是失敗時能不能追查。
14. 現場維運檢查清單
在定期執行 PowerShell 腳本之前,要確認下列各項:
- 先用
-SkipArchive只執行過讀取處理 - 接著用
-Preview確認過搬移預定動作 archive-targets.csv的對象是合理的archive-result.csv的Destination符合預期- 輸出資料夾中留下了帶日期時間的紀錄
- 確認發生錯誤時會留下
error.txt - 確認過工作排程器執行使用者的權限
- 確認過執行原則、簽章、公司內部規則
- 不要一開始就刪除,先用搬移的方式運作
- 已經決定好萬一要復原時該搬回哪裡
15. 結語
雖然說是 PowerShell 的應用,但不需要用到很多複雜的語法。實務上真正有效的,是先建立好下面這種模式:
- 把設定拆到 JSON 裡
- 先做讀取處理
- 用 CSV 與 JSON 留下紀錄
- 把變更處理拆成函式
- 準備相當於
-WhatIf的預演機制 - 用 transcript 與
error.txt讓事後可以追查 - 先手動執行確認,之後才定期執行
這次的腳本以日誌調查與封存為題材,但這種思考方式也可以用在其他業務上,例如:
- 檔案整理
- 報表輸出
- CSV 彙整
- 批次程式替換
- 舊資產的盤點
- 每日/每月的維運確認
PowerShell 用一行命令就很方便,但如果要用在業務上,遵守下面這個順序會比較安全:
先看 → 記錄 → 預演 → 執行 → 留下紀錄
做成這種形式之後,PowerShell 就不只是縮短作業時間的工具,而能當成讓維運更穩定的小型業務應用程式來使用。
參考連結
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
在 C#(CSharp)中執行 PowerShell 並以物件形式接收結果
本文從實務角度整理如何從 C# 啟動 PowerShell,並以 PSObject 而非字串來接收結果,涵蓋 PowerShell SDK、AddCommand、AddParameter、BaseObject、Properties 到錯誤處理。
用 Pester 整備 PowerShell 測試 ── 讓維運腳本不易損壞的實務做法
本文整理用 Pester v5 測試 PowerShell 腳本的實務步驟,涵蓋日期處理、檔案操作、刪除處理、Mock,一路到 CI 執行,協助安全地建立測試基礎。
PowerShell 實用指令集錦 ── 累積日常工作常用的小工具
本文整理 PowerShell 日常工作中常用的實用指令,說明 Measure-Object、Group-Object、Select-String、Compare-Object、Tee-Object、Start-Transcript 等指令的使用場景與時機。
PowerShell 指令基礎 ── 該先學會的操作與安全使用方式
為了讓 PowerShell 初學者在實務上不會迷路,本文整理了如何尋找 Cmdlet、管線、檔案操作、CSV 處理、執行原則,以及避免事故的確認步驟。
為 VBScript 停用做準備:VBA・企業內部工具盤點指南
本文整理 VBScript 分階段停用前,VBA、Excel 巨集與企業內部工具的資產盤點、靜態偵測、執行紀錄蒐集、替代技術選型、測試與分階段導入的做法。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
既有資產活用 & 遷移支援
在持續活用 COM / ActiveX / OCX 資產、原生程式碼與 32 位元相依的同時,協助規劃階段性的遷移。
常見問題
整理諮詢這個主題時常見的問題。
- 用 PowerShell 自動化日誌調查時,應該從什麼開始寫?
- 不要一開始就寫搬移或刪除這類變更處理,先只做讀取處理。先完成用 Get-ChildItem 找出對象日誌、用 Select-String 擷取錯誤行並輸出成 CSV 這部分。即使是在正式環境的資料夾中測試,也要先在這個讀取階段暫停,確認結果。之後再逐步進到把封存對象列表化、把搬移處理寫成函式,這樣比較安全。
- PowerShell 的 -WhatIf 要怎麼寫進腳本裡?
- 在函式的 CmdletBinding 屬性加上 SupportsShouldProcess,該函式就能使用 -WhatIf 與 -Confirm。重點是把 $PSCmdlet.ShouldProcess() 放在真正執行變更的處理(例如 Move-Item)之前,讓「要不要變更」的判斷就發生在變更動作之前。本文的腳本把 -Preview 開關以 -WhatIf:$Preview 的形式傳入,就能在不搬移的情況下先確認預定動作。
- PowerShell 腳本手動執行沒問題,但在工作排程器中卻失敗,原因是什麼?
- 常見原因是自己執行時的身分和工作排程器實際執行的使用者不同。要確認執行使用者是否有日誌資料夾的讀取權限、封存目的地的寫入權限,工作目錄(WorkingDirectory)是否正確,pwsh.exe 的路徑是否設定好,以及是否符合執行原則與簽章規則。失敗時,也要能靠 error.txt、transcript.txt 或工作排程器的歷程紀錄追查原因。
- 可以連舊日誌的刪除都自動化嗎?
- 不建議一開始就把刪除自動化。本文的腳本也只做到比刪除更安全的「搬移」為止。建議先用搬移的方式運作一段時間,確認 archive-targets.csv、archive-result.csv 這些紀錄都沒有問題後,再考慮把刪除自動化。也要事先決定好,萬一要復原時該搬回哪裡。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。