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:
- 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;
- 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

Copyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki-SL?
Send feedback