"La Gaceta de Linux ...¡haciendo de Linux algo un poco mas divertido!"


Introducción a la Escritura de Guiones de Shell

Por Ben Okopnik

Traducido al Español por: Horacio Arroyo
el día 19 de Abril 2001, para La Gaceta de Linux.


"¡No creerías cuantos administradores creen que puedes tener un bebé en un mes embarazando a nueve mujeres!"
-- Marc Wilson

Divagaciones Ocasionales

Bueno, este debe ser el último artículo en la serie "Introducción a la Escritura de Guiones de Shell" - Tuve una buena reacción de parte de una cantidad de lectores (¡y gracias a todos por sus afables comentarios!), pero ya hemos cubierto la mayoría de lo básico de la escritura de guiones de shell; y ese ha sido el propósito original de la serie. Aún debo rever algún punto en el futuro ("¡Oh, cielos, olvidé explicar XYZ!"), pero aquellos de ustedes que nos han seguido a lo largo de la serie ahora se pueden considerar a sí mísmos Expertos, calificados para llevar un maletín y sonar importantes... <Hehe> Bueno, al final deben tener una buena idea de cómo escribir un script y hacerlo funcionar - y esa es una conveniente habilidad.

Un Preciado Asistente

Bastante tiempo atrás, me encontré a mí mísmo en un dilema mientras escribía un guión (¡NO-O-O! ¡Cuan inusual! <Hehe>); tenía un array que contenía una lista de comandos que yo necesitaba ejecutar basándome en ciertas condiciones. Podía leer el array bastánte fácilmente, o imprimir cualquiera de las variables - pero lo que necesitaba era ¡ejecutarlo! ¿Que hacer? ¿Que hacer?... Como lo recuerdo, me rendí por falta de esa capacidad, y reescribí completamente el (bastante largo) guión (y no fué una experiencia agradable). "eval" pudo haber sido la solución.

He aquí como funciona - crear una variable llamada $cmd, como:

cmd='cat .bashrc|sort'

Es sólo un ejemplo - puedes usar cualquiér comando(s) válido(s).

Odin:~$ echo $cmd
cat .bashrc|sort
Odin:~$

- ¿pero como lo ejecutas? Sólo corriendo "cmd" produce un error:

Odin:~$ $cmd
cat: .bashrc|sort: No such file or directory
Odin:~$

Aquí es donde "eval" viene a aparecer: eval $cmd" evalúa el contenido de una variable como si hubiese sido ingresada desde la línea de comandos. Esto no es algo que ocurra muy a menudo... pero es una capacidad del shell que necesitas tener en cuenta.

Nota que "bash" no tiene problemas ejecutando un comando simple que está almacenado como una variable, algo como:

Odin:~$ N="cat .bashrc"
Odin:~$ $N
# ~/.bashrc: executed by bash(1) for non-login shells.

export PS1='\h:\w\$ '
umask 022
funciona bien. Es sólo cuando comandos más complejos, por ej. en aquellos que involucran +alias u operadores ("|", ">", ">>", etc.) es donde encontrarás problemas - y para esas ocaciones, "eval" es la respuesta.

Atrapado Como una Rata

Una de las técnicas estandard en la escritura de guiones (y en programación en general) es la de escribir datos en ficheros temporales - hay muchas razones para hacer esto. Pero, y esta es una grande, ¿qué pasa cuando el usuario interrumpe ese guión a la mitad de la ejecución? (Para aquellos de ustedes que tienen guiones como ese y no lo han pensado, lamento darles material para pesadillas. Al menos espero mostrales también la solución.)

Ustedes pensaron esto: un desorden. Muchos ficheros en "/tmp", quizás importantes datos quedaron colgados de una briza (para ser borrados en el próximo reinicio), ficheros que pensaron actualizados pero no lo están...Ay! ¿Qué hay de una manera de salir airosos, a pesar de un frenético usuario golpea-teclados que tiene que correr "Quake" AHORA?

El comando "trap" provee una respuesta (al decir del usuario es mucho más efectiva y agradable, pero puede tenerte hablando de eso).



#!/bin/bash

function cleanup ()
{
    stty intr "" # Ignora 'Ctrl-C'; déjale segir machacando...
    echo "Despierta, Neo."
    sleep 2; clear
    echo "Tu eres The Matrix."

    echo "Helo aquí denuevo."|mail admin -s "Update stopped by $USER"

    # Restaurar los datos originales
    tar xvzf /mnt/backup/accts_recvbl -C /usr/local/acct
    # Borrar las cosas de 'tmp'
    rm -rf /tmp/in_process/

    # OK, nos hemos hecho cargo de la limpieza. Ahora, es el tiempo de la VENGANZA!!!
    rm /usr/games/[xs]quake

    # Dale una contraseña nueva linda y fácil-de-usar...
    chpasswd $USER:~X%y!Z@zF%HG72F8b@Idiota&(~64sfgrnntQwvff########^

    # Hemos respaldado todas sus cosas... Oh, ¿que hace "--remove-files"?
    tar cvz --remove-files -f /mnt/timbuktu/bye-bye.tgz /home/$USER

    # Heh-heh-heh...
    umount /mnt/timbuktu

    stty intr ^C # Vuelta a la normalidad
    exit         # Ups, no quise hacer eso... Matar/colgar la shell.
}

trap 'cleanup' 2
 
...


Hay un poco de BOfH dentro de cada administrador. <sonrisa> (Para aquellos que no están familiarizados con "La Saga BOfH", esto es algo que debe leer cada administrador Unix; espantoso y odiosamente divertido. Busca en la Web.)

NO ejecutes este guión... si, se que es tentador. El punto de "trap" es, que podemos definir el comportamiento cuandoquiera que el usuario tipee `Ctrl-Break´ (o por si importa, cada vez que el script termina o es matado) y esto es mucho más útil que sólo salir del programa; esto te dá la oportunidad de ordenar, generar advertencias, etc.

"trap" también puede tomar otras señales; de hecho aún "kill", a pesar de su nombre, el mísmo no `mata´ un proceso - él le envía una señal. El proceso decide que hacer con la señal (una cruda descripción, pero generalmente correcta). Si deseas ver la lista entera de señales, solo tipea "trap -l" o "kill -l" o aún "killall -l" (lo que no lista los números de señales, solo los nombres). Las más comunmente usadas son: 1)SIGHUP, 2)SIGINT, 3)SIGQUIT, 9)SIGKILL, y 15)SIGTERM.

[Pero SIGKILL es inatrapable. -Ed.]

Existen también las señales `especiales´. Ellas son: 0)EXIT, que atrapa cualquier salida desde la shell, y DEBUG (sin número asignado), que puede - ¡he aquí una cosa ingeniosa! - ser usada como un script de shell reparador (el toma cada vez un comando simple ejecutado). DEBUG es realmente más que un ítem "solo informativo": puedes tener exactamente esta acción sin escribir ningún "traps", simplemente añadiendo "-x" a tu "hash-bang" (vea "EN CASO DE PROBLEMAS...").

"trap" es una poderosa herramientita. En LG·37, Jim Dennis da un breve fragmento de script que crea un directorio seguro bajo "/tmp" para esta clase de cosas - ficheros temporales que no quieres exponer al mundo. Un lindo artilugio; que usé varias veces.

En Caso de Problemas, Rompa el Vidrio

Hablando de arreglaproblemas, "bash" provee varia y muy útiles herramientas que pueden ayudarte a encotrar errores en tu script. Estos son parámetros - parte del conjunto de la sintaxis del comando - que son usados en la línea de "hash-bang" del mísmo script. Estos parámetros son:

-n     Lea la línea del script de shell, pero no la ejecute
-v     Imprima las líneas como son leídas
-x     Imprima $PS4 (el indicador de "nivel de indirección") y el comando ejecutado.

Encuentro que "nv" y "-x" son las más útiles invocaciones: una te da la exacta localización de una "mala" línea (puedes ver dónde el script puede colgarse); la otra, `ruidosa´ como es, es útil por distinguir dónde las cosas no están funcionando de la manera correcta (cuando, aunque la sintaxis es buena, la acción no es la que querías). Buenas herramientas ambas. Según pasa el tiempo y te acostumbras a la rareza de los reportes de error, probablemente las uses menos y menos, pero son invaluables para el novato escritor de guiones.
Para usarlas, simplemente modifica el "hash-bang" inicial:



#!/bin/bash -nv
...

Usa la Fuente, Luke

He aquí una línea familiar para cada programador "C":

#include <"stdio.h">

- un muy útil concepto, el de ficheros de fuente externos. Lo que esto significa es que un programador "C" puede escribir rutinas (funciones) que usará una y otra vez, almacenarlos en una `librería´ (un fichero externo), y traerlos cuando los necesite. Bueno - ¿no he dicho que el shell scripting es un lenguaje maduro y capáz? - nosotros podemos hacer lo mísmo! El fichero no siempre debe ser ejecutable; la sintaxis que usemos se ocupará de ello. El ejemplo debajo es un recorte de mi librería de función, "Funcky". Actualmente, es un sólo fichero, de un par de kb de extensión, y creciendo en espacio. Trato de mantener las funciones más útiles, porque no quiero llenar de basura el espacio (¿el concepto es aplicable a Linux? Debo descubrirlo....)

Hay un pequeño truco en el manejo de "bash" que vale la pena conocer: si creas una variable llamada BASH_ENV en tu .bash_profile, como:

export BASH_ENV="~/.bash_env"

y luego creas un fichero llamado ".bash_env" en tu directorio home, ese fichero será releido cada vez que inicies una shel ´no-login no-interactiva´ ej., un shell guión. Es un buen lugar para poner las cosas de inicialización es decir un guión de shell específico; es ahí donde yo tengo las fuentes de "Funky" - de esta manera, cualquier cambio en él está inmediatamente disponible para cualquier guión de shell.


func_list ()     # lista todas las funciones en Funky
{
    # Cada vez que necesito un recordatorio de cuales funciones tengo, cuales
    # fueron llamadas, y que hacen, sólo tipeo "func_list".
    # Un lindo ejemplo de recursión - una función que lista todas las funciones,
    # incluyéndose a si mísma.

    cat /usr/local/bin/Funky|grep \(\)
}

getch ()     # toma un caracter del teclado, no "Enter" necesariamente
{
    OLD_STTY=`stty -g`
    stty cbreak -echo
    GETCH=`dd if=/dev/tty bs=1 count=1 2>/dev/null`
    stty $OLD_STTY
}

...


No tan diferente de un guión, no? No es necesario un "hash-bang", puesto que este fichero no se ejecuta a sí mísmo.  Entonces, ¿cómo lo usamos en un guión? Helo aquí (pretenderemos que no fundamentamos "Funky" en "bash_env"):


#!/bin/bash

. Funky

declare -i Total=0

leave ()
{
    echo "¿Así que han estado de compras?"
    [ $Total -ne 0 ] && echo "Dat'll be $Total bucks, pal."
    echo "Que tengas un buen dia."
    exit
}

trap 'leave' 0
clear

while [ 1 ]
do
    echo
    echo "¿Que quieres que? Tengo pepinos, tomates, lechuga, cebollas,"
    echo "y radichetas hoy."
    echo

    # Esto es lo que llamamos una función basada...
    getch

    # ...y referencia a una variable creada por esa función.
    case $GETCH
    in
       C|c) Total=$Total+1; ;;
       T|t) Total=$Total+2; echo "Tomate maduro, ah?" ;;
       L|l) Total=$Total+2; echo "Yo escogí la lechuga personalmente." ;;
       O|o) Total=$Total+1; echo "Tan fresco que te hará llorar" ;;
       R|r) Total=$Total+2; echo "Radichetas realmente crujientes." ;;
       *) echo "No tengo náa como eso hoy, puesé mañá." ;;
    esac

    sleep 2
    clear

done


Note el período después de "Funky": ese es un alias para el comando "fuente". Cuando es `fuenteado´,"Funky" adquiere una propiedad interesante: es como si hubiésemos pedido a "bash" ejecutar un fichero, el sale y revisa el camino listado en $PATH. Puesto que mantengo a "Funky"en "/usr/local/bin" (parte de mi $PATH), no necesito dar un camino explícito para el.

Si vas a escribir un guión de shel, te sugiero fuertemente que empiezes tu propia `libreria´ de funciones. (SUGERENCIA: Roba las funciones del ejemplo anterior!) Antes de tipearlos una y otra vez, un simple argumento "fuente" te conseguirá un montón de caramelos `empaquetados´.

Sintetizando la Serie

Bueno - después de todo, un montón de tópicos han sido cubiertos, algunas "rarezas" explicadas; todas buenas cosas, información útil para el escritor de guiones. Hay un montón más de eso - recuerda, esta serie fué sólo una introducción a la escritura de guiones de shel - pero cualquiera que se me haya pegado desde el princípio y preserve en adelante mi marca de lógica retorcida (¡pobres compañeros! irremediablemente dañados, ni siquiera el mejor sicólogo del mundo podrá ayudarlos ahora....:) son capaces ahora de diseñar, escribir, y reparar un guión de shell medianamente decente. El resto - entendimiento y escritura de guiones más complejos, más complicados - sólo puede venir con la práctica, conocido de otra manera como "cometer un montón de errores". En ese espíritu, les deseo gran cantidad de "errores"!
 

Feliz Linuxeo!


La Cita Linux del Mes:
``Las palabras "comunidad" y "comunicación" tienen la mísma raíz.
Donde quiera que pongas una red de comunicaciones, pones una comunidad como
tal. Y donde cuandoquiera que lleves la red -- la confísques, la hagas ilegal, la cuelgues, levantes su precio más alla de lo pagable -- enotnces hieres a la comunidad.

Las comunidades  lucharán por defenderse a sí mísmas.  La gente luchará más duro y más reñido por defender sus comunidades, tanto como ellos lucharán por defender sus propias personalidasdes individuales.''
 -- Bruce Sterling, "Hacker Crackdown"



REFERENCES

Las páginas "man" para 'bash', 'builtins', 'stty' "Introducción a la Escritura de Guiones de Shell - Las Bases", LG #53
"Introducción a la Escritura de Guiones de Shell", LG #54
"Introducción a la Escritura de Guiones de Shell", LG #55
"Introducción a la Escritura de Guiones de Shell", LG #57


Copyright © 2000, Ben Okopnik
Publicado en el número 58 de Linux Gazette, Octubre 2000