Oct 262007
 

Introducción a las variables y parámetros

Cualquiera que conozca cualquier lenguaje de programación tendrá ya claro el concepto de variable, pero para los que no sabéis programar es conveniente hacer una pequeña introducción.

Todo el mundo sabe usar una calculadora. Todas las calculadoras tienen una o varias teclas de ‘memoria’, usando esas teclas podemos almacenar un valor con el que estamos trabajando, para poder recuperarlo después, también podemos hacer operaciones con ese valor, como sumarle o restarle cosas. Una variable, en programación, es muy similar a ese concepto, es un almacén donde guardo un dato, que puedo recuperar cuando lo necesito. Puedo modificar o borrar el dato cuando quiera, es por eso que a esa ‘memoria’ se da el nombre de variable. A diferencia de las calculadoras que normalmente solo tienen una ‘memoria’ con las variables tenemos cuantas necesitemos, es decir podemos almacenar no solo un valor sino muchos, cada uno en un espacio distinto. Para distinguir cada uno de estos espacios de almacenamiento es necesario darle un nombre a cada uno, los nombres los elige el usuario a su libre albedrío, pero siguiendo ciertas reglas.

A diferencia de nuestro ejemplo de la calculadora, que sólo trabaja con números, los ordenadores pueden procesar muchos tipos de datos, números, palabras, textos… las variables pueden almacenar cualquier tipo de datos.

En algunos lenguajes es necesario distinguir de que tipo es el dato que guardamos en una variable, en bash no, la misma variable puede contener en un momento dado un número y en otro un texto.

En realidad las variables son un caso concreto de un concepto más amplio que es el de parámetro. Un parámetro es cualquier entidad capaz de almacenar un valor susceptible de cambiar. Las variables son en realidad parámetros que nosotros podemos crear, destruir o modificar su valor a voluntad. Pero hay otros parámetros a los que no podemos asignar valores pero si consultarlos. Por ejemplo el código de error del último comando ejecutado no puede ser asignado ni modificado por nosotros sin embargo es un valor que puede consultarse.

Llamamos asignar un valor a una variable al hecho de almacenar un valor en el espacio designado por el nombre de ésta, cada variable solo puede contener un valor al mismo tiempo, y si asignamos un nuevo valor a una variable cualquier valor que tuviese previamente se destruye.

Variables, asignación y utilización

A una variable se le puede asignar un valor en bash mediante la orden:

$ NOMBRE=valor

Esta es una de las órdenes cuya primera palabra no es un comando, tal como estábamos acostumbrados a ver, la primera palabra es el nombre de una variable, luego lleva un signo igual y luego un valor, no se ponen espacios ni antes ni despues del signo igual. El nombre puede ser cualquier serie de caracteres alfabéticos (A-Z, a-z), numéricos (0-9) o el caracter de guión bajo ‘_’, pero el primer caracter no puede ser un número, lo siguiente son asignaciones válidas:

$ A=5
$ Mi_Nombre=Redy
$ JC45="Esto es un valor"
$ _T_65=Hola

Estas no valen:

$ 5jK=67 #(El nombre de la variable no puede empezar por un número)
$ A-47=Hola #(El nombre de la variable no puede tener signos, solo letras números y '_')
$ A = 7 #(No se pueden dejar espacios entre la variable y el '=' ni entre el '=' y el valor)
$ NombreCompleto=Redy Rodriguez #(Si el valor lleva espacios hay que usar comillas o escape)

Las letras mayusculas y minusculas en los nombres de variables son diferenciadoras, no es lo mismo A=7 que a=7, son dos variables distintas.

Otra cosa… podemos definir varias variables en la misma orden:

$ A=5 B=6 C=hola

Para obtener el valor de un parámetro, o de un variable, el bash utiliza un mecanismo de expansión, que funciona de manera similar a las expansiones vistas hasta ahora. Cuando bash encuentra en una palabra el signo ‘$’ todo lo que va a continuación se considera el nombre de un parámetro, y todos los caracteres desde el ‘$’ hasta el final de la palabra son sustituidos por el valor contenido en el parámetro (o variable) con dicho nombre.

$ A=Curso
$ B="Redy Rodríguez"
$ echo este $A lo escribe $B
este Curso lo escribe Redy Rodríguez
$ C=mente
$ echo este $A normal$C lo escribe $B
este Curso normalmente lo escribe Redy Rodríguez

Como ves el $ no tiene porque aparecer siempre al principio de la palabra, en este caso no hay ningun problema porque está al final de la palabra. Pero cuando queremos hacer una expansión de variable en medio de una palabra surge un problema: no sabemos donde acaba el nombre de la variable. Por ejemplo, esto no funcionaría como esperamos.

$ A=Normal
$ echo $Amente $B escribe este curso
Redy Rodríguez escribe este curso

Como después de $A no acaba la palabra, bash se cree que el nombre de la variable es ‘Amente’ y como esa variable no tiene ningún valor se sustituye toda la palabra por una cadena vacía. Para evitar este problema podemos recurrir a encerrar el nombre de la variable entre llaves. $A y ${A} significan exactamente lo mismo y se expanden igual pero con la segunda forma las llaves delimitan el nombre y así podemos ponerla en medio de otra palabra:

$ echo ${A}mente $B escribe este curso
Normalmente Redy Rodríguez escribe este curso

La sustitución de parámetros funcionará incluso dentro de expresiones encerradas entre comillas dobles, pero no entre comillas simples. Veamos ejemplos:

$ Nombre=Redy Apellido=Rodriguez
$ echo "Me llamo $Nombre $Apellido"
Me llamo Redy Rodriguez
$ echo 'Si pongo $Nombre $Apellido obtengo:' "$Nombre $Apellido"
Si pongo $Nombre $Apellido obtengo: Redy Rodriguez
$ Directorio=/home/
$ ls $Directorio

Aunque todo lo que hay dentro de las comillas es una sola palabra, ya ves que no es imprescindible usar las llaves cuando el nombre de la variable va seguido de un blanco o de otro signo que no pueda formar parte del nombre. Las llaves solo son imprescindibles cuando no se pueda interpretar dónde acaba el nombre de la variable, pero ante la duda, si las pones no pasa nada, solo el trabajo de teclear dos caracteres de mas.

La expansión de parámetros tendrá efecto en cualquier parte de una orden, incluso en el comando (esto puede sorprender a quienes conocéis otros lenguajes de programación), o en la propia asignación a la derecha del igual:

$ Comando="ls -l"
$ $Comando

$ Comando=ls
$ Argumento=-l
$ $Comando $Argumento

$ MeLlamo="$Nombre $Apellido"
$ Echo Me llamo $MeLLamo
Me llamo Redy Rodriguez

No obstante no se podrá usar a la izquierda del igual en una asignación. Lo siguiente no le asignará un valor a la variable A5 y dará error:

$ NombredeVariable=A5
$ $NombredeVariable='Valor de la variable'

La asignación de variables se puede hacer en la misma orden en la que se ejecuta un comando: de esta forma:


$ DISPLAY=:0 xgamma 1,2

Pero en ese caso la variable tiene un carácter temporal, y solo tendrá ese valor durante la ejecución de ese comando, volviendo a tomar al finalizar al valor antiguo. Intentaré demostrar esto:


$ A=0 B=3
$ A=1 B=5 eval 'echo $A $B'
1 5
$ echo $A $B
0 3

¿Porque uso eval ‘echo $A $B’ en lugar de un simple echo $A $B? Muy sencillo: Analicemos como funciona bash. Primero divide la línea en palabras y hace las expansiones, y luego ejecuta el comando. Si pusiera:


$ A=1 B=5 echo $A $B

Lo primero que se haría sería sustituir $A y $B por sus valores que inicialmente eran 0 y 3. Luego ejecutaría el comando, que consiste por una parte en asignar a la variable A y B unos nuevos valores y a continuación ejecutar el comando echo con dos parámetros que previamente se habían expandido a 0 y a 3. No era eso lo que queríamos ¿verdad?. Al encerrar entre comillas simples $A y $B no se expanden en un principio, pero cuando se ejecuta el comando eval si. (El comando eval lee la cadena que le pasamos como argumento y se la pasa al bash para que la ejecute como si se la hubiésemos tecleado.) ¿Se ha entendido esto? Hay que tener muy claro como funciona bash si no queremos tener extrañas sorpresas que parecen ir contra la lógica.

Dialogando con los scripts: El comando read

Un comando muy interesante y que funciona con variables es read. Este comando permite leer lo que tecleamos y lo almacena en una o varias variables. Veamos como funciona en un script:

#! /bin/bash
#
# Este script ilustra el uso del comando read
#
echo -n "¿Como te llamas? "
read NOMBRE
echo "Hola $NOMBRE; encantado de conocerte."

Lo guardamos como saluda2 en nuestra carpeta ~/bin y le damos permiso de ejecución.

$ saluda2
¿como te llamas? Redy
Hola Redy; encantado de conocerte.

El parámetro -n de echo sirve para que no salte a la línea siguiente tras escribir lo que le decimos. El read lee lo que le tecleamos hasta que pulsamos <enter> y lo almacena en la variable NOMBRE. Podríamos haber puesto el equivalente a estas dos líneas en una sola usando el argumento -p en el read. La palabra que sige a -p se mostrará en pantalla antes de leer del teclado. Modifiquemos nuestro script:

#! /bin/bash
#
# Este script ilustra el uso del comando read
#
read -p "¿Como te llamas? " NOMBRE
echo "Hola $NOMBRE; encantado de conocerte."

A veces nos interesa que read lea varios valores. Si después del read no ponemos ningun nombre de variable todo lo que tecleamos hasta pulsar <enter> se almacenará en la variable $REPLY. Si ponemos una sola variable se almacenará en esa variable, pero si ponemos varias irá tomando las palabras que le tecleemos (en el sentido que bash entiende las palabras) y almacenará la primera palabra en la primera variable, la segunda en la segunda variable, y así hasta la última. Si tecleamos más palabras que variables, la última variable contendrá todas las palabras que queden hasta el <enter> Y si tecleamos menos palabras que variables las últimas variables quedarán vacías.

#! /bin/bash
#
# Este script ilustra el uso del comando read
#
read -p "¿Cual es su nombre y apellido? " NOMBRE APELLIDO
echo "Encantado de conocerle sr. $APELLIDO."
echo "D. $NOMBRE, se nota que es usted una persona distinguida."

También podemos especificarle al read un tiempo máximo de espera. Si el usuario no le teclea nada después de ese tiempo finaliza (con error). Esto de los errores lo veremos más adelante. Para especificarle el tiempo pondremos -t y un valor en segundos como argumentos. (el || exit del final es para que no continúe en caso de error, ya lo veremos más adelante también).

#! /bin/bash
#
# Este script ilustra el uso del comando read
#
read -t 20 -p "¿Cual es su nombre y apellido? " NOMBRE APELLIDO || exit
echo "Encantado de conocerle sr. $APELLIDO."
echo "D. $NOMBRE, se nota que es usted una persona distinguida."

Ahora el script hace lo mismo, salvo que tiene ‘menos paciencia’, si tardas más de 20 segundos en contestar a su pregunta se cansa y termina.

Parámetros posicionales:

Otra manera de pasarle información a nuestros scripts es usando argumentos. Hasta ahora solo ejecutábamos el script sin más pero, a los scripts, al igual que al resto de programas se le puede pasar argumentos. Supongamos que hacemos un script para rescalar una imagen a un tamaño de 1000 pixels en su lado más largo, y además si no estaba ya en jpg, convertirlo con unos valores de compresión adecuados para que no pese mucho ni pierda mucha calidad. Vamos, lo ideal para subirlo a nuestra galería de fotolibre, por ejemplo. Todo eso lo haremos con imagemagick. Yo obtengo muy buenos resultados para hacer esto, con bastante buena calidad y no demasiado peso del archivo con el comando:

$ convert -filter Lanczos -resize 1000x1000 -sharpen 1x0.75 -blur 1x0.5 -quality 85 -strip -sampling-factor 2x2 -compress JPEG IMAGEN-ORIGINAL IMAGEN_RESCALADA.jpg

Suelen salir, dependiendo de la foto ficheros de no más de 200K en los que no se nota la pérdida de enfoque que produce el rescalado y creo que normalmente no genera artefactos. Si alguien tiene un sistema con el que se consigan mejores resultados que lo diga. Claro que el comando es un poco largo para recordarlo de memoria y para escribirlo entero cada vez que queremos redimensionar una foto ¿no? Así que ¿que os parece si hacemos un script que le llamemos ‘pasubir’ por ejemplo y que haga todo esto por nosotros con la imagen que le digamos?
Como cada vez vamos a hacer el proceso con una imagen distinta habrá que encontrar una manera de decirle al script la imagen que queremos tratar. Pues para eso están los argumentos. Si ejecutamos

$ pasubir imagen.tif

Estamos pasando un argumento al script. Podemos pasar varios. Luego en el script estos argumentos podemos leerlos consultando lo que llamamos parámetros posicionales. $1 será el primer argumento que hemos pasado, $2 el segundo $3 el tercero y así hasta $9. Si queremos ir más allá del 9 hay que poner ${10}, ${11} etc… En nuestro caso concreto con el 1 nos apañamos.

#! /bin/bash
#
# Este script convierte una imágen (si no lo está ya) a jpg y 
# la rescala a un máximo de 1000 pixels por su lado más largo. 
# la imágen de destino se llamará igual que la original pero añadiendo 
# al nombre el sufijo '_escalado.jpg'
# 
convert -filter Lanczos -resize 1000x1000 -sharpen 1x0.75 \
             -blur 1x0.5 -quality 85 -strip -sampling-factor 2x2 \
             -compress JPEG $1 $1_escalado.jpg

Ya expliqué lo de escapar los retornos de cario ¿no?. Si, lo dije en la entrega 3. Para los despistados lo repito: “Si pones un escape justo antes de dar al intro, anulas el efecto del intro, y se continúa introduciendo el comando en la siguiente línea, pero al final se tratará como si fuese una sola línea, esto puede ser útil para teclear líneas largas.” Para bash no es ningún problema que las líneas sean largas, pero cuando editamos un script las líneas largas se hacen incómodas de leer, así que porque no partirlas en varias, poniendo un caracter de escape ‘\’ justo al final de la línea. Mucho cuidad de no dejar espacios después del ‘\’ porque si lo hacemos escapamos el espacio y no el intro ¡eh!

Hala como ejercicio propongo mejorar el script para que no solo rescale al tamaño de 1000 pixels sino que seamos nosotros quienes le digamos el tamaño que queremos. ¿que os parece?