Tabla de contenidos
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 comoUnrestricted
. -
AllSigned
yRemoteSigned
: 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 deRemoteSigned
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:
Para establecer la política de ejecución de scripts:
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:
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:
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:
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:
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:
Si intentamos asignar un valor que no esté contenido en el conjunto especificado, PowerShell genera un error:
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:
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:
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.
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
:
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.
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:
Podemos anular de manera individual la evaluación de variables entre comillas dobles precediendo el nombre de la variable por un acento grave (backtick):
En el ejemplo anterior la variable es una cadena. Pero si fuera un objeto, nos encontraríamos con un escenario diferente:
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:
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:
Si ejecutamos el código anterior, tenemos:
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:
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.
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
):
..
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:
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:
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.
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:
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):
En PowerShell versión 3 podemos simplificar la sintaxis y hacer, sencillamente:
Podemos complicarlo un poco más y lanzar:
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
elseif
es equivalente a:
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
:
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:
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 unif
: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:
Del mismo modo podemos crear un bucle con Do ... Until
:
While
A diferencia del bucle Do ... While/Until
, los comandos dentro de un bucle While
pueden no ejecutarse nunca.
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.
La ejecución del script anterior daría como resultado:
For
La estructura del bucle For
es la siguiente:
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:
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:
- Hacer que funcione en la consola
- Convertir en script
- Parametrizar
Abrimos ISE (como Administrador). Ejecutamos el cmdlet que queremos en la consola:
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:
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):
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:
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:
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:
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:
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):
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:
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
:
El siguiente paso es agregar una nueva línea al script: [CmdletBinding()]
:
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
:
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:
## 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:
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:
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)]
:
Al indicar que el parámetro es obligatorio, al intentar ejecutar el script sin indicarlo, PowerShell pregunta por el valor del parámetro:
[CmdletBinding]
también proporciona otros parámetros comunes, como la posibilidad de que la salida del cmdlet se guarde en una variable:
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.
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:
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.
Si ejecutamos en la consola:
Nada espectacular.
Para simplificar, nos quedamos con una sola variable y la agregamos en los diferentes bloques:
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
:
Si ejecutamos directamente en la consola no vemos ninguna diferencia. Pero si lo ejecutamos como parte del pipeline:
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
yEnd
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:
Si ejecutamos la función usando el pipeline:
Y si la ejecutamos directamente en la línea de comandos:
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íamosinvoke-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.
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:
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).
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:
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:
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:
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
:
No es necesario poner ‘;’ al final de cada una de las propiedades en
$Prop
ya 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 @{...}
:
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:
Ahora, al ejecutar de nuevo el script, el resultado es:
Este es el aspecto que queremos que tenga… Pero podemos comprobar que es un objecto de verdad mediante:
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:
Finalmente, el código completo del cmdlet es:
Más sobre los parámetros
Hasta ahora hemos visto que los parámetros pueden tener atributos, como Mandatory
, ValueFromPipeline
, etc:
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.
Parámetros obligatorios
En primer lugar, indicamos que el parámetro $ComputerName
es obligatorio mediante:
Ahora, al ejecutar Get-CompInfo
sin especificar nigún parámetro, PowerShell nos pregunta sobre el parámetro obligatorio:
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 pulsemosIntro
sin proporcionar un valor, indicando que hemos acabado.
Si queremos que el parámetro acepte valores desde el pipeline, lo indicamos mediante:
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.
Si ejecutamos de nuevo el cmdlet, como ahora hemos definido un mensaje de ayuda, PowerShell muestra !?
para que podamos obtener ayuda al respecto:
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.
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:
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()
:
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:
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:
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
):
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.
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.
Si ejecutamos Get-Help verb-noun
(que es el nombre del cmdlet en esta función), obtenemos:
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
:
Al ejecutar get-hep get-compinfo
:
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:
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:
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:
En la ayuda detallada get-help get-compinfo -detailed
, observaremos:
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).
Si inspeccionamos el contenido de $e
obtenemos información, pero no toda la información; podemos comprobarlo mediante:
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
:
En la salida del comando observamos que tenemos una propiedad llamada TargetObject
, de manera que:
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:
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
.
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 bloqueTry ... 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:
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:
Verificamos que hemos importado todas las funciones contenidas en el módulo mediante:
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 deShow-Command New-ModuleManifest
.
Seguramente eliminaré esta parte o la reduciré al mínimo.