تطبيقات PowerShell المتقدمة — أتمتة آمنة لتحقيق السجلات (logs) وأرشفتها وإصدار التقارير عنها
· غو كومورا · PowerShell, Windows, أتمتة, تحقيق السجلات, تحسين التشغيل, الاستفادة من الأصول القائمة
1. الأساسيات التي ينبغي فهمها أولًا
في المقال السابق تناولنا أوامر PowerShell الأساسية، وخط الأنابيب (pipeline)، وإخراج CSV، وJSON، وسكربتات .ps1، والتحقق الآمن عبر -WhatIf. في هذا المقال، وكمتابعة لذلك، سنبني سكربتًا أكبر قليلًا وسهل الاستخدام في العمل الفعلي. الموضوع هو مهمة تشغيلية (operational task) من هذا النوع:
- تحقيق السجلات (logs)
- تجميع أسطر الأخطاء في ملف CSV
- سرد السجلات القديمة كمرشحة للأرشفة
- نقل السجلات القديمة عند الحاجة
- حفظ نتائج التنفيذ كدليل (audit trail)
المهم في تطبيقات PowerShell المتقدمة ليس إطالة الأوامر، بل بناء التدفق التالي:
- فصل الإعدادات
- بناء معالجة القراءة
- حفظ المخرجات
- فصل معالجة التغيير إلى دوال (functions)
- إجراء تجربة (rehearsal) باستخدام
-WhatIf - النظر في التشغيل التلقائي في النهاية
الأتمتة لا تعني «الاستغناء عن تحقق الإنسان». بل تعني تثبيت النقطة التي يجب التحقق عندها، وضمان بقاء نفس الدليل في كل مرة.
علمًا بأن الكود الوارد في هذا المقال منشور على GitHub كمجموعة عينات جاهزة للتشغيل مباشرة (السكربت النهائي، وملف الإعدادات، وسكربت إنشاء بيئة سجلات وهمية (dummy)، واختبارات Pester للتحقق من التحقيق والتجربة والنقل).
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 وملف JSON للملخص (summary) ونتائج 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، يمكن التعامل مع JSON ككائن (object) باستخدام ConvertFrom-Json. وعلى العكس، عند الحاجة لحفظ نتيجة المعالجة بصيغة JSON، تُستخدم ConvertTo-Json. ConvertTo-Json هي cmdlet تحوّل الكائنات إلى نصوص JSON، وعند التعامل مع مستويات تداخل عميقة يصبح تحديد -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. فصل معالجة التغيير إلى دالة (function)
افصل معالجة التغيير، مثل النقل، عن معالجة القراءة.
في PowerShell، إضافة SupportsShouldProcess إلى دالة (function) تجعلها قادرة على التعامل مع -WhatIf وConfirm-. تعرض -WhatIf «ما الذي سيتم تغييره» دون التنفيذ الفعلي، بينما -Confirm هي الآلية التي تعرض تأكيدًا قبل التنفيذ. للمزيد من التفاصيل، راجع about_Functions_CmdletBindingAttribute على Microsoft Learn.
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.
الملفات الثلاثة التي يجب مراجعتها في هذه المرحلة هي:
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 هي cmdlet تُستخدم لتسجيل أوامر جلسة PowerShell ومخرجات وحدة التحكم (console) في ملف نصي. في السكربتات التشغيلية، يسهّل ذلك التحقق لاحقًا من «متى تم التنفيذ، وتحت أي شروط، وما الذي ظهر».
11. التشغيل الدوري عبر Task Scheduler
إذا لم تكن هناك مشكلة عند التشغيل اليدوي، يمكن التشغيل الدوري عبر Task Scheduler.
فيما يلي مثال يبدأ بتشغيل يومي في الساعة 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 كائنًا يمثّل الأمر الذي ستنفذه المهمة (task)، بينما تُنشئ New-ScheduledTaskTrigger شروط بدء التشغيل مثل يوميًا أو أسبوعيًا أو عند تسجيل الدخول. وأخيرًا، تُسجَّل المهمة على الجهاز المحلي عبر Register-ScheduledTask.
في بيئة الإنتاج، تحقق أيضًا من النقاط التالية.
- يملك المستخدم المُنفِّذ صلاحية قراءة مجلد السجلات
- توجد صلاحية كتابة في وجهة الأرشفة
- مسار
pwsh.exeمعرَّف بشكل صحيح - السكربت متوافق مع سياسة التنفيذ (execution policy) وقواعد التوقيع
- يُعطي التشغيل اليدوي وتشغيل المهمة نفس النتيجة
- يمكن مراجعة
error.txtوسجل تنفيذ المهام عند الفشل
12. المشكلات الشائعة
| العرض | السبب | المعالجة |
|---|---|---|
| لا يُعثر على السجلات | LogPath غير صحيح |
تحقق باستخدام Test-Path وGet-ChildItem |
| ملف CSV فارغ | لا توجد سجلات ضمن الفترة المستهدفة | وسّع نطاق Days وتحقق مرة أخرى |
| ظهور النصوص بأحرف مشوّهة (mojibake) | ترميز أحرف السجل مختلف عمّا هو متوقع | تحقق من ترميز الإدخال والإخراج |
| لا يعمل عبر Task Scheduler | اختلاف المستخدم المُنفِّذ أو مجلد العمل | تحقق من WorkingDirectory والصلاحيات |
| عدد أهداف الأرشفة كبير جدًا | قيمة ArchiveDays قصيرة جدًا |
راجع archive-targets.csv وعدّل القيمة |
| وجهة النقل مختلفة عمّا هو متوقع | عدم فهم قاعدة الحفاظ على المسار النسبي | تحقق من Destination عبر -Preview |
| الفشل يقع في بيئة الإنتاج فقط | اختلاف في الصلاحيات أو السياسات أو وجود ملفات مقفلة | راجع error.txt وtranscript.txt |
على وجه الخصوص، عند التشغيل عبر Task Scheduler، قد يختلف «المستخدم الذي شغّلت السكربت تحته بنفسك» عن «المستخدم الذي تعمل تحته المهمة». إذا كان السكربت يعمل يدويًا لكنه يفشل كمهمة، فابدأ بالشك في الصلاحيات ومجلد العمل.
13. طريقة التفكير عند التعديل (customization)
هذا السكربت مُصمَّم على افتراض تعديله تدريجيًا ليتناسب مع بيئة العمل الفعلية، بدلًا من استخدامه كما هو. فيما يلي أمثلة شائعة على التعديل.
| ما تريد فعله | مكان التعديل |
|---|---|
تضمين ملفات .txt أيضًا |
تعديل Get-ChildItem -Filter *.log |
رؤية عدد من الأسطر قبل وبعد ERROR |
جلب الأسطر المحيطة عبر Get-Content بناءً على نتائج Select-String |
| ضغط الملفات قبل الأرشفة | إضافة Compress-Archive قبل Move-OldLogFile |
| إرسال إشعارات عبر البريد الإلكتروني | إضافة معالجة إشعارات بناءً على summary.json |
| فصل الإعدادات حسب كل تطبيق | تجهيز عدة ملفات JSON وتقسيم المهام (tasks) |
| أتمتة الحذف أيضًا | تشغيل عملية النقل لفترة معينة أولًا، ثم النظر في الأمر لاحقًا |
لكن من الأسلم عدم إضافة كل شيء منذ البداية. في السكربتات التشغيلية، إمكانية التتبع عند وقوع الفشل أهم من كثرة الوظائف.
14. قائمة تحقق للتشغيل الميداني
قبل تشغيل سكربت PowerShell بشكل دوري، تحقق من النقاط التالية.
- نفّذت معالجة القراءة فقط أولًا باستخدام
-SkipArchive - ثم تحققت من خطة النقل باستخدام
-Preview - كانت الأهداف في
archive-targets.csvمعقولة - كانت قيمة
Destinationفيarchive-result.csvمطابقة للمتوقع - بقي دليل يحمل طابعًا زمنيًا في مجلد الإخراج
- تحققت من بقاء
error.txtعند وقوع خطأ - تحققت من صلاحيات المستخدم المُنفِّذ للمهمة
- تحققت من سياسة التنفيذ والتوقيع والقواعد الداخلية للشركة
- شغّلت النظام بالنقل أولًا وليس بالحذف المباشر
- حدّدت وجهة الاستعادة في حال الحاجة للتراجع
15. الخلاصة
حتى عند الحديث عن تطبيقات PowerShell المتقدمة، لا حاجة لاستخدام قدر كبير من الصياغات المعقدة. ما ينفع فعليًا في العمل هو تجهيز هذا النمط (pattern):
- فصل الإعدادات في ملف JSON
- بناء معالجة القراءة أولًا
- حفظ الأدلة عبر CSV وJSON
- فصل معالجة التغيير إلى دوال
- تجهيز تجربة مماثلة لـ
-WhatIf - إتاحة التتبع عبر transcript وerror.txt
- التحقق عبر التشغيل اليدوي قبل التشغيل الدوري
يتناول سكربت هذا المقال تحقيق السجلات وأرشفتها كموضوع، لكن طريقة التفكير هذه قابلة للتطبيق على مهام أخرى أيضًا:
- تنظيم الملفات
- إصدار التقارير (reports)
- تجميع بيانات CSV
- استبدال الملفات الدفعي (batch)
- جرد الأصول القديمة
- التحقق التشغيلي اليومي والشهري
PowerShell مفيد حتى كأوامر من سطر واحد، لكن عند استخدامه في العمل، من الأسلم الالتزام بالترتيب التالي:
النظر ← التسجيل ← التجربة ← التنفيذ ← حفظ الدليل
عند اتباع هذا الشكل، لا يظل PowerShell مجرد أداة لاختصار العمل، بل يصبح تطبيقًا تشغيليًا صغيرًا يُستخدم لتحقيق استقرار العمليات.
روابط مرجعية
- مجموعة عينات الكود الكاملة لهذا المقال (السكربت النهائي، ملف الإعدادات، اختبارات Pester) - komurasoft-blog-samples (GitHub)
- PowerShell Documentation - Microsoft Learn
- ConvertTo-Json - Microsoft Learn
- about_Functions_CmdletBindingAttribute - Microsoft Learn
- Start-Transcript - Microsoft Learn
- New-ScheduledTaskAction - Microsoft Learn
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
دليل المراجعة الشاملة لـ VBA و Excel macro والأدوات الداخليّة استعدادًا لإيقاف VBScript - الجرد / الكشف الساكن / سجلّات التشغيل / اختيار البديل / الاختبار / النشر التدريجيّ
ملخّص عمليّ على صفحة واحدة لمسار الاستعداد لإيقاف VBScript تدريجيًّا: جرد VBA و Excel macro والأدوات الداخليّة، الكشف الساكن، تجميع سجلّا...
كيف نُطيل عمر أنظمة الويب الداخليّة المعتمدة على IE mode وكيف نخرج منها - تنظيم الاستراتيجيّات الميدانيّة من الإدارة المركزيّة لقائمة المواقع، إلى WebView2، والإعادة الهيكليّة التدريجيّة، ووصولًا إلى عزل VDI
نتناول استراتيجيّةً عمليّةً لإطالة عمر أنظمة الويب الداخليّة المعتمدة على IE mode والخروج منها تدريجيًّا عبر إدارة قائمة المواقع وتغليف W...
ما يجب التحقّق منه عندما لا يعمل ActiveX على Office 2024 / Microsoft 365 - الترتيب العمليّ لتغطية التعطيل الافتراضيّ، 32bit / 64bit، تسجيل COM، DLL التابعة، ووصولًا إلى IE mode
دليل عمليّ لتشخيص توقّف ActiveX على Office 2024 و Microsoft 365، يرتّب الفحوص من التعطيل الافتراضيّ إلى تطابق 32bit / 64bit وتسجيل COM وD...
شرح ترميزات النصّ ونهايات السطور في Windows - Shift_JIS و UTF-8 و UTF-16 و mojibake و CRLF و LF
دليل عمليّ يوضّح كيف تتعايش UTF-8 و CP932 و UTF-16 و BOM ونهايات CRLF/LF في Windows، وكيف تشخّص mojibake وتمنع تلف البيانات.
فهم ترميزات النصّ على Windows - لماذا يحدث mojibake وما الذي ينكسر عند دخول Linux إلى المعادلة
يشرح المقال لماذا يحدث mojibake على Windows عند تبادل الملفّات مع Linux، ويوضّح طبقات الترميز المنفصلة (CP932، UTF-8، PowerShell) لتشخيصه...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- عند أتمتة تحقيق السجلات باستخدام PowerShell، من أين ينبغي البدء؟
- لا تبدأ بكتابة عمليات التغيير مثل النقل أو الحذف مباشرة، بل ابنِ أولًا معالجة القراءة فقط. أكمل أولًا الجزء الذي يبحث عن السجلات المستهدفة باستخدام Get-ChildItem، ويستخرج أسطر الأخطاء باستخدام Select-String، ويصدّرها إلى CSV. حتى عند التجربة في مجلد بيئة الإنتاج، توقف عند مرحلة القراءة هذه وتحقق من النتائج أولًا. بعد ذلك، من الأنسب التقدم تدريجيًا نحو سرد أهداف الأرشفة ثم تحويل معالجة النقل إلى دالة (function).
- كيف يمكن دمج -WhatIf الخاص بـ PowerShell داخل السكربت؟
- بإضافة SupportsShouldProcess إلى سمة CmdletBinding الخاصة بالدالة (function)، تصبح تلك الدالة قادرة على استخدام -WhatIf وConfirm-. النقطة الجوهرية هي وضع $PSCmdlet.ShouldProcess() مباشرة قبل عملية التغيير الفعلية (مثل Move-Item)، بحيث يُتخذ قرار التغيير من عدمه قُبيل التغيير مباشرة. في سكربت هذا المقال، يُمرَّر المفتاح -Preview كـ -WhatIf:$Preview، مما يتيح استعراض الخطة فقط دون تنفيذ عملية النقل الفعلية.
- لماذا يعمل سكربت PowerShell عند التشغيل اليدوي لكنه يفشل عند تشغيله عبر Task Scheduler؟
- السبب النمطي هو اختلاف المستخدم الذي تعمل تحته المهمة (task) عن المستخدم الذي شغّلت السكربت يدويًا تحته. تحقق مما إذا كان لدى ذلك المستخدم صلاحية قراءة مجلد السجلات وصلاحية كتابة في وجهة الأرشفة، وما إذا كان مجلد العمل (WorkingDirectory) صحيحًا، وما إذا كان مسار pwsh.exe معرَّفًا بشكل صحيح، وما إذا كانت سياسة التنفيذ (execution policy) وقواعد التوقيع متوافقة. من المهم أيضًا التأكد من إمكانية تتبع حالات الفشل عبر error.txt وtranscript.txt وسجل تنفيذ المهام (task history).
- هل يجوز أتمتة حذف السجلات القديمة أيضًا؟
- لا يُنصح بأتمتة الحذف منذ البداية. سكربت هذا المقال أيضًا يتوقف عند «النقل» الآمن دون الوصول إلى الحذف. من الأفضل تشغيل عملية النقل لفترة معينة أولًا، ومراجعة الأدلة مثل archive-targets.csv وarchive-result.csv للتأكد من عدم وجود مشكلات، ثم النظر في أتمتة الحذف بعد ذلك. من المهم أيضًا تحديد وجهة الاستعادة مسبقًا في حال احتيج إلى التراجع.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة