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 \
3 for 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 \
7 not 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.O\
 9 bject, 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 -ExpandP\
2 roperty 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-M\
2 ember
3 TypeName: System.String
4 ...

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 buble 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 empie\
2 za por "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}