Conversación de Bar Parte IX
- Está bien, ya sé que vas a querer un "chopp" antes de empezar, pero tengo muchas ganas de enseñarte primero lo que hice, así que voy pidiéndote ya la bebida y enseguida te lo muestro.
- Mozo!, trae dos. El de él sin espuma para no ensuciarse el bigote...
- Mientras el "chopp" no llega, déjame recordarte que me pediste que rehiciera el
listartista con la pantalla formateada, en
loop, de forma que solamente termine cuando reciba un
<ENTER> puro en el nombre del artista. Eventuales mensajes de error y preguntas deberían ser mostradas en la antepenúltima línea de la pantalla utilizando las rutina
mandamsj.func y
pregunta.func que acabamos de desarrollar.
- Primero optimize el
mandamsj.func y el
pregunta.func, que quedaron así:
$ cat mandamsj.func
# La función recibe solamente un parámetro
# con el mensaje que se desea exhibir.
# Para no obligar al programador a pasar
# el msj entre comillas, usaremos $* (todos
# los parámetros, recuerdas?) y no $1.
Msj="$*"
TamMsj=${#Msj}
Col=$(((TotCols - TamMsj) / 2)) # Centra msj en la línea
tput cup $líneaMesj $Col
read -n1 -p "$Msj "
$ cat pregunta.func
# La función recibe 3 parámetros en el siguiente orden:
# $1 - mensaje a ser dado en pantalla
# $2 - Valor a ser acepto como respuesta default
# $3 - El otro valor aceptado
# Suponiendo que $1=Acepta?, $2=s y $3=n, la línea
# abajo colocaría en Msj el valor "Acepta? (s/n)"
Msj="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)"
TamMsj=${#Msj}
Col=$(((TotCols - TamMsj) / 2)) # Centra msj en la línea
tput cup $líneaMesj $Col
read -n1 -p "$Msj " SN
[ ! $SN ] && SN=$2 # Si vacía coloca default en SN
SN=$(echo $SN | tr A-Z a-z) # La salida de SN será en minúscula
tput cup $líneaMesj $Col; tput el # Borra msj de la pantalla
- Y aquí va el grandullón ahora:
$ cat listartista3
#!/bin/bash
# Dado un artista, muestra sus músicas
# versión 3
líneaMesj=$((`tput lines` - 3)) # línea que msjs serán dados al operador
TotCols=$(tput cols) # Ctd de columnas de la pantalla para encuadre de msjs
clear
echo "
+----------------------------------------------------+
| Lista Todas las Músicas de un Determinado Artista |
| ----- ----- -- ------- -- -- ----------- ------- |
| |
| Informe el Artista: |
+----------------------------------------------------+"
while true
do
tput cup 5 51; tput ech 31 # ech=Erase chars (31 caracteres para no borrar barra vertical)
read Nombre
if [ ! "$Nombre" ] # $Nombre está vacío?
then
. pregunta.func "Desea Salir?" s n
[ $SN = n ] && continue
break
fi
fgrep -iq "^$Nombre~" musicas || # fgrep no interpreta ^ como expresión regular
{
. mandamsjg.func "No existe música de este artista"
continue
}
tput cup 7 29; echo '| |'
LinActual=8
IFS="
:"
for ArtMus in $(cut -f2 -d^ musicas) # Excluye nombre del album
do
if echo "$ArtMus" | grep -iq "^$Nombre~"
then
tput cup $LinActual 29
echo -n '| '
echo $ArtMus | cut -f2 -d~
tput cup $LinActual 82
echo '|'
let LinActual++
if [ $LinActual -eq $líneaMesj ]
then
. mandamsj.func "Teclee Algo para Continuar..."
tput cup 7 0; tput ed # Borra la pantalla a partir de la línea 7
tput cup 7 29; echo '| |'
LinActual=8
fi
fi
done
tput cup $LinActual 29; echo '| |'
tput cup $((++LinActual)) 29
read -n1 -p "+-----------Teclee Algo para Nueva Consulta----------+"
tput cup 7 0; tput ed # Borra la pantalla a partir de la línea 7
done
- Caramba!, hoy llegaste con mucha fuerza! Pero me gustó la forma en que resolviste el problema y estructuraste el programa. Fue más trabajoso pero la presentación quedó excelente y usaste bastante las opciones del
tput. Vamos a comprobar el resultado con un álbum de
Emerson, Lake & Palmer que tengo registrado:
+----------------------------------------------------+
| Lista Todas las Músicas de un Determinado Artista |
| ----- ----- -- ------- -- -- ----------- ------- |
| |
| Informe el Artista: Emerson, Lake & Palmer |
+----------------------------------------------------+
| |
| Jerusalem |
| Toccata |
| Still ... You Turn Me On |
| Benny The Bouncer |
| Karn Evil 9 |
| |
+-----------Teclee Algo para Nueva Consulta----------+
Mejorando la escritura
- Ufa! Ahora ya lo sabes todo sobre lectura, pero sobre escritura apenas estás gateando. Ya sé que me vas a preguntar:
- Pero, no era con el comando
echo y con los redireccionamentos de salida que se escribe?
Si, con estos comandos escribes el 90% de las cosas necesarias, sin embargo, si necesitas escribir algo formateado te dará mucho trabajo. Para formatear la salida veremos ahora una instrucción muy interesante - el
printf - su sintaxis es la siguiente:
printf formato [argumento...]
En donde:
formato - es una cadena de caracteres que contiene 3 tipos de objetos:
- caracteres simples;
- caracteres para especificación de formato;
- secuencia de escape en el patrón del lenguaje C.
Argumento - es la cadena a ser impresa con el control del
formato.
Cada uno de los caracteres utilizados para especificación de
formato está precedido por el carácter
% y luego viene la especificación de
formato de acuerdo con la tabla:
| Tabla de los Caracteres de Formatación del printf |
% |
Imprime un %. no existe ninguna conversión |
| Letra |
La expresión será impresa como: |
c |
Simple caracter |
d |
Número en sistema decimal |
e |
Notación científica exponencial |
f |
Número con punto decimal (float) |
g |
El menor entre los formatos %e y %f con supresión de los ceros no significativos |
o |
Número en sistema octal |
s |
Cadena de caracteres |
x |
Número en sistema hexadecimal |
Las secuencias de
escape patrón del lenguaje C son siempre precedidas por una barra invertida (
\) y las reconocidas por el comando
printf son:
| Secuencias de Escape del printf |
t |
Avanza para la próxima marca de tabulación |
| Secuencia |
Efecto |
a |
Suena el bip |
b |
Vuelve una posición (backspace) |
f |
Salta para la próxima página lógica (form feed) |
n |
Salta para el inicio de la línea siguiente (line feed) |
r |
Vuelve para el inicio de la línea actual (carriage return) |
Y no se acaba aquí, todavía hay más! Hay muchas más cosas sobre esta instrucción, pero como son muchos detalles es por consiguiente aburrido de explicar y todavía peor de leer o estudiar, así que vamos directos a los ejemplos con sus comentarios, que no estoy aqui para aburrir a nadie.
$ printf "%c" "1 caracter"
1$ Error! Sólo listó 1 caracter y no saltó de línea al final
$ printf "%c\n" "1 caracter"
1 Saltó de línea pero todavia no listó la cadena entera
$ printf "%c caracteres\n" 1
1 caracter Esta es la forma correcta, el %c recebió el 1
$ a=2
$ printf "%c caracteres\n" $a
2 caracteres O %c recibió el valor de la variable $a
$ printf "%10c caracteres\n" $a
2 caracteres
$ printf "%10c\n" $a caracteres
2
c
Observa que en los dos últimos ejemplos, en virtud del
%c, sólo se listo un caracter de cada cadena. El
10 delante de la
c, no significa 10 caracteres. Un número después del signo de porcentaje (
%) significa el tamaño que la cadena tendrá depués de la ejecución del comando.
Y aqui vá un ejemplo:
$ printf "%d\n" 32
32
$ printf "%10d\n" 32
32 Rellena con blancos a la izquierda y con ceros
$ printf "%04d\n" 32
0032 04 despues % significa 4 dígitos con ceros a la izquierda
$ printf "%e\n" $(echo "scale=2 ; 100/6" | bc)
1.666000e+01 El default del %e es 6 decimales
$ printf "%.2e\n" `echo "scale=2 ; 100/6" | bc`
1.67e+01 El .2 especificó dos decimales
$ printf "%f\n" 32.3
32.300000 El default del %f es 6 decimales
$ printf "%.2f\n" 32.3
32.30 El .2 especificó dos decimales
$ printf "%.3f\n" `echo "scale=2 ; 100/6" | bc`
33.330 El bc devolvió 2 decimales. El printf colocó 0 a la derecha
$ printf "%o\n" 10
12 Convirtió el 10 en octal
$ printf "%03o\n" 27
033 Así la conversión queda con más apariencia de octal, sí?
$ printf "%s\n" Palabra
Palabra
$ printf "%15s\n" Palabra
Palabra Palabra con 15 caracteres rellenados con blancos
$ printf "%-15sNeves\n" Palabra
Palabra Neves El menos (-) rellenó a la derecha con blancos
$ printf "%.3s\n" Palabra
Pal 3 Corta y deja sólo las 3 primeras
$ printf "%10.3sa\n" Peteleca
Peta Pet con 10 caracteres concatenado con a (después del s)
$ printf "EJEMPLO %x\n" 45232
EJEMPLO b0b0 Transformó en hexa pero los zeros no combinan
$ printf "EJEMPLO %X\n" 45232
EJEMPLO B0B0? Así quedó mejor (Fíjate en la X mayúscula)
$ printf "%X %XL%X\n" 49354 192 10
C0CA? C0LA?
El último ejemplo no es
marketing y es bastante completo, voy a comentarlo paso a paso:
- El primer
%X convirtió 49354 en hexadecimal resultando C0CA (léase "ce", "cero", "ce" y "a");
- En seguida viene un espacio en blanco seguido por otro
%XL. El %X convirtió el 192 dando como resultado C0 que con el L hizo C0L;
- Y finalmente el último
%X transformó el 10 en A.
Como puedes notar, la instrucción
printf es bastante completa y compleja (por suerte el
echo lo resuelve casi todo).
Creo que cuando me decidí a explicar el
printf a través de ejemplos, acerté plenamente, porque no sabria como enumerar tantas reglitas sin hacer la lectura aburrida.
Principales Variables del Shell
El
Bash posee diversas variables que sirven para dar informaciones sobre el ambiente o alterarlo. Su número es muy grande y no pretendo mostrártelas todas sino una pequeña parte, y que pueden ayudarte en la elaboración de
scripts. Ahí van las principales:
| Principales variables del Bash |
| TMOUT |
Si tuviera un valor mayor que cero, este valor será tomado como el patrón de timeout del comando read. En el prompt, este valor es interpretado como el tiempo de espera a una acción antes de finalizar la sesión. Suponiendo que la variable contenga 30, el Shell dará logout 30 segundos después que el prompt esté sin ninguna acción. |
| Variável |
Conteúdo |
| CDPATH |
Contiene los caminos que serán recorridos para intentar localizar un directório especificado. A pesar de ser esta variable poco conocida, su uso debe ser incentivado por que nos ahorra mucho trabajo, principalmente en instalaciones con estructuras de directórios con bastante niveles. |
| HISTSIZE |
Limita el número de instrucciones que caben dentro del archivo histórico de comandos (normalmente .bash_history pero efectivamente es lo que está almacenado en la variable $HISTFILE). Su valor default es 500. |
| HOSTNAME |
El nombre del host actual (que también puede ser obtenido con el comando uname -n). |
| LANG |
Usada para determinar el idioma hablado en el país (más especificamente categoria de locale). |
| LINENO |
El número de la línea del script o de la función que está siendo ejecutada, su uso principal es para dar mensajes de error juntamente con las variables $0 (nombre del programa) y $FUNCNAME (nombre de la función en ejecución) |
| LOGNAME |
Almacena el nombre de login del usuário. |
| MAILCHECK |
Especifica, en segundos, la frecuencia con que el Shell verificará la presencia de correspondencia en los archivos indicados por las variables $MAILPATH o $MAIL. El tiempo patrón es de 60 segundos. Una vez que este tiempo expira, el Shell hará esta verificación antes de exhibir el próximo prompt primario (definido en $PS1). Si esta variable estuviera sin valor o con un valor menor o igual a cero, la verificación de nueva correspondencia no será efectuada. |
| PATH |
Caminos que serán recorridos para intentar localizar un archivo especificado. Como cada script es un archivo, en el caso de que uses el directorio actual (.) en su variable $PATH, no necesitarás usar el ./scrp para que scrp sea ejecutado. Basta hacer scrp. Este es el modo en que procedo aqui en el Bar. |
| PIPESTATUS |
Es una variable del tipo vector (array) que contiene una lista de valores de código de retorno del último pipeline ejecutado, o sea, un array que abriga cada uno de los $? de cada instrucción del último pipeline. |
| PROMPT_COMMAND |
Si esta variable recibe una instrucción, cada vez que tu des un <ENTER> directo en el prompt principal ($PS1), este comando será ejecutado. Es útil cuando se está repitiendo mucho una determinada instrucción. |
| PS1 |
Es el prompt principal. En "Conversa de Bar" usamos sus defaults: $ para el usuário común y # para el root, pero es muy frecuente que esté personalizado. Una curiosidad es que existen hasta concursos de quien programa el $PS1 más creativo. (clique para dar una googlada) |
| PS2 |
También llamado prompt de continuación, es aquél signo de mayor (>) que aparece después de un <ENTER> sin que el comando haya sido finalizado. |
| PWD |
Posee el camino completo ($PATH) del directório actual. Tiene el mismo efecto que el comando pwd. |
| RANDOM |
Cada vez que esta variable es llamada, devuelve un número entero, que es un número randómico entre 0 y 32767. |
| REPLY |
Usa esta variable para recuperar el último campo leído, en caso de que no tenga ninguna variable asociada. |
| SECONDS |
Esta variable contiene la cantidad de segundos en que el Shell actual está en uso. Úsala solamente para mostrar a un usuario aquello que llaman de sistema operacional, pero necesita de frecuentes boots. |
$ echo $CDPATH
.:..:~:/usr/local
$ pwd
/home/jneves/LM
$ cd bin
$ pwd
/usr/local/bin
Como /usr/local estaba en mi variable
$CDPATH, y no existía el directório
bin en ninguno de sus antecesores (
.,
.. e
~), el
cd fue ejecutado para
/usr/local/bin
$ date
Thu Apr 14 11:54:13 BRT 2005
$ LANG=pt_BR date
Qui Abr 14 11:55:14 BRT 2005
Con la especificación de la variable
LANG=pt_BR (portugués de Brasil), la fecha pasó a ser formateada en el patrón brasileño. Es interesante observar que no se uso punto y coma (
;) para separar la atribución de
LANG del comando
date.
$ who
jneves pts/0 Apr 11 16:26 (10.2.4.144)
jneves pts/1 Apr 12 12:04 (10.2.4.144)
$ who | grep ^botelho
$ echo ${PIPESTATUS[*]}
0 1
En este ejemplo mostramos que el usuário
botelho no estaba "logado", en seguida ejecutamos un
pipeline que lo filtraba. Se usa la notación
[*] en un
array para listar todos sus elementos, y de esta forma vimos que la primera instrucción (
who) fue bien ejecutada (código de retorno 0) y la siguiente (
grep), no (código de retorno 1).
Para generar randómicamente un entero entre 0 y 100, hacemos:
$ echo $((RANDOM%101))
73
O sea, tomamos el resto de la división por
101 del número randómico generado, porque el resto de la división de cualquier número por 101 varía entre 0 y 100.
$ read -p "Digite S o N: "
Digite S o N: N
$ echo $REPLY
N
Yo soy de la época en que la memoria era un bien precioso que costaba muuuuy caro. Entonces para tomar un
S o un
N, no acostumbro guardar un espacio especial y por lo tanto, tomo de la variable
$REPLY lo que se escribio.
Expansión de parámetros
Bien, mucho de lo que vimos hasta ahora son comandos externos al
Shell. Estos son de gran ayuda, facilitan la visualización, manutención y depuración del código, pero no son tan eficientes como los intrínsecos (
built-ins). Cuando nuestro problema sea prestaciones, debemos dar preferencia al uso de los intrínsecos y a partir de ahora te voy a mostrar algunas técnicas para que tu programa pise el acelerador.
En la tabla y ejemplos siguientes, veremos una serie de construcciones llamadas expansión (o substitución) de parámetros (
Parameter Expansion), que substituyen instrucciones como el
cut, el
expr, el
tr, el
sed y otras, de forma más ágil.
| Expansión de parámetros |
${cadena/%subcad1/subcad2} |
Si subcad1 s igual al fin de $cadena, entonces es cambiada por subcad2 |
| Expresión |
Resultado esperado |
${var:-padrón} |
Si var no tiene valor, el resultado de la expresión es padrón |
${#cadena} |
Tamaño de $cadena |
${cadena:posición} |
Extrae una sub-cadena de $cadena a partir de posición. Origen cero |
${cadena:posición:tamaño} |
Extrae una sub-cadena de $cadena a partir de posición con tamaño igual a tamaño. Origen cero |
${cadena#expr} |
Corta la menor ocurrencia de $cadena a la izquierda de la expresión expr |
${cadena##expr} |
Corta la mayor ocurrencia de $cadena a la izquierda de la expresión expr |
${cadena%expr} |
Corta la menor ocurrencia de $cadena a la derecha de la expresión expr |
${cadena%%expr} |
Corta la mayor ocurrencia de $cadena a la derecha de la expresión expr |
${cadena/subcad1/subcad2} |
Cambia en $cadena la primera ocurrencia de subcad1 por subcad2 |
${cadena//subcad1/subcad2} |
Cambia en $cadena todas las ocurrencias de subcad1 por subcad2 |
${cadena/#subcad1/subcad2} |
Si subcad1 es igual al inicio de $cadena, entonces es cambiada por subcad2 |
- Si en una pregunta el
S es ofrecido como valor default (patrón) y la salida va hacia la variable $SN, después de leer el valor podemos hacer:
SN=$(SN:-S}
De esta forma si el operador dió un simple
<ENTER> para confirmar que aceptó el valor
default, después de ejecutar esta instrucción, la variable tendrá el valor
S, en caso contrário, tendrá el valor tecleado.
- Para saber el tamaño de una cadena:
$ cadena=0123
$ echo ${#cadena}
4
- Para extraer de una cadena de la posición uno hasta el final hacemos:
$ cadena=abcdef
$ echo ${cadena:1}
bcdef
Fíjate que el origen es cero y no uno.
- En la misma variable
$cadena del ejemplo de arriba, para extraer 3 caracteres a partir de la 2ª posición:
$ echo ${cadena:2:3}
cde
Fíjate que nuevamente el origen de la posición es cero y no uno.
- Para suprimir todo a la izquierda de la primera ocurrencia de una cadena, haz:
$ cadena="Conversa de Bar"
$ echo ${cadena#*' '}
de Bar
$ echo "Conversa "${cadena#*' '}
Conversa de Bar
En este ejemplo fue suprimido a la izquierda todo lo que estuviera antes de la ocurrencia de la expresión
*' ', o sea, todo hasta el primer espacio en blanco.
Estos ejemplos también podrían ser escritos sin proteger el espacio de la interpretación del Shell (pero prefiero protegerlo para facilitar a legibilidad del código), mira:
$ echo ${cadena#* }
de Bar
$ echo "Conversa "${cadena#* }
Conversa de Bar
Fíjate que en la construcción de
expr está permitido el uso de metacaracteres.
- Utilizando el mismo valor de la variable
$cadena, observa como haríamos para tener solamente Bar:
$ echo ${cadena##*' '}
Bar
$ echo "Vamos 'Chopear' en el "${cadena##*' '}
Vamos 'Chopear' en el Bar
Esta vez suprimimos a la izquierda de la cadena la mayor ocurrencia de la expresión
expr. Así como en el caso anterior, el uso de metacaracteres está permitido.
Otro ejemplo mas útil: para que no aparezca el camino (
path) completo de tu programa (que, como ya sabemos está contenido en la variable
$0) en un mensaje de error, empieza tu texto de la siguiente forma:
echo Uso: ${0##*/} texto del mensaje de error
En este ejemplo sería suprimido por la izquerda todo hasta la última barra (
/) del camino (
path), quedando solamente el nombre del programa.
* El uso de porcentaje (
%) es como si mirasemos el simbolo (
#) en el espejo, o sea, son simétricos. Veamos un ejemplo para probarlo:
$ echo $cadena
Conversa de Bar
$ echo ${cadena%' '*}
Conversa de
$ echo ${cadena%%' '*}
Conversa
- Para cambiar la primera ocurrencia de una sub-cadena en una cadena por otra:
$ echo $cadena
Conversa de Bar
$ echo ${cadena/de/en el}
Conversa en el Bar
$ echo ${cadena/de /}
Conversa Bar
En este caso presta atención cuando vayas a usar metacaracteres, son unos comilones! Siempre combinarán con la mayor posibilidad, mira el ejemplo siguiente donde la intención era cambiar
Conversa de Bar por
Charla de Bar:
$ echo $cadena
Conversa de Bar
$ echo ${cadena/*a/Charla}
Charlar
La idea era cogerlo todo hasta la primera
a, pero lo que cambio fue todo hasta la última
a. Esto podría resolverse de diversas formas, veamos algunas:
$ echo ${cadena/*sa/Charla}
Charla de Bar
$ echo ${cadena/????????/Charla}
Charla de Bar
* Cambiando todas las ocurrencias de una subcadena por otra. Cuando hacemos:
$ echo ${cadena//a/o}
Converso de Bor
Cambiamos todas las letras
a por
o. Otro ejemplo más útil es para contar la cantidad de archivos existentes en el directorio en uso. Observa la linea siguiente:
$ ls | wc -l
30
Viste? El
wc produce una cantidad de espacios en blanco al inicio. Para eliminarlos podemos hacer:
$ CtdArChs=$(ls | wc -l) # CtdArchs recibe la salida del comando
$ echo ${CtdArChs// /}
30
En el último ejemplo, como sabía que la salida era compuesta de blancos y números, monté esta expresión para cambiar todos los espacios por nada. Fíjate que después de las dos primeras barras existe un espacio en blanco.
Otra forma de hacer la misma cosa sería:
$ echo ${CtdArChs/* /}
30
- Cambiando una sub-cadena en el inicio o en el fin de una variable. Vamos a usar como ejemplo el conocido pájaro del campo "Quero quero", conocido en otros países como "Tero tero". Para cambiarla al inicio hacemos:
$Pájaro="quero quero"
$ echo $Pájaro
quero quero
$ echo "Como dice el gaucho - "${Pájaro/#quero/no}
Como dice el gaucho - no quero
Para cambiarla al final hacemos:
$ echo "Como se dice en el norte - "${Pájaro/%quero/no}
Como se dice en el norte - quero no
- Ahora basta, la conversación de hoy fue muy aburrida porque hay muchas cosas para memorizar, así que lo principal es que hayas entendido lo que te dije y cuando lo necesites, consultes estas servilletas en las que escribí estas ayudas y después guárdalas para futuras consultas. Pero volviendo a lo que importa, ha llegado la hora de tomar otro y ver el partido de futbol. Para la próxima te voy a aflojar un poco y solo te voy a pedir lo siguiente: toma la rutina
pregunta.func, (de la cual hablamos en el inicio de nuestra conversa de hoy) y optimízala para que la variable
$SN reciba el valor
default por expansión de parámetros, como vimos.
- Mozo, no se olvide de mi y llene mi vaso.
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ém 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 - 23 Jan 2007