Translate

1 ene 2015

Aprendiendo PowerShell 3.0 [2]

En la última entrada sobre PowerShell vimos cómo manejar las funciones, cómo controlar algunos parámetros que les pasábamos y cómo hacer pequeñas comprobaciones para ver si eran números o no.

En esta parte vamos a centrarnos en el uso de los objetos y cómo recorrerlos, porque hay que recordar que todo en PowerShell son objetos, un dato muy distinto que no viene en la programación estructurada (MSDOS, C... ¿¡Pero quién usa estos lenguajes hoy en día, igualmente!?). Bueno, vamos a hacer una pequeña función dentro de nuestro script que, en principio, recoja todos los parámetros que le hemos pasado y compruebe si son archivos de nuestro directorio actual, es decir, si existen.

Lo primero que hay que hacer es definir la función y pasarle los argumentos, ¿pero cómo hacemos esto? Pues con la variable $args, que no es sino un array de argumentos como ya explicamos la última vez, donde usábamos $args[0] o $args[1]. Como primer paso, tendríamos algo así:


Primero definimos la función, que se llamará aprendiendoPS, y recibirá como argumento una variable llamada cantidad, que contendrá el valor de la cantidad de argumentos que le pasamos al invocarla, más abajo. Le pasamos también los argumentos con la variable args, y comprueba que se le haya pasado algo o no, en este caso podemos ver que sí, que la cantidad de parámetros es distinta de cero (lo que quiere decir que hay algo), y muestra la cantidad en pantalla y los argumentos. Si no le pasamos nada nos sacará el mensaje de color rojo.

Todo esto sigue siendo muy básico, así que vamos a pasar al siguiente nivel: comprobar que los argumentos que hemos pasado existen en nuestro directorio actual como archivos o como directorios. Para esto tenemos que recurrir al famoso Get-ChildItem (alias de LS o DIR), que nos devuelve un listado de todos los elementos del directorio:


Le hemos pasado esos tres argumentos (a, b y c) para que se ejecute correctamente el script, simplemente. Podemos ver que tenemos en la variable $listado la lista de archivos y directorios (en este caso no hay) de nuestro directorio actual. Como se ha mencionado más arriba, esto es un objeto, y podemos acceder a sus propiedades. La propiedad de un objeto, o su atributo, es por ejemplo Mode, o Name, o incluso Length. Exactamente, cada una de las columnas que nos devuelve el Get-ChildItem. Hay muchas más propiedades que no se muestran aquí, y que nos saldrán en el editor, pero bueno, de momento no las vamos a necesitar.

Vamos a probar a sacar por pantalla las propiedades Length y Name:


Accedemos a las propiedades con el puntito: $objetoCualquiera.propiedad (sigue este esquema, básicamente). Y como salida tenemos dos cosas muy interesantes, un cinco que es la longitud del listado, y todos los nombres. Vale, acabamos de llegar al punto más importante de la manipulación de objetos en este lenguaje. Saca un cinco porque, el contenido entero de la variable, contiene cinco palabras (lógico, ¿no?), y nos saca toda la retahíla de nombres juntos porque los toma todos como su único valor; como un solo elemento, no como un array de elementos.

Esto es un gran problema, porque la idea es ir comparando nuestro parámetro con cada uno de esos nombres para ver si existe o no, pero si ahora mismo comparáramos $arg[0] con $listado.Name, estaríamos comparando el primer archivo que queremos buscar con "propiedades.txtScript1.ps1Script2.ps1ScriptBlog.ps1usuario.txt", tal cual, y como es lógico también, nos va a decir que propiedades.txt no existe en nuestro directorio porque no es toda esa retahíla de nombres. ¿Cómo entonces conseguimos recorrer todos y cada uno de los elementos del objeto de forma individual? Pues de la siguiente forma:


Parece que la salida del comando, aparte del color verde, no ha cambiado en nada excepto que dejamos de usar la propiedad Length del objeto. Pero si nos fijamos en la declaración de la variable listado, nos daremos cuenta de que hemos utilizado una pipe, o tubería, con un ForEach-Object {$_}. Esto último nos permite hacer consultas como en lenguajes de bases de datos (SQL por ejemplo), y podemos sutituir el ForEach-Object por un Select-Object o un Where-Object (sentencias Select o Where de SQL). Pero vale, esto no tiene nada que ver con el ForEach-Object, solo lo comento para que se comprenda un poco la lógica que tiene el pipe ahí. El ForEach-Object (para cada objeto) nos devuelve todos los elementos, por así decirlo, desmenuzados (un array), no todos como un único valor como pasaba antes, que nos devolvía el bloque de nombres y ahí se quedaba.

La salida de la variable listado será la misma, pero la forma de tratar el objeto variará notablemente. Ahora podemos recorrer todos sus elementos como si de un fichero de texto se tratase, con un simple foreach, donde usamos la variable $archivo para que vaya recorriendo cada línea de la variable listado. De esta forma, a cada vuelta del bucle la variable archivo tendrá todos los valores del elemento del objeto que esté tratando en ese momento. Podemos ver que a cada vuelta imprime el nombre de cada elemento, cosa que antes no podía pasar, aunque me estoy dando cuenta de que con el Get-ChildItem la diferencia no varía mucho. De todos modos, imagino que será la forma general de tratar todos los objetos, como cuando importamos archivos XML o CSV y estos nos devuelven objetos.

En cualquier caso, una vez hayamos conseguido recorrer todos los nombres de nuestro directorio actual y tengamos acceso a los mismos mediante la propiedad Name, tendremos que recorrer nuestros parámetros, de forma que comparemos cada uno con todos los elementos de nuestro directorio y ver si existen o no. Para esto podemos utilizar un bucle for, que no foreach. Este tipo de bucles, junto con los bucles while, se suelen emplear cuando sabemos exactamente dónde va a terminar de dar vueltas, es decir, sabemos su límite. En este caso sabemos todos los parámetros que tenemos gracias a cantidad, así que sería lo ideal:


Podríamos traducir literalmente el bucle como: para $i igual a cero, hasta que $i sea menor o igual a $cantidad, incrementa $i de uno en uno a cada vuelta. Esto hará que, si le pasamos cinco parámetros, ejecutará el bucle cinco veces, una vez para cada uno, pero no podemos olvidar que tenemos dentro también otro bucle foreach, que nos obligará a comparar, por cada una de esas cinco vueltas, nuestro argumento con el elemento del $listado correspondiente.

En la salida podemos ver en color verde (he usado líneas rojas para que se viese mejor) que compara ¿a b c? Con todos los elementos de nuestro directorio, para volver a empezar de nuevo (indicando que la primera vuelta del bucle for ha terminado). En primer lugar, tenemos mal varias cosas, y lo he hecho así a propósito desde el principio. Estoy comparando $args[0] con cada elemento, y $args[0] debería ser a, no a b c. ¿Por qué pasa esto? Pues porque estamos trabajando con un array de parámetros, y sucede lo mismo que explicaba antes con los objetos: toma todos los parámetros en un mismo bloque. Lo podemos remediar así, añadiendo el símbolo de @ al invocar la función, en lugar del dolar:


Y ahora sí va bien, porque ya estamos comparando cada parámetro con todos los elementos de nuestro directorio actual. En este caso estoy usando $args[0], que es la letra a, así que lo ideal sería ir recorriendo eso con un número, que será $i.

Es suficiente con sustituir los Write-Host con comparaciones, y cuando haya encontrado coincidencias nos sacará por pantalla los argumentos que sí existen:


Bueno, hagamos un resumen de lo que hace esto:
Pasamos tres parámetros a la función (la letra a, propiedades.txt y una palabra sin sentido), así como la cantidad de los mismos. Dentro de la función hacemos un bucle donde estaremos repitiendo iteraciones tantas veces como parámetros hayamos pasado, y accederemos a cada parámetro utilizando la variable $args[$i], donde $i indica la posición del parámetro; como $i valdrá cero al principio, sacará el primer parámetro, y así sucesivamente hasta completarlos todos. Luego, dentro de cada vuelta recorremos todos los elementos de nuestro directorio actual y comparamos su nombre con el parámetro correspondiente. Si encontramos alguna coincidencia, es que ese parámetro que hemos pasado existe. En este caso, ni la letra a ni la palabra sin sentido existen como archivos o directorios, así que solamente coge el segundo parámetro que sí existe.

Hasta aquí va todo genial, ¿pero si nos piden que diferenciemos entre directorios y ficheros? ¿O que avisemos cada vez que no se encuentre nada de nada? Pues para eso añadimos un par de cositas muy sencillas:


Utilizamos la variable $coincidencias, que modificará su valor solo y solo sí, ha encontrado alguna coincidencia, sino, mostrará al final que no se ha encontrado nada, como se puede ver en la imagen.

Respecto al otro punto:


Tan sencillo como utilizar la propiedad Mode de $archivo, la cual nos indica si estamos tratando un directorio o un fichero (si tiene una 'd' o una 'a', si es uno no puede ser el otro, así que es sencillo usando -match). Para este ejemplo he creado un directorio con ese nombre para ver si funciona. Y no importa el orden en el que pasemos los parámetros, si existen, el script se asegurará de indicarnos que están ahí.

Espero que haya sido productivo, un saludo.