Привет, Хабр!

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

Комментарии (0)