Хабрахабр

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

Введение

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

kdpv

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

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

popov

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

Стайл гайды

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

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

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

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

Скрипт кадрирует изображение приводя его к квадрату и выполняет ресайз, думаю у вас есть задача делать аватарки для пользователей (нехватает разве что поворота по данным 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:\Users\snd3r\Nextcloud )

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

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

ErrorActionPreference

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

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

Для изменения поведения командлетов в случае возникновения ошибки существует глобальная переменная $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 "$PSScriptRoot\Log.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
}

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

Инструменты

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

Я обычно выбираю cmder в минимальной конфигурации, без гита и прочих бинарей, которые ставлю сам, хотя несколько лет тюнил свой конфиг для чистой Conemu. Первым делом стоит установить Conemu или его сборку Cmder, что не особо важно, так как на мой взгляд по настройкам в любом случае стоит пробежаться. Это действительно лучший эмулятор терминала для 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.

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

vscode

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

PSScriptAnalyzer

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

OTBS

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

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

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

layout

Source Control; Git, SVN

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

merge

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

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

Snippets

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

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

customobject

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

help

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

splatting

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

snippets

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

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

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

Pipeline и foreach

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

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

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

Get-Content D:\temp\SomeHeavy.log | Select-String '328117'

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

A ReadCount value of 0 reads the entire file in a single read operation. When reading from and writing to binary files, use the AsByteStream parameter and a value of 0 for the ReadCount parameter. 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:\temp\SomeHeavy.log -ReadCount 0 | Select-String '328117'

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

readcount

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

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

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

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

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

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

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

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

$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $line in ( Get-Content D:\temp\SomeHeavy.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:\temp\Result.log -Append -Encoding UTF8

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

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

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

-InputObject <psobject> Required? false Position? Named Accept pipeline input? true (ByValue) Parameter set name (All) Aliases None Dynamic? false

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

taskmgr

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

stringbuilder alloc

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

$StringBuilder = New-Object System.Text.StringBuilder
$StreamReader = New-Object System.IO.StreamReader 'D:\temp\SomeHeavy.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:\temp\Result.log -Append -Encoding UTF8

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

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

streamreader

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

$BufferSize = 104857600
$StringBuilder = New-Object System.Text.StringBuilder $BufferSize
$StreamReader = New-Object System.IO.StreamReader 'C:\temp\SomeHeavy.log'
while ( $line = $StreamReader.ReadLine()) { if ( $line -match '1443' ) { # проверка приближения к концу буфера if ( $StringBuilder.Length -gt ( $BufferSize - ( $BufferSize * 0.1 ))) { Out-File -InputObject $StringBuilder.ToString() -FilePath C:\temp\Result.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:\temp\Result.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

Наиболее дорогой, вы думаю догадались — отправка пайпом в Out-Null; мало того, такой способ подавления (присваивание результата в $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 'System32\Robocopy.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, командлет Foreach-Object не дожидается полного выполнения команды отправляющей данные в пайп — обработка происходит потоково, в конкретной ситуации, например, генерируя исключения сразуже, а не дожидаясь окончания процесса синхронизации. Внимательные читатели спросят, мол как так: в классе который борется за перформанс используется 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

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

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

binging

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

kaxaml

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

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

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

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

calm

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть