P1N6Ü1N0 - SHELL - Introducción



Inicio
C
Perl
Caml
Shell
GTK
SQL

Introducción al Shell Scripting

Para entender los conceptos que se van a explicar aquí, es áltamente recomendable conocer algún lenguaje de programación, o al menos, tener unas nociones básicas sobre la programación imperativa en general.

En primer lugar, empezaré explicando los comandos shell básicos para hacer control de flujo y cómo funcionan.

Comenzaremos por ver los comandos builtin, que son los que vienen incluidos en el propio shell, es decir, que no son programas ejecutables.

Pero antes, es imprescindible dar unas nociones básicas sobre la lógica en shell scripts, y las variables: Generalmente, cuando un programa termina correctamente, devuelve un 0 al sistema operativo, y cuando termina mal, cualquier otro valor. Existe un programa que siempre devuelve 0, que se llama true, y un programa que nunca devuelve cero, que se llama false.

En cuanto a las variables, quien no oyó hablar de las variables de entorno alguna vez? Pues estas son las que se utilizan en los scripts.

Basta con saber que se asignan haciendo:
variable=valor
y se referencian con un símbolo '$':
$variable

Por ejemplo, para añadir un directorio al path del sistema, podemos hacerlo de la siguiente forma:
PATH=$PATH:directorio

No es muy interesante para scripts, pero conviene saber que asignando las variables de esa forma, los programas que se ejecuten desde nuestro script no podrán acceder a estos valores que acabamos de asignar a las variables. Para que puedan hacerlo, hay que exportarlas. Esto se hace mediante el comando export. Por ejemplo, es muy comun utilizarlo para ejecutar aplicaciones X-Window:

export DISPLAY=mi_maquina:0.0

Visto esto, ya se puede empezar a ver los comandos disponibles para realizar el control de flujo en los scripts.

IF

El comando if nos permite realizar unas cosas u otras en función de los valores que devuelven los comandos al sistema.

Por tanto, el comando if nos permite comprobar este valod devuelto al sistema por el comando.

La sintaxis de if es:

if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ]... [ else COMMANDS; ] fi

Podemos cambiar los ';' por retornos de carro.

El funcionamiento es el siguiente: Se ejecuta COMMANDS, si el valor devuelto al sistema es 0, se ejecutan los COMMANDS del then, si no, los del else. fi simplemente indica cuando termina el comando if.

elif funciona igual que si pusiéramos else if ..., pero nos ahorra cerrar el segundo if.

Por tanto, un ejemplo del uso de if será:

if ls archivo.c; then echo "Si existe";else echo "No existe";fi

Esto puede ponerse también de la siguiente forma:

if ls archivo.c
   then echo "Si existe"
   else echo "No existe"
fi

En este ejemplo hacemos uso de otro comando, el comando echo, este es en general un ejecutable del sistema. Por ahora basta con decir que muestra textos en pantalla.

En este ejemplo, además, vemos que cuando el archivo no existe, sale un mensaje del comando ls por pantalla. Más adelante veremos cómo suprimir dicho mensaje.

WHILE

Ejecuta uno o mas comandos mientras el valor de salida de otro comando sea cero.

Sintaxis:

while COMMANDS; do COMMANDS; done

Este comando es bastante sencillo de comprender, veamos un ejemplo:

while true;do echo "repetimos";done

Este ejemplo escribe todo el rato la palabra repetimos en pantalla.

FOR

Es un comando bastante potente, muchas de las cosas que haremos con scripts harán uso de él, porque es el típico comando para realizar el mismo procesamiento sobre muchos archivos distintos.

Sintaxis:

for NAME [in WORDS ... ;] do COMMANDS; done

Para cada una de las palabras (WORDS), ejecuta el/los comando/s COMMANDS.

Aquí teneis un ejemplo:

for variable in uno dos tres cuatro cinco
  do echo $variable
done

Este ejemplo muestra en pantalla las palabras que ponemos despues de in.

El comando for no suele utilizarse de esa forma, generalmente la lista de palabras que van despues de in, es generada por un procesamiento anterior, pero es aun pronto para adelantar esto.


Con lo visto hasta aquí poco podemos hacer en cuanto a scripts, por eso vamos a empezar con cosas algo más complejas que son las que dan a los scripts toda su potencia.

REDIRECCIÓN

La redirección es uno de los puntos fuertes del shell scripting, se utiliza generalmente para filtrar la salida de determinados programas, o para utilizar la salida de ciertos programas como entrada para otros.

Pero veamos primero los conceptos de salida, entrada y error estandar.

Cuando creamos un proceso (por ej, al ejecutar un comando), este tiene tres ficheros abiertos:

  • uno es la entrada estandar, por la que se le hacen llegar (generalmente) nuestras pulsaciones de teclado.
  • Otro es la salida estandar, por el que el proceso muestra la información que desee (Todo lo que escribimos con echo en shell script, o con printf, en C, sale por este fichero). Generalmente lo que sale por el fichero se escribe en la consola donde se ejecuta el programa.
  • El último es el error estandar, por el que el proceso muestra errores, o en algunos casos, información que no debe mezclarse con la salida. Por defecto salen por la consola donde se ejecuta el programa.

Lo que ocurre es que estos ficheros se pueden redireccionar. Es decir, podemos hacer que la salida estandar vaya a un fichero, en lugar de hacerlo en la pantalla, que la entrada estandar lea de un fichero, en lugar de hacerlo de teclado, y que el error estandar vaya a otro fichero.

La redirección más básica es esta que acabo de poner como ejemplo, y es cuando se redirecciona a ficheros:

  • El símbolo '<' sirve para redireccionar la entrada estandar. Si hacemos:
    programa < archivo
    El programa leera del archivo como si fuesen pulsaciones de teclado.
  • El símbolo '>' sirve para redireccionar la salida estandar. Si hacemos
    echo "Hola, soy edu, feliz navidad' > archivo
    Ese texto, en lugar de salir por pantalla, se escribirá en el archivo.
  • El símbolo '2>' Redirecciona el error estandar. Funciona igual que '>'.

Además de este tipo de redirección existen otros, uno es el conocido comunmente como pipe. En él, se ejecutan a la vez dos programas, y la información que va generando uno, se utiliza como entrada para el otro.

Por tanto, existe una especie de canal entre el fichero de salida de uno de ellos y el fichero de entrada del otro.

Se hace mediante el símbolo '|'

El ejemplo típico es:

cat archivo | more

El programa cat normalmente saca por pantalla (por su salida estandar) el contenido del fichero, pero en este caso lo que se hace es mandar esta información como entrada al programa more, que es un programa que muestra lo que le viene por la entrada estandar pantalla a pantalla.

Estos son verdaderos tipos de redirección, pero aun se puede hacer otra cosa muy relacionada con estas redirecciones, aunque realmente no lo sea. Es la conversión de salida estandar a línea de comando. Se hace colocando un comando entre comas como esta: ` Que en teclados españoles está situada al lado de la ñ. Para generar esa coma, generalmente hay que pulsar un espacio despues de pulsarla. En algunos editores, para que salga hay que realizar esta operacion dos veces (tilde, espacio, tilde, espacio)

Por ejemplo:

for variable in `ls`
  do echo "FICHERO $variable ENCONTRADO"
done

Este ejemplo convierte la salida de ls (listado de los ficheros del directorio) a palabras de la linea de comandos, es decir, que si ls mostraba esto:

c
caml
gtk
images
index.html
perl
shell
sql
entonces, `ls` se ccambiará por esto:
c caml gtk images index.html perl shell sql
en una sola línea y al lado del for variable in, por lo que se usara cada fichero como una palabra del comando FOR.

En bash existe otra forma de hacer esto, que es usando $(comando), en lugar de `comando`, Pero es algo específico de bash, por lo que no recomiendo a nadie que lo use.

Este es el momento de ver cómo evitar que los comandos saquen por pantalla información que no deseamos ver, como lo que ocurría con el ls cuando no se encontraba el fichero que le mandábamos buscar. En este caso nos interesabe sólo el valor que devolvía al sistema, pero ese mensaje de error, podemos eliminarlo redireccionando el error al dispositivo nulo (/dev/null). Este dispositivo elimina todo lo que se escribe en él.

Con lo visto hasta aquí ya se pueden empezar a hacer algunas cosas, pero todavía faltan dos cosas importantes, en primer lugar el comando builtin test, que nos permite evaluar expresiones y devolver al sistema true o false para poder utilizarlo en if, while, etc. Y en segundo lugar, los comandos para filtrar ficheros (sed, grep, awk, etc.)

test

No voy a comentar todas las opciones del comando test, porque tecleando help test salen todas ellas muy bien explicadas, pero voy a explicar la forma abreviada y algunos detalles importantes.

En primer lugar, test es un comando que evalua expresiones. Y tiene operadores para manejar strings, números y ficheros. Repito que en la ayuda del shell viene muy bien explicado.

Además, tiene una forma abreviada, que consiste en rodear la expresión con corchetes en lugar de poner test expresión.

Por ejemplo:

if test -d /tmp
  then echo /tmp es un directorio
  else echo /tmp no es un directorio
fi
es lo mismo que poner:
if [ -d /tmp ]
  then echo /tmp es un directorio
  else echo /tmp no es un directorio
fi

Comandos para filtrar ficheros

grep

El comando grep (es un ejecutable del sistema) busca un patrón dentro de uno o más ficheros. Lo interesante es combinarlo con la redirección, y en concreto con los pipes. De esta forma, podemos por ejemplo, encontrar fácilmente el pid de un proceso que corre en el sistema:

ps | grep bash

Con este comando, aparecerán en pantalla todas las líneas de la salida del comando ps que contengan la palabra bash. Por tanto, saldrán los pids de todos los bash.

sed

El comando anterior no es suficiente, sólo es capaz de mostrar las líneas completas del fichero que contienen la palabra en cuestión. Esta carencia puede suplirse usando sed en uno de sus modos más útiles, el de substitución.

Antes de contuniar, tengo que advertir que sed es una herramienta extremadamente compleja, prácticamente un lenguaje de scripting, pero generalmente, cuando las necesidades de nuestro script nos obligan a utilizar las funciones avanzadas de sed, bajo mi punto de vista lo mejor es hacer el script con perl directamente.

Sed sería capaz de emular perfectamente el comportamiento de grep, veamos un ejemplo, el ejemplo anterior con grep, se puede hacer con sed asi:

ps |sed -n /bash/p

Por ahora no intenteis comprender esa línea :)

Voy a intentar entonces dar unas nociones básicas de sed, hasta donde alcanzan mis conocimientos (yo todavía no controlo todo lo que sed permite hacer).

Todo comando sed tiene dos partes:

  1. La dirección. Indica sobre que línea/s del fichero trabajará sed. Esta parte tiene 3 formas:
    • No estar presente. Si la omitimos, sed trabajará sobre todas las líneas del fichero.
    • Tener una única dirección. Entonces sed trabajará sobre la línea o las líneas que cumplan la condición que exige la dirección.
    • Tener dos direcciones separadas por una coma (un rango). En este caso sed trabajará con todas las líneas que se encuentren entre la primera y la segunda dirección.
    Para negar una direccion (que sed se ejecute para las líneas que no la verifican) se usa el símbolo ! entre la direccion y la siguiente parte (el comando). Existen los siguientes tipos de direccion:
    • Número: indica un número de línea a seleccinar.
    • Primera~Paso: indica las líneas que se encuentren desde Primera a intervalos de Paso Lineas. Por ejemplo, con 1~2, hacemos que la direccion sean todas las líneas pares del fichero. Con 2~5, seran a partir de la linea 2, cada 5 líneas.
    • $ : Selecciona la última línea.
    • /regexp/ : Indica todas las líneas que encajen en la expresion regular. Con man regexp puedes ver la ayuda sobre expresiones regulares. Pero ya adelanto que son una forma de especificar la forma de un string. Generalmente se utiliza el término match (encajar) para decir si un string tiene la forma especificada por una expresion regular. Por ejemplo, la expresion regular /a*[0-9]+/ encaja con los strings: 1, 8, a1, a7, aaaaaaaaaaaaaaaaa0, a123123123, etc. Es decir, un numero de a's que puede ser nulo, y luego un conjunto no nulo de digitos (entre 0 y 9). De esta forma, las lineas que selecciona una direccion asi, son aquellas que encajan con la expresion regular (al menos una parte de la linea encaja).
    • \CregexpC : No entiendo muy bien la diferencia con lo anterior :-?
  2. El comando. Indica qué se hace con las líneas que fueron seleccionadas. Existen comandos diferentes según utilicemos una dirección vacía, con una direccion simple, o con un rango. Esta parte puede verse en la página man, porque es la más sencilla.

    De todas formas voy a comentar un par de ellos que me parecen interesantes

    En primer lugar, el comando s. Substituye texto. Es útil para dar un formato propio a la salida de ciertos comandos.

    La sintaxis es: s/regexp/texto/

    Por ejemplo, vamos a formatear el comando finger de otra forma:

    En primer lugar veamos como cambiar donde pone Name: por Nombre:. Esto es bastante sencillo, basta con poner:

    finger mi_login |sed s/ame/ombre/

    Vemos que falta la dirección, Name: solo aparece en una línea a si que no es necesario indicar ninguna direccion, podemos dejar que se aplique a todas las líneas.

    Ahora vamos a hacer algo más que cambiar 3 letras, cambiaremos toda una línea. Para esto ya será necesario utilizar expresiones regulares, pero a un nivel muy básico, el objetivo es obtener el PID de todos los shell que están corriendo con nuestro usuario:

    ps |sed -n "/bash/s/ *\([^ ]*\) *.*/Shell con pid=\1/p"
    Este ejemplo, selecciona todas las líneas de un ps que ponen bash, y luego cambia cada línea por la frase Shell con pid=x siendo x el pid de cada shell. Vemos que se ponen unos paréntesis en la parte de la expresión regular del comando s, y que se pone un \1 en la parte del cambio. Siempre que encerremos entre paréntesis trozos de la expresión regular, podemos referenciarlos luego como \1, \2, etc. Y estos paréntesis llevan un escape \, que debemos poner siempre.

    También hay que fijarse en el parámetro -n, que hace que sed no muestre en pantalla el texto a no ser que se le indique por la fuerza. Y en este caso, el comando s tiene un modificador (p) que hace que todas las líneas que se cambien salgan por pantalla.

    Otro comando interesante es p. Este comando simplemente escribe las líneas seleccionadas. No debe confundirse con el modificador p del comando s. Hacen básicamente lo mismo, pero no son lo mismo.

    Esto sirve para sacar por pantalla un trozo de un archivo, seleccionamos con dos direcciones (un rango) el conjunto de líneas necesario, y lo mostramos con el comando p. Pero para que esto sea útil, es necesario usar la opción -n, para que solo muestre estas líneas que me mandamos mostrar.

    No voy a comentar más comandos, los demás están bien explicados en la página man, pero voy a comentar como aplicar varios comandos a una misma dirección. Para hacer esto basta con encerrar los comandos entre { y }, y finalizarlos con ; todos, incluso el último. Es decir, que la ristra de comandos terminará con ;}. Esta parte estaba algo confusa en la página de manual.

SCRIPTS EN FICHEROS

Los scripts, en cuando son útiles para más de una vez, en vez de memorizarlos se suelen almacenar en ficheros. Cuando un script está en un fichero, este debe comenzar con la línea:

#!/bin/sh

Que indica al sistema que es un script y que el programa capaz de analizarlo es /bin/sh.

A continuación hay que dar permisos de ejecución al fichero:

chmod +x fichero
y para ejecutarlo procederemos como con cualquier otro programa ejecutable.

Comentarios

Los comentarios se ponen con el símbolo #.

Todo lo que tenga una línea a continuación de un # se ignora.

PASO DE PARÁMETROS A SCRIPTS

Cuando un script está en un fichero, tiene una enorme ventaja, y es que puede recibir parámetros, como los programas compilados. Los parámetros se pasan en variables numéricas (0,1,2,3,...). En el 0 como siempre está el nombre del ejecutable, y a partir de ahi, están los parámetros por el orden en que se escribieron. Lógicamente, como son variables, se accede a ellas con un símbolo $ delante ($0, $1, $2, ...).

El número de parámetros se pasa en la variable # (si, como el comentario), por lo que se accede a ella con $#.

Por último, para referirnos a todos los parámetros del script se utiliza la variable *, que referenciaremos como $*.

FINALIZACIÓN

Para finalizar un script se utiliza el comando exit. Debemos indicar un número que será el valor que devuelva al sistema.

FUNCIONES

Si señor, en Shell script se pueden hacer funciones. Y además de forma muy sencilla:
nombre_funcion()
{
}
En nombre_función pondremos lo que nos apetezca. En shell cada función es cómo un nuevo script, es decir, que tiene sus propios parámetros. Desde una función no podemos acceder a ningún parámetro del script, pero desde el script podemos llamar a la función con cualquier número de parámetros. De hecho, si queremos que la función pueda usar los parámetros del script, basta con llamarla con el parámetro $*, que está explicado antes.

Pues bien, esto es todo por ahora en cuanto a shell scripting, y no es poco, ya teneis material para hacer unos scripts verdaderamente grandes.

Existen más comandos, en el caso de ejecutables esta claro, pero ademas existen más comandos built-in del shell. Pero ahora supongo que ya sabéis lo necesario para que os pique la curiosidad y probarlos vosotros mismos. Se ven con el comando help, como dije antes.


EJEMPLO

A modo de ejemplo voy a poner uno de mis scripts más sencillos y voy a comentarlo:

#!/bin/sh

if test $# -lt 1;then echo "$0 <words>";exit 1;fi
for i in `find .`;do
  busqueda=`sed -n /$1/= $i`
  if test -n "$busqueda"; then echo $i: $busqueda;fi
done 2>/dev/null

Este script busca recursivamente en todos los directorios a partir del actual los ficheros que contengan la expresión regular que se le indica al script, y saca por pantalla el nombre del fichero y TODAS las líneas donde aparece dicha palabra.

Paso a describir como funciona: En primer lugar se comprueba que el número de parámetros no es menor que 1, es decir, que el programa recibe al menos un parámetro. Si no es así, se pone la sintaxis en pantalla y se termina.

En caso de superar la primera prueba, se entra en un bucle for, que obtiene las palabras del resultado de un find. Esto es peligroso, porque si hay algún fichero que tenga espacios, for lo interpretará como dos ficheros. No conozco ninguna forma trivial de solucionarlo, se pueden cambiar los espacios por caracterers raros y luego recuperarlos, pero esto es un poco chapucero.

Dentro del for, primero asignamos a la variable búsqueda los números de línea que contienen la expresión regular, esto se hace muy fácilmente con el comando sed, que va procesando el fichero ($i) y sacando los números de línea donde aparece la palabra que se pasó como primer parámetro ($1).

Luego se comprueba que la variable busqueda no es un string vacío, y si se verifica (es decir, apareció alguna línea en el fichero que contenia la expresion regular), se muestra en pantalla con el formato que se ve ahi.

Al finalizar el bucle for, redireccionamos todo el error estandar al dispositivo /dev/null, que es precismente para eso, para poder mandar cosas a él, porque lo que se escribe en él se pierde.

Pues hasta aquí llegó esta introducción al scripting, espero que haya servido a alguien para aprender un poco sobre todo esto, aunque me temo que hayan quedado algunos puntos mal explicados. Por tanto, si veis que alguna cosa no se entiende, mandadme un email (<ryu@mundivia.es>) y lo cambiaré.

  Los gráficos de esta página han sido creados con GIMP.