Скрипт PowerShell под задачу

Пишет скрипт PowerShell с обработкой ошибок, логированием и режимом пробного прогона — так, чтобы его можно было запустить на рабочей машине и не бояться последствий.

// промпт
Ты — системный администратор с большим опытом в PowerShell. Ты пишешь скрипты, которые попадают на рабочие машины, поэтому по умолчанию делаешь их безопасными: сначала пробный прогон, потом действие. Ты не глушишь ошибки, чтобы скрипт «не падал», — ты их обрабатываешь осознанно. Ты объясняешь код, а не просто выдаёшь его. Напиши скрипт. **Параметры** - Что должен делать скрипт: {{zadaca}} - Где будет запускаться: {{okruzenie}} - Версия PowerShell: {{versiia_powershell}} - Как запускается — руками или по расписанию: {{sposob_zapuska}} - Права, под которыми выполняется: {{prava}} - Что категорически нельзя трогать: {{ograniceniia}} **Как действовать** 1. Сначала уточни задачу вслух: как ты её понял, какие крайние случаи видишь, что будет, если входных данных нет или их неожиданно много. Если в постановке есть неоднозначность — назови её и выбери разумное толкование, объяснив выбор. 2. Напиши скрипт с параметрами через param, а не с константами в теле. Скрипт с зашитыми путями невозможно переиспользовать и опасно править на ходу. 3. Обязательно предусмотри режим пробного прогона: по умолчанию скрипт показывает, что он <em>сделал бы</em>, и только с явным флагом выполняет действия. Для всего, что удаляет, перемещает или перезаписывает, это не роскошь. 4. Обрабатывай ошибки осознанно. Не оборачивай всё в подавление ошибок: раздели ожидаемые ситуации (файла нет, машина недоступна) и настоящие сбои. Первые обрабатывай и продолжай, вторые — прекращай работу с внятным сообщением. 5. Добавь логирование с временными метками — в файл и на экран. Для запуска по расписанию лог единственный способ понять, что произошло. 6. Верни осмысленный код завершения: ноль при успехе, ненулевой при ошибке. Планировщик и системы мониторинга ориентируются именно на него. 7. Прокомментируй ключевые места — почему так, а не что делает строка. И сопроводи скрипт разделом справки, чтобы работала встроенная помощь. 8. После кода перечисли: что скрипт делает необратимо, какие права ему нужны и почему, что проверить перед первым запуском на рабочей машине. **Формат ответа** Уточнение задачи, затем код целиком одним блоком, затем разбор ключевых решений, затем предупреждения и чек-лист перед запуском. Не выдумывай командлеты и параметры, которых не существует, — если не уверен в имени, скажи об этом прямо.
Заполните переменные
Пример ответа

Как я понял задачу

Нужно удалять из сетевой папки файлы старше заданного числа дней, запуск по расписанию раз в сутки, при этом нельзя трогать подпапку с архивом.

Крайние случаи, которые вижу:

  • Сетевая папка недоступна — это ожидаемая ситуация для сетевого пути, не сбой скрипта. Логируем и выходим с ненулевым кодом, чтобы мониторинг заметил.
  • Файл занят другим процессом — при удалении по расписанию это норма. Пропускаем конкретный файл, продолжаем работу, в конце сообщаем счёт пропущенных.
  • Файлов под условие не попало — не ошибка. Пишем в лог и выходим с нулём.
  • Символические ссылки — рекурсивный обход по ним может увести за пределы папки. Исключаю их явно.

Неоднозначность: «старше N дней» может считаться по дате создания или по дате последнего изменения. Для задач очистки практически всегда имеют в виду последнее изменение — беру его, но параметром это переключается.

#Requires -Version 5.1
<#
.SYNOPSIS
    Удаляет файлы старше заданного возраста, по умолчанию — в режиме пробного прогона.
.DESCRIPTION
    Обходит каталог рекурсивно, исключая указанные подпапки и символические ссылки.
    Без ключа -Execute ничего не удаляет, только показывает список кандидатов.
.EXAMPLE
    .\Clear-OldFiles.ps1 -Path \\server\share -Days 30
    Пробный прогон: показывает, что было бы удалено.
.EXAMPLE
    .\Clear-OldFiles.ps1 -Path \\server\share -Days 30 -ExcludeFolder 'archive' -Execute
    Реальное удаление.
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string]$Path,

    [Parameter(Mandatory)]
    [ValidateRange(1, 3650)]
    [int]$Days,

    [string[]]$ExcludeFolder = @(),

    [ValidateSet('LastWrite', 'Creation')]
    [string]$AgeBy = 'LastWrite',

    [string]$LogPath = "$PSScriptRoot\Clear-OldFiles.log",

    [switch]$Execute
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Write-Log {
    param([string]$Message, [ValidateSet('INFO','WARN','ERROR')][string]$Level = 'INFO')
    $line = '{0:yyyy-MM-dd HH:mm:ss} [{1}] {2}' -f (Get-Date), $Level, $Message
    Write-Host $line
    Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
}

$skipped = 0
$removed = 0
$freed   = 0L

try {
    Write-Log "Запуск. Путь: $Path, порог: $Days дн., режим: $(if ($Execute) { 'УДАЛЕНИЕ' } else { 'пробный прогон' })"

    # Ожидаемая ситуация для сетевого пути, а не сбой скрипта:
    # выходим с ненулевым кодом, чтобы это увидел мониторинг.
    if (-not (Test-Path -LiteralPath $Path)) {
        Write-Log "Путь недоступен: $Path" -Level ERROR
        exit 2
    }

    $threshold = (Get-Date).AddDays(-$Days)
    $property  = if ($AgeBy -eq 'Creation') { 'CreationTime' } else { 'LastWriteTime' }

    # -Force показывает скрытые файлы; ReparsePoint отсекает симлинки,
    # иначе рекурсия способна уйти за пределы каталога.
    $candidates = Get-ChildItem -LiteralPath $Path -File -Recurse -Force |
        Where-Object {
            $_.$property -lt $threshold -and
            -not $_.Attributes.HasFlag([IO.FileAttributes]::ReparsePoint)
        }

    if ($ExcludeFolder.Count -gt 0) {
        $candidates = $candidates | Where-Object {
            $relative = $_.FullName.Substring($Path.TrimEnd('\').Length)
            $segments = $relative.Split('\', [StringSplitOptions]::RemoveEmptyEntries)
            -not ($segments | Where-Object { $ExcludeFolder -contains $_ })
        }
    }

    Write-Log "Под условие попало файлов: $($candidates.Count)"

    foreach ($file in $candidates) {
        if (-not $Execute) {
            Write-Log "[пробный прогон] удалил бы: $($file.FullName) ($($file.$property))"
            continue
        }
        try {
            $size = $file.Length
            Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop
            $removed++
            $freed += $size
        }
        catch [System.IO.IOException] {
            # Занятый файл при плановой очистке — рабочая ситуация, не сбой.
            Write-Log "Пропущен (занят): $($file.FullName)" -Level WARN
            $skipped++
        }
        catch [System.UnauthorizedAccessException] {
            Write-Log "Пропущен (нет прав): $($file.FullName)" -Level WARN
            $skipped++
        }
    }

    $mb = [math]::Round($freed / 1MB, 2)
    Write-Log "Готово. Удалено: $removed, пропущено: $skipped, освобождено: $mb МБ"

    if (-not $Execute) {
        Write-Log 'Это был пробный прогон. Для реального удаления добавьте -Execute.'
    }

    exit 0
}
catch {
    Write-Log "Непредвиденная ошибка: $($_.Exception.Message)" -Level ERROR
    Write-Log $_.ScriptStackTrace -Level ERROR
    exit 1
}

Ключевые решения

  • Пробный прогон по умолчанию. Ключ называется -Execute, а не -WhatIf наоборот: чтобы удалить, нужно осознанное действие. Забытый флаг приводит к безобидному отчёту, а не к потере данных.
  • Разные catch на разные исключения. Занятый файл и отсутствие прав — ожидаемые ситуации, они логируются и не прерывают обход. Всё остальное всплывает во внешний catch и останавливает скрипт: непонятная ошибка при массовом удалении — повод прекратить, а не продолжать вслепую.
  • Set-StrictMode и ErrorActionPreference = Stop. Без них PowerShell молча продолжает работу после ошибок, и скрипт удаления «успешно» отрабатывает, ничего не сделав, — худший из возможных исходов.
  • Коды возврата 0 / 1 / 2. Планировщик видит только код. Двойка отдельно выделена для «путь недоступен»: эта ситуация чаще всего означает проблему сети, а не скрипта, и по коду её удобно отличать в мониторинге.
  • LiteralPath везде. Обычный -Path интерпретирует квадратные скобки как шаблон, и файлы с ними в имени тихо не обрабатываются.
  • Исключение по сегментам пути, а не подстрокой: проверка вхождения подстроки ошибочно исключила бы файл из папки «archive-2024».

Что необратимо и что проверить

  • Необратимо: Remove-Item удаляет мимо корзины. Восстановление — только из резервной копии.
  • Права: нужен доступ на чтение и удаление в целевой папке. Под учётной записью планировщика проверьте отдельно — она часто отличается от вашей, и это выясняется в первый же ночной запуск.
  • Перед первым боевым запуском: прогоните без -Execute и прочитайте лог целиком. Убедитесь, что исключения сработали и в списке нет ничего из архивной папки.
  • Проверьте политику выполнения в целевом окружении — иначе скрипт не запустится по расписанию.
  • Лог не ротируется. При ежедневном запуске он будет расти; заложите ротацию или чистку, иначе через год получите файл на несколько сотен мегабайт.

Похожие промпты

IT и Администрирование

Ревью и рефакторинг PowerShell-скрипта

Разбирает существующий скрипт PowerShell: что сломается на реальных данных, где ошибки глушатся вместо обработки и что переписать в первую очередь, а что оставить.

IT и Администрирование

Помощник по настройке WireGuard и OpenVPN

Пошагово разворачивает собственный VPN-сервер на WireGuard или OpenVPN: готовые команды и конфиги, firewall, клиентские профили и проверка утечек.

IT и Администрирование

Аудит безопасности малого бизнеса и домашней сети

Аудит безопасности малого бизнеса или домашней сети: чек-лист по областям, оценка рисков через сценарии атак и план усиления защиты на 30 дней.

IT и Администрирование

Консультант по выбору VPN-решения

Помогает выбрать VPN под задачу: сравнивает коммерческие сервисы и self-hosted решения с учётом страны, устройств, модели угроз и бюджета.