- Remove From My Forums
-
Вопрос
-
есть скрипт…
——————————————————
param ([int]$start, [int]$end = $start)
[string]$range = ‘192.168.111.’function GetWmiClass {
param ( $target = «» )
if ($wmi = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $comp -ErrorAction SilentlyContinue -Authentication Call) { return $wmi }
else { return $false }
}$num = $start
while ($num -le $end) {
$comp = $range + $num
Write-Host $comp»:`t`t» -NoNewline -ForegroundColor Gray
if (Test-Connection -ComputerName $comp -Quiet -Count 1) {
If ($wmi = GetWmiClass $comp ) {
Write-Host $wmi.Name»`t`t» -NoNewline -ForegroundColor DarkGreen
if ($wmi.UserName) { Write-Host $wmi.UserName -ForegroundColor DarkGreen }
else { Write-Host «Вход не выполнен» -ForegroundColor DarkGray }
}
else {
Write-Host «Ошибка доступа к WMI» -ForegroundColor DarkRed
}
}
else { Write-Host «Хост не доступен» -ForegroundColor DarkGray }
$num++
}
——————————————на некоторых хостах видим
——————————————
Get-WmiObject : Отказано в доступе. (Исключение из HRESULT: 0x80070005 (E_ACCESSDENIED))
D:dev.scriptsgetusers.ps1:6 знак:26
+ if ($wmi = Get-WmiObject <<<< -Class Win32_ComputerSystem -ComputerName $comp -ErrorAction SilentlyContinue -Authentication Call) { return $wmi }
+ CategoryInfo : NotSpecified: (:) [Get-WmiObject], UnauthorizedAccessException<br/>
+ FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
——————————————как сделать чтоб ошибка не выводилась в консоль при выполнении скрипта?
-ErrorAction SilentlyContinue указано, но ошибка всё равно прёт
эээ…
Ответы
-
Во-первых я испробовал get-wmiobject во всех комбинациях, и с 1.0 и с 2.0, с различными типами ошибок, но при -erroraction silentlycontinue ошибка не выводится. Вы можете отключить вывод ошибок глобально, установив для переменной $ErrorActionPreference значение silentlyContinue
Во-вторых, успешность команды лучше проверять не по наличию данных, а с помощью специальной переменной $? в которой содержится статус выполнения предыдущей команды.
AKA Xaegr, MCSE: Security, Messaging; MCITP: ServerEnterprise Administrator; Блог: http://xaegr.wordpress.com
-
Предложено в качестве ответа
23 января 2010 г. 6:35
-
Помечено в качестве ответа
OlegKrikun
23 января 2010 г. 10:26
-
Предложено в качестве ответа
- Introduction to Error Action in PowerShell
- Use the
-ErrorActionParameter in PowerShell - Setting Error Action Preferences in PowerShell

Whether we want to ignore error messages or terminate a script’s execution when an error occurs, Windows PowerShell has plenty of options for dealing with errors. This article will discuss multiple techniques in handling and suppressing errors.
Introduction to Error Action in PowerShell
Even though it is effortless to suppress Windows PowerShell errors, doing so isn’t always the best option (although it can be). If we carelessly tell PowerShell to hide errors, it can cause our script to behave unpredictably.
Suppressing error messages also makes troubleshooting and information gathering a lot more complicated. So tread lightly and be careful about using the following snippets that you will see in this article.
Use the -ErrorAction Parameter in PowerShell
The most common method for dealing with errors is to append the -ErrorAction parameter switch to a cmdlet. The -ErrorAction parameter switch lets PowerShell tell what to do if the cmdlet produces an error.
Command:
Get-Service 'svc_not_existing' -ErrorAction SilentlyContinue
In the command above, we are querying for a service that doesn’t exist. Usually, PowerShell will throw an error if the service doesn’t exist.
Since we use the -ErrorAction parameter, the script will continue as expected, like it doesn’t have an error.
Setting Error Action Preferences in PowerShell
If we need a script to behave in a certain way (such as suppressing errors), we might consider setting up some preference variables. Preference variables act as configuration settings for PowerShell.
We might use a preference variable to control the number of history items that PowerShell retains or force PowerShell to ask the user before performing specific actions.
For example, here is how you can use a preference variable to set the -ErrorAction parameter to SilentlyContinue for the entire session.
Command:
$ErrorActionPreference = 'SilentlyContinue'
There are many other error actions that we can specify for the ErrorAction switch parameter.
Continue: PowerShell will display the error message, but the script will continue to run.Ignore: PowerShell does not produce any error message, writes any error output on the host, and continues execution.Stop: PowerShell will display the error message and stop running the script.Inquire: PowerShell displays the error message but will ask for confirmation first if the user wants to continue.SilentlyContinue: PowerShell silently continues with code execution if the code does not work or has non-terminating errors.Suspend: PowerShell suspends the workflow of the script.
As previously mentioned, the -ErrorAction switch has to be used in conjunction with a PowerShell cmdlet. For instance, we used the Get-Process cmdlet to demonstrate how the ErrorAction switch works.
В Powershell существует несколько уровней ошибок и несколько способов их обработать. Проблемы одного уровня (Non-Terminating Errors) можно решить с помощью привычных для Powershell команд. Другой уровень ошибок (Terminating Errors) решается с помощью исключений (Exceptions) стандартного, для большинства языков, блока в виде Try, Catch и Finally.
Как Powershell обрабатывает ошибки
До рассмотрения основных методов посмотрим на теоретическую часть.
Автоматические переменные $Error
В Powershell существует множество переменных, которые создаются автоматически. Одна из таких переменных — $Error хранит в себе все ошибки за текущий сеанс PS. Например так я выведу количество ошибок и их сообщение за весь сеанс:
Get-TestTest
$Error
$Error.Count

При отсутствии каких либо ошибок мы бы получили пустой ответ, а счетчик будет равняться 0:

Переменная $Error являет массивом и мы можем по нему пройтись или обратиться по индексу что бы найти нужную ошибку:
$Error[0]
foreach ($item in $Error){$item}

Свойства объекта $Error
Так же как и все что создается в Powershell переменная $Error так же имеет свойства (дополнительную информацию) и методы. Названия свойств и методов можно увидеть через команду Get-Member:
$Error | Get-Member

Например, с помощью свойства InvocationInfo, мы можем вывести более структурный отчет об ошибки:
$Error[0].InvocationInfo

Методы объекта $Error
Например мы можем очистить логи ошибок используя clear:
$Error.clear()
Критические ошибки (Terminating Errors)
Критические (завершающие) ошибки останавливают работу скрипта. Например это может быть ошибка в названии командлета или параметра. В следующем примере команда должна была бы вернуть процессы «svchost» дважды, но из-за использования несуществующего параметра ‘—Error’ не выполнится вообще:
'svchost','svchost' | % {Get-Process -Name $PSItem} --Error

Не критические ошибки (Non-Terminating Errors)
Не критические (не завершающие) ошибки не остановят работу скрипта полностью, но могут вывести сообщение об этом. Это могут быть ошибки не в самих командлетах Powershell, а в значениях, которые вы используете. На предыдущем примере мы можем допустить опечатку в названии процессов, но команда все равно продолжит работу:
'svchost111','svchost' | % {Get-Process -Name $PSItem}

Как видно у нас появилась информация о проблеме с первым процессом ‘svchost111’, так как его не существует. Обычный процесс ‘svchost’ он у нас вывелся корректно.
Параметр ErrorVariable
Если вы не хотите использовать автоматическую переменную $Error, то сможете определять свою переменную индивидуально для каждой команды. Эта переменная определяется в параметре ErrorVariable:
'svchost111','svchost' | % {Get-Process -Name $PSItem } -ErrorVariable my_err_var
$my_err_var

Переменная будет иметь те же свойства, что и автоматическая:
$my_err_var.InvocationInfo

Обработка некритических ошибок
У нас есть два способа определения последующих действий при ‘Non-Terminating Errors’. Это правило можно задать локально и глобально (в рамках сессии). Мы сможем полностью остановить работу скрипта или вообще отменить вывод ошибок.
Приоритет ошибок с $ErrorActionPreference
Еще одна встроенная переменная в Powershell $ErrorActionPreference глобально определяет что должно случится, если у нас появится обычная ошибка. По умолчанию это значение равно ‘Continue’, что значит «вывести информацию об ошибке и продолжить работу»:
$ErrorActionPreference

Если мы поменяем значение этой переменной на ‘Stop’, то поведение скриптов и команд будет аналогично критичным ошибкам. Вы можете убедиться в этом на прошлом скрипте с неверным именем процесса:
$ErrorActionPreference = 'Stop'
'svchost111','svchost' | % {Get-Process -Name $PSItem}

Т.е. скрипт был остановлен в самом начале. Значение переменной будет храниться до момента завершения сессии Powershell. При перезагрузке компьютера, например, вернется значение по умолчанию.
Ниже значение, которые мы можем установить в переменной $ErrorActionPreference:
- Continue — вывод ошибки и продолжение работы;
- Inquire — приостановит работу скрипта и спросит о дальнейших действиях;
- SilentlyContinue — скрипт продолжит свою работу без вывода ошибок;
- Stop — остановка скрипта при первой ошибке.
Самый частый параметр, который мне приходится использовать — SilentlyContinue:
$ErrorActionPreference = 'SilentlyContinue'
'svchost111','svchost' | % {Get-Process -Name $PSItem}

Использование параметра ErrorAction
Переменная $ErrorActionPreference указывает глобальный приоритет, но мы можем определить такую логику в рамках команды с параметром ErrorAction. Этот параметр имеет больший приоритет чем $ErrorActionPreference. В следующем примере, глобальная переменная определяет полную остановку скрипта, а в параметр ErrorAction говорит «не выводить ошибок и продолжить работу»:
$ErrorActionPreference = 'Stop'
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'SilentlyContinue'}

Кроме ‘SilentlyContinue’ мы можем указывать те же параметры, что и в переменной $ErrorActionPreference.
Значение Stop, в обоих случаях, делает ошибку критической.
Обработка критических ошибок и исключений с Try, Catch и Finally
Когда мы ожидаем получить какую-то ошибку и добавить логику нужно использовать Try и Catch. Например, если в вариантах выше мы определяли нужно ли нам отображать ошибку или останавливать скрипт, то теперь сможем изменить выполнение скрипта или команды вообще. Блок Try и Catch работает только с критическими ошибками и в случаях если $ErrorActionPreference или ErrorAction имеют значение Stop.
Например, если с помощью Powershell мы пытаемся подключиться к множеству компьютеров один из них может быть выключен — это приведет к ошибке. Так как эту ситуацию мы можем предвидеть, то мы можем обработать ее. Процесс обработки ошибок называется исключением (Exception).
Синтаксис и логика работы команды следующая:
try {
# Пытаемся подключиться к компьютеру
}
catch [Имя исключения 1],[Имя исключения 2]{
# Раз компьютер не доступен, сделать то-то
}
finally {
# Блок, который выполняется в любом случае последним
}
Блок try мониторит ошибки и если она произойдет, то она добавится в переменную $Error и скрипт перейдет к блоку Catch. Так как ошибки могут быть разные (нет доступа, нет сети, блокирует правило фаервола и т.д.) то мы можем прописывать один блок Try и несколько Catch:
try {
# Пытаемся подключится
}
catch ['Нет сети']['Блокирует фаервол']{
# Записываем в файл
}
catch ['Нет прав на подключение']{
# Подключаемся под другим пользователем
}
Сам блок finally — не обязательный и используется редко. Он выполняется самым последним, после try и catch и не имеет каких-то условий.
Catch для всех типов исключений
Как и было показано выше мы можем использовать блок Catch для конкретного типа ошибок, например при проблемах с доступом. Если в этом месте ничего не указывать — в этом блоке будут обрабатываться все варианты ошибок:
try {
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'Stop'}
}
catch {
Write-Host "Какая-то неисправность" -ForegroundColor RED
}

Такой подход не рекомендуется использовать часто, так как вы можете пропустить что-то важное.
Мы можем вывести в блоке catch текст ошибки используя $PSItem.Exception:
try {
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'Stop'}
}
catch {
Write-Host "Какая-то неисправность" -ForegroundColor RED
$PSItem.Exception
}

Переменная $PSItem хранит информацию о текущей ошибке, а глобальная переменная $Error будет хранит информацию обо всех ошибках. Так, например, я выведу одну и ту же информацию:
$Error[0].Exception
Создание отдельных исключений
Что бы обработать отдельную ошибку сначала нужно найти ее имя. Это имя можно увидеть при получении свойств и методов у значения переменной $Error:
$Error[0].Exception | Get-Member

Так же сработает и в блоке Catch с $PSItem:

Для вывода только имени можно использовать свойство FullName:
$Error[0].Exception.GetType().FullName

Далее, это имя, мы вставляем в блок Catch:
try {
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'Stop'}
}
catch [Microsoft.PowerShell.Commands.ProcessCommandException]{
Write-Host "Произошла ошибка" -ForegroundColor RED
$PSItem.Exception
}

Так же, как и было описано выше мы можем усложнять эти блоки как угодно указывая множество исключений в одном catch.
Выброс своих исключений
Иногда нужно создать свои собственные исключения. Например мы можем запретить добавлять через какой-то скрипт названия содержащие маленькие буквы или сотрудников без указания возраста и т.д. Способов создать такие ошибки — два и они тоже делятся на критические и обычные.
Выброс с throw
Throw — выбрасывает ошибку, которая останавливает работу скрипта. Этот тип ошибок относится к критическим. Например мы можем указать только текст для дополнительной информации:
$name = 'AD.1'
if ($name -match '.'){
throw 'Запрещено использовать точки в названиях'
}

Если нужно, то мы можем использовать исключения, которые уже были созданы в Powershell:
$name = 'AD.1'
if ($name -like '*.*'){
throw [System.IO.FileNotFoundException]'Запрещено использовать точки в названиях'
}

Использование Write-Error
Команда Write-Error работает так же, как и ключ ErrorAction. Мы можем просто отобразить какую-то ошибку и продолжить выполнение скрипта:
$names = @('CL1', 'AD.1', 'CL3')
foreach ($name in $names){
if ($name -like '*.*'){
Write-Error -Message 'Обычная ошибка'
}
else{
$name
}
}

При необходимости мы можем использовать параметр ErrorAction. Значения этого параметра были описаны выше. Мы можем указать значение ‘Stop’, что полностью остановит выполнение скрипта:
$names = @('CL1', 'AD.1', 'CL3')
foreach ($name in $names){
if ($name -like '*.*'){
Write-Error -Message 'Обычная ошибка' -ErrorAction 'Stop'
}
else{
$name
}
}

Отличие команды Write-Error с ключом ErrorAction от обычных команд в том, что мы можем указывать исключения в параметре Exception:
Write-Error -Message 'Обычная ошибка' -ErrorAction 'Stop'
Write-Error -Message 'Исключение' -Exception [System.IO.FileNotFoundException] -ErrorAction 'Stop'

В Exception мы так же можем указывать сообщение. При этом оно будет отображаться в переменной $Error:
Write-Error -Exception [System.IO.FileNotFoundException]'Моё сообщение'
…
Теги:
#powershell
#ошибки
Введение
Эта статья адресована тем, кто уже познакомился с основами PowerShell, запускал какие-то скрипты со stackexchange и, вероятно, имеет свой текстовый файл с теми или иными сниппетами облегчающими повседневную работу. Целью её написания есть уменьшение энтропии, увеличение читаемости и поддерживаемости кода PowerShell используемого в вашей компании и, как следствие, повышение продуктивности администратора работающего с ним.

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

Книга также доступна на сайте автора, правда я не уверен в том насколько лицензионно чисто такое использование: 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, но тоже очень хороша.
Ниже пример того как оформить справку скрипта. Скрипт кадрирует изображение приводя его к квадрату и выполняет ресайз, думаю у вас есть задача делать аватарки для пользователей (нехватает разве что поворота по данным 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 догадается автоматически построить справку к скрипту. Такого рода справка буквально так и называется — справка основанная на коментарии:

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

Еще раз обращу внимание на то, что пренебрегать ей не стоит. Если не знаете что туда написать, напишите что-нибудь, сходите до кулера и по возвращении у вас точно будет понимание того, что нужно в написанном изменить. Это работает. Не стоит фанатично заполнять все ключевые слова, PowerShell разработан быть самодокументируемым и если вы дали осмысленные и полные имена параметрам, короткого предложения в разделе .SYNOPIS и одного примера вполне хватит.
Strict mode
PowerShell, как и многие другие скриптовые языки обладает динамической типизацией. У такого вида типизации есть много сторонников: написать простую, но мощную высокоуровневую логику — дело пары минут, но когда ваше решение начнет подбираться к тысяче строк, вы обязательно столкнетесь с хрупкостью такого подхода.
Автовывод типов неизменно на этапе тестирования формировавший массив в том месте где вы всегда получали набор элементов, обязательно подложит свинью в случае, если получит один элемент и вот уже в следующем условии, вместо проверки количества элементов вы получите количество символов или иной атрибут, в зависимости от типа элемента. Логика скрипта сломается, при этом среда исполнения сделает вид что все хорошо.
Установка строгого режима помогает избежать части такого рода проблем, но и требует от вас чуть больше кода, вроде инициализации переменных и явного приведения.
Включается этот режим командлетом Set-StrictMode -Version Latest, хотя есть другие варианты «строгости», мой выбор — использовать последний.
В примере ниже строгий режим отлавливает обращение к несуществующему свойству. Так как внутри папки находится лишь один элемент, тип переменной $Input в результате выполнения будет FileInfo, а не ожидаемый массив соответствующих элементов:

Для избежания такой проблемы, следует результат выполнения командлета явно привести к массиву:
$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

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

И да, теперь консоль можно очищать комбинацией CTRL + L, и забыть про cls.
Visual Studio Code
Редактор. Всё самое плохое, что я могу сказать про PowerShell, относится сугубо к PowerShell ISE, те кто видели первую версию с тремя панелями врядли забудут этот опыт. Отличающаяся кодировка терминала, отсутствие базовых возможностей редактора, вроде автоматического отступа, автозакрывающихся скобок, форматирования кода и целый набор порождаемых им антипаттернов про которые я вам не расскажу (на всякий случай) — это все про ISE.
Не используйте его, используйте Visual Studio Code с расширением PowerShell — тут есть всё, чего бы вы не захотели (в разумных пределах, конечно). И не забывайте, что в PoweShell до шестой версии (PowerShell Core 6.0) кодировка для скриптов — UTF8 BOM, иначе русский язык сломается.

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

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

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

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

Вот эти надписи между 303 и 304 строкой кликабельны, стоит нажать на все такие что появляются в документе в случае конфликта, сделать коммит фиксирующий изменения и отправить изменения на сервер. У — Удобство.
О том как работать с системами контроля версий доступно и с картинками написано в доке vscode, пробегитесь глазами до конца там кратко и хорошо.
Snippets
Сниппеты — своего рода макросы/шаблоны позволяющие ускорить написание кода. Однозначно must see.
Быстрое создание объекта:

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

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

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

Если после этого у вас появилось желание продолжить улучшать свое окружение, но вы еще ниразу не слышали про осом-листы, то вот же, положил. Так же, если у вас есть свой набор расширений пригождающихся вам при написании 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'
На моём файле лога размером чуть более одного гигабайта преимущество второго подхода почти в три раза:

Я советую вам не верить мне на слово, а самим проверить так ли это на файле похожего размера. В целом же 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 ограничивал себя тремя гигабайтами памяти:

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

Всем хорош предыдущий подход, кроме разве что того, что 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Гб. Конечный объем занятой памяти я заскринить не успел, но есть скрин того что происходило ближе к концу работы:

Поведение программы по расходу памяти вполне очевидно — вход у неё грубо говоря «труба», а выход — наш 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Гб при почти той же скорости выполнения:

Безусловно результаты по абсолютным числам утилизированной памяти будут отличаться от одной машины к другой, всё зависит от того сколько памяти вообще доступно на машине и соответственно насколько агрессивным будет её освобождение. Если память для вас критична, а несколько процентов производительности не так, то можно еще уменьшить её потребление заиспользовав 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):

Со второй версии 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
Пример выше призван уместить необходимые командлеты на минимальной единице площади, а не является хорошим паттерном написания асинхронного кода — не стоит делать асинхронный вызов для того, чтобы подождать его завершения. В качестве нестареющего примера использования джобов я рекомендую вам разобраться и подебажить вот этот скрипт распределенного пинга подсети.
Если вы решили не открывать ссылку, еще одна попытка с моей стороны:

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

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

# Хештейбл синхронизированный между потоками
$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/. А ещё там можно посмотреть пример биндинга объектов к форме, когда работает магия MVVM:

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

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

P.S. Boomburum, не поддерживать в 2019 подсветку синтаксиса powershell — стрёмный стрём.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какая версия powershell используется в вашей инфраструктуре?
6.82%
powershell v2 — у меня только семерки и я не хочу обновлять powershell
6
10.23%
powershell v3 — мне не нужны новые фичи, а тут есть все что нужно
9
57.95%
powershell v5 — я обновляю машины потому что активно использую powershell
51
25%
powershell v6 core — у меня даже на линуксе pwsh
22
Проголосовали 88 пользователей.
Воздержались 39 пользователей.

