Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Las funciones de PowerShell tienen varias características que mejoran considerablemente el modo en que los usuarios interactúan con ellas.
Una característica importante que a menudo se pasa por alto es la compatibilidad con -WhatIf y -Confirm, y que es fácil de agregar a las funciones. En este artículo, profundizaremos en la implementación de esta característica.
Nota:
La versión original de este artículo apareció en el blog escrito por @KevinMarquette. El equipo de PowerShell agradece a Kevin que comparta este contenido con nosotros. Visite su blog en PowerShellExplained.com.
Se trata de una característica sencilla que puede habilitar en sus funciones para proporcionar una red de seguridad a los usuarios que la necesiten. No hay nada más aterrador que ejecutar un comando que sabe que puede ser peligroso por primera vez. La opción de ejecutarlo con -WhatIf puede suponer una gran diferencia.
CommonParameters
Antes de echar un vistazo a la implementación de estos parámetros comunes, deseo echar un vistazo a cómo se usan.
Uso de -WhatIf
Cuando un comando admite el parámetro -WhatIf, le permite ver lo que habría hecho el comando en lugar de realizar cambios. Resulta interesante probar el impacto de un comando, especialmente antes de hacer algo destructivo.
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".
Si el comando implementa ShouldProcesscorrectamente, debería mostrar todos los cambios que habría realizado. A continuación se muestra un ejemplo de uso de un carácter comodín para eliminar varios archivos.
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".
Uso de -Confirm
Los comandos que admiten -WhatIf también admiten -Confirm. Esto le brinda la oportunidad de confirmar una acción antes de realizarla.
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"):
En este caso, tiene varias opciones que le permiten continuar, omitir un cambio o detener el script. El mensaje de ayuda describe cada una de estas opciones de este modo.
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"):
Localización
Este mensaje se localiza en PowerShell para que el idioma cambie según el idioma del sistema operativo. Esto es otro elemento del que PowerShell se ocupa automáticamente.
Parámetros de modificador
Vamos a echar un vistazo rápido a cómo pasar un valor a un parámetro de modificador. La razón principal por la que trato este aspecto es que es frecuente querer pasar valores de parámetro a las funciones que llama.
El primer enfoque es una sintaxis de parámetro específica que se puede usar para todos los parámetros, pero que en gran medida se usa para los parámetros de modificador. Especifique un signo de dos puntos para adjuntar un valor al parámetro.
Remove-Item -Path:* -WhatIf:$true
Puede hacer lo mismo con una variable.
$DoWhatIf = $true
Remove-Item -Path * -WhatIf:$DoWhatIf
El segundo enfoque es usar una tabla hash para expandir el valor.
$RemoveSplat = @{
Path = '*'
WhatIf = $true
}
Remove-Item @RemoveSplat
Si no está familiarizado con las tablas hash o la expansión, tengo otro artículo que trata todo lo que le interesa saber acerca de las tablas hash.
SupportsShouldProcess
El primer paso para habilitar la compatibilidad con -WhatIf y -Confirm es especificar SupportsShouldProcess en el elemento CmdletBinding de la función.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
Remove-Item .\myfile1.txt
}
Al especificar SupportsShouldProcess de esta manera, ahora podemos llamar a nuestra función con -WhatIf (o -Confirm).
PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
Observe que no he creado ningún parámetro llamado -WhatIf. Si se especifica SupportsShouldProcess, se crea automáticamente. Cuando se especifica el parámetro -WhatIf en Test-ShouldProcess, algunas cosas a las que llamamos también realizan el procesamiento de -WhatIf.
Nota:
Cuando se usa SupportsShouldProcess, PowerShell no agrega la variable $WhatIf a la función. No es necesario comprobar el valor de $WhatIf porque el método ShouldProcess() se encarga de ello.
Confiar pero verificar
Existe cierto peligro en confiar en que todo lo que llama aquí hereda valores -WhatIf. Para el resto de los ejemplos, voy a suponer que no funciona y ser muy explícito al hacer llamadas a otros comandos. Le recomiendo que haga lo mismo.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
Remove-Item .\myfile1.txt -WhatIf:$WhatIfPreference
}
Revisaremos los matices más adelante, una vez que tenga una mejor comprensión de todas las piezas en juego.
$PSCmdlet.ShouldProcess
El método que permite implementar SupportsShouldProcess es $PSCmdlet.ShouldProcess. Llame a $PSCmdlet.ShouldProcess(...) para ver si debe procesar alguna lógica y PowerShell se encarga del resto. Comencemos con un ejemplo:
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
$file = Get-ChildItem './myfile1.txt'
if($PSCmdlet.ShouldProcess($file.Name)){
$file.Delete()
}
}
La llamada a $PSCmdlet.ShouldProcess($file.name) comprueba -WhatIf (y el parámetro -Confirm) y, a continuación se ocupa de ello según corresponda. El elemento -WhatIf hace que ShouldProcess genere una descripción del cambio y devuelva $false:
PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
Una llamada mediante -Confirm pausa el script y pregunta al usuario si desea continuar. Devuelve $true si el usuario seleccionó 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"):
Una característica impresionante de $PSCmdlet.ShouldProcess es que se duplica como salida detallada. Me ayuda muchísimo habitualmente al implementar ShouldProcess.
PS> Test-ShouldProcess -Verbose
VERBOSE: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
Sobrecargas
Hay algunas sobrecargas diferentes para $PSCmdlet.ShouldProcess con distintos parámetros para personalizar los mensajes. Ya hemos visto el primero en el ejemplo anterior. Profundicemos en este aspecto.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
if($PSCmdlet.ShouldProcess('TARGET')){
# ...
}
}
Esto produce una salida que incluye el nombre de la función y el destino (valor del parámetro).
What if: Performing the operation "Test-ShouldProcess" on target "TARGET".
Al especificar un segundo parámetro como operación, se usa el valor de la operación en lugar del nombre de la función en el mensaje.
## $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".
La opción siguiente consiste en especificar tres parámetros para personalizar el mensaje por completo. Cuando se usan tres parámetros, el primero es el mensaje completo. Los dos segundos parámetros se siguen usando en la salida del mensaje de -Confirm.
## $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE
Referencia rápida de parámetro
Por si hubiera llegado aquí únicamente para averiguar qué parámetros debe usar, mostramos a continuación una referencia rápida en la que se muestra de qué manera los parámetros cambian el mensaje en los diferentes escenarios de -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
Yo suelo usar el que tiene dos parámetros.
ShouldProcessReason
Tenemos una cuarta sobrecarga que es más avanzada que las otras. Permite obtener la razón por la que se ha ejecutado ShouldProcess. Únicamente se trata aquí para que el tema quede completo, porque simplemente podemos verificar si $WhatIfPreference es $true en su lugar.
$reason = ''
if($PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION',[ref]$reason)){
Write-Output "Some Action"
}
$reason
Tenemos que pasar la variable $reason al cuarto parámetro como una variable de referencia con [ref]. ShouldProcess rellena $reason con el valor None o WhatIf. No he dicho que esto fuera útil, y nunca he tenido motivo para usarlo.
Dónde colocarlo
Utilice ShouldProcess para que los scripts sean más seguros. De este modo, se usa cuando los scripts están realizando cambios. Me gusta colocar la llamada $PSCmdlet.ShouldProcess lo más cerca posible del cambio.
## general logic and variable work
if ($PSCmdlet.ShouldProcess('TARGET','OPERATION')){
# Change goes here
}
Si estoy procesando una recopilación de elementos, lo llamo para cada elemento. De este modo, la llamada se coloca dentro del bucle foreach.
foreach ($node in $collection){
# general logic and variable work
if ($PSCmdlet.ShouldProcess($node,'OPERATION')){
# Change goes here
}
}
La razón por la que coloco ShouldProcess estrechamente alrededor del cambio es que quiero que se ejecute la mayor cantidad de código posible cuando se especifica -WhatIf. Deseo que la configuración y la validación se ejecuten, si es posible, para que el usuario pueda ver esos errores.
También me gusta usarlo en las pruebas de Pester que validan mis proyectos. Si tengo un fragmento de lógica que es difícil de simular en Pester, a menudo puedo encapsularlo en ShouldProcess y llamarlo con -WhatIf en mis pruebas. Es mejor probar parte del código que nada de él.
$WhatIfPreference
La primera variable de preferencia que tenemos es $WhatIfPreference. Es $false de manera predeterminada. Si se establece en $true, la función se ejecuta como si se hubiera especificado -WhatIf. Si establece esta configuración en la sesión, todos los comandos ejecutarán -WhatIf.
Cuando llama a una función con -WhatIf, el valor de $WhatIfPreference se establece en $true dentro del ámbito de la función.
ConfirmImpact
La mayoría de mis ejemplos son para -WhatIf, pero hasta ahora todo funciona también con -Confirm para preguntar al usuario. Puede establecer el elemento ConfirmImpact de la función en un valor alto y preguntar al usuario como si se hubiera llamado con -Confirm.
function Test-ShouldProcess {
[CmdletBinding(
SupportsShouldProcess,
ConfirmImpact = 'High'
)]
param()
if ($PSCmdlet.ShouldProcess('TARGET')){
Write-Output "Some Action"
}
}
Esta llamada a Test-ShouldProcess está llevando a cabo la acción de -Confirm debido al impacto de 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
El problema obvio es que ahora es más difícil de usar en otros scripts sin preguntar al usuario. En este caso, podemos pasar $false a -Confirm para suprimir el aviso.
PS> Test-ShouldProcess -Confirm:$false
Some Action
Explicaré cómo agregar compatibilidad con -Force en una sección posterior.
$ConfirmPreference
$ConfirmPreference es una variable automática que controla cuándo ConfirmImpact le pide que confirme la ejecución. Estos son los valores posibles para $ConfirmPreference y ConfirmImpact.
HighMediumLowNone
Con estos valores, puede especificar diferentes niveles de impacto para cada función. Si tiene $ConfirmPreference establecido en un valor superior a ConfirmImpact, no se le pedirá que confirme la ejecución.
De forma predeterminada, $ConfirmPreference está definido en High y ConfirmImpact está Medium. Si desea que la función pregunte automáticamente al usuario, establezca ConfirmImpact en High. De lo contrario, configúrelo en Medium si es destructivo y use Low si el comando siempre se ejecuta de manera segura en producción. Si lo establece en none, no se le preguntará aunque se haya especificado -Confirm (pero seguirá ofreciendo compatibilidad con -WhatIf).
Cuando llama a una función con -Confirm, el valor de $ConfirmPreference se establece en Low dentro del ámbito de la función.
Supresión de mensajes de confirmación anidados
Las funciones a las que se llama pueden seleccionar $ConfirmPreference. Esto puede crear escenarios en los que agrega un mensaje de confirmación y la función a la que llama también le pregunta al usuario.
Lo que suelo hacer es especificar -Confirm:$false en los comandos a los que llamo cuando ya me he ocupado del mensaje.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
$file = Get-ChildItem './myfile1.txt'
if($PSCmdlet.ShouldProcess($file.Name)){
Remove-Item -Path $file.FullName -Confirm:$false
}
}
Esto nos devuelve a una advertencia anterior: hay matices en cuanto a cuando -WhatIf no se pasa a una función y cuando -Confirm se pasa a una función. Prometo volver sobre ello más adelante.
$PSCmdlet.ShouldContinue
Si necesita más control de lo que proporciona ShouldProcess, puede desencadenar el aviso directamente con ShouldContinue. ShouldContinue omite $ConfirmPreference, ConfirmImpact, -Confirm, $WhatIfPreference y -WhatIf porque genera la advertencia cada vez que se ejecuta.
A simple vista, es fácil confundir ShouldProcess y ShouldContinue. Yo me acuerdo de usar ShouldProcess porque el parámetro se llama SupportsShouldProcess en CmdletBinding.
Debe usar ShouldProcess en casi todos los escenarios. Esa es la razón por la que he tratado ese método primero.
Veamos a ShouldContinue en acción.
function Test-ShouldContinue {
[CmdletBinding()]
param()
if($PSCmdlet.ShouldContinue('TARGET','OPERATION')){
Write-Output "Some Action"
}
}
Nos proporciona un mensaje más sencillo con menos opciones.
Test-ShouldContinue
Second
TARGET
[Y] Yes [N] No [S] Suspend [?] Help (default is "Y"):
El problema más importante con ShouldContinue es que requiere que el usuario lo ejecute de forma interactiva porque siempre le pregunta. Siempre debe crear herramientas que puedan utilizar otros scripts. La manera de hacerlo es mediante la implementación de -Force. Volveremos a este idea más adelante.
Sí a todo
Esto se controla automáticamente con ShouldProcess, pero tenemos que hacer más cosas para ShouldContinue. Hay una segunda sobrecarga de método en la que tenemos que pasar algunos valores por referencia para controlar la lógica.
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]"
}
}
}
He agregado un bucle foreach y una recopilación para mostrarlo en acción. He extraído la llamada ShouldContinue de la instrucción if para que sea más fácil de leer. La llamada a un método con cuatro parámetros comienza a ponerse un poco fea, pero he tratado de que tenga un aspecto lo más limpio posible.
Implementación de -Force
ShouldProcess y ShouldContinue necesitan implementar -Force de manera diferente. El truco para estas implementaciones es que ShouldProcess se debe ejecutar siempre, pero ShouldContinue no se debería ejecutar si se especifica -Force.
ShouldProcess -Force
Si establece ConfirmImpact en high, lo primero que los usuarios van a intentar es suprimirlo con -Force. Eso es lo primero que hago en cualquier caso.
Test-ShouldProcess -Force
Error: Test-ShouldProcess: A parameter cannot be found that matches parameter name 'force'.
Si lo recuerda de la sección ConfirmImpact, en realidad tienen que llamarlo así:
Test-ShouldProcess -Confirm:$false
No todos los usuarios saben que necesitan hacerlo y -Force no suprime ShouldContinue.
Por tanto, debemos implementar -Force para que nuestros usuarios lo vean claro. Echemos un vistazo aquí a este ejemplo completo:
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"
}
}
Agregamos nuestro propio modificador -Force como parámetro. El parámetro -Confirm se agrega automáticamente al usar SupportsShouldProcess en CmdletBinding. Pero cuando se usa SupportsShouldProcess, PowerShell no agrega la variable $Confirm a la función. Si se ejecuta en modo strict e intenta usar la variable $Confirm antes de que se haya definido, obtendrá un error. Para evitar el error, puede usar $PSBoundParameters para probar si el usuario pasó el parámetro.
if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) {
$ConfirmPreference = 'None'
}
Si el usuario especifica -Force, establecemos $ConfirmPreference en None en el ámbito local. Si el usuario también especifica -Confirm, ShoudProcess() respeta los valores del parámetro -Confirm.
if ($PSCmdlet.ShouldProcess('TARGET')){
Write-Output "Some Action"
}
Si alguien especifica -Force y -WhatIf, -WhatIf debe tener prioridad. Este enfoque conserva el procesamiento de -WhatIf porque ShouldProcess siempre se ejecuta.
No agregue una prueba para el valor $Force dentro de la instrucción if con ShouldProcess. Es un antipatrón para este escenario específico, aunque eso es lo que les muestro en la siguiente sección para ShouldContinue.
ShouldContinue -Force
Esta es la manera correcta de implementar -Force con ShouldContinue.
function Test-ShouldContinue {
[CmdletBinding()]
param(
[Switch]$Force
)
if($Force -or $PSCmdlet.ShouldContinue('TARGET','OPERATION')){
Write-Output "Some Action"
}
}
Al colocar $Force a la izquierda del operador -or, se evalúa primero. Al escribirlo de esta manera, se cortocircuita la ejecución de la instrucción if. Si $force es $true, no se ejecuta ShouldContinue.
PS> Test-ShouldContinue -Force
Some Action
No es necesario preocuparse por -Confirm o -WhatIf en este escenario porque no son compatibles con ShouldContinue. Esta es la razón por la que debe administrarse de manera diferente que ShouldProcess.
Problemas de ámbito
Se supone que el uso de -WhatIf y -Confirm se aplica a todo el contenido de las funciones y a todo lo que llaman. Para ello, se establece $WhatIfPreference en $true o se establece $ConfirmPreference en Low en el ámbito local de la función. Cuando se llama a otra función, las llamadas a ShouldProcess usan esos valores.
Realmente funciona correctamente la mayor parte del tiempo. Cada vez que se llama a un cmdlet integrado o a una función en el mismo ámbito, funciona. También funciona cuando se llama a un script o una función en un módulo de script desde la consola.
El único lugar en el que no funciona es cuando un script o un módulo de script llama a una función en otro módulo de script. Es posible que esto no parezca un problema importante, pero la mayoría de los módulos que crea o extrae de PSGallery son módulos de script.
El problema principal es que los módulos de script no heredan los valores de $WhatIfPreference o $ConfirmPreference (y otros) cuando se llaman desde funciones de otros módulos de script.
La mejor manera de resumir esto como regla general es que funciona correctamente para módulos binarios y nunca debe confiar en que funcione para módulos de script. Si no está seguro, pruébelo o simplemente asuma que no funciona correctamente.
Personalmente, creo que es muy peligroso porque crea escenarios en los que agrega compatibilidad con -WhatIf a varios módulos que funcionan correctamente de forma aislada, pero no funcionan correctamente cuando se llaman entre sí.
Tenemos un RFC de GitHub trabajando para solucionar este problema. Consulte Propagación de las preferencias de ejecución más allá del ámbito del módulo de script para obtener más detalles.
Conclusión
Tengo que buscar cómo usar ShouldProcess cada vez que lo necesito. Me llevó mucho tiempo distinguir ShouldProcess de ShouldContinue. Casi siempre tengo buscar los parámetros que quiero usar. Por lo tanto, no se preocupe si se sigue confundiendo de vez en cuando. Este artículo estará aquí cuando lo necesite. Estoy seguro de que yo mismo lo consultaré con frecuencia.
Si le gustó esta publicación, comparta sus reflexiones conmigo en Twitter mediante el vínculo siguiente. Siempre me gusta escuchar a personas que obtienen valor de mi contenido.