file descriptors (i de ii)

30 de junio de 2009

La potencia de los shells de Linux (como puede ser Bash) radica en la posibilidad de unir comandos pasando la información de uno hacia el otro. Tareas que a primera vista pueden resultar complicadas con un simple comando se hacen bastante sencillas con la unión de comandos usando conductores y redirectores.

En Linux/UNIX tenemos tres archivos llamados STDIN, STDOUT y STDERR. Estos archivos y las capacidades de la "standard I/O" nos ofrecen la posibilidad de controlar el flujo de datos que entran, salen o muestran información de error.
Como comportamiento estandard, un flujo de STDIN o entrada está provocado por las pulsaciones de un teclado. Por ejemplo, cuando arrancamos el programa "mail" este estará leyendo de STDIN todas las pulsaciones de teclado para ir construyendo el mail.
Para el caso de STDOUT, este como comportamiento estandard está enlazado con el monitor. Por tanto todo lo que enviemos aquí se verá reflejado por el monitor.
El caso de STDERR es similar al de STDOUT. Por defecto su contenido se enviará al monitor.

Estos archivos también reciben el nombre de "standard input", "standard output" y "standard error". También se les puede indentificar con un numero, "file descriptor 0", "file descriptor 1" y "file descriptor 2" respectivamente.

Cada vez que lanzamos un proceso asociamos estos tres file descriptors correspondientes.

Veamos ahora las dos herramientas que me permiten cambiar el flujo de datos de un sitio a otro. Estas herramientas son las pipes y los redirectors.
Las pipes se representan por el carácter "|" y su función es redirigir la STDOUT de un proceso a la STDIN de otro proceso igual o diferente. Es decir, el resultado que produzca un procesos (que normalmente lo veremos por el monitor) lo redireccionamos como si fueran pulsaciones de teclado a otro proceso.

Por ejemplo:

# cat /var/log/messages | grep kernel

El resultado de cat que sale por STDOUT se redirecciona al STDIN del comando grep. Por tanto lo que estamos haciendo es listar el contenido de /var/log/messages y sobre este resultado filtrar y buscar las lineas que contengan la palabra kernel.

Las pipes se pueden anidar formando comandos más complicados.

Por ejemplo:
# cat /etc/passwd | cut -d: -f6 | uniq | sort | nl
     1 /bin
     2 /bin
     3 /bin
     4 /dev
     5 /home/amperis
     6 /home/clamav
     7 /home/klog
     8 /home/saned
     9 /home/syslog
    10 /nonexistent
    11 /root
    12 /usr/games
    13 /usr/sbin
    14 /var/backups
    15 /var/cache/man
    16 /var/lib/avahi-autoipd
    17 /var/lib/gdm
    18 /var/lib/gnats
    19 /var/lib/libuuid
    20 /var/list
    21 /var/mail
    22 /var/run/avahi-daemon
    23 /var/run/dbus
    24 /var/run/hald
    25 /var/run/hplip
    26 /var/run/ircd
    27 /var/run/PolicyKit
    28 /var/run/pulse
    29 /var/spool/lpd
    30 /var/spool/news
    31 /var/spool/uucp
    32 /var/www

Con la siguiente anidación de pipes, listamos el contenido de /etc/passwd, luego recortamos para obtener la columna donde están los homes, ordenados el resultado y enumeramos las lineas. Lo que estamos haciendo es pasar el resultado que proporciona un proceso a la entrada del siguiente proceso. El ultimo proceso como no tiene ningún tipo de redirección mostrará su resultado por su STDOUT, es decir por el monitor.
No confundir "cat /etc/passwd | cut -d: -f6 | uniq | sort | nl" con "cat /etc/passwd; cut -d: -f6 ; uniq; sort; nl". En este último caso los procesos se ejecutan uno por uno, pero no existe ninguna relación entre ellos.

Con la herramienta de los redirectors lo que podemos hacer es redireccionar el contenido de un archivo a la entrada de un proceso o la salida de un proceso enviarla a un archivo.
Veamos un ejemplo de como enviar el contenido de un archivo a un proceso. Supongamos que tenemos un archivo llamado "email.txt" con el cuerpo del mensaje de un correo electrónico. Se lo queremos enviar a un usuario llamado "paco":
# mail paco < email.txt

Lo que estamos haciendo es redireccionar el contenido de fichero a la STDIN del proceso mail.

Veamos el caso contrario, es decir, cómo guardar el contenido de un proceso en un fichero. Supongamos que queremos guardar en un fichero de log las 10 ultimas lineas de otro log:
# tail /var/log/messages > mini.log

El comando tail muestras las 10 últimas lineas de /var/log/menssages. Esta salida la redireccionamos al fichero mini.log.
La redirección utilizando ">", siempre crear el archivo en caso de no existir. Si existe el archivo lo sobreescribe. También tenemos la posibilidad de hacer un append, es decir, si existe continuar por su ultima linea y si no existe crearlo. Para ello utilizamos los caracteres ">>":
# tail /var/log/messages >> minilog.log; sleep 60; tail /var/log/messages >> minilog.log

Ahora que ya conocemos estas dos herramientas podemos utilizarlas en conjunción. Veamos como almacenar el resultado del segundo ejemplo en un fichero:
# cat /etc/passwd | cut -d: -f6 | uniq | sort | nl > /tmp/homes.txt

Otro ejemplo utilizando todas las herramientas:
# tr '[a-z]' '[A-Z]' < /etc/passwd | cut -d: -f6 > homes.txt
# head homes.txt
/ROOT
/USR/SBIN
/BIN
/DEV
/BIN
/USR/GAMES
/VAR/CACHE/MAN
/VAR/SPOOL/LPD
/VAR/MAIL
/VAR/SPOOL/NEWS

En el ejemplo anterior enviamos el contenido de /etc/passwd al proceso tr que nos lo convierte todo en mayúsculas. El resultado de esto los cortamos para quedarnos con los homes de los usuarios y lo guardamos en el fichero homes.txt.