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

  1. caracteres simples;
  2. caracteres para especificación de formato;
  3. 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:

  1. El primer %X convirtió 49354 en hexadecimal resultando C0CA (léase "ce", "cero", "ce" y "a");
  2. 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;
  3. 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. smile

  • CDPATH
$ 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

  • LANG
$ 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.

  • PIPESTATUS
$ 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).

  • RANDOM

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.

  • REPLY
$ 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

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.