PowerShell ValidateSet: Completado de pestañas y valores de parámetro

Cuando estás escribiendo un script o función de PowerShell, a menudo deseas aceptar la entrada del usuario a través de parámetros. Si no limitas los valores que esos parámetros aceptan, no puedes garantizar que no haya situaciones en las que se proporcionen valores inapropiados. En este artículo, aprende cómo usar el atributo de validación de parámetro ValidateSet de PowerShell para limitar esos valores solo a los que defines.

Al escribir un script o función de PowerShell, puedes usar muchos atributos de validación diferentes para verificar que los valores suministrados a tus parámetros sean aceptables y alertar al usuario si no lo son.

Este artículo se centra en el atributo de validación ValidateSet. Aprenderás qué hace ValidateSet, por qué podrías querer usar ValidateSet en tu código y cómo hacerlo. También aprenderás sobre la función de autocompletado que se habilita con ValidateSet y ayudará a los usuarios de tu código a proporcionar valores de parámetro válidos.

Validar PowerShell: Una Breve Descripción General

ValidateSet es un atributo de parámetro que te permite definir un conjunto de elementos que solo se aceptan como valor para ese parámetro.

Por ejemplo, quizás tengas un script que esté definido para trabajar con controladores de dominio de Active Directory. Este script tiene un parámetro que especifica el nombre de un controlador de dominio. ¿No tendría sentido limitar la lista de valores aceptables a los nombres reales de los controladores de dominio? No hay razón para que el usuario pueda usar “foobar” como valor cuando ya sabes de antemano qué valores necesita el script. ValidateSet te brinda esa capacidad.

Requisitos

Este artículo será una guía de aprendizaje. Si planeas seguir, necesitarás lo siguiente:

  • Visual Studio Code u otro editor de código. Yo estaré usando Visual Studio Code.
  • Al menos PowerShell 5.1 para la mayoría del código en este artículo. Hay una sección que requiere PowerShell 6.1 o posterior y lo identificaré cuando lleguemos a eso

Todo el código en este artículo ha sido probado en los siguientes entornos:

Operating System PowerShell Versions
Windows 7 SP1 5.1, Core 6.2
Windows 10 1903 5.1, Core 6.2
Linux Mint 19.2 Core 6.2

Para ayudar a explicar los conceptos alrededor de ValidateSet vas a construir un pequeño script llamado Get-PlanetSize.ps1. Este script devuelve información sobre los tamaños de los planetas en nuestro sistema solar.

Comenzarás con un script simple y gradualmente mejorarás su capacidad para manejar la entrada del usuario final y facilitarles la búsqueda de sus posibles valores de parámetros.

Comenzando

Para empezar, copia y pega el código de PowerShell a continuación en tu editor de texto favorito y guárdalo como Get-PlanetSize.ps1.

$planets = [ordered]@{
     'Mercury' = 4879
     'Venus'   = 12104
     'Earth'   = 12756
     'Mars'    = 6805
     'Jupiter' = 142984
     'Saturn'  = 120536
     'Uranus'  = 51118
     'Neptune' = 49528
     'Pluto'   = 2306
 }
 $planets.keys | Foreach-Object {
     $output = "The diameter of planet {0} is {1} km" -f $_, $planets[$_]
     Write-Output $output
 }

Ejecuta el script desde un símbolo del sistema de PowerShell y deberías obtener lo siguiente:

PS51> .\Get-PlanetSize.ps1
 The diameter of planet Mercury is 4879 km
 The diameter of planet Venus is 12104 km
 The diameter of planet Earth is 12756 km
 The diameter of planet Mars is 6805 km
 The diameter of planet Jupiter is 142984 km
 The diameter of planet Saturn is 120536 km
 The diameter of planet Uranus is 51118 km
 The diameter of planet Neptune is 49528 km
 The diameter of planet Pluto is 2306 km

Informativo pero no muy flexible; se devuelve la información de cada planeta, incluso si solo deseas la información para Marte.

Quizás te gustaría tener la capacidad de especificar un solo planeta en lugar de devolver todos. Puedes hacer eso introduciendo un parámetro. Veamos cómo lograrlo a continuación.

Aceptando la entrada usando un parámetro

Para permitir que el script acepte un parámetro, agrega un bloque Param() en la parte superior del script. Llama al parámetro Planeta. Un bloque Param() adecuado se ve así.

En el ejemplo a continuación, la línea [Parameter(Mandatory)] asegura que siempre se suministre un nombre de planeta al script. Si falta, entonces el script solicitará uno.

Param(
     [Parameter(Mandatory)]
     $Planet
 )

La forma más sencilla de incorporar este parámetro Planeta en el script es cambiar la línea $planets.keys | Foreach-Object { a $Planeta | Foreach-Object {. Ahora no estás dependiendo estáticamente del hashtable definido anteriormente, sino que estás leyendo el valor del parámetro Planeta.

Ahora, si ejecutas el script y especificas un planeta usando el parámetro Planeta, solo verás información sobre ese planeta en particular.

PS51> .\Get-PlanetSize.ps1 -Planet Mars
 The diameter of planet Mars is 6805 km

Excelente. ¿Script completado? Quizás no.

Las opciones son demasiado abiertas

¿Qué sucede si intentas encontrar el diámetro del planeta Barsoom usando Get-PlanetSize.ps1?

PS51> .\Get-PlanetSize.ps1 -Planet Barsoom
The diameter of planet Barsoom is  km

Mmm, eso no está bien. Barsoom no está en la lista de planetas, pero el script se ejecuta de todos modos. ¿Cómo podemos solucionar esto?

El problema aquí es que el script acepta cualquier entrada y la utiliza, independientemente de si es un valor válido o no. El script necesita una forma de limitar qué valores se aceptan para el parámetro Planet. ¡Introduce ValidateSet!

Asegurando que Solo se Utilicen Ciertos Valores

A ValidateSet list is a comma-separated list of string values, wrapped in single or double-quotes. Adding a ValidateSet attribute to a script or function parameter consists of adding a line of text to the Param() block, as shown below. Replace the Param() block in your copy of Get-PlanetSize.ps1 with the one below and save the file.

Param(
     [Parameter(Mandatory)]
     [ValidateSet("Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto")]
     $Planet
 )

Intenta ejecutar el script nuevamente usando Barsoom como tu parámetro Planet. Ahora se devuelve un mensaje de error útil. El mensaje es específico en lo que salió mal e incluso proporciona una lista de valores posibles para el parámetro.

PS51> .\Get-PlanetSize.ps1 -Planet Barsoom
 Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. The argument "Barsoom" does not belong to the set
 "Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
 At line:1 char:32
 .\Get-PlanetSize.ps1 -Planet Barsoom
 ~~~
 CategoryInfo          : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
 FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1 

Haciendo que PowerShell Valide de Forma Sensible a Mayúsculas y Minúsculas

De forma predeterminada, el atributo ValidateSet es insensible a mayúsculas y minúsculas. Esto significa que permitirá cualquier cadena siempre que esté en la lista permitida con cualquier esquema de capitalización. Por ejemplo, el ejemplo anterior aceptará Mars tan fácilmente como aceptaría mars. Si es necesario, puedes forzar a ValidateSet a ser sensible a mayúsculas y minúsculas usando la opción IgnoreCase.

La opción IgnoreCase en ValidateSet, un atributo de validación, determina si los valores suministrados al parámetro coinciden exactamente con la lista de valores válidos. De forma predeterminada, IgnoreCase está establecido en $True (ignorar mayúsculas y minúsculas). Si lo estableces en $False, entonces suministrar mars como valor para el parámetro Planet para Get-PlanetSize.ps1 generarían un mensaje de error.

Usarías la opción IgnoreCase asignándole un valor $true al final de la lista de valores válidos como se muestra a continuación.

[ValidateSet("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", IgnoreCase = $false)]  

Ahora, cuando intentas usar un valor para `Planet` que no es exactamente como un valor en la lista, la validación fallará.

Usando la Completación de Tabulación

Otra ventaja de usar ValidateSet es que te brinda la completación de tabulación. Esto significa que puedes recorrer los posibles valores para un parámetro usando la tecla TAB. Esto mejora considerablemente la usabilidad de un script o función, especialmente desde la consola.

En los ejemplos a continuación, hay algunas cosas a tener en cuenta:

  • La completación de tabulación vuelve al primer valor después de haber mostrado el último.
  • Los valores se presentan en orden alfabético, aunque no estén listados en ValidateSet alfabéticamente.
  • Escribir una letra inicial y presionar TAB restringe los valores ofrecidos por la completación de tabulación a aquellos que comienzan con esa letra.
Cycling through parameter values using Tab Completion
Restricting values returned by Tab Completion

También puedes aprovechar la completación de tabulación de ValidateSet en el Entorno de Scripting Integrado de PowerShell (ISE), como se muestra en el ejemplo a continuación. La característica de Intellisense del ISE te muestra la lista de valores posibles en una caja de selección agradable.

Intellisense devuelve todos los valores que contienen la letra que escribes, en lugar de solo aquellos que comienzan con ella.

Tab Completion and Intellisense in ISE

Ahora que hemos cubierto los atributos de validación de ValidateSet tal como están en Windows 5.1, veamos qué se ha agregado en PowerShell Core 6.1 y veamos si eso puede darle a nuestro script más capacidades de validación.

Comprensión de los Cambios en ValidateSet en PowerShell 6.1

Con la llegada de PowerShell Core 6.1 se han añadido dos nuevas capacidades a los atributos de validación ValidateSet:

  • La propiedad ErrorMessage
  • Uso de clases en ValidateSet mediante acceso a System.Management.Automation.IValidateSetValuesGenerator

La propiedad ErrorMessage

El mensaje de error predeterminado generado al proporcionar un nombre de planeta incorrecto a Get-PlanetSize.ps1 es útil, pero un poco extenso:

PS61> .\Get-PlanetSize.ps1 -Planet Barsoom
 Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. The argument "Barsoom" does not belong to the set
 "Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
 At line:1 char:32
 .\Get-PlanetSize.ps1 -Planet Barsoom
 ~~~
 CategoryInfo          : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
 FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1 

Utiliza la propiedad ErrorMessage del atributo de validación ValidateSet para establecer un mensaje de error diferente, como se muestra en el siguiente ejemplo. {0} se reemplaza automáticamente con el valor enviado y {1} se reemplaza automáticamente con la lista de valores permitidos.

Param(
     [Parameter(Mandatory)]
     [ValidateSet("Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto",ErrorMessage="Value '{0}' is invalid. Try one of: '{1}'")]
     $Planet
 )

Sustituye el bloque Param() en el archivo de script y guárdalo. Luego prueba Get-PlanetSize.ps1 -Planet Barsoom de nuevo. Observa que el error ahora es mucho más conciso y descriptivo.

PS61> .\Get-PlanetSize.ps1 -Planet Barsoom
 Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. Value 'Barsoom' is invalid. Try one of: 'Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto'
 At line:1 char:32
 .\Get-PlanetSize.ps1 -Planet Barsoom
 ~~~
 CategoryInfo          : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
 FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1 

A continuación, echa un vistazo a una nueva forma de definir los valores aceptables en ValidateSet mediante una clase de PowerShell clase.

Clases de PowerShell

Los tipos personalizados, conocidos en PowerShell como clases, han estado disponibles desde la versión 5. Con la llegada de PowerShell Core 6.1, hay una nueva característica que permite el uso de una clase para proporcionar los valores para ValidateSet.

Utilizar una clase te permite eludir la limitación principal de un ValidateSet, que es estático. Es decir, está incrustado como parte de la función o script y solo se puede cambiar editando el propio script.

La nueva característica que funciona con ValidateSet es la capacidad de utilizar la clase System.Management.Automation.IValidateSetValuesGenerator. Podemos usar esto como base para nuestras propias clases mediante la herencia. Para trabajar con ValidateSet, la clase debe basarse en System.Management.Automation.IValidateSetValuesGenerator y debe implementar un método llamado GetValidValues().

El método GetValues() devuelve la lista de valores que deseas aceptar. Un bloque Param() con la lista estática de planetas reemplazada por una clase [Planet] se vería así. Este ejemplo aún no funcionará. Sigue leyendo para aprender cómo implementarlo.

Param(
     [Parameter(Mandator)]
     [ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
     $Planet
 )

Usar una Clase para una Lista de Valores ValidateSet: Un Ejemplo Real

Para demostrar el uso de una clase para una lista de valores ValidateSet, vas a reemplazar la lista estática de planetas utilizada anteriormente con una lista cargada desde un archivo de texto CSV. ¡Ya no necesitarás mantener una lista estática de valores dentro del propio script!

Creando una Fuente de Datos para la Clase

Primero, necesitarás crear un archivo CSV que contenga cada uno de los valores válidos. Para hacerlo, copia y pega este conjunto de datos en un nuevo archivo de texto y guárdalo como planets.csv en la misma carpeta que el script Get-PlanetSize.ps1.

Planet,Diameter
 "Mercury","4879"
 "Venus","12104"
 "Earth","12756"
 "Mars","6805"
 "Jupiter","142984"
 "Saturn","120536"
 "Uranus","51118"
 "Neptune","49528"
 "Pluto","2306"

Agregando la Clase al Script

Cualquier clase utilizada por ValidateSet debe estar definida antes de que ValidateSet intente usarla. Esto significa que la estructura de Get-PlanetSize.ps1 tal como está no funcionará.

La clase [Planet] debe estar definida antes de poder ser utilizada, por lo que debe ir al principio del script. Una definición de clase esquelética adecuada se ve así:

class Planet : System.Management.Automation.IValidateSetValuesGenerator {
     [String[]] GetValidValues() {
 }
 }

Dentro del método GetValidValues() de la clase, utiliza el cmdlet Import-CSV para importar el archivo de texto planets.csv creado anteriormente. El archivo se importa en una variable con alcance global, llamada $planets, para poder acceder a ella más adelante en el script.

Utiliza la instrucción return para devolver la lista de nombres de planetas a través de GetValidValues(). Después de estos cambios, la clase debería lucir así:

class Planet : System.Management.Automation.IValidateSetValuesGenerator {
     [String[]] GetValidValues() {
             $Global:planets = Import-CSV -Path planets.csv
             return ($Global:planets).Planet
     }
 }

A continuación, elimina la declaración del hash table $planets del script como se muestra a continuación. La variable global $planets, que se llena con la clase [Planet], contiene los datos del planeta.

$planets = [ordered]@{
     'Mercury' = 4879
     'Venus'   = 12104
     'Earth'   = 12756
     'Mars'    = 6805
     'Jupiter' = 142984
     'Saturn'  = 120536
     'Uranus'  = 51118
     'Neptune' = 49528
     'Pluto'   = 2306
 }

Ahora envuelve el código original restante en una función y llámala Get-PlanetDiameter para diferenciarla del nombre del script. Coloca el bloque Param() que estaba al principio del script dentro de la función. Reemplaza la lista estática de planetas con una referencia a la clase [Planet], como se muestra a continuación.

[ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]`

$output = "El diámetro del planeta {0} es {1} km" -f $_, $planets[$_]
con las siguientes dos líneas. Estas permiten que el script busque un planeta en la matriz de objetos creada por Import-CSV, en lugar de la tabla hash que creaste anteriormente y que has eliminado del script:

$targetplanet = $planets | Where -Property Planet -match $_
 $output = "The diameter of planet {0} is {1} km" -f $targetplanet.Planet, $targetplanet.Diameter

Después de este conjunto de cambios, tu script final debe lucir así:

class Planet : System.Management.Automation.IValidateSetValuesGenerator {
     [String[]] GetValidValues() {
         $Global:planets = Import-CSV -Path planets.csv
         return ($Global:planets).Planet
     }
 }
 Function Get-PlanetDiameter {
     Param(
         [Parameter(Mandatory)]
         [ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
         $Planet
     )
     $Planet | Foreach-Object {
         $targetplanet = $planets | Where -Property Planet -match $_
         $output = "The diameter of planet {0} is {1} km" -f $targetplanet.Planet, $targetplanet.Diameter
         Write-Output $output
     }
 }

Recuerda, a partir de este punto el script solo funciona en PowerShell 6.1 o posterior

Ahora, ¿cómo utilizas este script?

Ejecutando el script

No puedes usar la nueva versión del script directamente ejecutándolo. Todo el código útil está envuelto en una función. Necesitas dotear el archivo en su lugar para permitir el acceso a la función desde la sesión de PowerShell.

PS61> . .\Get-PlanetSize.ps1

Una vez dot-sourceado, ahora tienes acceso a la nueva función Get-PlanetDiameter en la sesión de PowerShell, con autocompletado.

¿Cuál es el beneficio de todo ese trabajo?”, escucho que preguntas. “El script parece funcionar de la misma manera, ¡pero es más difícil usar el código!”

Intenta esto:

  • Abre el archivo planets.csv que creaste anteriormente.
  • Agrega una nueva fila con un nuevo nombre y diámetro.
  • Guarda el archivo CSV.

En la misma sesión en la que originalmente importaste tu script, intenta buscar el diámetro del nuevo planeta usando Get-PlanetDiameter. ¡Funciona!

Usar una clase de esta manera nos brinda varios beneficios:

  • La lista de valores válidos ahora está separada del propio código, pero cualquier cambio en los valores del archivo es detectado por el script.
  • El archivo puede ser mantenido por alguien que nunca accede al script.
  • A more complex script could look up information from a spreadsheet, database, Active Directory or a web API.

Como puedes ver, las posibilidades son casi infinitas al usar una clase para proveer valores de ValidateSet.

Conclusión

Hemos cubierto mucho terreno mientras construíamos Get-PlanetSize.ps1, así que recapitulemos.

En este artículo has aprendido:

  • Qué es un atributo de validación ValidateSet y por qué podrías querer usarlo
  • Cómo agregar ValidateSet a una función PowerShell o script
  • Cómo funciona la autocompletación con ValidateSet
  • Cómo usar la propiedad IgnoreCase para controlar si tu ValidateSet distingue mayúsculas y minúsculas
  • Cómo usar la propiedad ErrorMessage con tu ValidateSet y PowerShell 6.1
  • Cómo usar una clase para hacer un ValidateSet dinámico con PowerShell 6.1

¿Qué estás esperando? ¡Comienza a usar ValidateSet hoy mismo!

Lecturas Adicionales

Source:
https://adamtheautomator.com/powershell-validateset/