Redirigir la salida de un comando a un read con una tubería (pipe)
El mundo de las diferentes shells de UNIX es inmenso. Los pobres mortales con tiempo finito sólo podemos aspirar a ir aprendiendo detalles de aquí y detalles de allá conforme nos van haciendo falta. No hay más que ver que la última revisión del Advanced Bash-Scripting Guide son 802 páginas que, además, necesitan ser leídas muy detenidamente. Y eso si hablamos sólo de bash
, porque cada una de las diferentes shells tiene sus mil y una peculiaridades.
La última vez que he sido consciente de mi enorme desconocimiento ha sido esta semana, cuando fruto de un comportamiento que me parecía ilógico me enredé con manuales y documentación de diferentes shells. ¿Habrá mucha gente capaz de dominar casi todos los aspectos de la shell?
Vayamos al grano… El problema que tuve fue que un script muy sencillo que en ksh
funcionaba sin problemas, en bash
no hacía lo que esperaba. El problema estaba en una línea como esta:
comando | read a b c
Para quien no lo conozca, el comando interno read
de las shells Bourne de UNIX sirve para leer líneas y almacenarlas en una variable de entorno. Si especificamos varias variables, el comando separará la línea por los espacios y asignará un trozo a cada variable y el resto a la última. Por ejemplo, si tenemos este fichero de texto:
Pedro tiene una casa Antonio pasea por el parque del barrio Juan come muchas manzanas
y lo pasamos por este script:
#!/bin/bash while read sujeto verbo complementos do echo Sujeto: $sujeto - Verbo: $verbo - Complementos: $complementos done < fichero.txt
La salida es esta:
$ bash script.sh Sujeto: Pedro - Verbo: tiene - Complementos: una casa Sujeto: Antonio - Verbo: pasea - Complementos: por el parque del barrio Sujeto: Juan - Verbo: come - Complementos: muchas manzanas
También podemos hacer esto para leer sólo la primera línea del fichero:
$ read frase < fichero.txt $ echo $frase Pedro tiene una casa
Sin embargo, si en vez de usar una redirección de la entrada estándar (<
) usamos una tubería (|
), resulta que, mientras que ksh
hace lo que esperamos, en bash
la variable de entorno no obtiene el valor que esperamos:
$ ksh $ cat fichero.txt | read frase $ echo $frase Pedro tiene una casa $ $ bash $ cat fichero.txt | read frase $ echo $frase $
Así, mientras que en ksh
podríamos leer con una tubería y un read
datos sobre un proceso obtenidos de la salida de un “ps -ef
” o las partes de la fecha obtenida con “date
“:
$ ksh $ ps -ef | grep gdm | read user pid ppid c stime tty time process $ echo $process /usr/sbin/gdm $ $ date Sat Oct 11 11:26:26 CEST 2008 $ date | read diasemana mes diames hora tz anyo $ echo $diasemana Sat $ echo $mes Oct $
En bash
, esto mismo, no funciona:
$ bash $ ps -ef | grep gdm | read user pid ppid c stime tty time process $ echo $process $ $ date | read diasemana mes diames hora tz anyo $ echo $diasemana $
¿Por qué estas diferencias entre bash
y ksh
?
Tenemos que tener en cuenta que cuando redirigimos con una tubería la salida de un comando a la entrada de otro comando:
$ comando1 | comando2
lo que la shell hace es crear un nuevo proceso para comando1
, otro nuevo proceso para comando2
y unir sus salida y entrada con los descriptores de ficheros creados con una llamada de sistema pipe.
Pero, qué pasa si comando2
no es un comando externo sino un comando interno de la shell (como pueden ser echo
, cd
, pwd
o read
). Ahí es donde viene la diferencia importante.
Si la shell ejecuta un read
en un subproceso, las variables que hayamos especificado sí que obtendrán el valor, pero la shell padre no heredará esas variables, ya que se han fijado en un proceso hijo. La diferencia en esto entre bash
y ksh
es que el bash
siempre ejecutará ambos lados de una tubería en subprocesos hijos (Bash Pipelines):
Each command in a pipeline is executed in its own subshell.
y en cambio, ksh
no ejecuta necesariamente el comando del lado derecho de la tubería en un subproceso, sólo si es necesario (Korn shell Commands):
Each command, except possibly the last, is run as a separate process; the shell waits for the last command to terminate.
Podemos comprobarlo en el siguiente ejemplo, en el que ejecutamos un read
y un echo
dentro de la misma subshell (señalizada con los paréntesis) y vemos que la variable sí que ha obtenido el valor que esperábamos, pero que lo que ocurre es que como se ejecuta en un subproceso, el proceso padre, la shell, no hereda la variable de entorno:
$ bash $ echo prueba | (read i; echo $i) prueba $ echo prueba | read i; echo $i $
También podemos ver que si ejecutamos un comando como el siguiente (no hace nada, lo usamos para que la shell se quede “enganchada” esperando leer una línea del teclado):
$ read | read
en otro terminal podemos ver que bash
ha creado dos procesos hijo extra, uno por cada lado de la tubería:
$ ps -ef | grep bash vicente 23019 23013 0 19:39 pts/0 00:00:00 -bash vicente 23291 23019 0 20:30 pts/0 00:00:00 -bash vicente 23292 23019 0 20:30 pts/0 00:00:00 -bash
En cambio, en ksh
, como el comando del lado derecho es interno, la shell no crea dos subprocesos hijo, sólo uno:
$ ps -ef |grep ksh vicente 23303 23019 0 20:34 pts/0 00:00:00 ksh vicente 23306 23303 0 20:34 pts/0 00:00:00 ksh
He encontrado algunos sitios que documentan este interesante y curioso asunto:
- If I pipe the output of a command into “read variable”, why doesn’t the output show up in $variable when the read command finishes?
- I’ve run into a problem where Linux’s “read” is not reading input from stdin when it is piped to.
- Linux “read” issue (continuación del anterior)
Si trabajamos en bash
y nos encontramos un problema que podríamos solucionar fácilmente con una redirección a un read
, es seguro que podremos encontrar una infinidad de formas alternativas para afrontar el mismo problema. Por ejemplo, un awk
nos puede servir en muchas ocasiones para trocear una línea.
En otras ocasiones, tal vez podamos apoyarnos en la sustitución de procesos. En general, conocemos bastante bien qué es la sustitución de comandos, que es sustituir un comando por su salida:
$ fecha=$(date) $ echo $fecha Mon Oct 12 20:48:32 CEST 2008
Pues la sustitución de procesos consiste en reemplazar un comando por un fichero que contiene su salida:
$ cat <(date) Mon Oct 12 20:49:38 CEST 2008
que sería equivalente a:
$ date | cat Mon Oct 12 20:51:32 CEST 2008
Para hacer esto, el comando sustituido manda su salida a un fichero /dev/fd/
y el comando que recibe su salida lo que hace es leer de ese fichero. Si queremos ver qué fichero se va a usar, como es precisamente el nombre del fichero lo que se le pasa al comando de la izquierda, podemos usar un echo
para mostrarlo:
$ echo <( ) /dev/fd/63 $ echo <( ls ) /dev/fd/63
Por tanto, cuando hacemos:
$ cat <(date) Mon Oct 12 20:49:38 CEST 2008
le decimos a la shell: Ejecuta el comando date
, guarda su salida en /dev/fd/63
(o en el fichero que esté disponible en ese momento), y ejecuta “cat /dev/fd/63
“.
Al principio hemos visto que en bash
, mientras que esto no funcionaba:
$ cat fichero.txt | read frase
esto sí que lo hacía, ya que bash
no genera un subproceso para el comando de la izquierda:
$ read frase < fichero.txt
Por el mismo motivo, mientras que esto no funciona en bash
:
$ ps -ef | grep gdm | read user pid ppid c stime tty time process
sí que podremos hacer lo mismo usando sustitución de procesos:
$ bash $ read user pid ppid c stime tty time process < <( ps -ef | grep gdm ) $ echo $process /usr/sbin/gdm
Lo que estamos haciendo es: Guardamos la salida de “ps -ef | grep gdm
” en /dev/fd/63
y a continuación:
$ read user pid ppid c stime tty time process < /dev/fd/63
con lo que conseguimos redirigir la salida de un comando a un read
.
Para colmo del despropósito, resulta que esto tampoco es portable entre bash
y ksh
. Mientras que ksh
acepta la sustitución de procesos en general, su redirección a un read
no funciona:
$ ksh $ cat <( date ) Mon Oct 13 13:16:52 CEST 2008 $ read i < <( date ) ksh: syntax error: `<(' unexpected
¡Bienvenidos al peculiar mundo del UNIX!
:wq
Huuuu … normalmente leo y leo las cosas que publicas y en realidad de todos los artículos 1 o 2 son de bajo contenido pero el 99.8% restante Dios .. lo lei y lo aprendi … Gracias por darte el tiempo de explicarnos al resto de los mortales con un gran grado de pedagogía
@Gutts ¡Gracias!
Tremendo. No hay más que decir.