Scripting Avanzado en PowerShell
Scripting Avanzado en PowerShell
Xavi Aznar
Buy on Leanpub

Introducci贸n

Work in Progress

Empezando con el scripting

Ya sabes como empieza todo; con un problema. Puedes resolverlo a mano. Quizás ya lo has hecho alguna vez. Pero es tedioso y muy costoso en tiempo. Sabes que hay una maneja mejor, más eficaz. Abres la consola de PowerShell, enlazas un par de cmdlets mediante un pipe y ¡listo!

Probablemente tengas unos cuantos de estos comandos enlazados - one-liners - guardados en el disco duro, en uno o varios ficheros de texto que reutilizas de vez en cuando, cambiando los valores hardcodeados en los cmdlets.

El objetivo de este libro es pasar de esos cmdlets que te solucionan la papeleta a escribir potentes scripts reutilizables que facilitan tu trabajo diario.

Así pasarás de ser alguien que resuelve problemas a ser quien crea las herramientas que transforman la manera de trabajar.

PowerShell v3 funciona en Window 7 SP1, Server 2008 SP2 o Server 2008 R2 SP1, Windows 8 y Windows 2012. PowerShell v3 requiere .Net 4.0.

Para empezar, recuerda siempre ejecutar Powershell como administrador.

Seguridad en PowerShell

PowerShell se ha diseñado para ser seguro, a diferencia de otros lenguajes de scripting como Visual Basic Script (vbs).

Al hacer doble click sobre un script PowerShell, no se ejecuta, si no que el script se abre con el editor de texto por defecto del sistema. De esta forma nadie puede ejecutar un script por error. Los ficheros ps1, por tanto, están asociados por defecto con Notepad.

Incluso si intentas ejecutar el script desde la línea de comandos -para evitar que alguien instale un script que suplante un comando de sistema, por ejemplo- hay que indicar la ruta completa al script (incluso cuando se ejecuta desde la misma carpeta del sistema).

Aún así, la política de ejecución de scripts en el sistema impide que los scripts se ejecuten; es necesario modificar la política de ejecución para permitir la ejecución de scripts PowerShell.

En Windows 2012 WinRM viene activado por defecto, ya que el objetivo es ser capaces de gestionar grandes cantidades de servidores con el menor esfuerzo posible.

Políticas de ejecución de scripts en PowerShell

En Windows 2012 R2 la política de ejecución por defecto es RemoteSigned. Ahora veremos qué seignifica ésto y de qué otras opciones disponemos:

  • Unrestricted: PowerShell permite ejecutar cualquier script, independientemente del origen (es decir, permite ejecutar scripts descargados desde internet). Es una mala idea establecer la política de ejecución de scripts como Unrestricted.
  • AllSigned y RemoteSigned: En cualquier de estos dos casos, para poder ejecutar un script, debe haber sido firmado mediante un certificado digital que identifique a su autor. AllSigned obliga a que cualquier script que vaya a ejecutarse deba estar firmado por alguien en quien confiamos. En el caso de RemoteSigned sólo deben estar firmados aquellos scripts descargados de internet; los scripts creados localmente se pueden ejecutar con normalidad.

En un entorno empresarial, podemos establecer la política de ejecución de scripts a través de políticas de dominio.

Para obtener el estado de la política de ejecución actual:

1 PS> Get-ExecutionPolicy

Para establecer la política de ejecución de scripts:

1 PS> Set-ExecutionPolicy RemoteSigned

Ejecutando scripts en PowerShell

Una vez hemos establecido la política de ejecución, creamos un script sencillo que obtiene el estado del servicio BITS (Background Intelligent Transfer Service) y lo guardamos como bits.ps1.

Si intentamos ejecutar bits.ps1, incluso desde la carpeta en la que se encuentra el script, PowerShell responde con un error, indicando que bits.ps1 no se reconoce como el nombre de un cmdlet válido.

Para poder ejecutarlo, debemos indicar la ruta completa al script o, si estamos en la misma carpeta donde se encuentra el script, debemos indicar .\bits.ps1 por seguridad.

PowerShell ISE

Cuando ejecutamos un script desde la línea de comandos, al finalizar la ejecución del mismo, las funciones, variables, etc se eliminan.

Para poder guardar las funciones que ejecutamos en la memoria -al menos, mientras la ventana de PowerShell siga abierta-, podemos usar el método del dot sourcing:

1 PS> . .\bits.ps1

Precediendo el nombre del script con un punto, las funciones definidas dentro del script dot sourceado estarán disponibles en la ventana de PowerShell y podrán ser utilizadas tanto desde la línea de comandos como en otros scripts que lancemos.

En general, usaremos Windows PowwerShell ISE para escribir y desarrollar los scripts, por lo que la técnica anterior, aunque es bueno conocerla, no debería ser usada frecuentemente.

Windows PowerShell ISE es un editor de texto especializado en PowerShell proporcionado por el equipo de desarrollo de PowerShell.

Es importante ejectuar Windows PowerShell ISE como administrador.

ISE muestra por defecto un panel lateral que permite explorar los cmdlets (puede mostrarse ejecutando Show-Commands en el panel de la consola). Al seleccionar un cmdlet se muestran los diferentes parámetros que acepta el cmdlet como un formulario, indicando aquellos parámetros que son obligatorios, el tipo de los parámetros, etc. Desde este panel podemos ejecutar el cmdlet en la cónsola o copiarlo al editor integrado en ISE.

ISE también cuenta con IntelliSense, que muestra ayudas para completar los nombres de cmdlets, parámetros, métodos, etc asociados al elemento que estamos escribiendo en cada momento.

Por supuesto, ISE también colorea el código para que sea más fácil detectar errores, proporcionando ayuda si dejamos paréntesis huérfanos y muchos otros detalles que facilitan enormemente el desarrollo de scripts.

La consola de ISE también proporciona IntelliSense, autocompletado, resaltado de sintaxis, copiar y pegar con Ctrl+C y Ctrl+V.

Quizás la configuración más adecuada sea la de tener el editor ocupando la pantalla completamente, con la consola oculta, ya que podemos cambiar entre la ventana de edición y la consola mediante Ctrl+R.

Mediante Ctrl+J ISE muestra snippets, trozos de código fuente para agilizar la creación de scripts (usándolos como plantillas). Otra funcionalidad interesante es la de poder ejecutar únicamente el texto seleccionado mediante F8 o todo el script con F5.

Desbloqueando scripts descargados de internet

La política de ejecución por defecto en Windows 2012 (y superior) es RemoteSigned, de manera que si descargamos un script de internet, no podremos ejecutarlo aunque lo revisemos y confiemos en que no contiene código malicioso, ya que la seguridad de PowerShell bloqueará su ejecución.

Tenemos dos opciones: establecer la política de ejecución de scripts como Unrestricted, que es una muy mala idea, o usar el cmdlet Unblock-File para deshacer el bloqueo del sistema (eliminando la marca de que el script ha sido descargado de internet).

PowerShell como leguaje de scripting

Variables

Las variables son espacio en memoria donde almacenar cosas.

Asignación de variables

La asignación de variables se realiza mediante el operador =. El nombre de la variable comienza siempre con el símbolo $. El nombre de la variable puede contener cualquier caracter, excepto :.

Para definir una varible cuyo nombre contenga espacios, el nombre debe ir entre llaves:

1 $MiVariable = 2
2 ${Mi variable} = "Hola mundo!"

Inspeccionando el contenido de una variable

La manera más sencilla de averiguar qué contiene una variable es escribiendo el nombre de la variable en la consola:

1 PS> $MiVariable
2 2

En los scripts, la manera correcta de mostrar el contenido de una variable es mediante el cmdlet Write-Output.

Asignando un tipo a una variable

Para definir una variable, simplemente le asignamos un valor. En realidad tenemos un conjunto de cmdlets que nos permiten definir una nueva varibale con New-Variable, especificar un valor con Set-Variable, etc. Sin embargo, es mucho más cómodo escribir directamente el nombre de la variable y asignarle un valor.

Al crear un variable, si no especificamos el tipo de variable, se considera un objeto.

Para especificar el tipo el tipo de variable, precedemos la definición por corchetes, indicando el tipo asignado:

1 [string]$MiNombre = "Xavi"

Si intentamos asignar un valor a una variable que no coincide con el tipo definido para la variable, obtenemos un error indicando que no se puede convertir el tipo proporcionado en el tipo definido para la variable.

Así pues, al asignar tipos a las variables estamos haciendo que PowerShell trabaje por nosotros validando los datos introducidos, lo que es una buena práctica y mejorará sustancialmente la calidad de los scripts que escribamos.

De hecho, a diferencia de la asignación en otros lenguajes de scripting, en PowerShell las variables son mucho más potentes. Observa el siguiente código:

1 [validateset("a","b","c")][string]$x = "a"

Si intentamos asignar un valor que no esté contenido en el conjunto especificado, PowerShell genera un error:

1 PS> $x = "test"
2 The variable cannot be validated because the value test is not a valid value for \
3 the x variable.

Otro concepto relacionado con los diferentes tipos de variables es lo que se llama casting y que podría traducirse como conversión. Vamos a ver un ejemplo:

1 PS> $x = [int]"1"
2 PS> $x
3 1
4 
5 PS> $x = "test"
6 PS> $x
7 test

En la primera asignación de la variable $x, pasamos la cadena "1" a la variable. Al ir precedida de un tipo -[int] en nuestro ejemplo-, PowerShell convierte la cadena "1" en el entero 1 y después lo asigna a $x. Esto es lo que se llama casting (conversión de algo en otro tipo).

A continuación, asignamos una cadena a $x y no tenemos ningún problema. Esto es así porque en la definición de $x no se ha especificado que sea de un tipo concreto; por tanto $x puede contener cualquier tipo de variable.

Sin embargo, si especificamos un tipo específico, al intentar asignar un valor de otro tipo, obtenemos un error, como en el ejemplo siguiente:

1 PS> [int]$x = "1"
2 PS> $x
3 1
4 
5 PS> $x = "test"
6 Cannot convert value "test" to type "System.Int32". Error: "Input string was not \
7 in a correct format."

El uso de tipos en los scripts está relacionado con la validación de los datos que se introducen como parámetros en los scripts, por lo que es una buena práctica especificarlos. Esto supone un cambio importante con respecto a la forma de trabajar de forma interactiva en el consola, donde no suelen especificarse los tipos de las variables.

El uso de tipos nos permite aprovechar la potencia de PowerShell y los métodos asociados a cada tipo de variable.

1 PS> $navidad = "25/12/2016"
2 PS> $navidad
3 25/12/2016

Si utilizamos IntelliSense, observaremos que al escribir $navidad., IntelliSense ofrece métodos relacionados con cadenas, como $navidad.Length, etc.

Sin embargo, si especificamos el tipo de la variable $navidad:

1 PS> [datetime]$navidad = "25/12/2016"
2 PS> $navidad
3 25/12/2016

En la consola no observamos ninguna diferencia con el ejemplo anterior. Sin embargo ahora, las opciones que nos ofrece IntelliSense son muy diferentes: escribe $navidad. y verás que los métodos que aparecen son Day, DayOfWeek, etc. Es decir, al especificar el tipo, PowerShell sabe que se trata de una fecha y tenemos disponibles todos los métodos asociados a la manipulación del tipo especificado que proporciona PowerShell.

Comillas

En PowerShell las comillas pueden ser simples '...' o dobles "..." ; dependiendo del tipo de comillas, PowerShell actúa de manera diferente.

1 PS> $palabra = "PowerShell"
2 PS> "Esta es la variable $palabra y $palabra MOLA!"
3 Esta es la variable PowerShell y PowerShell MOLA!

Al encontrar una variable entre comillas dobles, PowerShell sustituye la variable por su valor.

Si las comillas son simples, no se realiza la evaluación:

1 PS> $palabra = "PowerShell"
2 PS> 'Esta es la variable $palabra y $palabra MOLA!'
3 Esta es la variable $palabra y $palabra MOLA!

Podemos anular de manera individual la evaluación de variables entre comillas dobles precediendo el nombre de la variable por un acento grave (backtick):

1 PS> $palabra = "PowerShell"
2 PS> "Esta es la variable `$palabra y $palabra MOLA!"
3 Esta es la variable $palabra y PowerShell MOLA!

En el ejemplo anterior la variable es una cadena. Pero si fuera un objeto, nos encontraríamos con un escenario diferente:

1 PS> $p = Get-Process lsass
2 PS> "Process ID = $p.id"
3 Process ID is System.Diagnostics.Process (lsass).id

El resultado no es el esperado. De hecho, vemos el resultado System.Diagnostics.Process (lsass) es $p.ToString(), con lo que lo que hemos obtenido es la conversión de $p a una cadena, a la que se ha concatenado la cadena .ip. Y no queríamos ésto.

La solución es usar paréntesis:

1 PS> $p = Get-Process lsass
2 PS> "Process ID = $( $p.id )"
3 584

Ahora la salida sí que es la propiedad Id del objeto que tiene la información sobre el proceso lsass.

En realidad, usando $(...) dentro de una cadena, se ejecuta en primer lugar la expresión entre los paréntesis, que a continuación se convierte en una cadena para poder mostrarse en la consola.

Siguiendo con el ejemplo anterior, podemos escribir cualquier fragmento de código entre los paréntesis:

1 PS> $p = Get-Process lsass
2 PS> "Process ID = $( Read-Host -Prompt "Escribe algo" )"

Si ejecutamos el código anterior, tenemos:

1 PS> Escribe algo: Hola mundo!
2 Process ID = Hola Mundo!

Es decir, PowerShell evalúa el código que encuentra en $( ... ) y lo ejecuta siempre y cuando la expresión se encuentre entre comillas dobles. El código en $(...) se llama subexpresión.

Si repetimos el ejemplo anterior cambiando las comillas dobles por comillas simples, no se realiza evaluación del contenido, por lo que PowerShell no evalúa el código de la subexpresión y se muestra como parte de la cadena:

1 PS> $p = Get-Process lsass
2 PS> 'Process ID = $( Read-Host -Promtp "Escribe algo" )'
3 Process ID = $( Read-Host -Promtp "Escribe algo" )

Existe un tercer tipo de cadenas: las here-strings. Las here-strings son cadenas que se extienden en múltiples líneas, y en las que tampoco se realiza evaluación de las subexpresiones.

Objetos

Hemos visto en la sección anterior que en PowerShell, si no asignamos un tipo a una variable, por defecto se considera de tipo object.

En realidad, en PowerShell, todo es un objeto.

Así, cuando ejecutamos PS> $servicio = Get-Service -Name BITS en PowerShell, la variable $servicio no almacena una “cadena” con el “nombre”, el estado o cualquier otra propiedad relacionada con el servicio BITS: en PowerShell se almacena un objeto que contiene propiedades y métodos asociados con el servicio.

 1 PS> $servicio | Get-Member
 2    TypeName: System.ServiceProcess.ServiceController
 3 
 4 Name                      MemberType    Definition
 5 ----                      ----------    ----------
 6 Name                      AliasProperty Name = ServiceName
 7 RequiredServices          AliasProperty RequiredServices = ServicesDependedOn
 8 Disposed                  Event         System.EventHandler Disposed(System.Objec\
 9 t, System.Eve...
10 Close                     Method        void Close()
11 ...

Mediante Get-Member podemos inspeccionar el contenido de un objeto.

En la salida del comando observamos que en la variable $servicio tenemos almacenadas propiedades y métodos relacionados con el servicio. Por tanto, podemos lanzar $servicio.Start() y comprobar su estado mostrando la propiedad Status: $service.Status.

En realidad las variables pueden contener múltiples objetos (llamadas colecciones o arrays). Podemos almacenar el resultado de Get-Service en una variable. Como Get-Service devuelve una colección de objetos, podemos acceder a cualquier objeto almacenado indicando su índice en la colección (empezando a contar por 0):

 1 PS> $servicios = Get-Service
 2 PS> $servicios[0].Name
 3 Background Intelligent Transfer Service
 4 PS> $servicios[0].Status
 5 Stopped
 6 PS> $servicios[0].Name.ToUpper()
 7 BACKGROUND INTELLIGENT TRANSFER SERVICE
 8 PS> $servicios[4..8].Name
 9 AppMgmt
10 AppReadiness
11 AppXSvc
12 AudioEndpointBuilder
13 Audiosrv

.. es el operador rango, por lo que $servicios[4..8]es equivalente a $servicios[4], $servicios[5], $servicios[6], $servicios[7], $servicios[8].

Paréntesis

Al igual que en Matemáticas, en PowerShell los paréntesis ayudan a indicar la prioridad en la que deben ejecutarse las expresiones que contienen.

Podemos usar los paréntesis de la siguiente manera:

1 PS> Get-Service -ComputerName (Get-Content c:\computers.txt)

En el ejemplo anterior, la expresión entre paréntesis (Get-Content c:\computers.txt) se evalúa primero, obteniendo una lista de nombres de equipo que después se pasan al cmdlet Get-Service, para acabar obteniento una lista de los servicios en todos los equipos indicados en el fichero de texto.

Del mismo modo que usamos Get-Content para obtener el contenido de un fichero de texto, podemos usar Import-CSV para importar el contenido de un fichero CSV.

Es importante notar que, al importar un fichero -CSV o TXT-, no estamos obteniendo “cadenas” (strings) sino objetos:

1 PS> Import-CSV c:\computers.csv | Get-Member
2 	TypeName: System.Management.Automation.PSCustomObject
3 ...

Por tanto, si vamos a usar el contenido del fichero a través del pipeline, debemos seleccionar la propiedad específica que necesitamos para el siguiente cmdlet en el pipeline.

1 PS> Get-Service -ComputerName (Import-CSV c:\computers.csv) | Select -ExpandPrope\
2 rty ComputerName)

En el ejemplo anterior, primero se evalúa el contenido entre los paréntesis e importamos el fichero CSV. Obtenemos un objeto que contiene múltiples filas y columnas. El parámetro -ComputerName del cmdlet Get-Service espera una cadena, por lo que filtramos mediante Select la propiedad ComputerName y la expandimos, obteniendo su valor: la cadena con el nombre de cada equipo en el CSV bajo la columna que hemos etiquetado como ComputerName.

Si sólo usamos Select -Property, obtenemos un objeto que contiene la columna etiquetada como ComputerName del CSV:

1 PS> Import-Csv c:\computers.csv | select -property Computername | Get-Member 
2 TypeName: Selected.System.Management.Automation.PSCustomObject
3 ...

Como -ComputerName en Get-Service espera una cadena, el script anterior no funcionaría.

Repetimos el script anterior pero usando Select -ExpandProperty para comprobar que ahora sí que obtenemos una cadena (string):

1 PS> Import-Csv c:\computers.csv | select -ExpandProperty Computername | Get-Member
2 TypeName: System.String
3 ...

En PowerShell versión 3 podemos simplificar la sintaxis y hacer, sencillamente:

1 PS> Get-Service -ComputerName (Import-CSV c:\computers.csv).ComputerName

Podemos complicarlo un poco más y lanzar:

1 PS> Invoke-Command -ComputerName (
2 	Get-ADComputer -Filter "name -like '*c*'" |
3     Select -ExpandProperty Name) -ScriptBlock {Get-Service -Name Bits}

Primero se ejecuta lo que hay en el interior del paréntesis, con lo que PowerShell carga dinámicamente el módulo de Active Directory, se conecta al AD, obtiene una lista de objetos tipo computer que contienen una letra c, y a continuación lanza la ejecución remota de Get-Service en ellos mediante Invoke-Command.

Estructuras lógicas

Una estructura lógica sirve para realizar evaluaciones lógicas al comparar dos cosas:

If … Elseif … Else

1 If ($esto -eq $aquello) {
2 	#comandos
3 } elseif {$aquelloOtro -ne $otraCosa}
4 	# más comandos
5 } else {
6 	# todavía más comandos
7 }

elseif es equivalente a:

1 if ($esto -eq $aquello) {
2 	# comandos
3 } else {
4 	if ($$aquelloOtro -ne $otraCosa) {
5     	#más comandos
6     }
7 } ...

Así que para evitar anidar más y más bloques, simplemente los fusionamos en una nueva palabra clave elseif.

Switch

Otra estructura lógica que permite seleccionar en función de múltiples valores que puede tener una variable es switch:

1 Switch ($Status) {
2 	0 {"$status_text = 'Ok'"}
3 	1 ("$status_text = 'Error'")
4     ...
5     default ("$status_text = 'Estado desconocido'")
6 }

Aunque esta forma es más clara para algunos, la repetición constante de “$status_text = ‘blah’” no es la más elegante. Podemos mejorar el script anterior:

1 $status_text = Switch ($status) {
2 	0 {'Ok'}
3 	1 {'Error'}
4     ...
5     default {'Error desconocido'}
6 }

En PowerShell la mayoría de las expresiones son asignables. Esto significa que la asignación que hemos realizado del resultado del switch a una variable también podemos hacerlo con otras estructuras , como en el siguiente ejemplo para un if: powershell PS> $resultado = if ($false) {1} else {2} PS> $resultado 2

Estructuras de bucles

Las estructuras de blucles permiten repetir una misma acción más de una vez.

Tenemos diferentes estructuras de bucles, parecidas pero ligeramente diferentes.

Do … While/Until

Realiza una acción mientras se evalúa como $true la condición especificada en el While.

Los comandos dentro del bloque del Do se ejecutan como mínimo una vez.

Es importante que dentro del bloque Do se modifique la variable que usamos como control en la expresión que se evalúa en el While; en caso contrario podría darse un bucle infinito que no acabaría nunca.

Un ejemplo de bucle Do ... While es:

1 $i = 1
2 Do {
3 	Write-Output "Powershell $i"
4     $i++ # Incrementa i en 1
5 } While ($i -lt 5)

Del mismo modo podemos crear un bucle con Do ... Until:

1 $i = 1
2 Do {
3 	Write-Output "Powershell $i"
4     $i++2 # Incrementa i en 2
5 } Until ($i -gt 5)

While

A diferencia del bucle Do ... While/Until, los comandos dentro de un bucle While pueden no ejecutarse nunca.

1 While ($i -gt 1) {
2 	Write-Output "Powershell $i
3     $i-- # Decrementa el valor de $i en 1
4 }

Si en el ejemplo anterior $i tiene el valor $i = 0, la condición del While no se cumple (y los comandos del bloque no se ejecutarán). Si inicialmente $i = 5, el bucle While se repetirá hasta que deje de cumplirse la condición en el While.

Foreach

El bucle más usado en PowerShell es, sin duda alguna, Foreach.

La ventaja de Foreach respecto a Do..While/Until o While es que no tenemos que saber cuáles son los límites del bucle. Por ejemplo, cuando tenemos una colección de servicios, no sabemos si hay 5 o 50, por lo que con el bucle fearch, el bucle se repite tantas veces como elementos haya en la colección, parando cuando los ha recorrido todos, independientemente del número que haya.

1 $servicios = Get-Service -Name b* # Obtenemos los servicios cuyo nombre empieza p\
2 or "b"
3 foreach ($servicio in $servicios) {
4 	$servicio.DisplayName
5 }

La ejecución del script anterior daría como resultado:

1 Base Filtering Engine
2 Background Intelligent Transfer Service
3 Background Tasks Infrastructure Service
4 Computer Browser
5 Bluetooth Support Servic

For

La estructura del bucle For es la siguiente:

1 for($i=0; $i -lt 5; $i++) {
2 	# haz cosas
3 }

En un bucle for tenemos las mismas partes que en los bucles Do...While/Until comprimidos:

  • declaramos el valor inicial de la variable de control
  • especificamos la condición que debe verificarse para que se ejecuten los comandos del bucle
  • modificamos la variable de control al final en cada ciclo del bucle

Podemos combinar el operador rango con foreach para hacer algo similar a un bucle for usando el pipeline:

1 1..5 | foreach -process {start calc}

Scripts sencillos y funciones

Convirtiendo comandos en scripts

El proceso que vamos a seguir para convertir un conjunto de cmdlets, en una herramienta que podamos reutilizar, es:

  1. Hacer que funcione en la consola
  2. Convertir en script
  3. Parametrizar

Abrimos ISE (como Administrador). Ejecutamos el cmdlet que queremos en la consola:

1 PS> Get-WMIObject -Class win32_bios -computerName dc, vm1, vm2

Si lo ejecutamos y funciona, lo más conveniente es guardarlo en un fichero bios.ps1, para cuando lo volvamos a necesitar.

Una vez que hayamos creado el script, lo ejecutamos desde la línea de comandos para comprobar que funciona:

1 PS> .\bios.ps1

Sin embargo, ¿qué pasa si necesitamos obtener la informacion de diferentes equipos a los que hemos especificado en el script? Tenemos que buscar el script, abrirlo, identificar qué es lo que hay que cambiar y cambiarlo, volviendo a guardar el script para poder ejecutarlo.

Obviamente, esta no es la opción mas productiva.

Vamos a ver cual es el proceso a seguir partiendo del siguiente comando (guardado como dinfo.ps1, que tampoco es la mejor manera de llamar al script):

1 Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= 'c:'" -ComputerName loc\
2 alhost

A partir de este script que funciona, vamos a ir refinando y formalizando el script hasta convertirlo en una herramienta que otros puedan usar para solucionar problemas.

Si ejecutamos el código anterior:

1 DeviceID     : C:
2 DriveType    : 3
3 ProviderName : 
4 FreeSpace    : 11966959616
5 Size         : 26317156352
6 VolumeName   : System

En primer lugar, vamos a modificar la información que se muestra al ejecutar el comando para adecuarlo a nuestras necesidades.

Sólo queremos mostrar el tamaño y el espacio libre del disco C:, así que seleccionamos:

1 Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= 'c:'" -ComputerName loc\
2 alhost |
3     select pscomputername, DeviceID,
4     @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
5     @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }

Además de obtener las propiedades directamente del objecto devuelto por Get-WMIObject, usamos las propiedades calculadas o propiedades dinámicas para modificar la información obtenida del objeto y generar una nueva al vuelo. La primera propiedad calculada es el tamaño del disco: Name es el nombre de la propiedad y Expression es una expresión que, en nuestro caso, convierte el valor de $_.size en gigabytes y después lo redondea convirtiéndolo a un entero.

En general, en estas propiedades calculadas suelen utilizarse los nombres cortos n (para Name) y e (para Expression).

El resultado es mucho más agradable y profesional:

1 PSComputerName  deviceid Size(GB) Free(GB)
2 --------------  -------- -------- --------
3 WIN-DFHSBDD3VL1 C:             25       11

Eligiendo variables para parametrizar los scripts

El siguiente paso es identificar qué partes del script pueden cambiar si necesitamos volver a ejecutar el script. Por ejemplo, podemos estar interesados en el espacio libre de otro disco diferente al c: o de otro equipo que no sea el equipo local…

Así que sustituimos estos valores por variables. Aprovechamos para especificar el tipo de estas variables:

1 [string]$Drive = 'C:'
2 [string]$ComputerName = 'localhost'
3 
4 Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= '$Drive'" -ComputerName\
5  $ComputerName |
6     select pscomputername, DeviceID,
7     @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
8     @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }

Ejecutamos de nuevo el script para comprobar que sigue funcionando después de las modificaciones introducidas.

Hemos avanzado un paso más, ya que ahora, si alguien tiene que modificar el script, como mínimo tiene todas las variables al principio, lo que facilitará su tarea.

Añadiendo parámetros al script

Vamos a seguir adelante. La siguiente modificación es rodear las variables del script con el keyword param(...) (con las variables separadas por comas):

1 param(
2     [string]$Drive = 'C:',
3     [string]$ComputerName = 'localhost'
4 )
5 Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= '$Drive'" -ComputerName\
6  $ComputerName |
7     select pscomputername, DeviceID,
8     @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
9     @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }

De nuevo, si ejecutamos el script, todo sigue funcionando. Si verificamos el funcionamiento desde la consola de ISE, observamos que IntelliSense nos muestra una lista de los parámetros disponibles para nuestro script:

Script Autocompletion on ISE Console
Script Autocompletion on ISE Console

Hasta ahora, la ejecución del script estaba utilizando los valores especificados en la declaración de las variables como valores por defecto.

Sin embargo, como PowerShell ha identificado ComputerName y Drive como parámetros, podemos proporcionar valores diferentes desde la línea de comandos.

En el siguiente ejemplo, cambiamos el nombre del equipo donde queremos evaluar el espacio libre, aunque dejamos la opción por defecto para el parámetro Drive:

1 PS> .\dinfo.ps1 -ComputerName dc

El siguiente paso es agregar una nueva línea al script: [CmdletBinding()]:

 1 [CmdletBinding()]
 2 param(
 3     [string]$Drive = 'C:',
 4     [string]$ComputerName = 'localhost'
 5 )
 6 Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= '$Drive'" -ComputerName\
 7  $ComputerName |
 8     select pscomputername, DeviceID,
 9     @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
10     @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }

Con esta única línea, hemos transformado un script en un cmdlet. Al añadir [CmdletBinding()] PowerShell agrega un montón de funcionalidad al script, como veremos a continuación.

Sin embargo, para ejecutar este nuevo cmdlet, sigue siendo necesario escribir .\dinfo.ps1, lo que lo diferencia de un cmdlet “normal”.

Funciones

El siguiente paso es convertir el script en una función; para ello, rodeamos el código con la palabra clave function:

 1 function dinfo {
 2     [CmdletBinding()]
 3     param(
 4         [string]$Drive = 'C:',
 5         [string]$ComputerName = 'localhost'
 6     )
 7     Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= '$Drive'" -Computer\
 8 Name $ComputerName |
 9         select pscomputername, DeviceID,
10         @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
11         @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }
12 }

Si ejecutamos en ISE (mediante F5) la función, funciona perfectamente. En la consola de ISE ya podemos ejecutar dinfo para obtener la información del disco c: de la máquina local o de cualquier máquina remota proporcionando el nombre del equipo remoto.

Pero si intentamos repetir este proceso en desde una consola de PowerShell, obtenemos un error:

1 PS> .\dinfo.ps1
2 PS> dinfo
3 dinfo : The term 'dinfo' is not recognized as the name of a cmdlet, function, scr\
4 ipt file, or operable program. Check the spelling of the name, or if a path was i\
5 ncluded, verify that the path is correct and try again.

## Ejecutando tus scripts/funciones

El problema está en que, en la consola, al ejecutar una función, se carga en memoria. Pero al finalizar la ejecución, se descarga de la memoria y por tanto, no está disponible en la consola.

Por tanto, cuando estemos testeando funciones, lo que podemos hacer es utilizar la técnica del dot sourcing:

1 PS> . .\dinfo.ps1
2 PS> dinfo

En este caso, la función sí que estaría disponible, ya que con el dot sourcing de la función estamos indicando a PowerShell que no la descargue de la consola.

Es importante recordar que cada vez que modificamos la función o funciones en el fichero, debemos “dot sourcearlo” de nuevo (o estaremos usando la versión anterior, sin los últimos cambios).

Por tanto, ya estamos cerca de tener un cmdlet creado por nosotros mismos.

Antes de seguir, vamos a renombrar la función siguiendo el estilo recomendado para los cmdlets en la forma verbo-nombre:

 1 function Get-DiskInfo {
 2     [CmdletBinding()]
 3     param(
 4         [string]$Drive = 'C:',
 5         [string]$ComputerName = 'localhost'
 6     )
 7     Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= '$Drive'" -Computer\
 8 Name $ComputerName |
 9         select pscomputername, DeviceID,
10         @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
11         @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }
12 }

Seguimos refinando el script y ahora, gracias a [CmdletBinding()] podemos hacer que alguno (o todos los parámetros) sean obligatorios. Para ello, precedemos el parámetro con [Parameter(Mandatory=$true)]:

 1 function Get-DiskInfo {
 2     [CmdletBinding()]
 3     param(
 4         [string]$Drive,
 5         [Parameter(Mandatory=$true)]
 6         [string]$ComputerName
 7     )
 8     Get-WmiObject -Class win32_logicaldisk -Filter "deviceid= '$Drive'" -Computer\
 9 Name $ComputerName |
10         select pscomputername, DeviceID,
11         @{Name='Size(GB)'; Expression={$_.size / 1gb -as [int]} },
12         @{n='Free(GB)'; e={$_.FreeSpace / 1gb -as [int]} }
13 }

Al indicar que el parámetro es obligatorio, al intentar ejecutar el script sin indicarlo, PowerShell pregunta por el valor del parámetro:

1 PS> Get-DiskInfo
2 cmdlet Get-DiskInfo at command pipeline position 1
3 Supply values for the following parameters:
4 ComputerName: localhost
5 
6 PSComputerName  DeviceID Size(GB) Free(GB)
7 --------------  -------- -------- --------
8 WIN-DFHSBDD3VL1 C:             25       11

[CmdletBinding] también proporciona otros parámetros comunes, como la posibilidad de que la salida del cmdlet se guarde en una variable:

1 PS> Get-DiskInfo -ComputerName localhost -OutVariable $resultado
2 PS> $resultado
3 PSComputerName  DeviceID Size(GB) Free(GB)
4 --------------  -------- -------- --------
5 WIN-DFHSBDD3VL1 C:             25       11

Aunque hemos explicado cómo guardar el contenido de un script en memoria para disponer de las funciones definidas en él desde la consola, ¿cómo hacemos para eliminar estas funciones?.

La respuesta es accediendo al PSDrive de funciones y eliminarla.

 1 PS>Get-PSDrive
 2 
 3 Name           Used (GB)     Free (GB) Provider      Root
 4 ----           ---------     --------- --------      ----
 5 Alias                                  Alias
 6 C                  13,36         11,14 FileSystem    C:\
 7 Cert                                   Certificate   \
 8 D                                      FileSystem    D:\
 9 Env                                    Environment
10 Function                               Function
11 HKCU                                   Registry      HKEY_CURRENT_USER
12 HKLM                                   Registry      HKEY_LOCAL_MACHINE
13 Variable                               Variable
14 WSMan                                  WSMan

Ejecutamos dir function sobre este PSDrive y obtenemos una lista de las funciones disponibles; entre ellas, observamos que tenemos la función Get-DiskInfo que hemos almacenado mediante el dot sourcing.

Para eliminar la función, usamos Remove-Item function:\Get-Diskinfo. El problema es que no podemos estar seguros de que se haya eliminado completamente, por lo que el dot sourcing sólo debe usarse para realizar pruebas; la manera correcta de exportar las funciones será mediante los módulos.

En cualquier caso, para limpiar la memoria de la consola, lo más sencillo es cerrar la ventana y abrir una nueva o, si estamos trabajando con ISE, crear una nueva pestaña (de hecho, un nuevo workspace).

Siguiendo adelante con el script, una vez estamos satisfechos con el cmdlet Get-DiskInfo, probablemente añadiremos nuevas funciones al fichero .\diskinfo.ps1.

Mediante la técnica del dot sourcing tendremos disponibles las funciones definidas en el fichero para utilizarlas en los scripts, construyendo un conjunto de herramientas que nos ayuden a solucionar los problemas del día a día.

Funciones avanzadas

El propósito de las funciones avanzadas

El objetivo de las funciones avanzadas es crear funciones que se comporten del mismo modo que los cmdlets. Anteriormente (v1) los cmdlets sólo podrían crearse mediante .NET. En PowerShell V2 ya era posible crear cmdlets en .NET y en PowerShell. En V3, además, es posible crear cmdlets en Windows Workflow o en WMI.

Una plantilla de función avanzada

Como hemos visto en los módulos anteriores, una función avanzada incluye parámetros definidos por el usuario, parámetros comunes -agregados automáticamente por PowerShell, ayuda generada automáticamente a través de la descripción…

ISE proporciona plantillas de funciones avanzadas como parte de sus snippets (Ctrl+J), pero vamos a escribir una plantilla con los elementos esenciales:

 1 <#
 2     Ayuda basada en la información del comentario inicial de la función
 3 #>
 4 Function Verbo-Nombre {
 5     [CmdletBinding()]
 6     Param(
 7         [Parameter()][tipo]$Variable1,
 8         [Parameter()][tipo]$Variable2,
 9     )
10     
11     Begin {
12         <# Codigo #>
13     }
14     Process {
15         <# Codigo #>
16     }
17     End {
18         <# Codigo #>
19     }
20 }

Encontramos nuevos bloques: Begin, Process, End. Estas palabras claves marcan la diferencia entre una función (o un script basado en funciones) y un cmdlet normal. Básicamente, tienen que ver con el pipeline.

Vamos a descubrir para qué sirven estos bloques a través de la experimentación.

 1 <#
 2     Ayuda basada en la información del comentario inicial de la función
 3 #>
 4 Function Verbo-Nombre {
 5     [CmdletBinding()]
 6     Param(
 7         [Parameter()][string]$Variable1,
 8         [Parameter()][int]$Variable2
 9     )
10     
11     Begin {
12         "Begin"
13     }
14     Process {
15         "Process"
16     }
17     End {
18         "End"
19     }
20 }

Si ejecutamos en la consola:

1 PS> .\template.ps1
2 PS> Verbo-Nombre
3 Begin
4 Process
5 End

Nada espectacular.

Para simplificar, nos quedamos con una sola variable y la agregamos en los diferentes bloques:

 1 <#
 2     Ayuda basada en la información del comentario inicial de la función
 3 #>
 4 Function Verbo-Nombre {
 5     [CmdletBinding()]
 6     Param(
 7         [Parameter()][string]$Variable1
 8     )
 9     
10     Begin {
11         "Begin $Variable1"
12     }
13     Process {
14         "Process $Variable1"
15     }
16     End {
17         "End $Variable1"
18     }
19 }

Si ejecutamos la función, vemos que la variable pasada como parámetro está disponible en los tres bloques.

A continuación modificamos el parámetro añadiendo ValueFromPipeline=$True:

 1 <#
 2     Ayuda basada en la información del comentario inicial de la función
 3 #>
 4 Function Verbo-Nombre {
 5     [CmdletBinding()]
 6     Param(
 7         [Parameter(ValueFromPipeline=$true)]
 8         [string]$Variable1
 9     )
10     
11     Begin {
12         "Begin $Variable1"
13     }
14     Process {
15         "Process $Variable1"
16     }
17     End {
18         "End $Variable1"
19     }
20 }

Si ejecutamos directamente en la consola no vemos ninguna diferencia. Pero si lo ejecutamos como parte del pipeline:

1 PS> 1..5 | Verbo-Nombre
2 Begin 
3 Process 1
4 Process 2
5 Process 3
6 Process 4
7 Process 5
8 End 5

Vemos que en Begin no tenemos el valor de la variable, ya que no tiene un valor por defecto y por tanto no está definido.

A continuación, tenemos tantas ejecuciones del bloque Process como objetos están llegando a través del pipeline.

Los bloques Begin y End sólo se ejecutan una vez.

El bloque Begin está orientado a configurar código necesario para que se realice el procesado, configurando todo lo necesario para la ejecución del script.

El bloque End realiza tareas de limpieza una vez se ha realizado el trabajo. Y el bloque Process es el que realiza el procesado de la información.

Veamos un ejemplo:

 1 <# Help #>
 2 Function Get-Total {
 3     [cmdletbinding()]
 4     Param(
 5         [Parameter(ValueFromPipeline = $true)]
 6         [int]$x
 7     )
 8     Begin   { $total = 0 }
 9     Process { $total += $x }
10     End     { "Total = $total" }
11 }

Si ejecutamos la función usando el pipeline:

1 PS> 1..5 | Get-Total
2 Total = 15

Y si la ejecutamos directamente en la línea de comandos:

1 PS> Get-Total -x 2
2 Total = 2

En un caso más general, suponemos que el script va a insertar una serie de valores en una base de datos SQL. En el bloque Begin estableceríamos la cadena de conexión con la base de datos, en el bloque Process haríamos la inserción de valores, etc. Y en el bloque End cerraríamos la conexión y haríamos la lipieza de variables.

Por tanto, una vez entendemos qué es lo que hace cada bloque, incluso si utilizamos una simple línea de código encadenando varios cmdlets, la mejor práctica es copiar y pegar el código en el bloque Process y empezar a refinar la función a partir de ahí.

El bloque Process ejecuta el código en serie, es decir, no lo hace en paralelo, sino un objeto tras otro. Si queremos lanzar código en paralelo, usaríamos invoke-command -ComputerName (get-content servers.txt) {<# code #>}.

Creando y probando parámetros

En este apartado vamos a crear y probar parámetros en una función. Partimos de un script que obtendrá información sobre varios equipos.

 1 <#
 2 .Synopsis
 3    Obtiene la información de uno o varios equipos
 4 .DESCRIPTION
 5    Obtiene la información de uno o varios equipos
 6 .EXAMPLE
 7 #>
 8 function Get-CompInfo
 9 {
10     [CmdletBinding()]
11     Param
12     (
13         # Queremos soporte para varios equipos
14         [string]$ComputerName
15         # Interruptor para activar el log de errores
16         [switch]$ErrorLog
17         [string]$LogFile = 'c:\errorlog.txt'
18     )
19 
20     Begin   { }
21     Process { }
22     End     { }
23 }

Si ejecutamos get-help get-compinfo veremos que PowerShell está construyendo el fichero de ayuda con la información que hemos proporcionado en el primer bloque de comentarios. Más adelante nos centraremos en esta capacidad de PowerShell de facilitar la creación de la documentación de ayuda para nuestros scripts.

Volviendo al trabajo; el tipo del parámetro $ComputerName es del tipo <string>, pero no acepta valores múltiples. Para que el parámetro acepte múltiples valores, añadimos [] tras el tipo de la variable:

1 ...
2 # Queremos soporte para varios equipos
3 [string[]]$ComputerName
4 ...

De esta forma tan sencilla hacemos que el parámetro acepte múltiples valores; nuestro trabajo consistirá en controlar si el script está recibiendo uno o más valores y cómo gestionarlos en el código.

Durante la fase de desarrollo del script es habitual mostrar por pantalla qué está haciendo el script (para depurar errores cuando no hace lo que queremos).

Una de las ventajas de PowerShell a la hora de probar los parámetros de las funciones es que podemos usar cmdlets como Write-Verbose para comprobar que todo funciona, sin necesidad de usar Write-Output (que después habrá que comentar en la versión final para que no se muestre durante la ejecución).

1 Begin   {
2     If($ErrorLog) {
3         Write-Verbose 'Registro de errores activado.'
4     else {
5         Write-Verbose 'Registro de errores desactivado.'
6     }
7 }

Usando Write-Verbose, PowerShell muestra mensajes por pantalla únicamente si ejecutamos el script con el parámetro -Verbose.

Para comprobar si la activación del registro de errores funciona correctamente mediante el parámetro $ErrorLog, ejecutamos:

1 PS> Get-CompInfo -ErrorLog -verbose
2 VERBOSE: Registro de errores activado.
3 
4 PS> Get-CompInfo -verbose
5 VERBOSE: Registro de errores desactivado.

El parámetro $ErrorLog es del tipo Switch, de manera que sólo tiene dos valores ($true, si está presente, o $false si no lo está)

Para que el script soporte múltiples valores para el parámetro $ComputerName, usamos un bucle forearch; así funcionará tanto con un nombre de equipo o con múltiples:

 1 ...
 2 Begin   {
 3     If($ErrorLog) {
 4         Write-Verbose 'Registro de errores activado.'
 5     } else {
 6         Write-Verbose 'Registro de errores desactivado.'
 7     }
 8     Foreach ($Computer in $ComputerName) {
 9         Write-Verbose "Computer: $Computer"
10     }
11 }
12 ...

Añadiendo código

En el apartado anterior hemos montado la infraestructura necesaria para gestionar los diferentes parámetros de la función. Una vez hemos comprobado que todo funciona como esperamos, vamos a empezar a escribir el código de la función.

El código debe escribirse en el bloque Process, porque aunque no vayamos a utilizar la entrada a través del pipeline, quizás más adelante querramos hacerlo.

Vamos con el código:

1 ...
2 Process {
3     foreach ($Computer in $ComputerName) {
4         $os = Get-WmiObject -ComputerName $Computer -Class win32_operatingsystem
5         $Disk = Get-WmiObject -ComputerName $Computer -Class win32_logicaldisk -F\
6 ilter "DeviceID = 'c:'" 
7     }
8 }
9 ...

Usamos el mismo esquema que en el bloque Begin para gestionar múltiples elementos, usando un bucle foreach. A continuación lanzamos una consulta wmi para cada uno de los elementos.

Escribiendo objetos en el pipeline

Parte de la potencia de PowerShell se basa en la capacidad de enlazar la salida de un comando con la entrada del siguiente. Al tratarse de objetos, cada cmdlet puede modificar el objeto y pasarlo a través del pipeline.

Para que nuestro script se comporte como un cmdlet de primera clase, debe expresar el resultado en forma de objeto.

Así pues, vamos a construir un objeto que almacene la información que queremos de la manera que queremos.

Comenzamos con una hash table (un diccionario) con las propiedades que nos interesan de la información obtenida de las consultas wmi:

1 ...
2 $Prop = @{
3     'ComputerName' = $Computer
4     'OS Name'      = $os.caption
5     'OS Build'     = $os.buildnumber
6     'FreeSpace'    = $Disk.freespace / 1gb -as [int]
7 }
8 ...

No es necesario poner ‘;’ al final de cada una de las propiedades en $Propya que ; es un terminador de expresiones y sólo es necesario si queremos poner varias expresiones en una misma línea.

$Prop es una hash table, con valores en pares, de la forma key = value. No son conjuntos de valores ordenados, por lo que al mostrar las propiedades en la salida de un comando no se mostrarán necesariamente en el orden en el que hemos especificado las propiedades en la definición.

Podemos añadir [ordered] antes de la definición de la hash table @{...}:

1 ...
2 $Prop = [ordered]@{
3     'ComputerName' = $Computer
4     'OS Name'      = $os.caption
5     'OS Build'     = $os.buildnumber
6     'FreeSpace'    = $Disk.freespace / 1gb -as [int]
7 }
8 ...

En este caso, las propiedades sí que se mostrarán en el mismo orden en el que han sido definidas, aunque es más costoso a nivel computacional, ya que PowerShell debe ordenar las propiedades antes de mostrarlas.

El siguiente paso es crear un objeto con los datos de interés para poder enviarlo a través del pipeline.

Para crear un objeto en PowerShell, usamos el cmdlet New-Object. Aunque no es necesario, se recomienda especificar el tipo mediante -TypeName PSObject.

El objeto creado estará vacío, ya que no contiene ni propiedades ni métodos. Por tanto, será necesario asignar las propiedades y métodos que queremos asociar al objeto recién creado.

Una forma de hacerlo es en la definición del objeto, asignando una hash table:

1 $obj = New-Object -TypeName PSObject -Property $Prop

Ahora, al ejecutar de nuevo el script, el resultado es:

1 FreeSpace ComputerName OS Name                                           OS Build
2 --------- ------------ -------                                           --------
3        11 localhost    Microsoft Windows Server 2016 Technical Preview 4 10586   

Este es el aspecto que queremos que tenga… Pero podemos comprobar que es un objecto de verdad mediante:

1 PS> get-compinfo | get-member
2 TypeName: System.Management.Automation.PSCustomObject

Y como ahora el cmdlet devuelve un objecto, podemos seleccionar una de las propiedades o convertir la salida a CSV o JSON, etc usando el pipeline para pasar nuestro objeto a cualquier otro cmdlet:

 1 PS> Get-CompInfo -ComputerName localhost | select -Property "os name"
 2 
 3 OS Name
 4 -------
 5 Microsoft Windows Server 2016 Technical Preview 4
 6 
 7 PS> Get-CompInfo -ComputerName localhost | ConvertTo-Json
 8 {
 9     "FreeSpace":  11,
10     "ComputerName":  "localhost",
11     "OS Name":  "Microsoft Windows Server 2016 Technical Preview 4",
12     "OS Build":  "10586"
13 }

Finalmente, el código completo del cmdlet es:

 1 <#
 2 .Synopsis
 3    Obtiene la información de uno o varios equipos
 4 .DESCRIPTION
 5    Obtiene la información de uno o varios equipos
 6 .EXAMPLE
 7 #>
 8 function Get-CompInfo
 9 {
10     [CmdletBinding()]
11     Param
12     (
13         # Queremos soporte para varios equipos
14         [string[]]$ComputerName,
15         # Interruptor para activar el log de errores
16         [switch]$ErrorLog,
17         [string]$LogFile = 'c:\errorlog.txt'
18     )
19 
20     Begin   {
21         If($ErrorLog) {
22             Write-Verbose 'Registro de errores activado.'
23         } else {
24             Write-Verbose 'Registro de errores desactivado.'
25         }
26         foreach ($Computer in $ComputerName) {
27             Write-Verbose "Computer: $Computer"
28         }
29     }
30     Process {
31         foreach ($Computer in $ComputerName) {
32             $os = Get-WmiObject -ComputerName $Computer -Class win32_operatingsys\
33 tem
34             $Disk = Get-WmiObject -ComputerName $Computer -Class win32_logicaldis\
35 k -Filter "DeviceID = 'c:'"
36 
37             $Prop = @{
38                 'ComputerName' = $Computer;
39                 'OS Name'      = $os.caption;
40                 'OS Build'     = $os.buildnumber;
41                 'FreeSpace'    = $Disk.freespace / 1gb -as [int]
42             }
43         }
44         $obj = New-Object -TypeName PSObject -Property $Prop
45         Write-Output $obj
46     }
47     End     { }
48 }

Más sobre los parámetros

Hasta ahora hemos visto que los parámetros pueden tener atributos, como Mandatory, ValueFromPipeline, etc:

1 ...
2 Param(
3     [Parameters(Mandatory = $true)]
4     [string[]]$ComputerName
5     ...
6 )
7 ...

Pero tenemos muchas otras opciones, como validación de los parámetros y algunos otros que vamos a ver a continuación.

Puedes consultar información adicional sobre los atributos de los parámetros mediante Get-Help about_functions_advanced_parameters

Podemos usar el pipeline para redirigir la ayuda sobre un cmdlet al portapapeles mediante: powershell PS> Get-Help about_functions | clip A continuación, podemos pegar la información de la ayuda en un editor de texto para facilitar su lectura.

Vamos a seguir con el mismo código del cmdlet Get-CompInfo del apartado anterior, pero vamos a centrarnos ahora en los parámetros.

 1 ...
 2 [CmdletBinding()]
 3 Param
 4 (
 5     # Queremos soporte para varios equipos
 6     [string[]]$ComputerName,
 7     # Interruptor para activar el log de errores
 8     [switch]$ErrorLog,
 9     [string]$LogFile = 'c:\errorlog.txt'
10 )
11 ...

Parámetros obligatorios

En primer lugar, indicamos que el parámetro $ComputerName es obligatorio mediante:

 1 ...
 2 [CmdletBinding()]
 3 Param
 4 (
 5     # Queremos soporte para varios equipos
 6     [Parameter(Mandatory=$true)]
 7     [string[]]$ComputerName,
 8     # Interruptor para activar el log de errores
 9     [switch]$ErrorLog,
10     [string]$LogFile = 'c:\errorlog.txt'
11 )
12 ...

Ahora, al ejecutar Get-CompInfo sin especificar nigún parámetro, PowerShell nos pregunta sobre el parámetro obligatorio:

1 PS> Get-CompInfo 
2 cmdlet Get-CompInfo at command pipeline position 1
3 Supply values for the following parameters:
4 ComputerName[0]: 

Parámetros a través del pipeline

Como hemos indicado que $ComputerName puede contener múltiples valores, nos pide el primero. Cuando lo introducimos, nos pide un segundo valor. El cmdlet seguirá preguntando hasta que pulsemos Intro sin proporcionar un valor, indicando que hemos acabado.

Si queremos que el parámetro acepte valores desde el pipeline, lo indicamos mediante:

 1 ...
 2 [CmdletBinding()]
 3 Param
 4 (
 5     # Queremos soporte para varios equipos
 6     [Parameter( Mandatory=$true,
 7                 ValueFromPipeline = $true,
 8                 ValueFromPipelineByPropertyName = $true)]
 9     [string[]]$ComputerName,
10     # Interruptor para activar el log de errores
11     [switch]$ErrorLog,
12     [string]$LogFile = 'c:\errorlog.txt'
13 )
14 ...

Separamos los diferentes atributos para un parámetro mediante comas.

La diferencia entre ValueFromPipeline y ValueFromPipelineByPropertyName estriba en que en el primer caso el cmdlet espera recibir un objeto del mismo tipo, mientras que en el segundo, si no recibe un objecto exactamente del mismo tipo, busca una propiedad que se llame como alguno de los parámetros del cmdlet.

La diferencia se explica en detalle en el curso de la Microsoft Virtual Academy Tools & Scripting with PowerShell 3.0 Jump Start

Ayuda para los parámetros

Otro añadido interesante a los parámetros de la función es proporcionar ayuda al usuario del cmdlet.

 1 ...
 2 [CmdletBinding()]
 3 Param
 4 (
 5     # Queremos soporte para varios equipos
 6     [Parameter( Mandatory=$true,
 7                 ValueFromPipeline = $true,
 8                 ValueFromPipelineByPropertyName = $true,
 9                 HelpMessage = 'Uno o mas nombres de equipos')]
10     [string[]]$ComputerName,
11     # Interruptor para activar el log de errores
12     [switch]$ErrorLog,
13     [string]$LogFile = 'c:\errorlog.txt'
14 )
15 ...

Si ejecutamos de nuevo el cmdlet, como ahora hemos definido un mensaje de ayuda, PowerShell muestra !? para que podamos obtener ayuda al respecto:

1 PS> Get-CompInfo
2 cmdlet Get-CompInfo at command pipeline position 1
3 Supply values for the following parameters:
4 (Type !? for Help.)
5 ComputerName[0]: !?
6 Uno o mas nombres de equipos
7 ComputerName[0]: 

Al introducir !? se muestra el mensaje de ayuda que hemos especificado. Por tanto, es conveniente especificar un mensaje de ayuda que sea útil y claro.

Alias

Como PowerShell tiene una política de nombres estricta, para ayudar a aquellas personas que están acostumbradas a usar, por ejemplo hostname y no ComputerName, se pueden definir alias para los parámetros.

Además, definir alias para los parámetros también permite que los cmdlets encajen unos con otros más fácilmente a través del pipeline.

Si un cmdlet envía a través del pipeline un objeto que no dispone de una propiedad con el nombre que espera el cmdlet de destino, el cmdlet fallaría. Sin embargo, podemos crear un alias para la propiedad que contiene la información que nos interesa del objeto que llega a través del pipeline, de manera que, gracias al alias creado para el parámetro, pueda pasar la información al cmdlet de destino.

 1 ...
 2 [CmdletBinding()]
 3 Param
 4 (
 5     # Queremos soporte para varios equipos
 6     [Parameter( Mandatory=$true,
 7                 ValueFromPipeline = $true,
 8                 ValueFromPipelineByPropertyName = $true,
 9                 HelpMessage = 'Uno o mas nombres de equipos')]
10     [Alias('Hostname')]
11     [string[]]$ComputerName,
12     # Interruptor para activar el log de errores
13     [switch]$ErrorLog,
14     [string]$LogFile = 'c:\errorlog.txt'
15 )
16 ...

Al haber definido el alias Hostname para el parámetro ComputerName, ahora podemos ejecutar el cmdlet usando cualquiera de los dos nombres para el parámetro:

1 PS> Get-CompInfo -ComputerName
1 PS> Get-CompInfo -Hostname

En ISE, IntelliSense permite autocompletar el nombre del parámetro con cualquiera de los dos nombres asignados al parámetro.

Validación de los parámetros

Podemos establecer un conjunto de valores válidos para los valores introducidos en los parámetros. Si el valor introducido no es uno de los valores válidos, se genera un error.

Establecemos la validación de un parámetro mediante alguna de las reglas de validación que nos ofrece PowerShell; por ejemplo, que el valor introducido sea alguno de los valores especificado mediante ValidateSet():

 1 ...
 2 [CmdletBinding()]
 3 Param
 4 (
 5     # Queremos soporte para varios equipos
 6     [Parameter( Mandatory=$true,
 7                 ValueFromPipeline = $true,
 8                 ValueFromPipelineByPropertyName = $true,
 9                 HelpMessage = 'Uno o mas nombres de equipos')]
10     [Alias('Hostname')]
11     [ValidateSet('localhost', '.')]
12     [string[]]$ComputerName,
13     # Interruptor para activar el log de errores
14     [switch]$ErrorLog,
15     [string]$LogFile = 'c:\errorlog.txt'
16 )
17 ...

Si ejecutamos el cmdlet Get-CompInfo en la consola de ISE, observamos que se proporciona IntelliSense con las diferentes opciones válidas para el parámetro que estamos especificando.

Si pese a todo introducimos un valor diferente, obtenemos un error:

1 PS> Get-CompInfo -ComputerName dc
2 Get-CompInfo : Cannot validate argument on parameter 'ComputerName'. The argument\
3  "dc" does not belong to the set "localhost,." specified by the ValidateSet attri\
4 bute. Supply an argument that is in the set and then try the command again.

En el caso de disponer únicamente de un conjunto válido de opciones para un parámetro, las especificamos como un conjunto de validación.

Otros conjuntos de validación sería, por ejemplo, el número de parámetros que pueden validarse.

Por ejemplo si sólo queremos admitir un máximo de 2 nombres de equipos remotos, podemos hacer:

1 ...
2 [ValidateCount(0,2)]
3 [string[]]$ComputerName,
4 ...

Podemos validar la entrada de forma general mediante ValidatePattern(), usando expresiones regulares.

Por ejemplo, para realizar una primera aproximación para la validación de una dirección IP podemos usar una expresión regular como (aunque sigue permitiendo introducir direcciones no válidas como 999.999.999.999):

1 ...
2 [ValidatePattern("\b\d{1,3}\.\d{1,3}\.\d{1,3}\b")]
3 [string]$ip
4 ...

Otra de las reglas de validación extremadamente potente es usar un script para validar la entrada de datos, con lo que las posibilidades de validación son infinitas.

1 ...
2 [ValidateScript( { test-path $_ } )]
3 [string]$path
4 ...

En la ayuda: Get-Help about_functions_advanced_parameters disponemos de la lista completa de reglas de validación de parámetros que podemos usar en PowerShell.

Escribiendo ayuda

Habitualmente escribir ayuda es una de esas tareas que cuando se empieza en el mundo del scripting no se tiene en cuenta: simplemente se escribe el script que soluciona el problema y se olvida.

Pero con PowerShell escribir ayuda es tan sencillo que ya no hay excusas para no hacerlo.

Una de las manera más sencillas de escribir ayuda en un script PowerShell es mediante la ayuda basada en los comentarios. Es decir, el sistema de ayuda de PowerShell obtiene información relativa al script a través de los comentarios en el propio script.

Puedes encontrar toda la información al respecto mediante get-help about_Comment_Based_Help.

La ayuda basada en comentarios ofrece la información acerca de tu script estructurada de la misma forma que la ayuda de cualquier otro cmdlet.

Sin embargo, hay algunas limitaciones en cuanto a las posibilidades que ofrece la ayuda basada en comentarios.

La actualización de la ayuda mediante update-help no funciona con la ayuda basada en comentarios; sólo funciona para ayuda escrita en formato XML (que es la otra forma de escribir ayuda para los scripts en PowerShell). La ayuda en formato XML se puede colgar en una URL pública, de manera que los clientes pueden descargar una versión actualizada en sus equipos.

Sin embargo, la ayuda basada en XML es más complicada de escribir y, en general, sólo tiene sentido si hay una compañía que vende los scripts e interesada en mantenerlos por dinero.

Ayuda basada en comentarios

Lo primero a destacar de la ayuda basada en comentarios es que existen dos maneras de comentar en PowerShell.

En primer lugar, podemos comentar una línea predeciendo el comentario del símbolo #.

Pero para la ayuda basada en comentarios vamos a usar los bloques de comentarios, que se extienden varias líneas:

Este fragmento de código viene del snippet para una función avanzada de ISE.

 1 <#
 2 .Synopsis
 3    Short description
 4 .DESCRIPTION
 5    Long description
 6 .EXAMPLE
 7    Example of how to use this cmdlet
 8 .EXAMPLE
 9    Another example of how to use this cmdlet
10 #>
11 function Verb-Noun
12 {
13     [CmdletBinding()]
14     [Alias()]
15     [OutputType([int])]
16     Param
17     (
18         # Param1 help description
19         [Parameter(Mandatory=$true,
20                    ValueFromPipelineByPropertyName=$true,
21                    Position=0)]
22         $Param1,
23 
24         # Param2 help description
25         [int]
26         $Param2
27     )
28 
29     Begin {}
30     Process {}
31     End {}
32 }

Si ejecutamos Get-Help verb-noun (que es el nombre del cmdlet en esta función), obtenemos:

 1 PS C:\Windows\system32> Get-Help Verb-Noun
 2 
 3 NAME
 4     Verb-Noun
 5     
 6 SYNOPSIS
 7     Short description
 8     
 9     
10 SYNTAX
11     Verb-Noun [-Param1] <Object> [-Param2 <Int32>] [<CommonParameters>]
12     
13 DESCRIPTION
14     Long description
15 
16 RELATED LINKS
17 
18 REMARKS
19     To see the examples, type: "get-help Verb-Noun -examples".
20     For more information, type: "get-help Verb-Noun -detailed".
21     For technical information, type: "get-help Verb-Noun -full".
22 
23 PS C:\Windows\system32> 

Es decir, sin haber tenido que realizar ninguna acción especial, vemos que el sistema de ayuda integrado en PowerShell ha descubierto y formateado la información proporcionada en el bloque de inicial de comentarios.

En la ayuda vemos que se indican los nombres y tipos de los parámetros, si son obligatorios u opcionales e incluso cómo obtener información adicional sobre ejemplos o la ayuda detallada.

Toda esta información se obtiene directamente del bloque de comentarios del script.

Puedes comprobar que esta información se actualiza a medida que se modifica el script (por ejemplo cuando especificas un nuevo parámetro, el tipo de una variable, etc).

Volviendo al script Get-CompInfo:

 1 <#
 2 .Synopsis
 3    Obtiene información sobre los equipos indicados.
 4 .DESCRIPTION
 5    El script obtiene información detallada de:
 6    * El sistema operativo
 7    * La versión de build del SO.
 8    * El tamaño del disco c:
 9    * El espacio libre en el disco
10 .EXAMPLE
11     Get-CompInfo -ComputerName localhost
12 #>
13 ...

Al ejecutar get-hep get-compinfo:

 1 PS> Get-Help Get-CompInfo
 2 NAME
 3     Get-CompInfo
 4 SYNOPSIS
 5     Obtiene información sobre los equipos indicados.
 6 SYNTAX
 7     Get-CompInfo [-ComputerName] <String[]> [-ErrorLog] [[-LogFile] <String>] [<C\
 8 ommonParameters>]
 9 DESCRIPTION
10     El script obtiene información detallada de:
11     * El sistema operativo
12     * La versión de build del SO.
13     * El tamaño del disco c:
14     * El espacio libre en el disco
15 RELATED LINKS
16 
17 REMARKS
18     To see the examples, type: "get-help Get-CompInfo -examples".
19     For more information, type: "get-help Get-CompInfo -detailed".
20     For technical information, type: "get-help Get-CompInfo -full".

En el bloque de comentarios hemos incluido un ejemplo de uso; vamos a usar get-help get-compinfo -examples, como se sugiere al final de la ayuda para obtener ejemplos:

1 PS> Get-Help Get-CompInfo -Examples
2 NAME
3     Get-CompInfo
4 SYNOPSIS
5     Obtiene información sobre los equipos indicados.
6     -------------------------- EXAMPLE 1 --------------------------
7     PS C:\>Get-CompInfo -ComputerName localhost

Vemos que obtenemos la descripción contenida en el bloque de ayuda, pero además se muestra información acerca del uso del comando.

Gracias al sistema de ayuda de PowerShell, sin tener que escribir más que algunas líneas de comentarios en el script -que deberíamos incluir en cualquier caso-, podemos sacar el máximo rendimiento de esta ayuda implícita basada en los comentarios del script.

La separación entre el bloque de comentarios y la función que documenta puede ser como máximo de una línea en blanco; si hay dos o más líneas en blanco, PowerShell no es capaz de relacionar el bloque de comentarios como ayuda para la función que lo sucede.

Además de colocar el bloque de ayuda antes de la función, también es posible insertar el bloque de ayuda en la función:

 1 ...
 2 Function Verbo-Nombre {
 3     <#
 4     .Synopsis
 5        Obtiene información sobre los equipos indicados.
 6     ...
 7     #>
 8 
 9     # code
10 }
11 ...

Finalmente, también puede colocarse al final de la función, siempre y cuando el cierre del bloque de comentarios esté en la línea anterior al cierre de la función.

Aunque no hay una forma mejor que otra, como en ISE el snippet para la función avanzada incluye el bloque de ayuda al principio de la función, ésta es la forma más extendida.

La idea tras el sistema de ayuda, especialmente con los ejemplos, es proporcionar ejemplos de cómo se supone que se va a usar el cmdlet en situaciones reales.

Uno de los parámetros de get-help es -ShowCommand. Este parámetro muestra la ayuda en una ventana independiente, lo que facilita su lectura. Además, esta ventana contiene una caja de búsqueda, con la que podemos encontrar con más facilidad el contenido que buscamos.

El sistema de ayuda no sólo obtiene información de los bloques de comentarios al princio (o al final) de la función. En el caso de los parámetros, si antes de la definición del parámetro añadimos un comentario, el sistema de ayuda lo interpreta como información interesante sobre el parámetro y lo muestra en la ayuda:

1 ...
2 	# Indica el nombre del equipo del que obtendremos la información
3     [string[]]$ComputerName
4 ...

En la ayuda detallada get-help get-compinfo -detailed, observaremos:

1 ...
2 PARAMETERS
3     -ComputerName <String[]>
4         Indica el nombre del equipo del que obtendremos la información
5 ...

Gestión de errores

Revisar si falta algo aquí!!

Usar una variable para almacenar los errores nos permite averiguar para qué elementos ha fallado un determinado comando. Es decir, lanzamos un comando sobre 100 servidores y obtenemos que 3 han fallado. Usando las propiedades almacenadas de los errores almacenadas en la variable podemos obtener para qué elementos ha fallado.

Vamos a ver esto en acción con un ejemplo sencillo.

Lanzamos Get-Process -Id 13, 23, 37, con lo que obtendremos tres errores (todos los indicadores de procesos son pares).

1 PS> Get-Process -id 13, 23, 37 -ErrorVariable $e

Si inspeccionamos el contenido de $e obtenemos información, pero no toda la información; podemos comprobarlo mediante:

1 PS> $e[0] | get-member

En la salida observamos que tenemos un montón de propiedades disponibles. Sin embargo, si lanzamos $e[0] | fl * seguimos obteniendo únicamente parte de todas las propiedades disponibles. Esto es debido a que PowerShell trata de manera especial los errores.

Para forzar a que PowerShell muestre todas las propiedades disponibles, usamos el parámetro -Force:

1 PS> $e[0] | fl * -force

En la salida del comando observamos que tenemos una propiedad llamada TargetObject, de manera que:

1 PS> $e.TargetObject
2 13
3 23
4 37

Es decir, en $eTargetObject se almacenan los objetos sobre los que se ha intentado realizar la acción que ha fallado. Esto nos permite lanzar acciones correctoras sobre únicamente los objetos que han fallado:

1 PS> foreach($t in $e.TargetObject) {$t} # En este caso sólo mostramos los objetos

Si queremos modificar el comportamiento de PowerShell con respecto a la información que muestra relativa a los errores, podemos modificar la variable ErrorView:

Si no recordamos exactamente cómo se llama la variable, siempre podemos buscarla en el espacio de nombres de las variables: powershell PS> dir variable:*error*

Por defecto tiene el valor NormalView, pero también tenemos otra vista llamada CategoryView. Esta vista contiene la misma información, pero de una forma más compacta y sin traducir al idioma local, lo que permite buscar el error de forma más sencilla.

Gestión de errores usando Try...Catch

Imaginemos el siguiente escenario: lanzamos una serie de consultas WMI contra una lista de servidores, pero algunos de ellos están offline; la primera consulta WMI fallará, pero después pasaremos a la segunda, que fallará de nuevo, etc. El scriptt se ralentiza porque las consultas fallidas intentan contactar con los servidores caídos de nuevo, y lo mismo sucederá con las siguientes consultas.

La manera más sencilla de solucionar este problema sería, en cuanto se detecta un error, pasar al siguiente servidor de la lista y no seguir probando contra el mismo las siguientes consultas -que sabemos que fallarán.

Para ello, usamos Try ... Catch ... Finally.

1 $computer = 'notonline'
2 Try {
3 	$os = Get-WMIobject -ComputerName $computer -Class win32_operatingSystem `
4 			-ErrorAction Stop -ErrorVariable CurrentError
5 } Catch {
6 	Write-Warning "You hit an offline computer: $Computer."
7 }

La idea del bloque Try...Catch es que la acción en caso de error sea Stop, para que la ejecución se detenga y pase al bloque Catch. Si hemos especificado ErrorAction = SiletlyContinue, la ejecución continua y no se salta al bloque Catch.

Finally se ejecuta siempre en un bloque Try ... Catch y suele usarse para hacer limpieza o rollbacks, etc.

Registrando los errores

Si tenemos varias acciones que pueden fallar por el mismo motivo, especificamos la acción en caso de error en la primera. En el bloque Catch podemos especificar, usando un parámetro tipo switch el guardado del error en un fichero de texto, por ejemplo:

 1 ...
 2 catch {
 3 	Write-Warning "El equipo $computer no responde."
 4     if( $ErroLog ) {
 5     	Get-Date | Out-File $LogFile -Force # Lo sobreescribe si existe
 6         $Computer | Out-File $LogFile -Append
 7         $CurrentError | Out-File $LogFile -Append
 8     }
 9 ...
10 }

Otra opción es, en vez de escribir en un fichero, podemos usar los cmdlets Write-EventLog -LogName Application -Source MiScript -EntrType Information -Message "El script ha sido ejecutado" -EventID 12345.

De hecho, guardando información de cuándo se ha ejecutado el script y otra información útil que pueda consultarse a través del eventlog de la máquina.

Scripts y manifiesto de módulos

El siguiente paso es convertir los scripts en módulos.

Los módulos sirven para agrupar todas las heramientas personales en un solo fichero.

Crear un módulo es tan sencillo como grabar el script y en vez de guardarlo como .ps1, lo grabamos como .psm1.

Una vez tenemos el módulo, podemos importarlo indicando la ruta completa a la ubicación del fichero:

1 PS> Import-Module c:\scripts\misHerramientas.psm1

Verificamos que hemos importado todas las funciones contenidas en el módulo mediante:

1 PS> Get-Command -module misHerramientas

Pero todavía podemos ir un poco más allá y hacer que el módulo se importe automáticamente cuando un script intente usar una función contenida en él.

Para ello, debemos colocar el módulo en una ubicación especial. Podemos inspeccionar las ubicaciones en las que se buscan los módulos de forma automática mediante:

powershell PS> $env:PSModulePath -split ";" `

Usamos -split “;” para facilitar la lectura de las diferentes ubicaciones de la variable.

No es recomendable ubicar nuestros módulos personales en c:\windows\system32\windowsPowerShell\v1.0\modules\.

Para nuestros módulos personalizados, debemos usar la ubicación: c:\users\{nombre.usuario}\Documents\PowersShell\Modules.

La ruta c:\users\{nombre.usuario}\Documents\PowersShell\Modules no existe por defecto y debemos crearla manualmente.

En la carpeta Modules creamos una carpeta para nuestro módulo; esta carpeta debe llamarse exactamente como el módulo.

Una vez hemos copiado el módulo en esta ubicación especial, si ejecutamos un script que hace referencia a una de las funciones incluidas en nuestro módulo:

  • en la consola, tenemos autocompletado mediante la tecla Tab para las funciones incluidas en el módulo.
  • en los scripts, al hacer referencia a una función incluida en el módulo, PowerShell lo carga de forma dinámica sin que tengamos que importarlo explícitamente en el script.

Manifiesto

El manifiesto indica qué es lo que contiene el módulo. El manifiesto tambien permite definir dependencias, de manera que si, más adelante, cambiamos alguna de las dependencias de nuestro módulo, quienes usen el módulo sean conscientes de los cambios (en vez de encontrarse con que sus scripts fallan sin saber porqué).

El manifiesto también nos ayuda a gestionar las diferntes versiones que puede tener el módulo, recursos que necesita, etc.

Cuando PowerShell importa un módulo, busca:

  • Un manifiesto para el módulo: un fichero con el mismo nombre del módulo y extensión .psd1.
  • Un módulo binario como MisHerramientas.dll
  • Un módulo : MisHerramientas.psm1
  • El cmdlet New-ModuleManifest

Una manera muy sencilla de establecer los valores para las propiedades del cmdlet New-MmoduleManifest es a través de Show-Command New-ModuleManifest.

Seguramente eliminaré esta parte o la reduciré al mínimo.