Привет, Хабр!
Сегодня я хочу поделиться с вами опытом создания собственных командлетов для PowerShell. Я расскажу о том, как можно расширить стандартный функционал для решения специфичных задач. Если вы уже знакомы с базовыми возможностями PowerShell и чувствуете, что стандартный набор командлетов порой не охватывает все нюансы вашей инфраструктуры, эта статья для вас.
Зачем создавать кастомные командлеты?
Стандартные командлеты PowerShell весьма функциональны, но часто в проектах возникают ситуации, когда вам необходимо:
Инкапсулировать сложную бизнес‑логику в единый удобный интерфейс, избавляясь от дублирования кода.
Унифицировать процессы и стандартизировать выполнение задач в масштабируемой инфраструктуре.
Повысить безопасность и управляемость: собственные командлеты позволяют внедрить строгую валидацию параметров и отлаженную обработку ошибок, что критически важно в продакшене.
Я сам неоднократно сталкивался с необходимостью интегрировать специфичные операции в свои скрипты.
Варианты реализации кастомных командлетов
Можно идти двумя дорогами: либо писать функции на PowerShell (функции могут быть настолько крутыми, что их можно считать полноценными командлетами), либо использовать C# для создания настоящих .NET‑командлетов. Рассмотрим оба варианта.
Функции PowerShell
Наиболее быстрым и гибким способом расширить функциональность PowerShell является написание продвинутых функций. Используя атрибут [CmdletBinding()], можно создать функцию, которая будет вести себя как полноценный командлет. Пример функции для получения информации о системе с удалённого компьютера:
function Get-SystemInfo { [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [string]$ComputerName ) begin { Write-Verbose "Начинаю сбор информации о системе для $ComputerName." } process { try { # Получаем данные об операционной системе через CIM $sysInfo = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop Write-Output $sysInfo } catch { Write-Error "Ошибка при получении данных с компьютера $ComputerName: $_" } } end { Write-Verbose "Сбор информации завершён для $ComputerName." } }
Блоки begin, process и end позволяет функции корректно обрабатывать поток данных (pipeline). Атрибуты валидации проверяют корректность входных параметров, а обработка ошибок через try/catch делает код надежным.
Командлеты на C#
В случаях, когда требуется максимальная производительность или глубокая интеграция с .NET, имеет смысл разработать командлет на C#. Рассмотрим минимальный пример кастомного командлета:
using System; using System.Management.Automation; namespace MyCustomCmdlets { [Cmdlet(VerbsCommon.Get, "CustomInfo")] public class GetCustomInfoCommand : Cmdlet { [Parameter(Mandatory = true, Position = 0)] public string Target { get; set; } protected override void BeginProcessing() { WriteVerbose($"Инициализация обработки для {Target}."); } protected override void ProcessRecord() { try { // Здесь реализуйте логику получения данных о Target string info = $"Информация о {Target}: система функционирует корректно."; WriteObject(info); } catch (Exception ex) { WriteError(new ErrorRecord(ex, "GetCustomInfoFailed", ErrorCategory.NotSpecified, Target)); } } protected override void EndProcessing() { WriteVerbose("Обработка завершена."); } } }
После компиляции проекта в DLL можно импортировать его в PowerShell командой Import‑Module.
Структурирование модулей
Когда командлеты готовы, логичным шагом становится упаковка их в модуль. Хорошо структурированный модуль не только упрощает дальнейшую поддержку, но и повышает удобство использования. Рекомендуемая структура может выглядеть следующим образом:
MyCustomModule/ ├── MyCustomModule.psd1 # Манифест модуля с описанием версии, автора и экспортируемых компонентов ├── MyCustomModule.psm1 # Основной файл модуля, где происходит импорт функций и командлетов └── Public/ ├── Get-SystemInfo.ps1 # Файл с функцией Get-SystemInfo └── Get-CustomInfo.ps1 # Файл с командлетом Get-CustomInfo (или ссылка на сборку C#)
Пример файла манифеста (PSD1):
@{ ModuleVersion = '1.0.0' GUID = '12345678-90ab-cdef-1234-567890abcdef' Author = 'Ваше Имя' Description = 'Модуль для кастомных командлетов PowerShell для специализированного управления системами.' FunctionsToExport = @('Get-SystemInfo', 'Get-CustomInfo') CmdletsToExport = @() ScriptsToProcess = @() RequiredModules = @() }
Манифест позволяет задокументировать модуль, установить его версию и указать зависимости.
Тестирование с Pester
Для повышения надёжности кода важно интегрировать модульное тестирование. В PowerShell для этих целей отлично подходит Pester. Вот пример теста для функции Get‑SystemInfo:
Describe "Get-SystemInfo" { It "Должна вернуть объект при корректном имени компьютера" { $result = Get-SystemInfo -ComputerName "localhost" $result | Should -Not -BeNullOrEmpty } It "Должна генерировать ошибку при недоступном компьютере" { { Get-SystemInfo -ComputerName "не_существует" } | Should -Throw } }
Автоматизированное тестирование помогает обнаружить ошибки на ранних стадиях разработки.
Безопасность и валидация данных
Надёжность и безопасность — неотъемлемые требования к любому коду. Для предотвращения ошибок и обеспечения безопасности необходимо:
Применять атрибуты валидации:
[ValidateNotNullOrEmpty()],[ValidateScript()],[ValidateSet()]и т. д. гарантируют, что передаваемые параметры соответствуют ожиданиям.Оборачивать критичные участки кода в try/catch: Это позволяет корректно обрабатывать исключения и информировать пользователя об ошибках.
Внимательно работать с параметрами, особенно если они влияют на выполнение системных команд или обращение к внешним ресурсам.
Пример функции с усиленной валидацией:
function Invoke-SafeAction { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateScript({ Test-Path $_ })] [string]$FilePath ) try { Write-Verbose "Чтение файла: $FilePath." $content = Get-Content -Path $FilePath -ErrorAction Stop Write-Output $content } catch { Write-Error "Не удалось прочитать файл $FilePath: $_" } }
Интеграция с CI/CD
Я часто использую GitHub Actions или Azure DevOps для настройки конвейеров CI/CD. Пример файла конфигурации для GitHub Actions:
name: CI for PowerShell Module on: [push, pull_request] jobs: build: runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup PowerShell uses: actions/setup-powershell@v2 - name: Install Pester run: Install-Module -Name Pester -Force -Scope CurrentUser - name: Run tests run: Invoke-Pester -Script .\Tests\ -Output Detailed
Документирование кода
С комментариями PowerShell Help можно облегчить другим пользователям быстро разобраться в назначении функций. Пример документации для функции:
<# .SYNOPSIS Получает информацию о системе указанного компьютера. .DESCRIPTION Функция Get-SystemInfo подключается к удалённому компьютеру с использованием WMI/CIM и возвращает объект с информацией об операционной системе. .PARAMETER ComputerName Имя компьютера, с которого необходимо получить информацию. .EXAMPLE PS> Get-SystemInfo -ComputerName "localhost" Возвращает объект с данными операционной системы. .NOTES Проверьте, что у вас есть необходимые разрешения для выполнения WMI-запросов. #> function Get-SystemInfo { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ComputerName ) try { Write-Verbose "Подключаюсь к $ComputerName..." $sysInfo = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop Write-Output $sysInfo } catch { Write-Error "Ошибка при получении информации с компьютера $ComputerName: $_" } }
Пример командлета на C#
Создадим командлет на C#, который собирает системные метрики (CPU, память, диск) в несколько выборок с заданным интервалом:
using System; using System.Collections.Generic; using System.Management.Automation; using System.Threading; namespace CustomCmdlets { // Определяем объект для хранения метрик системы public class SystemMetrics { public DateTime Timestamp { get; set; } public double CpuUsage { get; set; } public double MemoryUsage { get; set; } public double DiskUsage { get; set; } public override string ToString() { return $"[{Timestamp}] CPU: {CpuUsage}% | Memory: {MemoryUsage}% | Disk: {DiskUsage}%"; } } [Cmdlet(VerbsCommon.Get, "CoolSystemMetrics", DefaultParameterSetName = "Default")] [OutputType(typeof(SystemMetrics))] public class GetCoolSystemMetricsCommand : Cmdlet { // Позволяет задать, какие метрики собирать (CPU, Memory, Disk) [Parameter(Mandatory = false, Position = 0)] public string[] Metrics { get; set; } // Интервал между выборками (в миллисекундах) [Parameter(Mandatory = false, Position = 1)] public int SampleInterval { get; set; } = 1000; // Количество выборок [Parameter(Mandatory = false, Position = 2)] public int SampleCount { get; set; } = 5; // Параметр, позволяющий включить расширенное логирование (verbose debug info) [Parameter(Mandatory = false)] public SwitchParameter EnableDebug { get; set; } // Метод инициализации, где задаем значения по умолчанию и проводим валидацию параметров protected override void BeginProcessing() { WriteVerbose("Инициализация сбора метрик системы..."); // Если не указаны метрики, собираем все if (Metrics == null || Metrics.Length == 0) { Metrics = new string[] { "CPU", "Memory", "Disk" }; WriteVerbose("Не указаны метрики – собираем все: CPU, Memory, Disk."); } else { WriteVerbose($"Собираем указанные метрики: {string.Join(", ", Metrics)}."); } } // Основной метод обработки: собираем заданное количество выборок protected override void ProcessRecord() { var results = new List<SystemMetrics>(); for (int i = 0; i < SampleCount; i++) { if (EnableDebug) { WriteVerbose($"Начало выборки {i + 1} из {SampleCount}."); } var metrics = new SystemMetrics { Timestamp = DateTime.Now }; try { if (Array.Exists(Metrics, m => m.Equals("CPU", StringComparison.OrdinalIgnoreCase))) { metrics.CpuUsage = GetCpuUsage(); } if (Array.Exists(Metrics, m => m.Equals("Memory", StringComparison.OrdinalIgnoreCase))) { metrics.MemoryUsage = GetMemoryUsage(); } if (Array.Exists(Metrics, m => m.Equals("Disk", StringComparison.OrdinalIgnoreCase))) { metrics.DiskUsage = GetDiskUsage(); } results.Add(metrics); WriteVerbose($"Выборка {i + 1} успешно выполнена."); } catch (Exception ex) { WriteError(new ErrorRecord(ex, "MetricCollectionFailed", ErrorCategory.NotSpecified, null)); } // Задержка между выборками Thread.Sleep(SampleInterval); } // Выводим результаты по одной записи (поддержка pipeline) foreach (var metric in results) { WriteObject(metric); } } // Метод для получения CPU usage (для демонстрации используется рандом) private double GetCpuUsage() { // В реальном продакшене здесь следует использовать PerformanceCounter или WMI-запросы Random rnd = new Random(); double value = Math.Round(rnd.NextDouble() * 100, 2); WriteVerbose($"Получено CPU: {value}%."); return value; } // Метод для получения Memory usage private double GetMemoryUsage() { Random rnd = new Random(); double value = Math.Round(rnd.NextDouble() * 100, 2); WriteVerbose($"Получена Memory: {value}%."); return value; } // Метод для получения Disk usage private double GetDiskUsage() { Random rnd = new Random(); double value = Math.Round(rnd.NextDouble() * 100, 2); WriteVerbose($"Получен Disk: {value}%."); return value; } } }
В методах BeginProcessing и ProcessRecord происходит инициализация и основной сбор данных, с использованием случайных значений для демонстрации (в реальных сценариях заменяется на вызовы через PerformanceCounter или WMI). Командлет поддерживает CI/CD и вывод результатов через WriteObject, помогая интегрировать процесс в pipeline с подробным логированием.
Если у вас возникнут вопросы или вы захотите обсудить детали реализации — пишите в комментариях.
Изучить Windows на продвинутом уровне и расширить карьерные возможности в IT можно на онлайн‑курсе «Администратор Windows».
