Прикладной PowerShell — безопасная автоматизация анализа логов, архивирования и отчётности

· · PowerShell, Windows, Автоматизация, Анализ логов, Улучшение эксплуатации, Использование существующих ресурсов

1. Что важно понять с самого начала

В прошлый раз мы разобрали базовые команды PowerShell, конвейер, вывод в CSV, JSON, скрипты .ps1 и безопасную проверку с помощью -WhatIf. В этот раз, в продолжение темы, мы напишем чуть более крупный скрипт, удобный для практической работы. В качестве примера возьмём такую эксплуатационную задачу:

  1. Проанализировать логи
  2. Собрать строки с ошибками в CSV
  3. Составить список старых логов как кандидатов на архивирование
  4. При необходимости переместить старые логи
  5. Сохранить результаты выполнения как аудиторский след

В прикладном PowerShell важна не длина команд, а выстраивание следующего потока действий:

  1. Вынести настройки отдельно
  2. Реализовать обработку для чтения
  3. Сохранять вывод
  4. Вынести изменяющую обработку в функции
  5. Провести репетицию с -WhatIf
  6. Рассмотреть автоматический запуск в последнюю очередь

Автоматизация — это не «отказ от проверки человеком». Это фиксация того, что именно нужно проверять, и обеспечение того, чтобы при каждом запуске оставался одинаковый аудиторский след.

Код, приведённый в этой статье, опубликован на GitHub в виде готового к запуску набора примеров (финальная версия скрипта, файл конфигурации, скрипт создания тестового окружения с логами и 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, 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 в production.

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; при работе с глубокой вложенностью важно указывать параметр -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

До этого момента всё сводится только к чтению. Даже при тестировании на production-папке стоит остановиться именно на этом этапе.

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.

На этом этапе стоит проверить три файла:

  • log-hits.csv
  • archive-targets.csv
  • archive-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 в текстовый файл. В эксплуатационных скриптах это упрощает последующую проверку того, «когда, при каких условиях и что было получено».

11. Регулярный запуск через планировщик заданий

Если ручной запуск не выявил проблем, скрипт можно запускать по расписанию через планировщик заданий (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 создаёт объект, представляющий команду, которую выполнит задача, а New-ScheduledTaskTrigger задаёт условия запуска: ежедневно, еженедельно, при входе в систему и т. д. Наконец, Register-ScheduledTask регистрирует задачу на локальном компьютере.

В production-эксплуатации также стоит проверить следующее:

  • у учётной записи, от имени которой выполняется задача, есть право чтения папки с логами
  • есть право записи в папку назначения архива
  • путь к pwsh.exe доступен
  • соответствие политике выполнения скриптов и правилам подписи
  • ручной запуск и запуск через задачу дают одинаковый результат
  • при сбое можно проверить error.txt и журнал задачи

12. Частые проблемы

Симптом Причина Решение
Логи не находятся Неверно указан LogPath Проверьте с помощью Test-Path и Get-ChildItem
CSV получается пустым За выбранный период подходящих логов нет Расширьте Days и проверьте снова
Японский текст отображается некорректно Кодировка лога отличается от ожидаемой Проверьте кодировки ввода и вывода
Не работает как задача Отличается пользователь выполнения или рабочая папка Проверьте WorkingDirectory и права доступа
Слишком много кандидатов на архивирование Слишком маленькое значение ArchiveDays Посмотрите archive-targets.csv и скорректируйте значение
Место назначения не совпадает с ожидаемым Непонимание правила сохранения относительного пути Проверьте Destination с помощью -Preview
Сбой только в production Различия в правах доступа, политике или заблокированных файлах Проверьте error.txt и transcript.txt

В частности, при запуске через планировщик заданий пользователь, «от имени которого выполняли скрипт вы сами», и «пользователь, от имени которого выполняется задача», могут отличаться. Если скрипт работает вручную, но завершается ошибкой в задаче, в первую очередь стоит проверить права доступа и рабочую папку.

13. Как подходить к доработке скрипта

Этот скрипт рассчитан не столько на использование «как есть», сколько на постепенную доработку под конкретную задачу. Вот типичные примеры доработок.

Что нужно Что менять
Включить также .txt Изменить Get-ChildItem -Filter *.log
Посмотреть несколько строк вокруг ERROR На основе результатов Select-String получить окружающие строки через Get-Content
Сжимать перед архивированием Добавить Compress-Archive перед Move-OldLogFile
Отправлять уведомления по почте Добавить обработку уведомлений на основе summary.json
Разделить настройки по приложениям Подготовить несколько 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 становится не просто инструментом для сокращения ручной работы, а небольшим бизнес-приложением, стабилизирующим эксплуатацию.

Справочные ссылки

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Практическое руководство по Process Monitor (ProcMon) — как за 10 минут выяснить, почему «настройки не читаются» или возникает ACCESS DENIED

«Исправил конфигурационный файл, но изменения не применяются», «вчера всё работало, а сегодня приложение не запускается» — прежде чем лез...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

С чего начать автоматизацию анализа логов в PowerShell?
Не пишите сразу изменяющие операции вроде перемещения или удаления — начните только с чтения. Сначала полностью реализуйте поиск нужных логов через Get-ChildItem, извлечение строк с ошибками через Select-String и вывод результата в CSV. Даже при тестировании на production-папке стоит один раз остановиться на этом этапе чтения и проверить результат. Затем безопаснее двигаться поэтапно: сначала составить список кандидатов на архивирование, потом вынести перемещение файлов в отдельную функцию.
Как встроить -WhatIf в PowerShell-скрипт?
Если добавить SupportsShouldProcess в атрибут CmdletBinding функции, она получает поддержку -WhatIf и -Confirm. Ключевой момент — разместить $PSCmdlet.ShouldProcess() непосредственно перед фактической изменяющей операцией (например, Move-Item), чтобы решение о том, вносить ли изменение, принималось прямо перед самим изменением. В скрипте из этой статьи переключатель -Preview передаётся как -WhatIf:$Preview, что позволяет проверить план перемещения, не выполняя его на самом деле.
Почему PowerShell-скрипт работает вручную, но завершается ошибкой в планировщике заданий?
Типичная причина — учётная запись, от имени которой выполняется задача, отличается от той, под которой вы запускали скрипт вручную. Проверьте, есть ли у этой учётной записи право чтения папки с логами и право записи в папку архива, корректна ли рабочая папка (WorkingDirectory), доступен ли путь к pwsh.exe и соответствует ли скрипт политике выполнения и правилам подписи. Важно также заранее настроить скрипт так, чтобы при сбое можно было разобраться в причинах по error.txt, transcript.txt и журналу задачи.
Стоит ли сразу автоматизировать и удаление старых логов?
Автоматизировать удаление с самого начала не рекомендуется. Скрипт из этой статьи тоже ограничивается безопасным «перемещением», а не удалением. Сначала стоит какое-то время поработать в режиме перемещения, убедиться по аудиторским следам вроде archive-targets.csv и archive-result.csv, что проблем нет, и только после этого рассматривать автоматизацию удаления. Также важно заранее определить, куда восстанавливать файлы в случае необходимости отката.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог