PowerShell, дамп моего опыта

в 5:20, , рубрики: powershell, runspace, wpf, системное администрирование

Введение

Эта статья адресована тем, кто уже познакомился с основами PowerShell, запускал какие-то скрипты со stackexchange и, вероятно, имеет свой текстовый файл с теми или иными сниппетами облегчающими повседневную работу. Целью её написания есть уменьшение энтропии, увеличение читаемости и поддерживаемости кода PowerShell используемого в вашей компании и, как следствие, повышение продуктивности администратора работающего с ним.

kdpv

На своём предыдущем месте работы я в силу специфики задач и несовершенства мира, сильно прокачал навык работы с PowerShell. После смены работы нагрузки такого рода сильно снизились и всё что было вот-вот еще на кончиках пальцев стало всё глубже тонуть под опытом решения новых задач на новых технологиях. От того эта статья претендует быть лишь тем, чем себя объявляет, раскрывая список тем, который на мой взгляд был бы полезен мне самому лет 7 назад, тогда, когда моё знакомство с этим инструментом только начиналось.

Если вы не понимаете почему PowerShell — объектно-ориентированный шелл, какие от этого появляются бонусы и зачем это вообще надо, я, не смотря на возгласы хейтеров посоветую вам хорошую книгу быстро вводящую в суть этой среды — Попов Андрей Владимирович, Введение в Windows PowerShell. Да, она про старую версию PS, да, язык обрел некоторые расширения и улучшения, но эта книга хороша тем, что описывая ранний этап развития этой среды невольно делает акцент лишь фундаментальных вещах. Синтаксический сахар, которым обросла среда я думаю вы быстро и без того воспримите поняв как работает сама концепция. Прочтение этой книги займет у вас буквально пару вечеров, возвращайтесь после прочтения.

popov

Книга также доступна на сайте автора, правда я не уверен в том насколько лицензионно чисто такое использование: https://andpop.ru/courses/winscript/books/posh_popov.pdf

Стайл гайды

Оформление скриптов согласно стайлгайдам хорошая практика во всех случаях её применения, вряд ли тут может быть два мнения. Некоторые экосистемы позаботились об этом на уровне родного тулинга, из очевидного в голову приходит pep8 в сообществе Python и go fmt в Golang. Это бесценные инструменты экономящие время, к сожалению отсутствующие в стандартной поставке PowerShell, а от того переносящие проблему на нашу с вами голову. Единственным на текущий момент способом решения проблемы единого форматирования кода является вырабатывание рефлексов путем многократного написания кода удовлетворяющего стайлгайдам (на самом деле нет).

Стайлгайды в силу отсутствия официально утвержденных и подробно описанных компанией Микрософт были рождены в сообществе во времена PowerShell v3 и с тех пор развиваются в открытом виде на гитхабе: PowerShellPracticeAndStyle. Это заслуживающий внимания репозиторий для любого, кто хоть раз пользовался кнопкой "Save" в PowerShell ise.

Если попытаться сделать выжимку, то вероятно сведется она к следующим пунктам:

  • В PowerShell используется PascalCase для именования переменных, командлетов, имен модулей и практически всего, за исключением операторов;
  • Операторы языка, такие как if, switch, break, process, -match пишутся сугубо строчными буквами;
  • Фигурные скобки расставляются единственно верным способом, иначе еще называемым стилем Кернигана и Ричи ведущим свою историю из книги The C Programming Language;
  • Не используйте алиасы нигде кроме интерактивного сеанса консоли, не пишите в файл скрипта никаких ps | ? processname -eq firefox | %{$ws=0}{$ws+=$_.workingset}{$ws/1MB};
  • Указывайте явно имена параметров, поведение командлетов и их сигнатура может поменяться, плюс к этому, человеку незнакомому с конкретным командлетом это добавит контекста;
  • Оформляйте параметры вызова скриптов, а не пишите внутри скрипта функцию и последней строчкой вызов этой функции с необходимостью изменять значения глобальных переменных вместо указания параметров;
  • Указывайте [CmdletBinding()] — это подарит вашему командлету -Verbose и -Debug флаги и много других полезных фичей. Несмотря на твердость позиции некоторых пуристов в сообществе, я не сторонник указывания этого атрибута в простых инлайн-функциях и фильтрах состоящих из буквальных нескольких строк;
  • Пишите comment-based справку: одно предложение, ссылку на тикет, пример вызова;
  • Указывайте необходимую версию PowerShell в секции #requires;
  • Используйте Set-StrictMode -Version Latest, это поможет вам избежать проблем описанных ниже;
  • Обрабатывайте ошибки;
  • Не спешите переписывать всё на PowerShell. PowerShell — это в первую очередь шелл и вызывать бинари — его прямая задача. Нет ничего плохого в том, чтобы заиспользовать robocopy в скрипте, она, конечно, не rsync, но тоже очень хороша.

Comment Based Help

Ниже пример того как оформить справку скрипта. Скрипт кадрирует изображение приводя его к квадрату и выполняет ресайз, думаю у вас есть задача делать аватарки для пользователей (нехватает разве что поворота по данным exif). В разделе .EXAMPLE есть пример использования, попробуйте. В силу того, что PowerShell выполняется средой CLR, той же что и прочие dotnet языки, у него есть возможность использовать всю мощь библиотек dotnet:

<#
    .SYNOPSIS
    Resize-Image resizes an image file

    .DESCRIPTION
    This function uses the native .NET API to crop a square and resize an image file

    .PARAMETER InputFile
    Specify the path to the image

    .PARAMETER OutputFile
    Specify the path to the resized image

    .PARAMETER SquareHeight
    Define the size of the side of the square of the cropped image.

    .PARAMETER Quality
    Jpeg compression ratio

    .EXAMPLE
    Resize the image to a specific size:
    .Resize-Image.ps1 -InputFile "C:userpic.jpg" -OutputFile "C:userpic-400.jpg"-SquareHeight 400
#>

# requires -version 3

[CmdletBinding()]
Param(
    [Parameter( Mandatory )]
    [string]$InputFile,
    [Parameter( Mandatory )]
    [string]$OutputFile,
    [Parameter( Mandatory )]
    [int32]$SquareHeight,
    [ValidateRange( 1, 100 )]
    [int]$Quality = 85
)

# Add System.Drawing assembly
Add-Type -AssemblyName System.Drawing

# Open image file
$Image = [System.Drawing.Image]::FromFile( $InputFile )

# Calculate the offset for centering the image
$SquareSide = if ( $Image.Height -lt $Image.Width ) {
    $Image.Height
    $Offset = 0
} else {
    $Image.Width
    $Offset = ( $Image.Height - $Image.Width ) / 2
}
# Create empty square canvas for the new image
$SquareImage = New-Object System.Drawing.Bitmap( $SquareSide, $SquareSide )
$SquareImage.SetResolution( $Image.HorizontalResolution, $Image.VerticalResolution )

# Draw new image on the empty canvas
$Canvas = [System.Drawing.Graphics]::FromImage( $SquareImage )
$Canvas.DrawImage( $Image, 0, -$Offset )

# Resize image
$ResultImage = New-Object System.Drawing.Bitmap( $SquareHeight, $SquareHeight )
$Canvas = [System.Drawing.Graphics]::FromImage( $ResultImage )
$Canvas.DrawImage( $SquareImage, 0, 0, $SquareHeight, $SquareHeight )

$ImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
    Where-Object MimeType -eq 'image/jpeg'

# https://msdn.microsoft.com/ru-ru/library/hwkztaft(v=vs.110).aspx
$EncoderQuality     = [System.Drawing.Imaging.Encoder]::Quality
$EncoderParameters  = New-Object System.Drawing.Imaging.EncoderParameters( 1 )
$EncoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter( $EncoderQuality, $Quality )

# Save the image
$ResultImage.Save( $OutputFile, $ImageCodecInfo, $EncoderParameters )

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

help

Мало того, при вызове скрипта будут работать подсказки по параметрам, будь то консоль PowerShell, будь то редактор кода:

inline help

Еще раз обращу внимание на то, что пренебрегать ей не стоит. Если не знаете что туда написать, напишите что-нибудь, сходите до кулера и по возвращении у вас точно будет понимание того, что нужно в написанном изменить. Это работает. Не стоит фанатично заполнять все ключевые слова, PowerShell разработан быть самодокументируемым и если вы дали осмысленные и полные имена параметрам, короткого предложения в разделе .SYNOPIS и одного примера вполне хватит.

Strict mode

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

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

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

Включается этот режим командлетом Set-StrictMode -Version Latest, хотя есть другие варианты "строгости", мой выбор — использовать последний.

В примере ниже строгий режим отлавливает обращение к несуществующему свойству. Так как внутри папки находится лишь один элемент, тип переменной $Input в результате выполнения будет FileInfo, а не ожидаемый массив соответствующих элементов:

strict

Для избежания такой проблемы, следует результат выполнения командлета явно привести к массиву:

$Items = @( Get-ChildItem C:Userssnd3rNextcloud )

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

Обработка ошибок

ErrorActionPreference

Просматривая чужие скрипты, например на гитхабе, я часто вижу либо полное игнорирование механизма обработки ошибок, либо явное включение режима тихого продолжения работы в случае возникновения ошибки. Вопрос обработки ошибок, безусловно, не самый простой в программировании вообще и в скриптах в частности, но игнорирования он определенно не заслуживает. По-умолчанию, PowerShell в случае возникновения ошибки выводит её и продолжает работу (я немного упростил концепцию, но ниже будет ссылка на гит-книгу по этой теме). Это удобно, в случае если вам срочно нужно распространить обновление широкоиспользуемой в домене программы на все машины, не дожидаясь пока она разольется на все точки деплоя sccm или распространится иным используемым у вас способом. Неприятно прерывать и перезапускать процесс в случае если одна из машин выключена, это правда.

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

Для изменения поведения командлетов в случае возникновения ошибки существует глобальная переменная $ErrorActionPreference, со следующим списком возможных значений: Stop, Inquire, Continue, Suspend, SilentlyContinue.

Я рекомендую всегда использовать значение Stop, когда количество скриптов или их сложность перестают сохраняться на стеке в голове, лучше быть уверенным, что в любюй непредвиденной ситуации скрипт остановит свою работу, а не наломает дров "по-тихому", закончив выполнение "успешно".

Помимо остановки скрипта в случае, если что-то пошло не так есть еще одно обязательное условие его применения — обработка исключительных ситуаций. Для этого есть конструкция try/catch, но работает только в том случае, если ошибка вызывает остановку выполнения. Не обязательно остановка должна быть включена на уровне всего скрипта, ErrorAction можно устанавливать и на уровне командлета параметром:

Get-ChildItem 'C:System Volume Information' -ErrorAction 'Stop'

Собственно такая возможность и определяет две логичные стратегии: разрешать все ошибки "по-умолчанию" и выставлять ErrorAction только для критичных мест, где их и обрабатывать; либо включать на уровне всего скрипта путем задания значения глобальной переменной и задавать -ErrorAction 'Continue' на некритичных операциях. Я всегда выбираю второй вариант, не спешу вам его навязывать, рекомендую лишь один раз разобраться в этом вопросе и использовать этот полезный инструмент.

try/catch

В обработчике ошибок можно делать матчинг по типу исключения и оперировать потоком исполнения или, например, добавлять чуть больше информации. Не смотря на то, что используя операторы try/catch/throw/trap можно выстроить весь поток выполнения скрипта, следует категорически этого избегать, так как такой способ оперирования выполнением мало того, что считается крайним антипаттерном, из разряда "goto-лапши", так еще и сильно просаживает производительность.

#requires -version 3
$ErrorActionPreference = 'Stop'

# создание объекта логгера, код которого смотрите ниже,
# инкапсулирующего знание о пути к логу и формату записи
$Logger = Get-Logger "$PSScriptRootLog.txt"

# глобальная ловушка ошибок
trap {
    $Logger.AddErrorRecord( $_ )
    exit 1
}

# счётчик попыток подключения
$count = 1;
while ( $true ) {
    try {
        # попытка подключения
        $StorageServers = @( Get-ADGroupMember -Identity StorageServers | Select-Object -Expand Name )
    } catch [System.Management.Automation.CommandNotFoundException] {
        # выбрасываемое наружу исключение в силу того, что нет смысла продолжать выполнение без установки модуля
        throw "Командлет Get-ADGroupMember недоступен, требуется добавить фичу Active Directory module for PowerShell; $( $_.Exception.Message )"
    } catch [System.TimeoutException] {
        # переход к следующей итерации цикла в случае если количество попыток не превышено
        if ( $count -le 3 ) { $count++; Start-Sleep -S 10; continue }
        # остановка выполнения и выбрасывание исключения наружу в силу невозможности получения необходимых данных
        throw "Подключение к серверу небыло установленно из-за ошибки таймаута соединения, было произведено $count попыток; $( $_.Exception.Message )"
    }
    # выход из цикла в случае отсутствия исключительных ситуаций
    break
}

Стоит отметить оператор trap — это глобальная ловушка ошибок. Она ловит все что не было обработано на более низких уровнях, либо выброшено наружу из обработчика исключения в силу невозможности самостоятельного исправления ситуации.

Помимо описанного выше объектно-ориентированного подхода исключений, PowerShell предоставляет и более привычные, совместимые с другими "классическими" шеллами концепции, например потоков ошибок, кодов возврата и переменных накапливающих ошибки. Всё это безусловно удобно, иногда безальтернативно, но выходит за рамки этого, в целом обзорного, топика. К счастью на эту тему есть хорошая открытая книга на github.

Код логгера, который я использую когда нет уверенности что в системе будет PowerShell 5 (где можно описать более удобно класс логгера), попробуйте его, он может быть вам полезен в силу своей простоты и краткости, дополнительные методы вы, уверен, добавите без труда.:

# Фабрика логгеров "для бедных", совместимая с PowerShell v3
function Get-Logger {
    [CmdletBinding()]
    param (
        [Parameter( Mandatory = $true )]
        [string] $LogPath,
        [string] $TimeFormat = 'yyyy-MM-dd HH:mm:ss'
    )

    $LogsDir = [System.IO.Path]::GetDirectoryName( $LogPath )
    New-Item $LogsDir -ItemType Directory -Force | Out-Null
    New-Item $LogPath -ItemType File -Force      | Out-Null

    $Logger = [PSCustomObject]@{
        LogPath    = $LogPath
        TimeFormat = $TimeFormat
    }

    Add-Member -InputObject $Logger -MemberType ScriptMethod AddErrorRecord -Value {
        param(
            [Parameter( Mandatory = $true )]
            [string]$String
        )
        "$( Get-Date -Format 'yyyy-MM-dd HH:mm:ss' ) [Error] $String" | Out-File $this.LogPath -Append
    }
    return $Logger
}

Повторю идею — не игнорируйте обработку ошибок. Это сэкономит ваше время и нервы в длительной перспективе.
Не думайте, что выполнение скрипта несмотря ни на что — хорошо. Хорошо — это вовремя упасть не наломав дров.

Инструменты

Начать улучшение инструментов работы с PowerShell стоит безусловно с эмулятора консоли. Я часто слышал от сторонников альтернативных ос, что консоль в windows плоха и что это вообще не консоль, а дос и проч. Мало кто адекватно мог сформулировать свои претензии на этот счет, но если кому-то удавалось, то на деле оказывалось что все проблемы можно решить. Подробнее о терминалах и новой консоли в windows на хабре уже было, там всё более чем ок.

Первым делом стоит установить Conemu или его сборку Cmder, что не особо важно, так как на мой взгляд по настройкам в любом случае стоит пробежаться. Я обычно выбираю cmder в минимальной конфигурации, без гита и прочих бинарей, которые ставлю сам, хотя несколько лет тюнил свой конфиг для чистой Conemu. Это действительно лучший эмулятор терминала для windows, позволяющий разделять экран (для любителей tmux/screen), создавать вкладки и включить quake-style режим консоли:

Conemu

cmder

Следущим шагом рекомендую поставить модули: oh-my-posh, posh-git и PSReadLine. Первые два сделают промт приятнее, добавив в него информацию о текущей сессии, статусе последней выполненной команды, индикатор привелегий и статус гит-репозитория в текущем расположении. PSReadLine сильно прокачивает промт, добавляя например поиск по истории введенных команд (CRTL + R) и удобные подсказки для командлетов по CRTL + Space:

readline

И да, теперь консоль можно очищать комбинацией CTRL + L, и забыть про cls.

Visual Studio Code

Редактор. Всё самое плохое, что я могу сказать про PowerShell, относится сугубо к PowerShell ISE, те кто видели первую версию с тремя панелями врядли забудут этот опыт. Отличающаяся кодировка терминала, отсутствие базовых возможностей редактора, вроде автоматического отступа, автозакрывающихся скобок, форматирования кода и целый набор порождаемых им антипаттернов про которые я вам не расскажу (на всякий случай) — это все про ISE.

Не используйте его, используйте Visual Studio Code с расширением PowerShell — тут есть всё, чего бы вы не захотели (в разумных пределах, конечно). И не забывайте, что в PoweShell до шестой версии (PowerShell Core 6.0) кодировка для скриптов — UTF8 BOM, иначе русский язык сломается.

vscode

Помимо подсветки синтаксиса, подсказки методов и возможности дебага скриптов, плагин устанавливает линтер, который так же поможет вам следовать закрепившимся в сообществе практикам, например в один клик (по лампочке) развернет сокращения. На деле это обычный модуль, который можно поставить и независимо, например добавить его в ваш пайплайн подписи скриптов: PSScriptAnalyzer

PSScriptAnalyzer

Задать параметры форматирования кода можно в настройках расширения, по всем настройкам (и редактора и расширений) есть поиск: File - Preferences - Settings:

OTBS

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

Стоит запомнить, что любое действие в VS Code можно выполнить из центра управления, вызываемого комбинацией CTRL + Shift + P. Отформатировать вставленный из чата кусок кода, отсортировать строки по алфавиту, поменять индент с пробелов на табы и проч, всё это в центре управления.

Например включить полный экран и расположение редактора по центру:

layout

Source Control; Git, SVN

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

merge

Вот эти надписи между 303 и 304 строкой кликабельны, стоит нажать на все такие что появляются в документе в случае конфликта, сделать коммит фиксирующий изменения и отправить изменения на сервер. У — Удобство.

О том как работать с системами контроля версий доступно и с картинками написано в доке vscode, пробегитесь глазами до конца там кратко и хорошо.

Snippets

Сниппеты — своего рода макросы/шаблоны позволяющие ускорить написание кода. Однозначно must see.

Быстрое создание объекта:

customobject

Рыба для comment-based help:

help

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

splatting

Просмотр всех доступных сниппетов доступен по Ctrl + Alt + J:

snippets

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

Производительность

Тема производительности не такая простая как может показаться на первый взгляд. С одной стороны, преждевременные оптимизации могут сильно снизить читаемость и поддерживаемость кода, сэкономив 300мс времени выполнения скрипта, обычное время работы которого может быть десяток минут, применение их в таком случае безусловно деструктивно. С другой — есть несколько довольно простых приемов, повышающих как читаемость кода так и скорость его работы, использовать которые вполне уместно на постоянной основе. Ниже я расскажу о некоторых из них, в случае если же перформанс для вас всё, а читаемость уходит на второй план в силу жестких временных ограничений простоя сервиса на время обслуживания, рекомендую обратиться к профильной литературе.

Pipeline и foreach

Самый простой и всегда рабочий способ поднять производительность — уйти от использования пайпов. В силу типобезопасности и удобства работы ради, PowerShell пропуская элементы через пайп оборачивает каждый из них в объект. В dotnet языках такое поведение называется боксинг. Боксинг хорош, он гарантирует безопасность, но у него есть своя цена, которую порой нет смысла платить.

Первым шагом поднять производительность и на мой взгляд повысить читаемость можно убрав все применения командлета Foreach-Object и заменив его на оператор foreach. Вас может смутить то, что на самом деле это две разных сущности, ведь foreach является алиасом для Foreach-Object — на практике главное отличие в том, что foreach не принимает значения из пайплайна, при этом работает по опыту до трех раз быстрее.

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

Get-Content D:tempSomeHeavy.log | Select-String '328117'

В примере выше первый этап поставленной задачи — выбор необходимых записей пройденый самым очевидным путем, хорош своей когнитивной легкостью и выразительностью. При этом он содержит место сильно снижающее производительность — пайплайн, точнее будет сказать что виноват не сам пайплайн, а поведение командлета Get-Content. Для передачи данных по конвейеру он читает файл построчно и оборачивает каждую строку лога из базового типа string в объект, сильно увеличивая его размер, уменьшая локальность данных в кеше и делая не особо нужную нам работу. Избежать этого просто — нужно указать ему параметром о том, что эти данные нужно прочитать полностью за один раз:

When reading from and writing to binary files, use the AsByteStream parameter and a value of 0 for the ReadCount parameter. A ReadCount value of 0 reads the entire file in a single read operation. The default ReadCount value, 1, reads one byte in each read operation and converts each byte into a separate object, which causes errors when you use the Set-Content cmdlet to write the bytes to a file unless you use AsByteStream

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content

Get-Content D:tempSomeHeavy.log -ReadCount 0 | Select-String '328117'

На моём файле лога размером чуть более одного гигабайта преимущество второго подхода почти в три раза:

readcount

Я советую вам не верить мне на слово, а самим проверить так ли это на файле похожего размера. В целом же Select-String даёт хороший результат и если итоговое время вас устраивает — пора остановиться с оптимизацией этой части скрипта. В случае если итоговое время выполнения скрипта до сих пор сильно зависит от этапа получения данных, можно еще немного снизить время выборки данных заменив командлет Select-String. Это очень мощный и удобный инструмент, но что бы таким быть Select-String добавляет некоторое количество метаданных в свой вывод, опять же производя не бесплатную по времени работу, мы можем отказаться от лишних метаданных и сопутствующей работы заменив командлет оператором языка:

foreach ( $line in ( Get-Content D:tempSomeHeavy.log -ReadCount 0 )) {
    if ( $line -match '328117' ) {
        $line
    }
}

На моих тестах время выполнения уменьшилось до 30 секунд, то-есть я выйграл 30%, при этом уже на моём примере оно того не особо стоило, так как кода стало больше и хоть они банальный, но нежелание разобраться в нем у стороннего наблюдателя, по-моему опыту, увеличилось вдвое (как население в тех замках ;-). Если вы оперируете десятками гигабайт логов, то это несомненно ваш путь. Что еще хотелось бы отметить в приведенном выше коде, так это оператор -match; суть его — поиск совпадения по регулярному выражению. В конкретном случае в силу простоты этого выражения поиск сводится к вычислительно простому поиску по подстроке, но так бывает не всегда — от сложности вашего регулярного выражения время выполнения может увеличиваться с любой вообразимой вами прогрессией — регулярые выражения все-таки Тьюринг полный язык, будьте с ними осторожны.

Следующая задача — как-то обработать этот лог, напишем решение "в лоб" с добавлением в каждую отобранную строчку текущей даты и запись в файл через пайп:

foreach ( $line in ( Get-Content D:tempSomeHeavy.log -ReadCount 0 )) {
    if ( $line -match '328117' ) {
        "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" | Out-File D:tempResult.log -Append
    }
}

Результаты выполнения замеренные командлетом Measure-Command:

Hours             : 2
Minutes           : 20
Seconds           : 9
Milliseconds      : 101

Попробуем улучшить результат. Думаю многим очевидно, что запись каждой отдельной строки в файл не самая оптимальная операция, куда лучше сделать накопительный буфер который периодически сбрасывать на диск, в идеале сбросить его один раз. Так же стоит отметить, что строки в PowerShell неизменяемы и любая операция со строкой порождает новую область в памяти, куда записывается новая строка, а старая остается ждать сборщик мусора — это дорого и по скорости и по памяти. Для решения этой проблемы в дотнете есть специализированный класс, который позволяет изменять строки, при этом инкапсулируя логику более аккуратного выделения памяти и имя ему — StringBuilder. При создании класса выделяется буфер в оперативной памяти в который добавляются новые строки без повторного выделения памяти, в том случае если размера буфера не хватает для добавления новой строки, то создается новый вдвое большего размера и работа продолжается с ним. Помимо того что такая стратегия сильно уменьшает количество выделений памяти, её еще можно подтюнить если знать примерный объем памяти который будут занимать строки и задать его в конструкторе при создании объекта.

$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $line in ( Get-Content D:tempSomeHeavy.log -ReadCount 0 )) {
    if ( $line -match '328117' ) {
        $null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" )
    }
}
Out-File -InputObject $StringBuilder.ToString() -FilePath D:tempResult.log -Append -Encoding UTF8

Время выполнения этого кода всего 5 минут, вместо прошлых двух с половиной часов:

Hours             : 0
Minutes           : 5
Seconds           : 37
Milliseconds      : 150

Стоит отметить конструкцию Out-File -InputObject, суть её в том, чтобы в очередной раз избавиться от пайплайна. Такой способ быстрее пайпа и работает со всеми командлетами — любое значение которое в сигнатуре командлета является значением принимаемым из пайпа может быть задано параметром. Наиболее простой способ узнать какой именно параметр принимает себе проходящие через пайп значения — выполнить Get-Help на командлете с параметром -Full, среди списка параметров один должен содержать в себе Accept pipeline input? true (ByValue):

-InputObject <psobject>

    Required?                    false
    Position?                    Named
    Accept pipeline input?       true (ByValue)
    Parameter set name           (All)
    Aliases                      None
    Dynamic?                     false

В обоих случаях PowerShell ограничивал себя тремя гигабайтами памяти:

taskmgr

Во время работы StringBuilder на каждой итерации выводит в консоль информацию о выделенной на текущий момент памяти и размере добавленных элементов из которой можно взять примерные значения для установки в конструкторе:

stringbuilder alloc

Всем хорош предыдущий подход, кроме разве что того, что 3Гб это 3Гб. Попробуем уменьшить потребление памяти и заиспользуем другой dotnet-класс написаный для решения таких проблем — StreamReader.

$StringBuilder = New-Object System.Text.StringBuilder
$StreamReader  = New-Object System.IO.StreamReader 'D:tempSomeHeavy.log'
while ( $line = $StreamReader.ReadLine()) {
    if ( $line -match '328117' ) {
        $null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" )
    }
}
$StreamReader.Dispose()
Out-File -InputObject $StringBuilder.ToString() -FilePath C:tempResult.log -Append -Encoding UTF8

Hours             : 0
Minutes           : 5
Seconds           : 33
Milliseconds      : 657

Время выполнения осталось практически тем же, но потребление памяти и его характер изменились. Если в предыдущем примере при чтении файла в памяти занималось место сразу под весь файл, у меня это больше гигабайта, а работа скрипта характеризовалась утилизацией трех гигабайт, то при использовании стримридера, занятая процессором память медленно увеличивалась пока не дошла до 2Гб. Конечный объем занятой памяти я заскринить не успел, но есть скрин того что происходило ближе к концу работы:

streamreader

Поведение программы по расходу памяти вполне очевидно — вход у неё грубо говоря "поток", а выход — наш StringBuilder — "бассейн" который и разливается до конца работы программы. Зададим размер буфера, что бы убрать лишние аллокации (я выбрал 100МБ) и начнем сбрасывать содержимое в файл при приближении к концу буфера. Последнюю проверку я реализовал в лоб — сравниваю прошел ли буфер отметку в 90% от общего размера (может быть эту операцию имеет смысл вынести из цикла, проверьте сами разницу во времени):

$BufferSize     = 104857600
$StringBuilder  = New-Object System.Text.StringBuilder $BufferSize
$StreamReader   = New-Object System.IO.StreamReader 'C:tempSomeHeavy.log'
while ( $line = $StreamReader.ReadLine()) {
    if ( $line -match '1443' ) {
        # проверка приближения к концу буфера
        if ( $StringBuilder.Length -gt ( $BufferSize - ( $BufferSize * 0.1 ))) {
            Out-File -InputObject $StringBuilder.ToString() -FilePath C:tempResult.log -Append -Encoding UTF8
            $StringBuilder.Clear()
        }
        $null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" )
    }
}
Out-File -InputObject $StringBuilder.ToString() -FilePath C:tempResult.log -Append -Encoding UTF8
$StreamReader.Dispose()

Hours             : 0
Minutes           : 5
Seconds           : 53
Milliseconds      : 417

Максимальное потребление памяти составило 1Гб при почти той же скорости выполнения:

streamreader with dump

Безусловно результаты по абсолютным числам утилизированной памяти будут отличаться от одной машины к другой, всё зависит от того сколько памяти вообще доступно на машине и соответственно насколько агрессивным будет её освобождение. Если память для вас критична, а несколько процентов производительности не так, то можно еще уменьшить её потребление заиспользовав StreamWriter, он как стримридер, только стримрайтер ;-) Оставлю вам его для самостоятельного изучения, а то мне уже кажется я тут засиделся, ибо конца этому нет.

По-моему идея обозначена довольно явно — любую проблему люди уже решали, всегда следует начать искать решение в стандартной библиотеке. Главное эту проблему локализовать, хотя вероятно еще важнее — не выдумывать. Если Select-String и Out-File вас устраивают по времени, машина не встает и не падает с OutOfMemoryException, то используйте их — простота и выразительность важнее.

Нативные бинарники

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

Пример задачи, в прошлом решенной мной с помощью StringBuilder и вызова консольной команды dir — получение относительных путей всех файлов в каталоге и подкаталогах (большого количества файлов). С использованием нативной команды время выполнения меньше в пять раз:

$CurrentPath = ( Get-Location ).Path + ''
$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $Line in ( &cmd /c dir /b /s /a-d )) {
    $null = $StringBuilder.AppendLine( $Line.Replace( $CurrentPath, '.' ))
}
$StringBuilder.ToString()

Hours             : 0
Minutes           : 0
Seconds           : 3
Milliseconds      : 9

$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $Line in ( Get-ChildItem -File -Recurse | Resolve-Path -Relative )) {
    $null = $StringBuilder.AppendLine( $Line )
}
$StringBuilder.ToString()

Hours             : 0
Minutes           : 0
Seconds           : 16
Milliseconds      : 337

Присваивание результата работы в $null — наиболее дешевый способ подавления вывода. Наиболее дорогой, вы думаю догадались — отправка пайпом в Out-Null; мало того, такой способ подавления (присваивание результата в $null) еще и уменьшает время выполнения, хоть и незначительно.

# быстро:
$null = $StringBuilder.AppendLine( $Line )

# медленно:
$StringBuilder.AppendLine( $Line ) | Out-Null

Однажды у меня стояла задача синхронизировать каталоги с большим количеством файлов, при этом это была лишь часть работы довольно большого скрипта, этап подготовки. Реализация синхронизации каталогов с помощью Compare-Object, хоть и выглядела достойно и компактно, затрачивала на свою работу времени больше, чем весь планируемый мною временной бюджет скрипта. Выходом из этой ситуации стало использование широкоизвестной в узких кругах утилиты robocopy.exe, компромисом же стало написание враппера (точнее класса для PowerShell 5), кодом которого спешу с вами поделиться:

class Robocopy {
    [String]$RobocopyPath

    Robocopy () {
        $this.RobocopyPath = Join-Path $env:SystemRoot 'System32Robocopy.exe'
        if ( -not ( Test-Path $this.RobocopyPath -PathType Leaf )) {
            throw 'Исполняемый файл робокопи не найден'
        }

    }
    [void]CopyFile ( [String]$SourceFile, [String]$DestinationFolder ) {
        $this.CopyFile( $SourceFile, $DestinationFolder, $false )
    }
    [void]CopyFile ( [String]$SourceFile, [String]$DestinationFolder, [bool]$Archive ) {
        $FileName   = [IO.Path]::GetFileName( $SourceFile )
        $FolderName = [IO.Path]::GetDirectoryName( $SourceFile )

        $Arguments = @( '/R:0', '/NP', '/NC', '/NS', '/NJH', '/NJS', '/NDL' )
        if ( $Archive ) {
            $Arguments += $( '/A+:a' )
        }
        $ErrorFlag = $false
        &$this.RobocopyPath $FolderName $DestinationFolder $FileName $Arguments | Foreach-Object {
            if ( $ErrorFlag ) {
                $ErrorFlag = $false
                throw "$_ $ErrorString"
            } else {
                if ( $_ -match '(?<=(0x[da-f]{8}))(?<text>(.+$))' ) {
                    $ErrorFlag   = $true
                    $ErrorString = $matches.text
                } else {
                    $Logger.AddRecord( $_.Trim())
                }
            }
        }
        if ( $LASTEXITCODE -eq 8 ) {
            throw 'Some files or directories could not be copied'
        }
        if ( $LASTEXITCODE -eq 16 ) {
            throw 'Robocopy did not copy any files. Check the command line parameters and verify that Robocopy has enough rights to write to the destination folder.'
        }
    }
    [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder ) {
        $this.SyncFolders( $SourceFolder, $DestinationFolder, '*.*', '', $false )
    }
    [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [Bool]$Archive ) {
        $this.SyncFolders( $SourceFolder, $DestinationFolder, '*.*', '', $Archive )
    }
    [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include ) {
        $this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, '', $false )
    }
    [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [Bool]$Archive ) {
        $this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, '', $Archive )
    }
    [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [String]$Exclude ) {
        $this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, $Exclude, $false )
    }
    [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [String]$Exclude, [Bool]$Archive ) {
        $Arguments = @( '/MIR', '/R:0', '/NP', '/NC', '/NS', '/NJH', '/NJS', '/NDL' )
        if ( $Exclude ) {
            $Arguments += $( '/XF' )
            $Arguments += $Exclude.Split(' ')
        }
        if ( $Archive ) {
            $Arguments += $( '/A+:a' )
        }
        $ErrorFlag = $false
        &$this.RobocopyPath $SourceFolder $DestinationFolder $Include $Arguments | Foreach-Object {
            if ( $ErrorFlag ) {
                $ErrorFlag = $false
                throw "$_ $ErrorString"
            } else {
                if ( $_ -match '(?<=(0x[da-f]{8}))(?<text>(.+$))' ) {
                    $ErrorFlag = $true
                    $ErrorString = $matches.text
                } else {
                    $Logger.AddRecord( $_.Trim())
                }
            }
        }
        if ( $LASTEXITCODE -eq 8 ) {
            throw 'Some files or directories could not be copied'
        }
        if ( $LASTEXITCODE -eq 16 ) {
            throw 'Robocopy did not copy any files. Check the command line parameters and verify that Robocopy has enough rights to write to the destination folder.'
        }
    }
}

Код этот я писал несколько лет назад и некоторые решения в нем могут показаться не лучшими, но проводить глубокую ревизию его мне в данный момент времени крайне лениво (иначе конца написанию этой статьи не будет), если у вас есть желание сделать его лучше — добро пожаловать в коментарии.

Внимательные читатели спросят, мол как так: в классе который борется за перформанс используется Foreach-Object!? Это правда, и приведенный пример один из случаев уместного использования этого командлета и вот почему: в отличие от foreach, командлет Foreach-Object не дожидается полного выполнения команды отправляющей данные в пайп — обработка происходит потоково, в конкретной ситуации, например, генерируя исключения сразуже, а не дожидаясь окончания процесса синхронизации. Парсинг вывода утилиты подходящее этому командлету место.

Использование описанного выше враппера до банального простое, стоит лишь добавить обработку исключений:

$Robocopy = New-Object Robocopy

# копирование одного файла
$Robocopy.CopyFile( $Source, $Dest )

# синхронизация папок
$Robocopy.SyncFolders( $SourceDir, $DestDir )

# синхронизация только файлов .xml и установка архивного бита
$Robocopy.SyncFolders( $SourceDir, $DestDir , '*.xml', $true )

# синхронизация всех файлов кроме *.zip *.tmp *.log и установка архивного бита
$Robocopy.SyncFolders( $SourceDir, $DestDir, '*.*', '*.zip *.tmp *.log', $true )

Послевкусие

Производительность скриптов тема обширная и отчасти холиварная — микрооптимизации могут забрать времени на реализацию и тестирование больше чем принесут, поддерживаемость и читаемость кода может снизиться настолько, что цена его поддержки будет выше чем профит от использования такого решения; вместе с тем, есть ряд простых рекомендаций делающих ваш код проще, понятне и быстрее, стоит лишь начать их использовать:

  • использовать оператор foreach вместо командлета Foreach-Object в скриптах;

  • минимизировать количество пайплайнов;

  • читать/писать файлы разом, а не построчно;

  • использовать StringBuilder и прочие специализированные классы;

  • профилировать код и понимать узкие места, прежде чем что-то оптимизировать;

  • не стыдиться вызывать нативные бинарники (пастить "батники" в скрипты не стоит);

И пожалуй главное еще раз: не спешите оптимизировать что-то без реальной необходимости, преждевременная оптимизация, она как преждевременная эякуляция.

Jobs

Бывает так, что вы уже оптимизировали всё, что казалось необходимым и пришли к некоторому компромису, между читаемость и скоростью, но то ли данных стало больше, то ли дальнейшие алгоритмические трюки себя не оправдывают, а сократить время работы нужно. В этом случае незаменимым помощником может стать параллельность исполнения некоторых частей кода. Убедиться стоит разве что в том, что не будет проблем с IO, если вы вдруг решили что скорости диска хватит на любое количество потоков вмещающихся в память.

Минутка юмора на счет ssd

Вот так происходит первая загрузка свежеустановленной Windows Server 2019 в Hyper-V на ssd (решилось миграцией виртуалки на hdd):

2019ssd

Со второй версии PowerShell доступны командлеты для работы с заданиями (Get-Command *-Job), подробнее можно почитать например тут.

Ничего концептуально сложного в них нет, оформляем скриптблок, запускаем задание, получаем результаты в нужный момент:

$Job = Start-Job -ScriptBlock {
    Write-Output 'Good night'
    Start-Sleep -S 10
    Write-Output 'Good morning'
}

$Job | Wait-Job | Receive-Job
Remove-Job $Job

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

Если вы решили не открывать ссылку, еще одна попытка с моей стороны:

jobs
https://xaegr.wordpress.com/2011/07/12/threadping/

Проблема, которая вроде бы и не проблема, но обозначена — каждый джоб хочет немного памяти, что бы быть быстрее и запускается полноценным процессом операционной системы со всеми плюсами и минусами этого подхода. Вот так, например, умирает приведенный выше джоб (50 мегабайт — это 50 мегабайт):

job dies

Джобы помогут решить любые ваши задачи параллельно и сделать это удобно. Обязательно изучите этот механизм, джобы — это лучший выбор для простого решения задачи на довольно высоком уровне абстракции, при этом уложиться получится в минимальное количество строк. Помните одну важную вещь — ваши скрипты должны быть читаемыми для людей которые придут после вас, скрипты пишут для людей.

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

Runspaces

Концепции ранспейсов посвещена целая серия статей статей в блоге майкрософта и я очень рекомендую обратиться к первоисточнику — Beginning Use of PowerShell Runspaces: Part 1. Коротко, ранспейс — это отдельный поток PowerShell который работает в том же процессе операционной системы, от того не имея оверхеда на новый процесс. Если концепция легких потоков вам нравится и вы хотите пускать их десятками (нет, концепции каналов в PowerShell нет), то у меня для вас хорошая новость: для удобства вся низкоуровневая логика вот в этом репозитарии модуля на гитхаб (там есть гифки) уже обернута в более знакомую концепцию джобов. А пока покажу как работать с ними руками, но первую ссылку из этого абзаца не забывайте посетить в любом случае.

В качестве примера использования ранспейсов могу привести скелет простой WPF формы, отрисовка которой происходит в том же потоке операционной системы что и основной процесс PowerShell, но в отдельном потоке рантайма. Взамодействие с ним происходит через потокобезопасный хэштейбл — вам не нужно писать никаких мьютексов, всё уже работает. Плюс такого подхода — вы можете в основном скрипте реализовать любой сложности и длительности работы алгоритм, блокировка которым основного потока исполнения не приведет к "зависанию" формы. Пруф в последней строке скрипта.

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

wpf

# Хештейбл синхронизированный между потоками
$GUISyncHash = [hashtable]::Synchronized(@{})

<#
    WPF форма
#>
$GUISyncHash.FormXAML = [xml](@"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Sample WPF Form" Height="510" Width="410" ResizeMode="NoResize">
    <Grid>
        <Label Content="Пример формы" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="37" Width="374" FontSize="18"/>
        <Label Content="Откуда" HorizontalAlignment="Left" Margin="16,64,0,0" VerticalAlignment="Top" Height="26" Width="48"/>
        <TextBox x:Name="BackupPath" HorizontalAlignment="Left" Height="23" Margin="69,68,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="300"/>
        <Label Content="Куда" HorizontalAlignment="Left" Margin="16,103,0,0" VerticalAlignment="Top" Height="26" Width="35"/>
        <TextBox x:Name="RestorePath" HorizontalAlignment="Left" Height="23" Margin="69,107,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="300"/>
        <Button x:Name="FirstButton" Content="√" HorizontalAlignment="Left" Margin="357,68,0,0" VerticalAlignment="Top" Width="23" Height="23"/>
        <Button x:Name="SecondButton" Content="√" HorizontalAlignment="Left" Margin="357,107,0,0" VerticalAlignment="Top" Width="23" Height="23"/>
        <CheckBox x:Name="Check" Content="Сделать мне хорошо" HorizontalAlignment="Left" Margin="16,146,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.113,-0.267" Width="172"/>
        <Button x:Name="Go" Content="Go" HorizontalAlignment="Left" Margin="298,173,0,0" VerticalAlignment="Top" Width="82" Height="26"/>
        <ComboBox x:Name="Droplist" HorizontalAlignment="Left" Margin="16,173,0,0" VerticalAlignment="Top" Width="172" Height="26"/>
        <ListBox x:Name="ListBox" HorizontalAlignment="Left" Height="250" Margin="16,210,0,0" VerticalAlignment="Top" Width="364"/>
    </Grid>
</Window>
"@)

<#
    Поток формы
#>
$GUISyncHash.GUIThread = {
    $GUISyncHash.Window       = [Windows.Markup.XamlReader]::Load(( New-Object System.Xml.XmlNodeReader $GUISyncHash.FormXAML ))
    $GUISyncHash.Check        = $GUISyncHash.Window.FindName( "Check" )
    $GUISyncHash.GO           = $GUISyncHash.Window.FindName( "Go" )
    $GUISyncHash.ListBox      = $GUISyncHash.Window.FindName( "ListBox" )
    $GUISyncHash.BackupPath   = $GUISyncHash.Window.FindName( "BackupPath" )
    $GUISyncHash.RestorePath  = $GUISyncHash.Window.FindName( "RestorePath" )
    $GUISyncHash.FirstButton  = $GUISyncHash.Window.FindName( "FirstButton" )
    $GUISyncHash.SecondButton = $GUISyncHash.Window.FindName( "SecondButton" )
    $GUISyncHash.Droplist     = $GUISyncHash.Window.FindName( "Droplist" )

    $GUISyncHash.Window.Add_SourceInitialized({
        $GUISyncHash.GO.IsEnabled = $true
    })

    $GUISyncHash.FirstButton.Add_Click( {
        $GUISyncHash.ListBox.Items.Add( 'Click FirstButton' )
    })

    $GUISyncHash.SecondButton.Add_Click( {
        $GUISyncHash.ListBox.Items.Add( 'Click SecondButton' )
    })

    $GUISyncHash.GO.Add_Click( {
        $GUISyncHash.ListBox.Items.Add( 'Click GO' )
    })

    $GUISyncHash.Window.Add_Closed( {
        Stop-Process -Id $PID -Force
    })

    $null = $GUISyncHash.Window.ShowDialog()
}

$Runspace = @{}
$Runspace.Runspace = [RunspaceFactory]::CreateRunspace()
$Runspace.Runspace.ApartmentState = "STA"
$Runspace.Runspace.ThreadOptions = "ReuseThread"
$Runspace.Runspace.Open()
$Runspace.psCmd = { Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase }.GetPowerShell()
$Runspace.Runspace.SessionStateProxy.SetVariable( 'GUISyncHash', $GUISyncHash )
$Runspace.psCmd.Runspace = $Runspace.Runspace
$Runspace.Handle = $Runspace.psCmd.AddScript( $GUISyncHash.GUIThread ).BeginInvoke()

Start-Sleep -S 1

$GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] {
    $GUISyncHash.ListBox.Items.Add( 'Привет' )
})

$GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] {
    $GUISyncHash.ListBox.Items.Add( 'Наполняю выпадающее меню' )
})

foreach ( $item in 1..5 ) {
    $GUISyncHash.Droplist.Dispatcher.Invoke( "Normal", [action] {
        $GUISyncHash.Droplist.Items.Add( $item )
        $GUISyncHash.Droplist.SelectedIndex = 0
    })
}

$GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] {
    $GUISyncHash.ListBox.Items.Add( 'While ( $true ) { Start-Sleep -S 10 }' )
})

while ( $true ) { Start-Sleep -S 10 }

Еще один пример работы с WPF можете посмотреть в моём репозитории на github, там один поток и всё довольно просто, ну и еще он позволяет читать smart диска: https://github.com/snd3r/GetDiskSmart/. А ещё там можно посмотреть пример биндинга объектов к форме, когда работает магия MVC:

binging

Если на вашем компьютере не стоит старшая Visual Studio, например потому что ваша организация не удовлетворяет требований к бесплатному использованию Community Edition или у вас нет желания добавлять в систему программу установка которой будет необратима без резервной копии раздела, то на гитхабе есть простой инструмент для рисования простых xaml-форм для wpf — https://github.com/punker76/kaxaml

kaxaml

Вместо заключения

PowerShell — мощная и удобная среда для работы с Windows-инфраструктурой. Он хорош концептуально, он удобен своим синтаксисом и самодокументирующими названиями командлетов, он может в перформанс себя как среды и вас как специалиста, стоит лишь разобраться в концепциях которыми он оперирует и начать получать удовольствие.

Когда я начинал писать эту статью, в планах была небольшая заметка по оформлению кода и наглядной демонстрации преимуществ ответственного отношения к стайлгайдам, под названием "PowerShell, хорошие практики", но Остапа понесло. Опыта написания такого размера технических статей у меня до этого небыло, поэтому прошу извинить за некоторую сумбурность повествования — писалось всё последовательно из головы, в лучших традициях снятия дампа сознания. При этом я старался давать ссылки на заслуживаюшие внимания ресурсы и рекомендую вам сделать все их фиолетовыми. Эти ссылки хоть на деле и из первой страницы гугла, но во-первых гугол у всех разный, во-вторых я все же сверял свои ожидания с контентом и только в случае совпадения добавлял в статью.

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

calm

P.S. Boomburum, не поддерживать в 2019 подсветку синтаксиса powershell — стрёмный стрём.

Автор: snd3r

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js