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 VIII



- Hola amigo, como estás?

- Muy bien!, quería mostrarte lo que hice, pero ya sé que tu quieres ir rápido a lo que interesa, no?

- Solo para llevarte la contraria, hoy voy a dejar que me muestres tu "programita". Venga, muéstrame lo que hiciste.

- Ahhh ... el ejercicio que me pasaste es muy extenso. Yo lo resolví así:

$ cat musinc5 #!/bin/bash # Registra CDs (versión 5) # clear LineaMesg=$((`tput lines` - 3)) # Linea que define cuantos mensajes serán pasados de una vez a la pantalla TotCols=$(tput cols) # Cantidad de columnas de la pantalla para encuadrar mensajes echo "   Inclusión de Músicas     ========= == =======       Título del Álbum:   | Este campo fue   Pista: < creado solamente para   | orientar como llenar   Nombre de la Música:     Intérprete:" # Pantalla montada con un único echo while true do tput cup 5 38; tput el # Posiciona y limpia linea read Albun [ ! "$Albun" ] && # Operador pulso <ENTER> { Msg="Desea Terminar? (S/n)"         TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1)) read -n1 SN tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla [ $SN = "N" -o $SN = "n" ] && continue # $SN es igual a N o (-o) n? clear; exit # Fin de la ejecución } grep "^$Albun\^" musicas > /dev/null && { Msg="Este álbum ya está incluido"         TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" read -n1 tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla continue # Vuelve para leer otro álbum } Reg="$Albun^" # $Reg recibirá los datos para grabación elArtista= # Variable que graba artista anterior while true do ((Track++)) tput cup 7 38 echo $Track tput cup 9 38 # Posiciona para leer música read Musica [ "$Musica" ] || # Si el operador escribio ... { Msg="Fin del Álbum? (S/n)"             TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1) read -n1 SN tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla [ "$SN" = N -o "$SN" = n ]&&continue # $SN es igual a N o (-o) n? break # Sale del loop para grabar } tput cup 11 38 # Posiciona para leer Artista [ "$elArtista" ]&& echo -n "($elArtista) " # Artista anterior es default read Artista [ "$Artista" ] && elArtista="$Artista" Reg="$Reg$elArtista~$Musica:" # Montando registro tput cup 9 38; tput el # Borra Música de la pantalla tput cup 11 38; tput el # Borra Artista de la pantalla done echo "$Reg" >> musicas # Graba registro en el fin del archivo sort musicas -0 musicas # Clasifica el archivo done

- Si, el programa esta bien, esta todo bien estructurado, pero me gustaría comentarte un poco lo que hiciste:

  • Solo para recordarte, las siguientes construcciones: [ ! $Albun ] && y [ $Musica ] || representan lo mismo, en el primer caso, comprobamos si la variable $Album no (!) tiene nada dentro, entonces (&&) ... y en el segundo, comprobamos lo mismo en $Musica, si no (||) ...
  • Si te quejaste por el tamaño, es porque todavía no te pase algunos trucos. Fíjate que la mayor parte del script es para dar mensajes centrados en la penúltima linea de la pantalla. Fíjate también que algunos mensajes piden un S o un N y otros son sólo de advertencia. Sería el caso típico del uso de funciones, que serían escritas solamente una vez y llamadas para ejecutar en diversos puntos del script . Voy a hacer dos funciones para resolver estos casos y vamos a incorporarlas a tu programa para ver el resultado final.

Funciones

- Mozo! Ahora tráeme dos "chops" bien helados, uno sin espuma, para que me de inspiración.

    Pregunta ()
        {
        #  La función recibe 3 parámetros en el siguiente orden:
        #  $1 - Mensaje que será mostrado en la pantalla
        #  $2 - Valor que será aceptado como respuesta por defecto
        #  $3 - Otro valor aceptado
        #  Suponiendo que $1=Acepta?, $2=s y $3=n, la linea a
        #  seguir colocaría en Msg el valor "Acepta? (S/n)"
        local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)"
        local TamMsg=${#Msg}
        local Col=$(((TotCols - TamMsg) / 2))  # Centra mensaje en la linea
        tput cup $LineaMesg $Col
        echo "$Msg"
        tput cup $LineaMesg $((Col + TamMsg + 1))
        read -n1 SN
        [ ! $SN ] && SN=$2                     # Si esta vacía coloca por defecto en SN
        echo $SN | tr A-Z a-z                  # La salida de SN será en minúscula
        tput cup $LineaMesg $Col; tput el      # Borra mensaje de la pantalla
        return                                 # Sale de la función
        }

Como podemos ver, una función es definida cuando hacemos nombre_de_la_función () y todo su cuerpo esta entre llaves ({}). Así como charlamos aquí en el Bar sobre pasar parámetros, las funciones los reciben de la misma forma, o sea, son parámetros de posición ($1, $2, ..., $n) y todas las reglas que se aplican al pase de parámetros para programas, también valen para funciones, pero es muy importante aclarar que los parámetros pasados hacia un programa no se mezclan con aquellos que éste pasó hacia sus funciones. Esto significa, por ejemplo, que el $1 de un script es diferente del $1 de una de sus funciones.

Fíjate que las variables $Msg, $TamMsg y $Col son de uso restringido de esta rutina, y por eso fueron creadas como local. La finalidad de esto es simplemente economizar memoria, ya que al salir de la rutina, todas serán destruidas y si no hubiese usado esta opción, se quedarían residentes en la memoria.

La linea de código que crea local Msg, junta el texto recibido ($1) abre paréntesis, la respuesta default ($2) en mayúscula, una barra, la otra respuesta ($3) en minúscula y finaliza cerrando el paréntesis. Uso esta forma para, que al mismo tiempo, pueda mostrar las opciones disponibles y destacar la respuesta ofrecida como default.

Casi al final de la rutina, la respuesta recibida ($SN) se pasa a minúscula de forma que en el cuerpo del programa no se necesite hacer esta prueba.

Veamos ahora como quedaría la función para presentar un mensaje en la pantalla:

    function MandaMsg
        {
        # La función recibe solamente un parámetro
        # con el mensaje que se desea mostrar,
        # para no obligar al programador que pase
        # el mensaje entre comillas, usaremos $* (todos
        # los parámetros, te acuerdas?) y no $1.
        local Msg="$*"
        local TamMsg=${#Msg}
        local Col=$(((TotCols - TamMsg) / 2)) # Centra el mensaje en la linea
        tput cup $LineaMesg $Col
        echo "$Msg"
        read -n1
        tput cup $LineaMesg $Col; tput el     # Borra el mensaje de la pantalla
        return                                # Sale de la función
        }

Esta es otra forma de definir una función: no la llamamos como en el ejemplo anterior usando una construcción con la sintaxis nombre_de_la_función (), sino como function nombre_de_la_función. No tiene ninguna diferencia con la anterior, excepto que, como consta en los comentarios, usamos la variable $* que como ya sabemos es el conjunto de todos los parámetros pasados, para que el programador no necesite usar comillas envolviendo el mensaje que desea pasar para la función.

Para terminar con este blá-blá-blá vamos a ver entonces las alteraciones que el programa necesita cuando usamos el concepto de funciones:

$ cat musinc6 #!/bin/bash # Registra CDs (versión 6) #

# Área de las variables globales LineaMesg=$((`tput lines` - 3)) # Linea que mensajes serán dados para el operador TotCols=$(tput cols) # Cantidad de columnas de la pantalla para encuadrar mensajes

# Área de las funciones Pregunta () { # La función recibe 3 parámetros en el siguiente orden: # $1 - Mensaje que será mostrado en la pantalla # $2 - Valor que será aceptado como respuesta default # $3 - Otro valor aceptado # Suponiendo que $1=Acepta?, $2=s y $3=n, la linea a # seguir colocaría en Msg el valor "Acepta? (S/n)" local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" local TamMsg=${#Msg} local Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 # Si vacia coloca default en SN echo $SN | tr A-Z a-z # La salida de SN será en minúscula tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla return # Sale de la función } function MandaMsg { # La función recibe solamente un parametro # con el mensaje que se desea mostrar, # para no obligar al programador que pase # el mensaje entre comillas, usaremos $* (todos # los parametros, te acuerdas?) y no $1. local Msg="$*" local TamMsg=${#Msg} local Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" read -n1 tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla return # Sale de la función }

# El cuerpo del programa propiamente dicho comienza aqui clear echo "   Inclusión de Músicas     ========= == =======       Título del Álbun:   | Este campo fue   Pista: < creado solamente para   | orientar como llenar   Nombre de la Música:     Intérprete:" # Pantalla montada con un único echo while true do tput cup 5 38; tput el # Posiciona y limpia linea read Albun [ ! "$Albun" ] && # Operador dió { Pregunta "Desea Terminar" s n [ $SN = "n" ] && continue # Ahora sólo verifico minúsculas clear; exit # Fin de la ejecución } grep -iq "^$Albun\^" musicas 2> /dev/null && {     MandaMsg Este álbun ya esta catastrado continue # Vuelve para leer otro álbun } Reg="$Albun^" # $Reg recibirá los datos de grabación elArtista= # Grabará artista anterior while true do ((Track++)) tput cup 7 38 echo $Track tput cup 9 38 # Posiciona para leer música read Musica [ "$Musica" ] || # Si el operador dio ... { Pregunta "Fin de Álbun?" s n [ "$SN" = n ] && continue # Ahora solo prueba la minuscula break # Sale del loop para grabar datos } tput cup 11 38 # Posiciona para leer Artista [ "$elArtista" ]&& echo -n "($elArtista) " # Artista anterior es default read Artista [ "$Artista" ] && elArtista="$Artista" Reg="$Reg$elArtista~$Musica:" # Montando registro tput cup 9 38; tput el # Borra Musica de la pantalla tput cup 11 38; tput el # Borra Artista de la pantalla done echo "$Reg" >> musicas # Graba registro en el fin del archivo sort musicas -o musicas # Clasifica el archivo done

Fijate que la estructura del _script_esta como en el gráfico de abajo:

  Cuerpo del Programa  
Variables Globales
Funciones

Esta estructuración es debida a que el Shell es un lenguaje interpretado y así el programa es leído de izquierda a derecha y de arriba para abajo. De esa forma, para que una variable sea vista simultáneamente por el script y sus funciones, debe ser declarada (o inicializada) antes de cualquier otra cosa. Las funciones deben ser declaradas antes del cuerpo del programa propiamente dicho para que en el lugar en que el programador mencione su nombre, el interprete Shell ya lo haya localizado antes y registrado que es una función.

Una cosa muy útil en el uso de funciones es tratar de hacerlas lo más generales posible, de forma que sirvan para otras aplicaciones, sin necesidad de tener que reescribirlas. Esas dos que acabamos de ver tienen uso general, pues es dificil hallar un script que tenga una entrada de datos por teclado que no use una rutina del tipo de la MandaMsg o no interaccione con el operador a través de algo parecido a Pregunta.

Consejo de amigo: crea un archivo y cada función nueva que programes, añádela a este archivo. Así con el tiempo tendrás una bella biblioteca de funciones que te ahorrará mucho tiempo de programación.

El comando source

Fíjate si notas algo diferente en la salida del ls siguiente:

$ ls -la .bash_profile -rw-r--r-- 1 Julio unknown 4511 Mar 18 17:45 .bash_profile

No mires la respuesta y vuelve a prestar atención! De acuerdo, ya que no tienes paciencia para pensar y prefieres leer la respuesta, te voy a dar una pista: me parece que ya sabes que el .bash_profile es uno de los programas que son automáticamente "ejecutados" cuando tu te logeas (ARRGGHH! Odio este término). Ahora que te dí esta ayuda, mira nuevamente la salida del ls y dime que hay de diferente en ella.

Como te dije el .bash_profile es "ejecutado" en el momento del logon y fíjate que no tiene ningúna prerrogativa de ejecución. Esto ocurre porque si tu lo ejecutaras como cualquier otro script simple, cuando terminara su ejecución, todo el ambiente generado por él moriría junto con el Shell en el cual fue ejecutado (te acuerdas que todos los scripts son ejecutados en subshells, verdad?).

Pues bien, es para cosas así que existe el comando source, también conocido por . (punto). Este comando hace que no sea creado un nuevo Shell (un subshell) para ejecutar el programa que le es pasado como parámetro.

Mejor un ejemplo que 10.000 palabras. Mira el scriptiziño siguiente:

$ cat script_bobo cd .. ls

Simplemente debería ir hacia el directório superior del directório actual. Vamos a ejecutar unos comandos que incluyen el script_bobo y vamos a analizar los resultados:

$ pwd /home/jneves $ script_bobo jneves juliana paula silvie $ pwd /home/jneves

Si yo mandé subir un directório, porque no subió? Subió sí! El subshell que fue creado para ejecutar el script subió y listó los directórios de los cuatro usuarios debajo del /home, solo que así que el script acabó, el subshell se fue al limbo y con él, todo el ambiente creado. Mira ahora como la cosa cambia:

$ source script_bobo jneves juliana paula silvie $ pwd /home $ cd - /home/jneves $ . script_bobo jneves juliana paula silvie $ pwd /home

Ahh! Ahora sí! Siendo pasado como parámetro del comando source o . (punto), el script fue ejecutado en el Shell corriente dejando en este, todo el ambiente creado. Ahora damos un rewind hacia el inicio de la explicación sobre este comando. Un poco antes, hablamos del .bash_profile, y a estas alturas ya debes saber que su tarea es, inmediatamente después del login, dejar el ambiente de trabajo preparado para el usuário, y ahora entendemos que por eso es ejecutado usando esta construcción.

Y ahora debes estarte preguntando, sólo sirve para eso este comando?, y yo te digo que sí, pero eso nos trae una cantidad de ventajas y una de las más usadas es tratar funciones como rutinas externas. Mira una forma diferente de hacer nuestro programa para incluir CDs en el archivo musicas:

$ cat musinc7 #!/bin/bash # Registra CDs (versión7) #

# Área de varibles globales LinhaMesg=$((`tput lines` - 3)) # Línea que msgs serán dadas para operador TotCols=$(tput cols) # Qtd colunas de la tela para encuadrar msgs

# El cuerpo del programa propriamente dicho comienza aqui clear echo " Inclusión de Músicas     ======== == =======       Título do Álbum:   | Este campo fue   Pista: < creado solamente para   | orientar como llenar   Nombre de la Música:     Intérprete:" # Pantalla montada con un único echo while true do tput cup 5 38; tput el # Posiciona y limpa línea read Album [ ! "$Album" ] && # Operador dió { source pergunta.func "Desea Terminar" s n [ $SN = "n" ] && continue # Ahora sólo verifico minúsculas clear; exit # Fin de la ejecución } grep -iq "^$Album\^" musicas 2> /dev/null && { . mandamsg.func Este álbum ya está catastrado continue # Vuelve para leer otro álbum } Reg="$Album^" # $Reg reciberá los datos de grabación oArtista= # Guardará artista anterior while true do ((Faixa++)) tput cup 7 38 echo $Faixa tput cup 9 38 # Posiciona para leer música read Musica [ "$Musica" ] || # Si el operador hubiese dado ... { . pergunta.func "Fin del Álbum?" s n [ "$SN" = n ] && continue # Ahora sólo verifico minúsculas break # Sale del loop para grabar datos } tput cup 11 38 # Posiciona para leer Artista [ "$oArtista" ] && echo -n "($oArtista) " # Artista anter. é default read Artista [ "$Artista" ] && oArtista="$Artista" Reg="$Reg$oArtista~$Musica:" # Montando registro tput cup 9 38; tput el # Borra Musica de la pantalla tput cup 11 38; tput el # Borra Artista de la pantalla done echo "$Reg" >> musicas # Graba registro en el fin del archivo sort musicas -o musicas # Clasifica el archivo done

Ahora el programa disminuyo considerablemente de tamaño y las funciones fueron cambiadas por archivos externos llamados pergunta.func y mandamsg.func, que de esta forma, pueden ser llamados por cualquer otro programa y con eso, reutilizando su código.

Por motivos meramente didácticos las ejecuciones de pergunta.func y mandamsg.func están siendo llamadas por source y por . (punto) indiscriminadamente, sin embargo, prefiero el source por ser más visible, lo que le da mayor legibilidad al código y facilita su manutención posteriormente.

Mira ahora como quedaron estos dos archivos:

$ cat pergunta.func # La función recibe 3 parámetros en el siguiente orden: # $1 - Mensaje a ser enviado a la pantalla # $2 - Valor que sera aceptado como respuesta por defecto # $3 - El otro valor aceptado # Suponiendo que $1=Acepta?, $2=s y $3=n, en la línea # de abajo colocaría en Msg el valor "Acepta? (s/n)" Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra msg en la línea tput cup $LinhaMesg $Col echo "$Msg" tput cup $LinhaMesg $((Col + TamMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 # Si esta vacía coloca default en SN echo $SN | tr A-Z a-z # La salida de SN será en minúscula tput cup $LinhaMesg $Col; tput el # Borra msg de la pantalla $ cat mandamsg.func # La función recibe solamente un parámetro # con el mensaje que se desea exhibir, # para no obligar al programador a pasar # el msg entre comillas, usaremos $* (todos # los parámetro, recuerdas?) y no $1. Msg="$*" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra msg en la línea tput cup $LinhaMesg $Col echo "$Msg" read -n1 tput cup $LinhaMesg $Col; tput el # Borra msg de la pantalla

En ambos archivos, hice solamente dos cambios que veremos en las observaciones que siguen, sin embargo tengo tres observaciones más para hacer:

  1. Las variables no están siendo declaradas como local, porque está es una directiva que solamente puede ser usada en el cuerpo de funciones y por consiguiente, estas variables permanecen en el ambiente del Shell, llenándolo de basura;
  2. El comando return no está presente pero podría estarlo, sin alterar en nada la lógica, ya que sólo serviría para indicar un eventual error vía un código de retorno previamente establecido (por ejemplo return 1, return 2, ...), siendo que el return y return 0 son idénticos y significan rutina ejecutada sin errores;
  3. El comando que estamos acostumbrados a usar para generar código de retorno es el exit, pero la salida de una rutina externa no puede ser hecha de esta forma, porque por estar siendo ejecutada en el mismo Shell que el script llamador, el exit simplemente cerraría este Shell, terminando la ejecución de todo el script;
  4. De donde surgió la variable LinhaMesg? Ella vino del musinc7, porque habia sido declarada antes de la llamada de las rutinas (sin olvidar que el Shell que está interpretando el script y estas rutinas, es el mismo para todos);
  5. Si decidiste usar rutinas externas, no seas haragán, abunda en los comentarios (principalmente sobre el pasaje de los parámetros) para facilitar la manutención y su uso para otros programas en el futuro.

- Bien, ahora ya tienes una cantidad de novedades para mejorar los scripts que hicimos. Te acuerdas del programa listartista en el cual pasabas el nombre de un artista como parámetro y él devolvia sus músicas? Era así:

$ cat listartista #!/bin/bash # Dado un artista, muestra sus músicas # versión 2

if [ $# -eq 0 ] then echo Usted debería haber pasado al menos un parámetro exit 1 fi

IFS=" :" for ArtMus in $(cut -f2 -d^ musicas) do echo "$ArtMus" | grep -i "^$*~" > /dev/null && echo $ArtMus | cut -f2 -d~ done

- Claro que me acuerdo!...

- Entonces para afirmar los conceptos que te pasé, hazlo con la pantalla formateada, en loop, de forma que solamente termine cuando reciba un <ENTER> puro en el nombre del artista. Ahhh! Cuando la lista llegue a la antepenúltima línea de la pantalla, el programa deberá detenerse para que el operador pueda leerlas, o sea, imagina que la pantalla tenga 25 lineas. Cada 22 músicas listadas (cantidad de líneas menos 3) el programa aguardará a que el operador teclee algo para entonces continuar. Eventuales mensajes de error deben ser pasados usando la rutina mandamsg.func que acabamos de hacer.

- Mozo, trae dos más, el mio con poca presión...

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 - 10 Jan 2007

-- PatricioReich - 24 Nov 2006

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.