Programación del Shell Linux

Aquí tenemos un libro libre y completo sobre Shell

La sed del "conocimiento libre" es muy bienvenida.

Comandos Shell Script

Diagrama Sintáctico
Júlio Neves
Júlio Neves
Home | Artículos Português InglesAgradecimientos | Links Amigos

Comprar el libro

Changelogs

  Conversación de Bar 1  

  Conversación de Bar 2  

  Conversación de Bar 3  

  Conversación de Bar 4  

  Conversación de Bar 5  

  Conversación de Bar 6  

  Conversación de Bar 7  

  Conversación de Bar 8  

  Conversación de Bar 9  

  Conversación de Bar 10  

  Conversación de Bar 11  

  Aperitivo  

Conversación de Bar - Parte VII



- Como dijiste? Repite que no te entiendo! Se te derritieron los pensamientos para hacer el scriptiziño que te pedí?

- Si, realmente tuve que colocar mucha materia gris en la pantalla, pero creo que lo conseguí! Bueno, por lo menos en los tests que hice la cosa funcionó, pero tu siempre me colocas piedras en el camino!

- No sera tanto, programar en shell es muy fácil, lo que vale son los consejos y los detalles de programación que te doy, que no son triviales. Las correcciones que te hago, son justamente para mostrarlos. Pero vamos a pedir dos "chopps" y le echo una ojeada a tu script.

- Mozo, trae dos. No te olvides que uno es sin espuma!


$ cat restaura #!/bin/bash # # Restaura archivos borrados vía erreeme #

if [ $# -eq 0 ] then echo "Uso: $0 " exit 1 fi # Lee el nombre del directorio original en la última línea Dir=`tail -1 /tmp/$LOGNAME/$1` # O grep -v borra la última línea y crea el # archivo con directorio y nombres originales grep -v $Dir /tmp/$LOGNAME/$1 > $Dir/$1 # Borra el archivo que ya estaba moribundo rm /tmp/$LOGNAME/$1

- Un momento, déjame ver se lo entendí. Primero colocas en la variable Dir la última línea del archivo cuyo nombre está formado por /tmp/nombre del operador ($LOGNAME)/parámetro pasado con el nombre del archivo a ser restaurado ($1). Enseguida el grep -v que montaste borra esa línea en que estaba el nombre del directorio, o sea, siempre es la última y manda lo que resta del archivo, que sería el archivo ya limpio, hacia el directorio original para después borrar el archivo del "cubo de la basura"; S E N S A C I O N A L! Impecable! Ningun error! Lo viste? ya le estás tomando las medidas al shell!

- Entonces vamos a continuar, basta ya de bla-bla-bla, de que vas a hablar hoy?

- Ah! estoy viendo que el bichito del Shell se te contagió. Que bueno, vamos a ver como se pueden (y deben) leer datos y formatear pantallas, pero primero vamos a conocer un comando que te da todas las herramientas para que formatees tu pantalla de entrada de datos.

El comando tput

Este comando se usa principalmente para posicionar el cursor en la pantalla, sin embargo también es muy usado para borrar datos de la pantalla, saber la cantidad de líneas y columnas de la pantalla, posicionar correctamente un campo, borrar un campo cuya entrada se detectó como error. En fin, casi toda la formatación de la pantalla es hecha por este comando.

Unos pocos atributos del comando tput pueden eventualmente no funcionar, esto en el caso de que el modelo de terminal definido por la variable $TERM, no tenga esta posibilidad incorporada.

En la tabla siguiente, se presentan los principales atributos del comando y los efectos ejecutados sobre la pantalla, pero debes saber que existen muchos más que esos, mira sino:

$ tput it 8

Este ejemplo devolvió el tamaño inicial del <TAB> ( Initial T ab), y dime una cosa: para que quiero saber eso? Si quieres saber todo sobre el comando tput (y mira que es de nunca acabar), vea a: http://www.cs.utah.edu/dept/old/texinfo/tput/tput.html#SEC4.

Principales Opciones del Comando tput
rc   Restore Cursor position - Coloca el cursor en la posición marcada por el último sc
Opciones de tput   Efecto
cup lin col   CUrsor Position - Posiciona el cursor en la línea lin y columna col. El origen es cero
bold   Coloca la pantalla en modo de realce
rev   Coloca la pantalla en modo de vídeo inverso
smso   Idéntico al anterior
smul   A partir de esta instrucción, los caracteres tecleados aparecerán sublineados en la pantalla
blink   Los caracteres tecleados aparecerán intermitentes
sgr0   Después de usar uno de los atributos de arriba, se usa este para restaurar la pantalla a su modo normal
reset   Limpia la pantalla y restaura sus definiciones de acuerdo con el terminfo o sea, la pantalla vuelve al patrón definido por la variable $TERM  
lines   Devuelve la cantidad de líneas de la pantalla en el momento de la instrucción
cols   Devuelve la cantidad de columnas de la pantalla en el momento de la instrucción
el   Erase Line - Borra la línea a partir de la posición del cursor
ed   Erase Display - Borra la pantalla a partir de la posición del cursor
il n   Insert Lines - Introduce n líneas a partir de la posición del cursor
dl n   Delete Lines - Borra n líneas a partir de la posición del cursor
ech n   Erase CHaracters - Borra n caracteres a partir de la posición del cursor
sc   Save Cursor position - Graba la posición del cursor

Vamos a hacer un programa bien sencillo para mostrar algunos atributos de este comando. Es el famoso usado y abusado Hola Mundo, sólo que esta frase será escrita en el centro de la pantalla y en vídeo inverso y después de eso, el cursor volverá hasta la posición en que estaba antes de escribir esta frase tan creativa. Observa:

$ cat hola.sh #!/bin/bash # Script bobo para testar # el comando tput (versión 1)

Columnas=`tput cols` # Grabando cantidad columnas Líneas=`tput lines` # Grabando cantidad líneas Línea=$((Líneas / 2)) # Cual es la línea del medio de la pantalla? Columna=$(((Columnas - 11) / 2)) # Centrando el mensaje en la pantalla tput sc # Grabando posición del cursor tput cup $Línea $Columna # Posicionándose para escribir tput rev # Vídeo inverso echo Hola Mundo! tput sgr0 # Restaura el vídeo a normal tput rc # Restaura el cursor a la posición original

Como el programa ya está todo comentado, creo que la única explicación necesaria sería para la línea en que es creada la variable Columna y lo extraño allí es aquél número 11, este numero es el tamaño de la cadena que pretendo escribir (Hola Mundo).

De esta forma, este programa solamente conseguiría centrar cadenas de 11 caracteres, sin embargo, mira esto:

$ var=Conversa $ echo ${#var} 8 $ var="Conversa de Bar" $ echo ${#var} 15

Ahhh, mejoró! Entonces ahora sabemos que la construcción ${#variable} devuelve la cantidad de caracteres de variable. De esta forma, vamos a optimizar nuestro programa para que escriba en vídeo inverso y en el centro de la pantalla, la cadena pasada como parámetro y que después el cursor vuelva a la posición en que estaba antes de la ejecución del script.

$ cat hola.sh #!/bin/bash # Script bobo para testar # el comando tput (versión 2)

Columnas=`tput cols` # Grabando cantidad columnas Líneas=`tput lines` # Grabando cantidad líneas Línea=$((Líneas / 2)) # Cual es la línea del medio de la pantalla? Columna=$(((Columnas - ${#1}) / 2)) #Centrando el mensaje en la pantalla put sc # Grabando posición del cursor tput cup $Línea $Columna # Posicionándose para escribir tput rev # Vídeo inverso echo $1 tput sgr0 # Restaura vídeo a normal tput rc # Restaura cursor en la posición original

Este script es igual al anterior, sólo que cambiamos el valor fijo de la versión anterior (9), por ${#1}, donde éste 1 es el $1 o sea, esta construcción devuelve el tamaño del primer parámetro pasado para el programa. Si el parámetro que yo quisiese pasar tuviese espacios en blanco, tendría que colocarlo todo entre comillas, sino el $1 sería solamente el primer pedazo. Para evitar este problema, es solo necesario substituir el $1 por $*, que como sabemos es el conjunto de todos los parámetros. Entonces aquella línea quedaría así:

    Columna=`$(((Columnas - ${#*}) / 2))` #Centrando el mensaje en la pantalla

y la línea echo $1 pasaría a ser echo $*. Pero no te olvides de que cuando lo ejecutes, tienes que pasar la frase que deseas centrar como un parámetro.

Y ahora podemos leer los dados de la pantalla

Bien, a partir de ahora vamos a aprender todo sobre lectura, solo que no te puedo enseñar a leer las cartas o el futuro, porque sino ya seria rico, estaria en un pub de Londres, tomando scotch y no en un bar tomando "chopp". Pero vamos a continuar.

La última vez que nos encontramos aquí ya te dí una introducción sobre el comando read. Para comenzar su análisis más detallada. fíjate en esto:

$ read var1 var2 var3 Conversa de Bar $ echo $var1 Conversa $ echo $var2 de $ echo $var3 Bar $ read var1 var2 Conversa de Bar $ echo $var1 Conversa $ echo $var2 de Bar

Como viste, el read recibe una lista separada por espacios en blanco y coloca cada ítem de esta lista en una variable. Si la cantidad de variables es menor que la cantidad de ítems, la última variable recibe el resto de los parámetros.

Yo mencioné una lista separada por espacios en blanco? Pero ahora que ya lo conoces todo sobre el $IFS (Inter Field Separator) que te presenté cuando hablamos del comando for, todavía crees eso? Vamos a verificarlo directamente en el prompt:

$ oIFS="$IFS" $ IFS=: $ read var1 var2 var3 Conversa de Bar $ echo $var1 Conversa de Bar $ echo $var2

$ echo $var3

$ read var1 var2 var3 Conversa:de:Bar $ echo $var1 Conversa $ echo $var2 de $ echo $var3 Bar $ IFS="$oIFS"

Te diste cuenta, estaba equivocado! La verdad es que el read lee una lista, así como el for, separada por los caracteres de la variable $IFS. Fíjate entonces como esto puede facilitarte la vida:

$ grep julio /etc/passwd julio:x:500:544:Julio C. Neves - 7070:/home/julio:/bin/bash $ oIFS="$IFS" # Grabando IFS $ IFS=: $ grep julio /etc/passwd | read lname lixo uid gid coment home shell $ echo -e "$lname\n$uid\n$gid\n$coment\n$home\n$shell" julio 500 544 Julio C. Neves - 7070 /home/julio /bin/bash $ IFS="$oIFS" # Restaurando IFS

Como viste, la salda del grep fue redireccionada hacia el comando read que leyó todos los campos de una sola vez. La opción -e del echo fue usada para que el \n fuera entendido como un salto de línea (new line), y no como un literal.

En el Bash existen diversas opciones del read que sirven para facilitarte la vida. Observa la siguiente tabla:

Opciones del comando read en Bash
  -s     Lo que está siendo tecleado no aparece en la pantalla  
  Opción     Acción
  -p prompt     Escribe el prompt antes de hacer la lectura  
  -n num     Lee hasta num caracteres  
  -t seg     Espera seg segundos para que concluya la lectura  

Y ahora directo a los ejemplos cortos para demostrar estas opciones.

Para leer un campo "Matrícula":

$ echo -n "Matricula: "; read Mat # -n no salta línea Matricula: 12345 $ echo $Mat 12345

O simplificando con la opción -p:

$ read -p "Matricula: " Mat Matricula: 12345 $ echo $Mat 12345

Para leer una determinada cantidad de caracteres:

$ read -n5 -p"CEP: " Num ; read -n3 -p- Compl CEP: 12345-678$ $ echo $Num 12345 $ echo $Compl 678

En este ejemplo hicimos dos read: uno para la primera parte del CEP y otra para su complemento y de este modo formateamos la entrada de datos. El signo de pesos ($) después del último número tecleado, es porque el read no tiene el new-line implicito por default como lo tiene el echo.

Para leer hasta que un determinado tiempo termine (conocido como time out):

$ read -t2 -p "Digite su nombre completo: " Nom || echo 'Ah perezoso!' Escriba su nombre completo: JAh perezoso! $ echo $Nom

$

Obviamente esto fue una broma, ya que solo tenía 3 segundos para escribir mi nombre completo y sólo me dio tiempo de teclear una J (aquella pegada al Ah), pero me sirvió para mostrar dos cosas:

  1. El comando después del par de barras verticales (||) (el o lógico, te acuerdas?) será ejecutado en el caso que la escritura no haya terminado en el tiempo estipulado;
  2. La variable Nom permaneció vacía. Esta tendrá valores solamente cuando el <ENTER> sea tecleado.

Para leer un dato sin ser mostrado en la pantalla:

$ read -sp "Seña: " Seña: $ echo $REPLY secreto :)

Aprovecho un error para mostrarte un detalle de programación. Cuando escribi la primera línea, me olvidé de colocar el nombre de la variable que iría a recibir la contraseña, y sólo noté eso cuando fui a listar su valor. Por suerte la variable $REPLY del Bash, posee la última cadena leída y me aproveché de esto para no perder el viaje. Verifica tu mismo lo que acabo de hacer.

Pero el ejemplo que dí, era para mostrar que la opción -s impide que lo que está siendo tecleado se vea en la pantalla. Como en el ejemplo anterior, la falta del new-line hizo con que el prompt del comando ($) permaneciese en la misma línea.

Bien, ahora que sabemos leer de la pantalla, veamos como se leen los datos de los archivos.

Vamos a leer archivos?

Como ya te habia dicho y te debes de acordar, el while verifica un comando y ejecuta un bloque de instrucciones mientras este comando de una respuesta correcta. Cuando estás leyendo un archivo que te dá permiso de lectura, el read sólo dará una respuesta errónea cuando alcance el EOF (end of file), de esta forma podemos leer un archivo de dos maneras:

1 - Redireccionando la entrada del archivo hacia el bloque del while así:

    while read Línea
    do
        echo $Línea
    done < archivo

2 - Redireccionando la salida de un cat hacia el while, de la siguiente forma:

    cat archivo |
    while read Línea 
    do
        echo $Línea
    done

Cada uno de los procesos tiene sus ventajas y desventajas:

Ventajas del primer proceso:

  • Es más rápido;
  • No necesita de un subshell para asistirlo;

Desventaja del primer proceso:

  • en un bloque de instrucciones grande, el redireccionamento queda poco visible, lo que a veces perjudica la visualización del código;

Ventaja del segundo proceso:

  • Como el nombre del archivo está antes del while, es más fácil la visualización del código.

Desventajas del segundo proceso:

  • El Pipe (|) llama un subshell para interpretarlo, volviendo el proceso más lento, pesado y a veces problemático (mira los ejemplos que siguen).
$ cat readpipe.sh #!/bin/bash # readpipe.sh # Ejemplo de read pasando archivo por pipe.

Ultimo="(vacío)" cat $0 | # Pasando el arch. del script ($0) p/ while while read Línea do Ultimo="$Línea" echo "-$Ultimo-" done echo "Acabó, Último=:$Ultimo:"

Vamos a ver su ejecución:

$ readpipe.sh -#!/bin/bash- -# readpipe.sh- -# Ejemplo de read pasando archivo por pipe.- -- -Ultimo="(vacío)"- -cat $0 | # Pasando el arch. del script ($0) p/ while- -while read Línea- -do- -Ultimo="$Línea"- -echo "-$Ultimo-"- -done- -echo "Acabó, Último=:$Ultimo:"- Acabó, Último=:(vacío):

Como viste, el script lista todas sus própias líneas con un signo de menos (-) antes y otro después y al final muestra el contenido de la variable $Ultimo. Sin embargo, observa que el contenido de esta variable permanece como (vacío).

- Será que la variable no fue actualizada?

- Lo fue, y eso puede ser comprobado porque la línea echo "-$Ultimo-" lista correctamente las líneas.

- Entonces que paso?

- Pues que como ya te dije, el bloque de instrucciones redireccionado por el pipe (|) es ejecutado en un subshell y allí las variables son actualizadas. Cuando este subshell termina, las actualizaciones de las variables se van junto con él, para los quintos infiernos. Observa que voy a hacer un pequeño cambio pasando el archivo por redireccionamento de entrada (<) y así las cosas pasarán a funcionar de una forma más perfecta:

$ cat redirread.sh #!/bin/bash # redirread.sh # Ejemplo de read pasando archivo por redireccionamento de entrada (<).

Ultimo="(vacío)" while read Línea do Ultimo="$Línea" echo "-$Ultimo-" done < $0 # Pasando el arch. del script ($0) p/ while echo "Acabó, Último=:$Ultimo:"

Y mira su ejecución sin errores:

$ redirread.sh -#!/bin/bash- -# redirread.sh- -# Ejemplo de read pasando archivo por redireccionamento de entrada (<).- -- -Ultimo="(vacío)"- -while read Línea- -do- -Ultimo="$Línea"- -echo "-$Ultimo-"- -done < $0 # Pasando el arch. del script ($0) p/ while- -echo "Acabó, Último=:$Ultimo:"- Acabó, Último=:echo "Acabó, Último=:$Ultimo:":

Bien amigos de la Red Shell, para finalizar el comando read sólo falta un pequeño e importante detalle que voy a mostrar utilizando un ejemplo práctico. Imagina que quieres listar en pantalla un archivo y que cada diez registros esta lista se detenga para que el operador pueda leer el contenido de la pantalla y sólo volverá a funcionar (scroll) después que el operador pulse cualquier tecla. Para no gastar absurdamente papel (de la Linux Magazine), voy a hacer esta lista en la horizontal y mi archivo (numeros), que tiene 30 registros solamente con números secuenciales. Mira:

$ seq 30 > numeros $ cat 10porpag.sh #!/bin/bash # Prg de test para escribir # 10 líneas y parar para leer # Versión 1

while read Num do let ContLin++ # Contando... echo -n "$Num " # -n para no saltar línea ((ContLin % 10)) > /dev/null || read done < numeros

Como forma de hacer un programa genérico creamos la variable $ContLin (por que en la vida real, los registros no son solamente números secuenciales) y pararemos para leer cuando el resto de la división por 10 sea cero (mandando la salida para /dev/null de forma de que no aparezca en la pantalla, ensuciandola). Sin embargo, cuando fui a ejecutarlo me dio el siguiente error:

$ 10porpag.sh 1 2 3 4 5 6 7 8 9 10 12 13 14 15 16 17 18 19 20 21 23 24 25 26 27 28 29 30

Fíjate que falta el número 11 y que la lista no se paro en el read. Lo que paso es que la entrada del loop estaba redireccionada desde el archivo numeros y de esta forma, la lectura fue hecha encima de este archivo, así perdimos el 11 (y tambiém el 22).

Vamos a mostrar entonces como debería quedar para funcionar correctamente:

$ cat 10porpag.sh #!/bin/bash # Prg de test para escribir # 10 líneas y parar para leer # Versión 2

while read Num do let ContLin++ # Contando... echo -n "$Num " # -n para no saltar línea ((ContLin % 10)) > /dev/null || read < /dev/tty done < numeros

Observa que ahora la entrada del read fue redireccionada desde /dev/tty, que no es nada más que el terminal corriente, forzando de esta forma que la lectura sera hecha del teclado y no de números. Es bueno resaltar que esto no sucede solamente cuando usamos el redireccionamento de entrada, se hubieramos usado el redireccionamento via pipe (|), habría pasado lo mismo.

Observa ahora su ejecución:

$ 10porpag.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Esto está casi bien, pero falta un poco para quedar excelente. Vamos a mejorar un poco el ejemplo para que lo reproduzcas y verifiques (pero antes de verificar, aumenta el número de registros de numeros o reduce el tamaño de la pantalla, para que haya un salto de página).

$ cat 10porpag.sh #!/bin/bash # Prg de test para escribir # 10 líneas y parar para leer # Versión 3

clear while read Num do ((ContLin++)) # Contando... echo "$Num" ((ContLin % (`tput lines` - 3))) || { read -n1 -p"Teclee Algo " < /dev/tty # para leer cualquier caracter clear # limpia la pantalla despues de la lectura } done < numeros

El cambio principal hecho en este ejemplo, es con relación al salto de página, ya que esta hecho en cada cantidad-de-líneas-de-pantalla (tput lines) menos (-) 3, o sea, si la pantalla tiene 25 líneas, listará 22 registros y parará para su lectura. En el comando read también fue hecha una alteración, incluyendo un -n1 para leer solamente un caracter sin ser necesariamente un <ENTER> y la opción -p para dar el mensaje.

- Bien amigo mio, por hoy ya basta porque me parece que estás saturado de esto...

- No, no lo estoy, realmente puede continuar...

- Si tu no lo estás, yo sí... Pero ya que estás tan entusiasmado con el Shell, te voy a dejar un ejercicio de aprendizaje que mejorara tu CDteca y que es bastante simple. Reescribe tu programa que registra CDs para montar toda la pantalla con un único echo y que después vaya posicionandose frente a cada campo para recibir los valores que serán tecleados por el operador.

Y no te olvides, cualquer duda o falta de compañia para tomar una cerveza o hasta para hablar mal de los políticos lo único que tienes que hacer es mandarme un e-mail para julio.neves@gmail.com. Voy aprovechar también para mandar mi aviso publicitario: puedes decirle a los amigos que quien quiera hacer un curso nota diez de programación en Shell que mande un e-mail para julio.neves@uniriotec.br para informarse.

Gracias y hasta la próxima

-- HumbertoPina - 17 Jan 2007

Licencia Creative Commons - Reconocimiento y no comercial (CC) 2009 Por Visitantes del Bar de Júlio Neves.
Todo el contenido de esta página puede ser usada de acuerdo a la Creative Commons License: Atribuição-UsoNãoComercial-PermanênciaDaLicença.