Поделиться через


Все, что вы хотели знать о ShouldProcess

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

Примечание.

Оригинал этой статьи впервые был опубликован в блоге автора @KevinMarquette. Группа разработчиков PowerShell благодарит Кевина за то, что он поделился с нами этими материалами. Читайте его блог — PowerShellExplained.com.

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

ОбщиеПараметры

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

Использование параметра -WhatIf

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

PS C:\temp> Get-ChildItem
    Directory: C:\temp
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         4/19/2021   8:59 AM              0 importantfile.txt
-a----         4/19/2021   8:58 AM              0 myfile1.txt
-a----         4/19/2021   8:59 AM              0 myfile2.txt

PS C:\temp> Remove-Item -Path .\myfile1.txt -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".

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

PS C:\temp> Remove-Item -Path * -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
What if: Performing the operation "Remove File" on target "C:\Temp\myfile2.txt".
What if: Performing the operation "Remove File" on target "C:\Temp\importantfile.txt".

Использование опции -Confirm

Команды, поддерживающие -WhatIf, также поддерживают -Confirm. Это дает возможность подтвердить действие перед его выполнением.

PS C:\temp> Remove-Item .\myfile1.txt -Confirm

Confirm
Are you sure you want to perform this action?
Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

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

Y - Continue with only the next step of the operation.
A - Continue with all the steps of the operation.
N - Skip this operation and proceed with the next operation.
L - Skip this operation and all subsequent operations.
S - Pause the current pipeline and return to the command prompt. Type "exit" to resume the pipeline.
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

Локализация

Этот запрос локализован в PowerShell, поэтому язык изменяется в зависимости от языка операционной системы. Это еще одна вещь, которую PowerShell управляет для вас.

Параметры переключения

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

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

Remove-Item -Path:* -WhatIf:$true

То же самое можно сделать и с переменной.

$DoWhatIf = $true
Remove-Item -Path * -WhatIf:$DoWhatIf

Второй подход заключается в использовании хэш-таблицы для разброски значения.

$RemoveSplat = @{
    Path = '*'
    WhatIf = $true
}
Remove-Item @RemoveSplat

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

Поддерживает ShouldProcess

Чтобы включить поддержку -WhatIf и -Confirm, для начала необходимо указать SupportsShouldProcess в CmdletBinding функции.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    Remove-Item .\myfile1.txt
}

Указав SupportsShouldProcess таким образом, можно вызвать функцию с параметром -WhatIf (или -Confirm).

PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".

Обратите внимание, что я не создаю параметр с именем -WhatIf. Он создается автоматически, если задать свойство SupportsShouldProcess. Если задать параметр -WhatIf для Test-ShouldProcess, некоторые из вызываемых объектов также выполняют обработку -WhatIf.

Примечание.

При использовании SupportsShouldProcessPowerShell не добавляет $WhatIf переменную в функцию. Вам не нужно проверять значение $WhatIf , так как ShouldProcess() метод заботится об этом.

Доверяйте, но проверяйте

Опасно полагаться на то, что всё, что вы вызываете, наследует значения -WhatIf. В остальных примерах я буду предполагать, что это не работает, и при вызове других команд буду очень подробно задавать все параметры. Рекомендую и вам придерживаться того же подхода.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    Remove-Item .\myfile1.txt -WhatIf:$WhatIfPreference
}

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

$PSCmdlet.ShouldProcess

Метод, который позволяет реализовать свойство SupportsShouldProcess, называется $PSCmdlet.ShouldProcess. Вы вызываете $PSCmdlet.ShouldProcess(...), чтобы узнать, нужно ли обработать ту или иную логику, а остальное PowerShell выполняет автоматически. Давайте начнем с примера.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $file = Get-ChildItem './myfile1.txt'
    if($PSCmdlet.ShouldProcess($file.Name)){
        $file.Delete()
    }
}

Вызов $PSCmdlet.ShouldProcess($file.Name) проверяет наличие параметра -WhatIf-Confirm) и обрабатывает его соответствующим образом. -WhatIf приводит к тому, что ShouldProcess выводит описание изменения и возвращает $false.

PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".

Вызов с -Confirm приводит к приостановке выполнения скрипта и отображению для пользователя вариантов продолжения. Он возвращает $true, если пользователь выбрал Y.

PS> Test-ShouldProcess -Confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

Замечательной особенностью $PSCmdlet.ShouldProcess является то, что он также используется как подробный вывод. Я часто использую это при реализации ShouldProcess.

PS> Test-ShouldProcess -Verbose
VERBOSE: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".

Перегрузки

Для $PSCmdlet.ShouldProcess существует несколько различных перегрузок с разными параметрами для настройки сообщений. Первая из них уже использовалась в приведенном выше примере. Давайте подробнее рассмотрим каждую из них.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    if($PSCmdlet.ShouldProcess('TARGET')){
        # ...
    }
}

Эта выводит результат, включающий имя функции и целевой объект (значение параметра).

What if: Performing the operation "Test-ShouldProcess" on target "TARGET".

Если указать второй параметр в качестве операции, вместо имени функции в сообщении будет использоваться значение операции.

## $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".

Следующий вариант — указать три параметра для полной настройки сообщения. Если используются три параметра, первый из них является сообщением в целом. Два других параметра по-прежнему используются в выходных данных сообщения -Confirm.

## $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE

Быстрая справка по параметрам

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

## $PSCmdlet.ShouldProcess('TARGET')
What if: Performing the operation "FUNCTION_NAME" on target "TARGET".

## $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".

## $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE

Я обычно использую метод с двумя параметрами.

ПричинаОбработки

Существует также четвертая перегрузка. Она сложнее, чем друге, Позволяет узнать причину, по которой было выполнено ShouldProcess. Я добавляю ее здесь для полноты картины, так как вместо этого можно просто проверить, действительно ли значение $WhatIfPreference равно $true.

$reason = ''
if($PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION',[ref]$reason)){
    Write-Output "Some Action"
}
$reason

Переменную $reason необходимо передать в четвертый параметр в виде ссылочной переменной с [ref]. ShouldProcess заполняет $reason значением None или WhatIf. Я бы не назвал это полезной возможностью, так что у меня нет причин ее использовать.

Где это разместить?

Метод ShouldProcess используется, чтобы сделать скрипты безопаснее. Вы используете его, когда ваши скрипты вносят изменения. Я предпочитаю размещать вызов $PSCmdlet.ShouldProcess как можно ближе к изменению.

## general logic and variable work
if ($PSCmdlet.ShouldProcess('TARGET','OPERATION')){
    # Change goes here
}

Если я обрабатываю коллекцию элементов, я вызываю это для каждого элемента. Таким образом, вызов помещается внутри цикла foreach.

foreach ($node in $collection){
    # general logic and variable work
    if ($PSCmdlet.ShouldProcess($node,'OPERATION')){
        # Change goes here
    }
}

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

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

$WhatIfPreference

Первая привилегированная переменная — $WhatIfPreference. По умолчанию ее значение $false. Если задать для нее значение $true, функция выполняется так, как если бы вы указали -WhatIf. Если задать это в сеансе, все команды будут выполняться с параметром -WhatIf.

При вызове функции с параметром -WhatIf значение $WhatIfPreference становится равным $true в области действия функции.

ConfirmImpact

Большинство моих примеров касаются -WhatIf, но все это также работает и при использовании -Confirm для отправки запроса пользователю. Для аргумента ConfirmImpact функции можно задать значение high, и тогда она будет выводить запрос пользователю, как если бы была вызвана с параметром -Confirm.

function Test-ShouldProcess {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param()

    if ($PSCmdlet.ShouldProcess('TARGET')){
        Write-Output "Some Action"
    }
}

Этот вызов Test-ShouldProcess осуществляет действие -Confirm из-за влияния High.

PS> Test-ShouldProcess

Confirm
Are you sure you want to perform this action?
Performing the operation "Test-ShouldProcess" on target "TARGET".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): y
Some Action

Очевидная проблема заключается в том, что теперь ее сложнее использовать в других скриптах без запроса пользователя. В этом случае можно передать $false в -Confirm, чтобы подавить запрос.

PS> Test-ShouldProcess -Confirm:$false
Some Action

Я расскажу, как добавить поддержку -Force в одном из следующих разделов.

$ПодтвердитьПредпочтение

$ConfirmPreference — это автоматическая переменная, определяющая, когда ConfirmImpact запрашивает подтверждение выполнения. Ниже приведены возможные значения для $ConfirmPreference и ConfirmImpact.

  • High
  • Medium
  • Low
  • None

С помощью этих значений можно указать различные уровни влияния для каждой функции. Если заданное для $ConfirmPreference значение выше, чем значение ConfirmImpact, запрос на подтверждение выполнения не выводится.

По умолчанию для $ConfirmPreference задано значение High, а для ConfirmImpact — значение Medium. Если вы хотите, чтобы функция автоматически запрашивала подтверждение пользователя, задайте для аргумента ConfirmImpact значение High. В противном случае задайте для него значение Medium, если команда выполняет необратимое действие, и Low, если она всегда безопасна для выполнения в рабочей среде. Если задать для него значение none, запрос на подтверждение не выводится, даже если задан параметр -Confirm (но поддержка -WhatIf по-прежнему предоставляется).

В случае вызова функции с параметром -Confirm значение $ConfirmPreference становится равным Low в области действия функции.

Подавление вложенных запросов на подтверждение

Вызываемые функции могут получать значение $ConfirmPreference. Это может привести к ситуации, когда вы добавляете запрос на подтверждение, но при этом вызываемая функция также запрашивает его у пользователя.

Я обычно предпочитаю использовать -Confirm:$false для вызываемых команд после того, как запрос уже обработан.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $file = Get-ChildItem './myfile1.txt'
    if($PSCmdlet.ShouldProcess($file.Name)){
        Remove-Item -Path $file.FullName -Confirm:$false
    }
}

Это возвращает нас к предыдущему предупреждению: существуют нюансы в том, когда -WhatIf не используется при вызове функции, и когда -Confirm используется в качестве аргумента функции. Обещаю вернуться к этому вопросу позже.

$PSCmdlet.ShouldContinue

Если вам требуется более жесткий контроль, чем обеспечивает ShouldProcess, можно запустить запрос непосредственно с помощью ShouldContinue. ShouldContinue пропускает $ConfirmPreference, ConfirmImpact, -Confirm, $WhatIfPreference и -WhatIf, так как запрос выводится при каждом выполнении.

ShouldProcess и ShouldContinue можно легко перепутать. Я обычно помню, что нужно использовать ShouldProcess, поскольку параметр в SupportsShouldProcess называется CmdletBinding. Практически во всех сценариях следует использовать ShouldProcess. Именно поэтому я сначала рассматривал именно этот метод.

Давайте посмотрим на метод ShouldContinue в действии.

function Test-ShouldContinue {
    [CmdletBinding()]
    param()

    if($PSCmdlet.ShouldContinue('TARGET','OPERATION')){
        Write-Output "Some Action"
    }
}

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

Test-ShouldContinue

Second
TARGET
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"):

Основная проблема ShouldContinue заключается в том, что пользователю необходимо работать в интерактивном режиме, так как всегда отображается запрос. Вам всегда следует создавать такие средства, которые могут использоваться другими скриптами. Для этого используется реализация -Force. Я вернусь к этой идее позже.

Да для всех

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

function Test-ShouldContinue {
    [CmdletBinding()]
    param()

    $collection = 1..5
    $yesToAll = $false
    $noToAll = $false

    foreach($target in $collection) {

        $continue = $PSCmdlet.ShouldContinue(
                "TARGET_$target",
                'OPERATION',
                [ref]$yesToAll,
                [ref]$noToAll
            )

        if ($continue){
            Write-Output "Some Action [$target]"
        }
    }
}

Я добавил цикл foreach и коллекцию, чтобы показать их в действии. Я извлек вызов ShouldContinue из инструкции if, чтобы упростить чтение. Вызов метода с четырьмя параметрами начинает выглядеть немного некрасиво, но я постарался сделать его как можно более чистым.

Использование -Force

ShouldProcess и ShouldContinue должны реализовывать -Force различными способами. Хитрость таких реализаций заключается в том, что метод ShouldProcess всегда должен выполняться, а ShouldContinue не должен выполняться, если указано -Force.

Параметр ShouldProcess -Force

Если задать для ConfirmImpact значение high, первое, что попытаются сделать пользователи, — это подавить его с помощью -Force. Во всяком случае, это первое, что делаю я.

Test-ShouldProcess -Force
Error: Test-ShouldProcess: A parameter cannot be found that matches parameter name 'force'.

Если вы припомните раздел ConfirmImpact, то поймете, что им на самом деле нужно вызвать его следующим образом.

Test-ShouldProcess -Confirm:$false

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

function Test-ShouldProcess {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param(
        [switch]$Force
    )

    if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) {
        $ConfirmPreference = 'None'
    }

    if ($PSCmdlet.ShouldProcess('TARGET')) {
        Write-Output "Some Action"
    }
}

Мы добавим собственный переключатель -Force в качестве параметра. Параметр -Confirm автоматически добавляется при использовании SupportsShouldProcess в CmdletBinding. Однако при использовании SupportsShouldProcessPowerShell не добавляет $Confirm переменную в функцию. Если вы работаете в строгом режиме и пытаетесь использовать $Confirm переменную до ее определения, вы получите ошибку. Чтобы избежать ошибки, вы можете использовать $PSBoundParameters для проверки, был ли параметр передан пользователем.

if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) {
    $ConfirmPreference = 'None'
}

Если пользователь указывает -Force, то мы устанавливаем $ConfirmPreference в None в локальной области. Если пользователь также указывает -Confirm , ShoudProcess() то учитывает значения -Confirm параметра.

if ($PSCmdlet.ShouldProcess('TARGET')){
    Write-Output "Some Action"
}

Если указать как -Force, так и -WhatIf, приоритет будет у -WhatIf. При таком подходе обработка -WhatIf сохраняется, так как ShouldProcess выполняется всегда.

Не добавляйте тест для $Force значения внутри if инструкции с помощью инструкции ShouldProcess. Это неподходящий вариант для данного конкретного сценария, несмотря на то, что я продемонстрирую его в следующем разделе, посвященном ShouldContinue.

Параметр -Force метода ShouldContinue

Это правильный способ реализации -Force с ShouldContinue.

function Test-ShouldContinue {
    [CmdletBinding()]
    param(
        [switch]$Force
    )

    if($Force -or $PSCmdlet.ShouldContinue('TARGET','OPERATION')){
        Write-Output "Some Action"
    }
}

Если поместить переменную $Force слева от оператора -or, она вычисляется первой. При такой записи выполнение инструкции if прерывается. Если значение $Force равно $true, ShouldContinue не выполняется.

PS> Test-ShouldContinue -Force
Some Action

В этом сценарии не нужно беспокоиться о параметрах -Confirm и -WhatIf, так как они не поддерживаются ShouldContinue. Именно поэтому с данным методом следует работать иначе, чем с ShouldProcess.

Проблемы с областью действия

Предполагается, что использование -WhatIf и -Confirm должно применяться ко всем элементам внутри ваших функций и ко всему, что они вызывают. Для этого они задают значение $WhatIfPreference для параметра $true или значение $ConfirmPreference для параметра Low в локальной области функции. Когда вы вызываете другую функцию, вызовы ShouldProcess используют эти значения.

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

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

Основная проблема заключается в том, что модули скриптов не наследуют значения $WhatIfPreference или $ConfirmPreference (и некоторых других) при вызове из функций в других модулях скриптов.

Чтобы подвести итоги, сформулируем общее правило: данный подход работает правильно для двоичных модулей, но при работе и с модулями скриптов полагаться на него не следует. Если вы не уверены, проверьте его или просто считайте, что он не работает правильно.

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

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

Заключение

Мне нужно узнать, как использовать ShouldProcess всякий раз, когда это необходимо. Мне пришлось потратить много времени, чтобы научиться отличать ShouldProcess от ShouldContinue. Мне почти всегда нужно искать данные о том, какие параметры следует использовать. Поэтому не беспокойтесь, если вы все еще путаете их время от времени. Эта статья будет всегда у вас под рукой. Уверен, что я сам буду часто к ней обращаться.

Если вам понравилась эта публикация, поделитесь со мной своими идеями в Twitter по приведенной ниже ссылке. Мне всегда приятно общаться с людьми, которым были полезны мои материалы.