Привет, Хабр!
Сегодня я хочу поделиться с вами опытом создания собственных командлетов для 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».