Ene 082008
 

Concepto de entradas y salidas

Titulo el artículo ‘La fontanería’, porque vamos a hablar de streams (esta palabra en inglés significa arroyo, o corriente de agua), pero en informática es un flujo de datos, y de eso vamos a hablar de como fluyen los datos entrando y saliendo de los programas y de como podemos canalizarlos.

Esto no es algo propio de la shell bash, es algo inherente a Linux, y a cualquier Unix en general, no se puede entender un sistema operativo tipo Unix sin entender una de sus principales características, el ‘stream‘. Un stream es un canal de entrada-salida de datos. Cada proceso en Linux para comunicarse con el exterior dispone al menos de tres streams, llamados stdin (entrada estándar), stdout (salida estándar) y stderr (salida de errores), veámoslo con más detalle.

Todo proceso se comunica con el exterior en dos posibles direcciones: puede leer datos y escribir datos, y dispone como mínimo de un canal (stream) por el que puede leer datos y dos por los que puede escribir. Al canal por el que lee se le llama ‘la entrada estándar (stdin)’. Normalmente ese canal está conectado a la consola de modo que lee lo que tecleamos. Los canales por los que escribe duelen ser dos, de modo que si necesita dar una información estándar suele escribirla por el canal que denominamos ‘la salida estándar (stdout)’, si lo que tiene es que dar información de un error lo hará por ‘la salida de error (stderr)’. Habitualmente tanto la salida estándar como la salida de error están conectadas a la consola, por lo que en principio veremos ambas por pantalla sin distinguir una de la otra. Lo bueno de todo esto es que tanto los streams de entrada como los de salida los podemos ‘enchufar’ a dónde queramos. Si enchufamos un stream de salida a un fichero no veremos la salida por pantalla sino que quedará grabada en ese fichero, y si conectamos un fichero a un stream de entrada cuando el programa espere que le tecleemos algo, en lugar de leer de teclado leerá de ese fichero. Muy útil para automatizar tareas que esperan que le tecleemos algo.

Para que veáis que existen dos salidas vamos a probar lo siguiente aunque no lo entendáis de momento.

$ cd ~/ejercicios_curso_bash
$ ls fichero_que_no_existe
ls: fichero_que_no_existe: No existe el fichero o el directorio

Ahora creamos un fichero y hacemos lo mismo.

$ touch fichero_que_existe
$ ls fichero_que_existe
fichero_que_existe

Aparentemente en ambos casos el programa ls nos da la información de error o la que le pedimos por la pantalla, pero en realidad lo hace por dos caminos distintos: si le decimos que redirija su salida estándar hacia ninguna parte (o sea hacia /dev/null), en el primer caso seguimos obteniendo el mensaje en pantalla pero en el segundo caso no vemos nada.

$ ls fichero_que_no_existe >/dev/null
ls: fichero_que_no_existe: No existe el fichero o el directorio
$ ls fichero_que_existe >/dev/null

Y si lo que redirigimos es la salida de error hacia ninguna parte ocurrirá al revés:

$ ls fichero_que_no_existe 2>/dev/null

$ ls fichero_que_existe 2>/dev/null
fichero_que_existe

El comando ls, aunque dispone de ella, normalmente no hace uso de la entrada estándar, nos da la información necesaria por la salida que corresponda, pero no espera recibir ninguna información del exterior. Veamos otro caso que sí espera información de la entrada estándar.

$ rm -i fichero_que_existe
 rm: ¿borrar `fichero_que_existe'? (s/n)

Vamos a contestarle que no, no queremos borrarlo de momento, cuando tecleamos la n y damos intro, lo que hacemos es poner esos datos en la entrada estándar del programa, ya que la entrada estándar está conectada a la consola. Ricemos un poco el rizo: primero escribiremos una ‘y’ y una ‘n’ en sendos ficheros, y luego usaremos la orden rm conectando la entrada estándar a esos ficheros en vez de a la consola:

$ echo y >contesta_que_si
$ echo n >contesta_que_no
$ rm -iv fichero_que_existe <contesta_que_no  rm: ¿borrar `fichero_que_existe'? (s/n)

Vemos que rm pregunta, pero no espera a que le contestemos, ha leído la ‘n’ del fichero, y no ha borrado nada, en cambio si tecleamos

$ rm -iv fichero_que_existe <contesta_que_si rm: ¿borrar `fichero_que_existe'? (s/n)
borrando fichero_que_existe

El fichero se borra como cabía esperar.

Hasta aquí lo único que me interesa que te haya quedado claro es que todo programa tiene por defecto una entrada de la que puede leer datos y dos salidas en las que puede escribir, y que esas entradas y salidas que normalmente están conectadas a la consola se pueden redirigir.

Esto es lo interesante, todos los streams pueden conectarse a archivos, a dispositivos, o unos con otros.

Redirigiendo las salidas.

Para redirigir la salida estándar a otro fichero o dispositivo usaremos un signo de mayor (que opcionalmente puede estar precedido del número 1, el 1 indica la salida estándar y el 2 la salida de error, si no se pone ningún número se entiende que nos estamos refiriendo a la salida estándar). Si redirigimos la salida hacia un fichero existente lo sobreescribiremos con la nueva información borrándose su contenido anterior. Si no queremos que se sobrescriba, si no que se añada al final usaremos dos signos de mayor en vez de uno, por ejemplo.

$ echo Esto se va a sobreescribir >fichero_de_prueba
$ echo esto es una prueba 1>fichero_de_prueba
$ echo a la que le añadimos una línea >>fichero_de_prueba
$ echo y luego otra >>fichero_de_prueba
$ cat fichero_de_prueba
esto es una prueba
a la que le añadimos una línea
y luego otra

Para redirigir la salida de error es más o menos lo mismo, solo que en vez de poner el 1 (o no poner nada) pondremos un 2

$ ls fichero_que_no_existe 2>fichero_de_prueba
$ ls fichero_que_tampoco_existe 2>>fichero_de_prueba
$ cat fichero_de_prueba
ls: fichero_que_no_existe: No existe el fichero o el directorio
ls: fichero_que_tampoco_existe: No existe el fichero o el directorio

Si queremos redirigir ambas salidas hacia ficheros distintos podemos hacerlo, por ejemplo:

$ ls contesta_q* fichero_no_existe 1>fichero_de_prueba 2>fichero_de_error
$ cat fichero_de_prueba
contesta_que_no
contesta_que_si
$ cat fichero_de_error
ls: fichero_no_existe: No existe el fichero o el directorio

Incluso podemos redirigir ambas hacia el mismo fichero, para hacer esto podemos poner

$ ls contesta_q* fichero_no_existe 1>fichero_de_prueba 2>fichero_de_prueba 

O podemos usar esta forma simplificada que significa ‘redirige la salida estándar hacia un fichero, y la salida de error al mismo sitio que la estándar’.

$ ls contesta_q* fichero_no_existe 1>fichero_de_prueba 2>&1

Observa que el orden de las redirecciones es significativo. Por ejemplo, la orden

$ ls > fichero_de_prueba 2>&1

dirige ambas, la salida estándar y la de errores, al fichero_de_prueba, mientras que la orden

$ ls 2>&1 > fichero_de_prueba

dirige solamente la salida estándar al fichero_de_prueba, porque la salida de errores estándar se ha duplicado como salida estándar antes de que ésta se redirigiera al fichero.

Para los que somos vagos escribiendo decir que hay una forma más breve de poner ‘comando >fichero 2>&1′ que es ‘comando &>fichero’

Aunque no suele hacerse, quizá porque no queda muy claro; hay que decir que los signos de redirección pueden ponerse antes o después del comando, como se quiera. Así que es lo mismo poner…

$ >fichero_de_prueba echo hola

que…

$ echo hola >fichero de prueba

Redirigiendo la entrada:

Ya hemos visto algún ejemplo, contestando que si o que no al rm, para redirigir la entrada, y hacer que el programa lea los datos que espera desde un fichero u otro dispositivo usaremos el signo de menor, hay que asegurarse de que de donde leemos hay suficientes datos, por ejemplo si hacemos

$ rm -iv fichero_de* <contesta_que_si
rm: ¿borrar `fichero_de_error'? (s/n)
borrando fichero_de_error
rm: ¿borrar `fichero_de_prueba'? (s/n)

Se borra el primer fichero, pero no el segundo, porque en el fichero contesta_que_si solo hay una ‘y’

Muy usado en scripts es la redirección que los anglosajones llaman here-document y que no sé como traducir, algo así como redirigir de aqui mismo. Para ver un ejemplo, ¿conoceis el comando mail? Además de ser usado para leer el correo si tecleas ‘mail usuario’ o ‘mail usuario@host.dom’ permite mandar correo a otros usuarios de nuestra máquina o a cualquier cuenta de correo del mundo si tenemos bien configurado el MTA. Lo normal es que pongamos mail y el usuario al que queremos enviar el mensaje y luego comenzamos a teclear, primero el asunto y a continuación el texto. Pulsamos Control-D cuando acabamos.

mail usuario@gmail.com
subjet: Hola. Me acabo de comprar una cámara nueva
Me he comprado la nueva Olimpius 3-E y no sabes que pedazo
de fotos hace. Es una pasada de cámara.
Estoy como un niño con zapatos nuevos.

<aquí pulso Control-D>

Si quisieramos automatizar el envío de mensajes podríamos grabar el subject y el texto en un fichero y luego ejecutar:

mail usuario@gmail.com <fichero-mensaje

Pero en un script seguramente queda mejor así:

#! /bin/bash
#
# Ejemplo de redirección here-document
#
mail $1 < <FIN_DE_MENSAJE && echo "Mensaje navideño enviado correctamete a $1"
Felices fiestas
Os deseo una Feliz Navidad
Y un próspero año nuevo
FIN_DE_MENSAJE
# Ahora podríamos seguir poniendo comandos de bash si fuera necesario.
# La redirección terminó en la línea que contiene la palabra FIN_DE_MENSAJE

Estamos usando la redirección here-document fijaros que en lugar de un único signo < ponemos dos consecutivos y sin espacios en medio y a continuación una palabra (Que no esté en el texto que queremos redirigir). Todo el texto que aparezca en las siguientes líneas, hasta encontrar una línea que contenga únicamente la palabra en cuestión, se pondrá en la entrada estándar del comando, tal como si lo hubiésemos redirigido de un fichero. Si queremos ponerlo más bonito podemos indentar el texto usando tabuladores al principio de la línea (nunca espacios) y en lugar de poner

comando <<PALABRA

ponemos la palabra precedida de un signo menos, así

comando <<-PALABRA

eso hará que todos los tabuladores al principio de línea se quiten mientras se van inyectando las líneas en la entrada estándar del proceso.

Otra forma parecida de redirección de entrada es usar tres signos de menor <<< seguidos de una palabra, en ese caso la palabra se expande primero aplicando las reglas de expansión de bash y su resultado se inyecta en la entrada. Con esto podríamos por ejemplo almacenar un texto en una variable y usarlo para inyectar ese texto en la entrada estándar de un proceso.

Tuberías, o como encadenar la salida del uno con la entrada del otro.

Hemos visto como redirigir las entradas y las salidas a ficheros, pero es
más interesante ejecutar varios comandos en tubería, de forma que la salida
de uno se use como entrada del otro. Por ejemplo, el comando yes envía a su
salida sucesivas letras ‘y’ sin parar. Si hacemos

$ yes | rm -iv fichero_de_* contesta_que_*

Estaremos redirigiendo la salida estándar del comando yes a la entrada estándar del comando rm, con lo cual cada vez que rm pregunte si estás seguro de que quieres borrar el fichero, tendrá una ‘y’ en su entrada estandar.

Ahora usaremos algunos comandos nuevos, pero no los voy a explicar a fondo. Ya los veremos con más detalle a medida que vaya avanzando el curso. Del comando ps ya hablamos cuando explicabamos la gstión de procesos, y el comando grep busca y presenta las líneas que contengan una palabra o expresión (o que no la contengan si usamos la opción -v).

Supongamos que queremos buscar todos los procesos del usuario ‘nobody’ en la lista de procesos, podríamos redirigir la salida de ‘ps axu’ hacia un fichero y luego buscar con grep en ese fichero la palabra ‘nobody’, pero es mucho más sencillo hacer

$ ps axu | grep nobody
nobody 30631 SW Nov03 0:00 [httpd]
nobody 30632 SW Nov03 0:00 [httpd]
nobody 30633 SW Nov03 0:00 [httpd]
nobody 30634 SW Nov03 0:00 [httpd]
redy 15299 S 18:40 0:00 grep nobody

Uyyyy! casi, el último no me interesaba sacarlo, ya que en realidad no es de nobody, aunque aparece dicha palabra, no importa, me viene al pelo para que veáis que todavía puedo seguir encadenado salidas con entradas:

$ ps axu | grep nobody | grep -v "grep nobody"
nobody 30631 SW Nov03 0:00 [httpd]
nobody 30632 SW Nov03 0:00 [httpd]
nobody 30633 SW Nov03 0:00 [httpd]
nobody 30634 SW Nov03 0:00 [httpd]

Así ya está mejor….

El signo ‘|’ (tubería) lo que hace es encadenar varios procesos que se ejecutan simultaneamente, de manera parecida a como lo hacíamos con el signo ‘&’ pero además la salida estándar del primero se conecta a la entrada estándar del segundo, digamos que el segundo comando actúa como un filtro de la salida del primero.

El primer comando de una secuencia de ellos unidos por tuberías se ejecuta en la shell actual, pero los subsiguientes se ejecutan en subshells distintas cada una hija de la anterior.

Algunos comandos que suelen actuar como filtros

Hemos visto como redirigir las entradas y salidas encaminando el flujo de información de ficheros a procesos, de procesos a ficheros, y entre procesos tal como se nos antoje. Ahora es el momento de estudiar algunos comandos que por su peculiar modo de comportarse son especialmente útiles usados de este modo, canalizan los streams, haciendo alguna operación con la información, por ello se dice que actúan a modo de filtros. Hay que decir que no tienen ninguna característica especial, en cuanto al modo de programarlos, quiero decir que no son distintos a otros comandos, cualquier comando interno o cualquier programa puede funcionar como filtro, solo tiene que hacer algo con la información que recibe por la entrada estándar, y enviar el resultado a una de sus salidas, normalmente la salida estándar.

Un filtro que ya vimos es ‘grep’, grep coge líneas de la entrada
estándar, o de uno o varios ficheros, y muestra en la salida estándar
solamente aquellas líneas que contienen una determinada expresión. Pruébalo,
pon

$ grep hola

Ahora teclea frases y verás como las que contengan la cadena ‘hola’ las repite. Cuando te canses pulsa <ctl>D para indicarle que has terminado, es el carácter que representa el final de fichero, o del flujo de información. Es muy útil para buscar lo que nos interesa en un fichero o en un programa que muestra mucha información.

Por ejemplo si usas ‘ls –help’ se te muestran muchísimas opciones, pero nos interesa conocer solo las que guardan relación con la ordenación, podemos hacer:

$ ls --help | grep ordena
-c ordena por la fecha de modificación (ctime);
-f no ordena, utiliza -aU, no utiliza -lst
-r, --reverse invierte el orden, en su caso
-S ordena los ficheros por tamaño
-t ordena por la fecha de modificación
-u ordena por la fecha de último acceso (atime)
-U no ordena; muestra las entradas en el orden
-v ordena por versión
-X ordena alfabéticamente por la extensión de

y así solo veremos las líneas que contienen ‘ordena’. Otros dos filtros útiles son more y less. More muestra la información que cabe en pantalla y hace una pausa hasta que pulsamos espacio, luego continúa, pruébalo. Less hace lo mismo pero nos permite avanzar o retroceder con las flechas (pulsa q para salir). Pruébalos:

$ ls --help |more
$ ls --help |less

ls lista los archivos pero no nos dice cuantos hay. No hay problema hay un filtro que cuenta las líneas, es wc, en realidad cuenta líneas, palabras y caracteres, pero lo único que nos interesa en este caso es contar líneas, ya que se muestra un fichero en cada línea.

$ ls |wc -l

Otro comando interesante es nl que numera las líneas que recibe por la entrada estándar, prueba

$ ls |nl

Los comandos tail y head, muestran sólo las últimas o las primeras líneas de un archivo o de la entrada estándar si no se especifica el número de líneas muestran 10, pero pueden especificarse con la opción -n. Prueba por ejemplo:

$ ls --help |nl -ba |head -n 6
$ ls --help |nl -ba |tail -n 12

No os explico todas las opciones de estos comandos, porque eso ya no es parte de bash, además el curso se alargaría demasiado. Hay un proverbio que dice aquello de que si te doy un pez comerás hoy, y si te enseño a pescar comerás muchos días. Pues te voy a enseñar a pescar, es decir a usar el comando man, si necesitas saber más sobre cualquier comando como por ejemplo sobre el wc usa

$ man wc

También puedes ver una breve ayuda de de la mayoría de los comandos comando usándolo con el argumento –help, por ejemplo

$ wc --help