تطبيقات PowerShell المتقدّمة ── أتمتة آمنة لتحقيق السجلّات وأرشفتها وإصدار التقارير عنها
· آخر تحديث: · غو كومورا · PowerShell, Windows, أتمتة, تحقيق السجلّات, تحسين التشغيل, الاستفادة من الأصول القائمة
سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621529)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
غو كومورا (2026). تطبيقات PowerShell المتقدّمة ── أتمتة آمنة لتحقيق السجلّات وأرشفتها وإصدار التقارير عنها. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621529 https://comcomponent.com/ar/blog/2026/06/02/000-powershell-script-log-maintenance-automation/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621529
- DOI (هذه النسخة)
- 10.5281/zenodo.22240939
1. ما ينبغي فهمه أوّلاً
في المقال السابق رتّبنا الأوامر الأساسيّة في PowerShell، وخطّ الأنابيب (pipeline)، وإخراج CSV، وJSON، وسكربتات .ps1، والتحقّق الآمن عبر -WhatIf. في هذا المقال، وكمتابعة لذلك، سنبني سكربتاً أكبر قليلاً وسهل الاستخدام في العمل الفعليّ. الموضوع مهمّة تشغيليّة من هذا النوع:
- تحقيق السجلّات (logs)
- تجميع أسطر الأخطاء في ملفّ CSV
- سرد السجلّات القديمة كمرشّحة للأرشفة
- نقل السجلّات القديمة عند الحاجة
- حفظ نتائج التنفيذ كأثر (audit trail)
المهمّ في تطبيقات PowerShell المتقدّمة ليس إطالة الأوامر، بل بناء التدفّق التالي:
- فصل الإعدادات
- بناء معالجة القراءة
- حفظ المخرجات
- فصل معالجة التغيير إلى دوالّ (functions)
- إجراء تجربة باستخدام
-WhatIf - النظر في التشغيل التلقائيّ في النهاية
الأتمتة لا تعني «الاستغناء عن تحقّق الإنسان». بل تعني تثبيت النقطة التي يجب التحقّق عندها، وضمان بقاء الأثر نفسه في كلّ مرّة.
علماً بأنّ الشيفرة الواردة في هذا المقال منشورة على GitHub كمجموعة عيّنات جاهزة للتشغيل مباشرة (السكربت النهائيّ، وملفّ الإعدادات، وسكربت إنشاء بيئة سجلّات وهميّة، واختبارات Pester للتحقّق من التحقيق والتجربة والنقل).
powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)
موضع هذا المقال
مقالات PowerShell ثلاث متتابعة. ليست سلسلة مرقّمة، لكن القراءة بهذا الترتيب تربط التدفّق.
| الترتيب | المقال | النطاق |
|---|---|---|
| 1 | أساسيّات أوامر PowerShell ── العمليّات الأولى الواجب تعلّمها والاستخدام الآمن | البحث عن الأوامر، وخطّ الأنابيب، وCSV / JSON، و.ps1، و-WhatIf |
| 2 | مجموعة أوامر PowerShell العمليّة ── إضافة وظائف صغيرة شائعة الاستخدام في العمل اليوميّ | قطع التجميع والمقارنة والاستخراج وإخراج الأثر |
| 3 | هذا المقال | جمع القطع في سكربت تشغيليّ واحد، وبناء الإعداد والأثر والتجربة والتشغيل الدوريّ |
ما يلزم كفرضيّة هو محتوى المقال 1 فقط. يمكن متابعة هذا المقال دون قراءة 2. «المقال السابق» في المقدّمة يشير إلى 1.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 16، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. ما سنبنيه في هذا المقال
سنبني سكربتاً باسم Invoke-LogMaintenance.ps1.
أبرز وظائفه كالتالي.
| الوظيفة | المحتوى |
|---|---|
| البحث عن السجلّات | البحث عن ملفّات .log ضمن المجلّد المحدّد |
| تحديد الفترة الزمنيّة | تحقيق السجلّات التي حُدِّثت خلال آخر N يوماً فقط |
| استخراج الأخطاء | استخراج الأسطر التي تحتوي على ERROR أو WARN أو FATAL وغيرها |
| إخراج CSV | حفظ نتائج البحث في log-hits.csv |
| سرد السجلّات القديمة | حفظ السجلّات الأقدم من N يوماً في archive-targets.csv |
| الأرشفة | نقل السجلّات القديمة إلى مجلّد منفصل |
| التشغيل التجريبيّ | عند تحديد -Preview، يُعرض المخطّط فقط دون نقل فعليّ |
| سجلّ التنفيذ | حفظ transcript و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 يمكن التعامل مع JSON ككائن عبر ConvertFrom-Json. وبالعكس، عند إبقاء نتيجة المعالجة في JSON استخدم ConvertTo-Json. ConvertTo-Json أمر يحوّل الكائن إلى سلسلة JSON، وعند التعامل مع طبقات عميقة يصير تحديد -Depth مهمّاً.
Patterns يُعامل كتعبير نمطيّ
موضع واحد يستحقّ التنبيه مسبقاً. قيم Patterns تُمرَّر في النهاية إلى Select-String -Pattern. وSelect-String يفسّر هذه القيمة كتعبير نمطيّ (regular expression) افتراضيّاً.
أي أنّ كتابة ERROR. تعني «ERROR يليه أيّ حرف واحد»، فتصيب أيضاً ERRORS وERROR:. وبالمقابل لا تصيب سطراً فيه ERROR وحده. والسلاسل التي تتضمّن شرطة مائلة عكسيّة مثل C:\App تُعامل كما هي كمحارف تحكّم في التعبير النمطيّ.
- إن أردت استخدامه كتعبير نمطيّ: اكتبه كما هو. يمكن الكتابة مثل
"ERROR|FATAL"أو"\[ERROR\]" - إن أردت المطابقة على السلسلة حرفياً: ضع نتيجة
[regex]::Escape("ERROR.")في JSON، أو أضف-SimpleMatchإلىSelect-Stringفي السكربت
كذلك لا يميّز Select-String افتراضيّاً بين الأحرف الكبيرة والصغيرة. يلتقط أسطر error وأسطر ERROR. إن أردت التمييز فأضف -CaseSensitive.
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 آلية لإخراج تأكيد قبل التنفيذ. التفاصيل في 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 أمر يسجّل أوامر جلسة PowerShell وإخراج وحدة التحكّم في ملفّ نصّي. في سكربتات التشغيل يسهّل لاحقاً تأكيد «متى، وبأيّ شروط، وماذا ظهر».
«الشكل الصحيح» للنواتج
في المرّة الأولى انظر إلى ما إذا كان المحتوى كما هو متوقَّع، لا إلى مجرّد ظهور الملفّات. محتوى السجلّات وهميّ، لكن ترتيب الأعمدة والبنود هو ما يخرجه سكربت هذا المقال.
log-hits.csv أربعة أعمدة: Path / LineNumber / Pattern / Line. وExport-Csv يحيط كلّ العناصر بـ " افتراضيّاً.
"Path","LineNumber","Pattern","Line"
"C:\App\Logs\app.log","128","ERROR","2026-06-02 02:14:51 [ERROR] OrderService: timeout while calling /api/stock"
"C:\App\Logs\app.log","301","WARN","2026-06-02 02:41:03 [WARN] OrderService: retry 1/3"
"C:\App\Logs\batch.log","57","FATAL","2026-06-02 03:00:12 [FATAL] nightly batch aborted"
عمود Pattern يحتوي سلسلة النمط التي أصابت، كما هي. وهي تقابل القيم المكتوبة في Patterns بملفّ الإعداد، فيمكن استخدامها لتجميع «بأيّ كلمة مفتاحيّة ظهر كم عنصراً».
archive-targets.csv ثلاثة أعمدة: FullName / Length / LastWriteTime.
"FullName","Length","LastWriteTime"
"C:\App\Logs\old\app-202401.log","10485760","2024/01/31 23:59:58"
archive-result.csv أربعة أعمدة: Source / Destination / Status / Message. عند إضافة -Preview يصير Status هو Preview، وعند التنفيذ يصير Moved.
"Source","Destination","Status","Message"
"C:\App\Logs\old\app-202401.log","C:\App\Archive\20260602-030000\old\app-202401.log","Moved",""
تدوين التاريخ والوقت يحوّله Export-Csv إلى سلسلة وفق إعدادات لغة نظام التشغيل، فيختلف بحسب البيئة. إن كنت ستمرّر CSV إلى نظام لاحق فتأكّد من هذه النقطة مسبقاً.
summary.json ملخّص الأعداد وشروط التنفيذ. تُهرَّب المسارات إلى \\ وفق مواصفة JSON.
{
"CheckedAt": "2026-06-02T03:00:07",
"ComputerName": "OPS-01",
"LogPath": "C:\\App\\Logs",
"ReportDirectory": "C:\\App\\Reports\\20260602-030000",
"HitCount": 3,
"ArchiveTargetCount": 1,
"ArchiveResultCount": 1,
"Preview": false,
"SkipArchive": false
}
في المراقبة والتأكيد اليوميّ يسهل الأمر إن اكتفيت بالنظر إلى summary.json. يمكن تشغيل الأمر بحيث لا تُفتح ملفات CSV إلا في اليوم الذي يقفز فيه HitCount أو يزيد ArchiveTargetCount فجأة.
علماً بأنّ CSV يُنشأ حتّى إن كان عدد الأهداف صفراً. ذلك لأنّ Export-CsvWithHeader يكتب ملفّاً بسطر الرأس فقط. التصميم لمنع الخلط بين «الملفّ غير موجود» و«عدد المطابقات كان صفراً».
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 المهمّة على الحاسوب المحلّي.
تأكيد أنّ التسجيل نجح
مرور أمر التسجيل وحده لا يعني بعد أنّ المهمّة «تعمل». قبل انتظار التنفيذ، راجع محتوى التسجيل بالعين.
من الواجهة الرسوميّة افتح جدولة المهام (taskschd.msc)، واختر من الجزء الأيسر «مكتبة جدولة المهام» (Task Scheduler Library)، فيظهر AppLogMaintenance الذي سجّلته للتوّ في القائمة. انظر إلى الأعمدة التالية.
| العمود | طريقة النظر |
|---|---|
| الحالة (Status) | «جاهز» (Ready) تعني أنّها مفعَّلة. إن كانت «معطَّلة» (Disabled) فلن تعمل حتّى لو جاء المشغِّل |
| المشغِّل (Triggers) | هل الشرط كما نويت، مثل «يوميّاً عند 3:00» |
| وقت التنفيذ التالي (Next Run Time) | أقرب موعد مقرَّر. إن كان فارغاً فاشكّ في المشغِّل أو في التفعيل/التعطيل |
| نتيجة التنفيذ السابق (Last Run Result) | تُملأ بعد التنفيذ. 0x0 نجاح، و0x1 خطأ عامّ، و0x41301 قيد التنفيذ |
إن لم تظهر هذه الأعمدة في القائمة، فانقر بزرّ الفأرة الأيمن على رأس العمود وأضف الأعمدة المعروضة.
يمكن تأكيد الأمر نفسه من PowerShell. على الأجهزة التي لا تُفتح فيها الواجهة الرسوميّة يكون هذا أسرع.
# 登録内容を見る
Get-ScheduledTask -TaskName "AppLogMaintenance" |
Select-Object TaskName, TaskPath, State
# 実行結果を見る
Get-ScheduledTaskInfo -TaskName "AppLogMaintenance" |
Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime
إن لم يكن LastTaskResult مساوياً 0، فانظر أوّلاً إلى error.txt وtranscript.txt في مجلّد التقرير.
غير أنّه إن لم تُقلع المهمّة نفسها، فالسكرپت لم ينفّذ سطراً واحداً ولن يُنشأ حتّى مجلّد التقرير. عندئذ لا تنظر إلى الخصائص التي تُفتح بالنقر الأيمن، بل اختر المهمّة في القائمة وانظر إلى علامة التبويب «السجلّ» (History) في الأسفل. السجلّ قد يكون معطَّلاً افتراضيّاً، لذا فعِّله عبر «تمكين سجلّ كلّ المهام» (Enable All Tasks History) في الجزء الأيمن، ثمّ تأكّد بالإقلاع اليدويّ عبر «تشغيل» (Run) في قائمة النقر الأيمن.
في تشغيل الإنتاج أكّد أيضاً النقاط التالية.
- لمستخدم التنفيذ صلاحية قراءة مجلّد السجلّات
- لديه صلاحية الكتابة إلى وجهة الأرشفة
- مسار
pwsh.exeمتاح - يتوافق مع سياسة تنفيذ السكرپت وقواعد التوقيع
- النتيجة نفسها في التشغيل اليدويّ وتشغيل المهمّة
- يمكن تأكيد
error.txtأو سجلّ المهمّة عند الفشل
12. العثرات الشائعة
| العَرَض | السبب | المعالجة |
|---|---|---|
| تعذّر العثور على السجلّات | LogPath خاطئ |
أكّد بـ Test-Path وGet-ChildItem |
| CSV يصبح فارغاً | لا سجلّات مطابقة في الفترة المستهدفة | وسِّع Days وتأكّد |
| تشوّه الحروف اليابانية | ترميز السجلّ يختلف عن المتوقَّع | أكّد ترميز الإدخال والإخراج |
| لا يعمل عبر المهمّة | مستخدم التنفيذ أو مجلّد العمل مختلف | أكّد WorkingDirectory والصلاحيات |
| أهداف الأرشفة أكثر ممّا ينبغي | ArchiveDays أقصر ممّا ينبغي |
اضبط بالنظر إلى archive-targets.csv |
| وجهة النقل تختلف عن المتوقَّع | لم تُفهم قاعدة الإبقاء على المسار النسبيّ | أكّد Destination عبر -Preview |
| يفشل في الإنتاج فقط | اختلاف الصلاحيات أو السياسات أو الملفّات المقفلة | أكّد error.txt وtranscript.txt |
خصوصاً عند التشغيل عبر جدولة المهام، قد يختلف «عندما تنفّذ بنفسك» عن «مستخدم تنفيذ المهمّة». إن عمل يدوياً وفشل عبر المهمّة، فاشكّ أوّلاً في الصلاحيات ومجلّد العمل.
13. فكر التعديل إن عدّلت
هذا السكرپت مفترض أن يُعدَّل قليلاً بحسب الميدان أكثر ممّا يُستخدم كما هو. أمثلة التعديل الشائعة كالتالي.
| ما تريد فعله | موضع التعديل |
|---|---|
استهداف .txt أيضاً |
غيّر Get-ChildItem -Filter *.log |
النظر إلى عدّة أسطر قبل ERROR وبعده |
انطلاقاً من نتيجة Select-String اجلب الأسطر المحيطة بـ Get-Content |
| الضغط ثمّ الأرشفة | أضف Compress-Archive قبل Move-OldLogFile |
| الإشعار بالبريد | أضف معالجة إشعار انطلاقاً من summary.json |
| فصل الإعداد حسب التطبيق | جهّز عدّة ملفات JSON وافصل المهام |
| أتمتة الحذف أيضاً | انظر في الأمر بعد فترة من تشغيل النقل |
غير أنّ إدخال الكلّ من البداية أقلّ أماناً. في سكربت التشغيل، إمكانيّة التتبّع عند الفشل أهمّ من كثرة الوظائف.
طريقة الإشعار بالبريد تُحسم أوّلاً
«أريد إشعاراً بالبريد» طلب شائع، لكنّ هذا الموضع وحده يلزم حسم طريقته قبل التنفيذ.
أمر PowerShell Send-MailMessage مذكور صراحة على Microsoft Learn بأنّه «obsolete (قديم)»، ويُنصح بعدم استخدامه لأنّه لا يضمن اتّصالاً آمناً بخادم SMTP. لا يوجد أمر خلَف مباشر داخل PowerShell؛ تُذكر كبدائل مكتبات مثل MailKit، وفي بيئة Exchange Online حزمة Microsoft Graph PowerShell SDK عبر Send-MgUserMail.
الطريقة تنقسم بحسب البيئة.
| البيئة | طريقة الإشعار |
|---|---|
| مرحّل SMTP داخليّ بلا مصادقة ولا TLS | يعمل أيضاً بـ Send-MailMessage، لكن احبسه في دالة واحدة استعداداً للحذف المستقبليّ |
| SMTP بمصادقة أو يلزم TLS | استخدم مكتبة مثل MailKit |
| بيئة Microsoft 365 | عبر Microsoft Graph (Send-MgUserMail) |
| البريد غير لازم | Webhook مثل Teams، أو ربط ملفّات بمنصّة المراقبة |
في أيّ طريقة، اجعل معالجة الإشعار قراءة summary.json والإرسال فقط، وافصلها عن المعالجة الأصليّة. الشكل الذي يكتمل فيه تحقيق السجلّات والأرشفة حتّى إن فشل الإشعار هو ما ينفع في العمل الفعليّ.
14. قائمة تحقّق للتشغيل في الميدان
قبل التشغيل الدوريّ لسكربت PowerShell أكّد النقاط التالية.
- نفّذت أوّلاً معالجة القراءة فقط عبر
-SkipArchive - أكّدت بعد ذلك خطّة النقل عبر
-Preview - كانت أهداف
archive-targets.csvسائغة - كان
Destinationفيarchive-result.csvكما هو متوقَّع - بقي أثر مؤرَّخ في مجلّد وجهة الإخراج
- أكّدت بقاء
error.txtعند الخطأ - أكّدت صلاحيات مستخدم تنفيذ المهمّة
- أكّدت سياسة التنفيذ والتوقيع والقواعد الداخليّة
- تشغّل أوّلاً بالنقل لا بالحذف المباغت
- حدّدت وجهة الإرجاع عند الاستعادة
15. الخلاصة
حتّى في تطبيقات PowerShell المتقدّمة، لا يلزم استخدام كثير من التراكيب الصعبة. ما ينفع في العمل هو تثبيت قالب كهذا.
- فصل الإعداد إلى JSON
- بناء معالجة القراءة أوّلاً
- ترك أثر في CSV وJSON
- فصل معالجة التغيير إلى دوالّ
- تجهيز تجربة تعادل
-WhatIf - تمكين التتبّع عبر transcript و
error.txt - التأكيد بالتشغيل اليدويّ ثمّ التشغيل الدوريّ
سكربت هذا المقال موضوعه تحقيق السجلّات والأرشفة، لكنّ الفكر يصلح لأعمال أخرى أيضاً.
- ترتيب الملفّات
- إخراج التقارير
- تجميع CSV
- استبدال الدفعات
- جرد الأصول القديمة
- تأكيد التشغيل اليوميّ والشهريّ
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
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
معالجة الأخطاء وتصميم إعادة التنفيذ في PowerShell ── من فخّ عدم نجاعة try/catch حتى القاعدة الثابتة لـ exit code وإعادة المحاولة
نرتّب من منظور عمليّ الفرق بين الخطأ المنهي وغير المنهي في PowerShell، وفخّ عدم نجاعة try/catch والقاعدة الثابتة لـ -ErrorAction Stop، وا...
سياسة تنفيذ PowerShell وتوقيع السكربتات ── دليل عمليّ للخروج من تشغيل «السدّ بـ Bypass»
سياسة تنفيذ PowerShell «جهاز أمان لا حدّ أمنيّ (security boundary)». نرتّب الفروق بين RemoteSigned وغيرها، وأولويّة النطاقات، وMark of th...
طريقة تشغيل PowerShell من C# (CSharp) واستقبال النتيجة ككائن
نرتّب من منظور عمليّ طريقة تشغيل PowerShell من C# واستقبال النتيجة ككائن PSObject بدل نصّ، بدءًا من PowerShell SDK وAddCommand وAddParame...
تهيئة اختبارات PowerShell عبر Pester ── الأسلوب العمليّ لجعل سكربتات التشغيل أقلّ عرضةً للكسر
نُنظِّم الإجراءات العمليّة لاختبار سكربتات PowerShell عبر Pester v5، بدءاً من معالجة التواريخ وعمليّات الملفّات وعمليّات الحذف، مروراً با...
مجموعة أوامر PowerShell العمليّة ── إضافة وظائف صغيرة شائعة الاستخدام في العمل اليوميّ
نرتّب هنا أوامر PowerShell العمليّة المستخدَمة في العمل اليوميّ، مثل مواضع استخدام Measure-Object وGroup-Object وSelect-String وCompare-O...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- عند أتمتة تحقيق السجلّات باستخدام PowerShell، من أين ينبغي البدء؟
- لا تبدأ بكتابة عمليّات التغيير مثل النقل أو الحذف مباشرة، بل ابنِ أوّلاً معالجة القراءة فقط. أكمل أوّلاً الجزء الذي يبحث عن السجلّات المستهدفة باستخدام Get-ChildItem، ويستخرج أسطر الأخطاء باستخدام Select-String، ويصدّرها إلى CSV. حتّى عند التجربة في مجلّد بيئة الإنتاج، توقّف عند مرحلة القراءة هذه وتحقّق من النتائج أوّلاً. بعد ذلك، من الأنسب التقدّم تدريجيّاً نحو سرد أهداف الأرشفة ثمّ تحويل معالجة النقل إلى دالة (function).
- كيف يمكن دمج -WhatIf الخاصّ بـ PowerShell داخل السكربت؟
- بإضافة SupportsShouldProcess إلى سمة CmdletBinding الخاصّة بالدالة (function)، تصبح تلك الدالة قادرة على استخدام -WhatIf و-Confirm. النقطة الجوهريّة هي وضع $PSCmdlet.ShouldProcess() مباشرة قبل عمليّة التغيير الفعليّة (مثل Move-Item)، بحيث يُتخذ قرار التغيير من عدمه قُبيل التغيير مباشرة. في سكربت هذا المقال، يُمرَّر المفتاح -Preview كـ -WhatIf:$Preview، ممّا يتيح استعراض الخطّة فقط دون تنفيذ عمليّة النقل الفعليّة.
- لماذا يعمل سكربت PowerShell عند التشغيل اليدويّ لكنّه يفشل عند تشغيله عبر جدولة المهام؟
- السبب النمطيّ هو اختلاف المستخدم الذي تعمل تحته المهمّة (task) عن المستخدم الذي شغّلت السكربت يدويّاً تحته. تحقّق ممّا إذا كان لدى ذلك المستخدم صلاحية قراءة مجلّد السجلّات وصلاحية كتابة في وجهة الأرشفة، وما إذا كان مجلّد العمل (WorkingDirectory) صحيحاً، وما إذا كان مسار pwsh.exe معرَّفاً بشكل صحيح، وما إذا كانت سياسة التنفيذ (execution policy) وقواعد التوقيع متوافقة. من المهمّ أيضاً التأكّد من إمكانيّة تتبّع حالات الفشل عبر error.txt وtranscript.txt وسجلّ تنفيذ المهام (task history).
- هل يجوز أتمتة حذف السجلّات القديمة أيضاً؟
- لا يُنصح بأتمتة الحذف منذ البداية. سكربت هذا المقال أيضاً يتوقّف عند «النقل» الآمن دون الوصول إلى الحذف. من الأفضل تشغيل عمليّة النقل لفترة معيّنة أوّلاً، ومراجعة الأدلّة مثل archive-targets.csv وarchive-result.csv للتأكّد من عدم وجود مشكلات، ثمّ النظر في أتمتة الحذف بعد ذلك. من المهمّ أيضاً تحديد وجهة الاستعادة مسبقاً في حال احتيج إلى التراجع.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.