Lo hice y lo entendí

El blog de Vicente Navarro
13 oct

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:

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

Entradas relacionadas

4 Comentarios a “Redirigir la salida de un comando a un read con una tubería (pipe)”

Trackbacks y pingbacks:

Tema LHYLE09, creado por Vicente Navarro