sitio personal de Rodrigo Garcia Saenz.

Charla jugando con redireccion en bash

permalink.

Actualizado - 10 September 2024

He propuesto dar esta charla para el Debian Day 2017 en La Paz y este artículo es para quienes no puedan asistir a la charla o quieren revisarla con más calma.

Mostraré las formas en las que he estado jugando con operadores de redireccionamineto y tuberías en bash como interprete de comandos en GNU/Linux. (descargar las diapositivas (155KB))


Brevemente sobre file-descriptors

Un programa o comando en GNU/Linux puede hacer muchas cosas y entre estas esta interactuar con otros programas o usuarios extrayendo información de algún lugar y también poner los resultados de lo que hace en otro lugar.

Típicamente el "lugar" desde donde los programas extraen información es un stream al que se le llama la entrada estándar o stdin (que viene de standard input). El lugar hacia donde reportan sus acciones es la salida estándar o stdout y donde reportan errores se llama el error estándar stderr. Los streams que utilizan los procesos son listados en file-descriptors asociados a cada proceso.

Estos tres streams son los más utilizados y estandarizados pero en este sistema operativo se pueden usar otros mas y podríamos utilizarlos como nos convega.

Un descriptor de archivo o file-descriptor es como un indicador que dice los streams que usa un archivo. En sistemas UNIX todo es un archivo, y al estar GNU/Linux basado en UNIX, un proceso tienen una serie de archivos asociados a este que lo describen y ayudan a gestionarlo. (en este https://pastebin.com/raw/PnUj8UPt puedes ver otra descripción sencilla sobre file-descriptors)

Por ejemplo podemos ver los archivos asociados un proceso, en este caso el proceso de la terminal que tenemos abierta con:

lsof -p $BASHPID

Donde lsof es un comando que se utiliza para ver los archivos abiertos y con -p $BASHPID filtramos solo los archivos del identificador de proceso (PID) de la misma terminal abierta, en bash la variable $BASHPID es la que alamcena el PID de la terminal abierta, para filtrar solamente los file-descriptors asociados al proceso de la terminal acutal podemos usar:

lsof +f g -ap $BASHPID -d 0,1,2

(Ejemplo extraído de http://wiki.bash-hackers.org/howto/redirection_tutorial)

El resultado muestra entre muchas cosas los file descriptros (FD) usados por el proceso con PID 6841:

COMMAND  PID   USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF NODE NAME
bash    6481 lorilu    0u   CHR        RW  136,5      0t0    8 /dev/pts/5
bash    6481 lorilu    1u   CHR        RW  136,5      0t0    8 /dev/pts/5
bash    6481 lorilu    2u   CHR        RW  136,5      0t0    8 /dev/pts/5

En el caso anterior con -d 0,1,2 filtramos los file descriptors 0,1,2 que se asocian a stdin, stdout y stderr respectivamente ya que:

         Stream                File-descriptor (FD)

 stdin (entrada estandar) --->      0 
 stdout (salida estandar) --->      1
 stderr (error estandar)  --->      2

Si queremos ver todos los file descriptors asociados a un proceso, podemos listar el contenido del directorio /proc/<PID>/fd, donde <PID> viene a ser el PID del proceso.

Los operadores de redirección entre los que están > >> < n>m &> o las tuberías | ayudan a manipular la fuente o destino de información con la que los programas interactúan y fuerón programados para brindar un alto nivel de flexiblidad.


Sobre operadores de redirección

Como sabemos que los programas actúan sobre streams definidos por file-descriptors ahora podemos utilizar los operadores de redirección, que están brevemente descritos en http://www.catonmat.net/download/bash-redirections-cheat-sheet.pdf

>

  • comando > archivo

El operador > redirije el stdout y lo guarda en un archivo.

# guarda la salida estandar de cada comando en archivos nuevos
ls > lista.txt
ps -aux --forest > lista_procesos.txt

Al usar comando > archivo se debe tener en cuenta que si archivo ya existe su contenido sera reemplazado por la salida estándar de comando.

>>

  • comando >> archivo

>> al igual que > redirije el stdout, pero a diferencia de este adiciona el contenido de stdout al archivo y no lo reemplaza.

&> y &>>

  • comando &> archivo
  • comando &>> archivo

Por defecto redirije la stdout y stderr al archivo, pero puede usarse tambien de otros file-descriptors que veremos mas adelante.

<

  • comando < archivo

Redirecciona el contenido del archivo a la stdin, si el comando lee la entrada estandar estara en realidad leyendo el contenido del archivo y lo usara como argumento.

|

  • comando1 | comando2

Redirije stdout del comando1 a stdin y si el comando2 la lee la usara como argumento.

n>

  • comando 2> archivo

Funciona igual que > solo que en este caso 2> redirije el file-descriptor 2 que es stderr al archivo, de hecho se puede usar cualquier file desriptor por ejemplo comando 11> archivo que usara el file-descriptor 11 y lo guardara en archivo. El comportamiento es el mismo con n>>.

n<

  • comando 1< archivo

Al igual que < redirije el contenido de archivo al file-descriptor 1.

n>&m

  • comando 2>&1

Este operador hace que lo que haya escrito en el file-descriptor 2 (stderr) vaya al file-descriptor 1 (stdout)

El orden importa en bash

Bash evalúa los operadores de redireccionamiento de izquierda a derecha.

Por ejemplo si queremos guardar en un archivo el error y salida estandar de un comando se hace asi, por ejemplo para ls:

ls algo-que-no-existe > archivo 2>&1

Gráficamente se puede ver por pasos como funciona esto:

Primero.- Al evaluar bash > archivo los file-descriptors quedan de esta forma:

    File descriptor (FD)     Archivo

           0  --------->     /dev/pts/5
           1  --------->     archivo
           2  --------->     /dev/pts/5

Hay que notar que /dev/pts/5 es la terminal y por ende lo esta muestra en la pantalla, se ve que la entrada estandar stdin y stderr apuntan a la terminal actual /dev/pts/5. En cambio stdout está apuntando a archivo

Luego.- bash evalúa 2>&1 que hara que los file-descriptors queden de esta forma:

    File descriptor (FD)     Archivo

           0  --------->     /dev/pts/5
           1  --------->     archivo
                  /
           2  ---´

Se ve que 2>&1 hace que lo que esta en stderr (file descriptor 2) se duplique en 1 (se copie en 1), y antes el file descriptor 1 estaba apuntando a archivo. De esta forma tanto stderr y stdout se guardaran en archivo.

Se puede ver además que bash ejecuta primero los operadores de redirección antes que los comandos.

Otro ejemplo si queremos ver con el comando less el error estandar y la salida estandar de un comando podemos usar lo siguiente:

  • comando1 2>&1 | less

Esto hace que el stderr vaya hacia stdout y como | redirije stdout al stdin del comando que esta a la derecha, less recibira los dos streams como argumentos.


Ejemplos de utilidad

En lo que queda de este tutorial se usarán ejemplos concretos para algunos operadores de redirección.

-

# obtener los 10 procesos que usan mas memoria RAM de todos los usuarios
ps aux --sort -rss | head

# obtener los 10 procesos que usan mas memoria RAM del usuario 'lorilu'
ps aux --sort -rss | head | grep -E "^lorilu"

En estos dos primeros ejemplos usamos ps --sort -rss para que ordene los procesos de todos los usuarios por el campo RSS que se refiere a la memoria residente o la cantidad de memoria RAM que ocupa acutalmente un proceso, usamos | para redirigir la stdout de ps a la stdin de head que muestra las 10 primeras líneas. Al agregar | grep -E "^lorilu" hacemos que grep reciba la salida de head y filtre solamente las líneas que empiezan en "lorilu" que es el nombre del usuario de quien buscamos los procesos abiertos, se busca solo en el principio de la línea por que ps pone el nombre del usuario al principio de cada línea.

-

# itera linea por linea en archivo.txt
while read -r linea 
do
   echo "$linea"
   # extas ...
done < archivo.txt

Este script muestra que bash ejecuta primero los operadores de redirección (en este caso <) antes que los comandos. En la parte final del script bash, < archivo.txt hace que stdin apunte al contenido de archivo.txt y esto hace que el bucle while itere sobre el contenido del archivo y haga que read -r lea cada linea y guarde el resultado de cada iteración en la variable linea, echo simplmente muestra la línea actual. Dentro el blucle se pueden poner una cantidad indefinida de comandos para procesar cada línea a conveniencia.

-

# renombra los archivos cuyos nombres tienen espacios en blanco y los reemplaza por _
for nombre in *
do
    nombre_sin_espacios=$(echo $nombre | tr " " "_")
    mv "$nombre" "$nombre_sin_espacios" 2> /dev/null
done

A veces es problemático tener archivos con nombres con espacios en blanco como "archivo numero 1" lo que hace el script anterior es renombrarlo como "archivo_numero_1".

El bucle for actua iterando en * que coincide con cualquier archivo dentro el directorio actual y guardando cada nombre de archivo en nombre en cada iteración. Luego echo $nombre | tr " " "_" hace que tr reemplaze cualquier caracter " " por "_" y la salida de esto se guarda en la variable nombre_sin_espacios.

Luego se usa mv "$nombre" "$nombre_sin_espacios" para renombrar el archivo por su "equivalente" sin espacios en blanco.

La parte 2> /dev/null es opcional ya que para el caso en que los archivos no tengan nombres con espacios en blanco, mv mostrará un error ya que los archivos son del mismo nombre y no pueden renombrarse, ese error va hacia stderr y se mostraría en pantalla si no fuese por 2> /dev/null que pone stderr en /dev/null haciendo que se pierda y no se muestre en pantalla los errores producidos por mv, esto es es como silenciar los errores.

-

# Cifra una carpeta con la clave "123456" comprimiendola primero
tar -cf - carpeta/ | gpg --symmetric --no-use-agent --passphrase "123456" > carpeta.gpg

El comando tar puede comprimir archivos o carpetas pero requiere escribir el resultado en algun lugar por ejemplo tar -cf carpeta.tar.gz carpeta/ lo guarda en el archivo carpeta.tar.gz. En este caso para enviar su salida directamente a otro programa se usa tar - donde - es una opción de tar para enviar el resultado a stdout (salida estándar).

Luego a medida que la carpeta se va comprimiendo, mediante | se envía el resultado a gpg --symmetric que cifrará lo que lea de stdin y con > carpeta.gpg se guarda la salida estándard (stdout) en el archivo carpeta.gpg. Las opciones -no-use-agent --passphrase "123456" son para indicarle a gpg que no le pida al usuario ingresar la clave mediante teclado y en lugar de eso utilice "123456" para cifrar el mensaje.

-

# Enviar una carpeta al servidor algo.com a medida que es comprimida
# primero se requiere que en el servidor algo.com se tenga un puerto escuchando
# el trafico en un puerto dado para recibir el contenido enviado

# En el servidor
nc -l -p 5151 | tar -C . -xvf -

Primero en el servidor "algo.com" abrimos el puerto 5151 haciendo que nc (netcat) escuche el tráfico en este puerto. nc enviara a stdout el tráfico que reciba satisfactoriamente y con | hacemos que tar -xvf - descomprima lo que reciba en stdin y lo guarde en el directorio actual con las opciones -C .

# En el cliente (la maquina que envia la carpeta)
tar -cf - carpeta/ | nc -q 0 algo.com 5151

Como el servidor esta escuchando el tráfico en el puerto 5151, usamos tar -cf - carpeta/ para comprimir la carpeta y enviar el resultado a stdout. Con | lo redirijimos al stdin de nc algo.com 5151 que envía lo que lea del stdin al servidor algo.com por el puerto 5151. La opción -q 0 de netcat hace que cuando se detecte EOF (fin de archivo) de la entrada estándard al cabo de 0 segundos se cierre el programa.

De esta forma se puede enviar un archivo mediante TCP a medida que es comprimido y al llegar al servidor se ira descomprimiendo, se puede obviamente combinar esto con otros comandos para por ejemplo cifrar el contenido o analizar el envío en tiempo real.

-

# convertir una secuencia de imagenes .jpg en un archivo pdf ordenando alfabeticamente
ls *.jpg | sort -n | tr '\n' ' ' | sed 's/$/\ doc.pdf/' | xargs convert - doc.pdf

Solución extraída de este hilo en stack-overflow

Con ls *.jpg listamos todos los archivos de imagen .jpg, luego sort -n los ordena alfabéticamente, la lista ordenada la recibe tr '\n' ' ' que reemplaza los saltos de línea \n por espacios en blanco , después sed 's/$/\ mydoc.pdf/' agrega al final de la línea un espacio en blanco " " y la cadena "doc.pdf" esto por que el comando convert requiere recibir las imagenes seguido del nombre del archivo pdf que creará a partir de los archivos de imágenes que le preceden.

Finalmente mediante | xargs convert - doc.pdf se hacen varias cosas, primero el comando xargs se utiliza para pasarle como lista de argumentos la salida stdout de lo que esta a la izquierda de | al comando que esta a la derecha, en este caso es convert - doc.pdf que tomará los nombres de los archivos de imagen y a partir de estos creará el archivo doc.pdf.

Esto es útil cuando se requiren hacer reportes rápidos en pdf de una lista larga de imágenes.

--> Espero este pequeño compendio de utilidades te haya servido y trataré de reunir más pronto. Si tienes sugerencias u observaciones ponte en contacto

Lectura adicional

Consejo del día

Apoya el desarrollo y uso de software libre. El software libre respeta los derechos de quienes lo usan y quienes lo desarrollan, tiene el principio de ayudar al prójimo compartiendo tecnología y conocimiento.