"La Gazeta de Linux...haciendo Linux un poco mas divertido!"


Introducción a la creación de guiones de Shell

Por: Ben Okopnik

Traducción al Español por: Edgar Peraza
el día 5 de Septiembre 2000, para La Gaceta de Linux.


"Cuando el único martillo que tienes es C++, el mundo entero parece un pulgar."
-- Keith Hodges

Pensamientos Profunfos

En este punto de la serie, estamos lo bastante cerca de lo que considero el límite superior de la creación basica de guiones de shell; todavia hay unas pocas áreas que me gustaria cubrir, pero mucho de los temas involucrados son bastante, umm, complicado. Un buen ejemplo es el comando `tput' que estaré tratando este mes: para comprender realmente que hace, tan opuesto a solo usarlo, Ud necesitaría aprender acerca de la controversia "termcap/terminfo" (uno de los principales temas en el debate "Why UNIX Sucks")- un profundo, complicado y horroroso tema (para una simple y decente explicación, vea el "fixkeys.tgz" de Hans de Goede, el cual contiene un claro y pequeño "HOWTO". Para un estudio más profundo, el Keyboard-and-Console-HOWTO es una maravillosa referencia sobre la materia). Trataré de darle sentido a pesar de la confusión, pero esta advertido...

Funcionalmente suyo

El concepto de función no es dificil, y es realmente muy util: ellas son simplemente bloques de código que pueden ejecutar bajo una simple etiqueta. A diferencia de un guión, ellas no crean un nuevo subshell sino que se ejecutan dentro del actual. Ellas pueden ser usadas dentro de un guión, o solas.

Veamos como una función trabaja dentro de un guión: (text version)

#!/bin/bash
#
# "venice_beach" - translates English to beach-bunny

function kewl ()        # Makes everything, like, totally rad, dude!
{
     [ -z $1 ] &&& {
        echo "That was bogus, dude."
        return
     }

     echo "Like, I'm thinkin', dude, gimme a minute..."
     sleep 3
     echo $@', dude!'
     # While the function runs, positional parameters ($1, etc.)
     # refer to those given the function - not the shell script.
}

clear

kewl $(echo "$@"|tr -d "[:punct:]")    # Strip off all punctuation

Este, umm, guión increiblemente importante deberia imprimir la linea "I'm thinkin'..." seguida por una completa y corta lista de parametros:

Odin:~$ venice_beach Right on
Like, I'm thinkin', dude, gimme a minute...
Right on, dude!

Odin:~$ venice_beach Rad.
Like, I'm thinkin', dude, gimme a minute...
Rad, dude!

Odin:~$ venice_beach Dude!
Like, I'm thinkin', dude, gimme a minute...
Dude, dude!

Las funciones también pueden ser cargadas en el entorno, e invocadas como un guión de shell; hablaremos acerca de la fuentes de funciones después. Para aquellos quienes usan Midnight Commander, vean la función "mc ()" descrita en su página del manual - esta es muy útil y es cargada desde el ".bashrc".

Nota importante: las funciones son creadas como "function pour_the_beer () { ... }" o "pour_the_beer () { ... }" (la palabra clave es opcional); ellas son llamadas como "pour_the_beer" (sin paréntesis). También, tenga cuidado (as in, _do not_ unless you really mean it) al usar una instrucción "exit" en una función: yaque Ud. esta corriendo el código en el shell actual, esta hara que Ud. salga de su actual shell (es decir, del "login")! Terminar un guión de shell de esta manera puede producir muy feos resultados, como un `hung' shell, que debe ser terminado desde otro VT (al menos, yo le he experimentado). La intrucción que debe terminar una función sin terminar el shell es "return".

Simple, Libres, y Fáciles

Todo lo que hemos discutido hasta ahora en esta serie tiene un suposición sobrentendida: que el guión que Ud. esta ecribiendo esta siendo guardado y reusado. Para la mayoría de guiones, esto es lo que Ud quiere - pero que pasa ¿si tiene una situación donde necesite la estructura del guión, y solo la usará una vez (es decir, no necesita ni quiere crear un archivo)? La respuesta es - Hagalo:

Odin:~$ (
> echo
> [ $blood_caffeine_concentration -lt 5ppm ] &&& {
> echo $LOW_CAFFEINE_ERROR
> while [ $coffee_cup != "full" ]
> do
> brew ttyS2 # Enable coffeepot via /dev/ttyS2
> echo "Drip..."
> sleep 1m
> done
> echo
>
> while [ $coffee_cup != "empty" ]
> do
> sip_slowly # Coffee ingestion binary, from coffee_1.6.12-4.tgz
> done
> }
>
> echo "Aaaahhh!"
> echo
> )
Coffee Not Found: Operator Halted!
Drip...
Drip...
Drip...

Aaaahhh!

Odin:~$

Tipear un caracter `(' le dice al "bash" que le gustaria crear un subshell y ejecutar, dentro de ese subshell, el código que sigue - y esto es lo que un guión hace. El caracter final, `)', obviamente le dice al subshell 'cierre y ejecute'. Para una equivalencia en una función ( es decir, código que se ejecuta dentro del actual shell), los delimitadores son `{' y `}'.

Por supuesto, algo como un simple ciclo o una simple condicional 'if' no requiere de esto:

Odin:~$ for fname in *.c
> do
> echo $fname
> cc $fname -o $(basename $fname .c)
> done

"bash" el los suficientemente listo para reconocer un comando multi-parte de este tipo - una útil clase de cosa cuando hay que tener valor para tipear mas de una linea de sintasis (no es una situación común en instrucciones 'for' o 'while'). A proposito, es una bonita cosa que sucede cuando Ud presiona flecha-arriba para repetir el último comando: "bash" reproducirá todo en una sola linea - con los apropiados punto y comas adicionados. El Ingenio, de aquella personas del GNU...

El "hash-bang" ("#!/bin/bash") no es necesario para un guión. pero este debería estar al principio de un archivo guión. Usted sabe que este estará ejecutandose como un subshell de "bash" (al menos espero que Ud esta corriendo "bash" mientras escribe y prueba un guión de "bash"...), mientras que con un archivo de guión Ud nunca estará seguro: la elección del shell del usuarios es variable, asi que el "hash-bang" es necesario para asegurarse que el quión usa el interprete correcto.

EL mejor plan para los Ratones y Los Hombres

Para escribir buenos guiones de shell, Ud tiene que aprender buena programación. Conocer simplemente las entradas y las salidas de los comandos que "bash" aceptará esta lejos de todo esto - el primer paso en la solución de problemas es la definición del problema, y definir exactamente que necesita hacer puede ser tanto o más desafiante que escribir el actual guión

Uno de los primeros guiones que escribi, "bkgr" ( selector aleatorio del fondo para X), tenia un problema - Llame a esto un "race condition", esto significa algo diferente en la terminología de Unix - que tomo un largo tiempo y gran numero de reescritura para resolverlo. "bkgr" es ejecutado como parte de mi ".xinitrc":

...
# start some nice programs
bkgr &
rxvt-xterm -geometry 78x28+0+26 -tn xterm -fn 10x20 -iconic &
coolicon &
icewm

OK, por el libro - Yo envie al background todos los procesos excepto el último, "icewm" (de esta forma, el administrador de ventanas mantiene X "ariba", y terminar este termina el servidor). Aqui esta el problema: "bkgr" corre, y "dibuja" mi imagen de fondo en la ventana principal; bien, hasta ahora. Entonces, "icewm" corre - y dibuja un fondo gris-verdoso sobre este (hasta yo he podido descubrir, que no hay otra forma de deshabilitar esto sin hackear el codigo).

¿Qué hacer? No podemos poner "bkgr" despues de "icewm" - el WM se perderá. Cuanto debe esperar "bkgr", digamos 3 segundos... oh, esto no trabajará: esto simplemente retrasa el comienzo de "icewm" por 3 segundos. OK, Como hacemos entoces (en el "bkgr"):

...
while [ -z "$(ps ax|grep icewm)" ] # Check via 'ps' if "icewm" is up
do
    sleep 1                        # If not, wait, then loop
done
...

Esto deberia trabajar, yaque esto retardaría la carga de la actual "ventana principal de trabajo" y despues se levantaría el "icewm"!


Esto no trabajó, por tres principales razones.

Razón #1: trate la siguiente lines "ps az|grep", desde su linea de comando, para cualquier proceso que este corrinedo; por ejemplo, tipee

ps ax|grep init

Trate este varias veces. Lo que Ud obtendrá, aleatoriamente, es o una o dos lineas: como "init", o "init" y el "grep init", donde "ps" significa capturar la linea que Ud esta actualmente ejecutando!

Razón #2: "icewm" comienza, toma un segundo o dos en cargar, y entonces muestra la vetana principal. Peor aún, ese retardo inicial toma significatimanete más tiempo que el subsecuente recomienzo. "Así," Ud diría, "haga el retraso en el ciclo un poco mas largo!" Eso no trabaja - Yo tengo dos maquinas, un viejo laptop y un desktop, y el laptop es horriblemente lento en comparasión; Ud no puede "aumentar" el retardo a una de las maquina y esperar que este trabaje en ambas... y en mi no-tan-humilde opinion, un guión debería ser universal - Ud no debería tener que "ajustar" este para una maquina dada. Al menos, esta clase de ajustes debería ser minimizados, y preferiblemente eliminados por completo.

Una de las cosa que también causó problemas en este punto es que algunos de mis imágenes era muy grandes - por ejemplo, mi foto del Centro Espacial Kennedy - y toma varios segundos en cargar. El efecto general fue permitir a las grandes imágenes trabajar con "bkgr", mientras las pequeñas eran repintadas - y tratando de forzar la espera resultó en un significante retraso en el proceso de inicio del X, una insostenible situación.

Razón #3: Se suponia que "bkgr" era un selector aleatorio de fondos así como un selector de fondo de inicio - significando esto que si no queria el fondo original, yo solo correria este otra vez y obtendría otro. Un retraso mas largo de un segundo, de todos modos hace que una imagen tome tiempo para dibujar, esto no era acptable

Que lío!. Que era necesario hacer, cuál era el retraso que debia mantener para que al correr "icewm", entoces fijarlo entre el comienzo de "icewm" y "el levante de la ventana principal". La primera cosa que traté fue crear un fiable `detector' para "icewm"

...
delay=0
X="$(ps ax)"

while [ $(echo $X|grep -c icewm) -lt 1 ]
do
   [ $delay -eq 0 ] && (delay=1; sleep 3)
   [ $delay -eq 1 ] && sleep 1
   X="$(ps ax)"
done
...

'SX' obtiene el conjunto de valores de "$(ps ax9", una larga cadena listando los procesos que estan corriendo en el cual verificamos la presencia de "icewm" con un ciclo condicional. Lo que hace la diferencia es que "ps ax" y "grep" no están corrieno al mismo tiempo: una corre dentro (y delante) del ciclo, la otra es parte del test del ciclo (un ingenioso pequeño truco, que vale recordar). Este registro cuenta uno si "icewm" esta corriendo y cero si no lo esta. Desafortunadamente, debido al recargado tiempo - especificamente en la demora entre el comienzo de la X y las repeticiones - esto no eralo suficientemente bueno. Despues de varios experimentos, aqui esta la versión que trabaja:

...
delay=0
until [ ! $(xv -root -quit /usr/share/Eterm/tiny.gif) ]
do
    delay=1
    sleep 1
done
[ delay -eq 1 ] && sleep 3
...

Lo que estoy haciendo aqui es cargar una imagen de 1x1-pixel y verificar que "xv" ha manejado esta exitosamente, si no es asi, yo continuo el ciclo. Ona vez esta ha sido hecho - y esto solo significa que X ha alcanzado un punto donde acepatará aquellas directrices de un programa - yo fijo en unos 3 segundos el retraso ( pero solo si hice el ciclo; si "icewm" esta realmente levantado, ningun retraso es requerido o deseado). Esto parece trabajar muy bien no importa cual es la "cuenta de inicio". Corriendo este de esta forma, no he tenido una imagen sobrepintada o un retraso mayor de un segundo. Estaba un poco interesado acerca de el efecto de cargar todos los "xv" uno detras de otro, pero el tiempo de inicio del X con y sin "bkgr" ponía esto a prueba: No encontre una diferencia medible ( supongo que, cuando "xv" termnina con un código de error es problable que no consuma muchos recurso.)

Note que el guión resultante es solo más un poco mas grande que el original - lo que tomó todo este tiempo para fijarlo no fue escribir algo grande, complejo o magico sino comprender el problema y definir la solución... aun pienso que esto fue estraño.

Hay un numero de erres de programación que conocer: "condicion race" (un asunto de seguridad, no un conflicto de tiempo), el `problema banana', el `error fencepost/Obi-Wan'...(Si, son interesantes nombres; cada uno tiene su historia.) Leer un poco de la teoria de programación le beneficiaría a cualquiera que quiera aprender a escribir guiones de shell; si no quiere repetir los mismos erros de otros. Mi referencia favorita es un antiguo manual de "C", fuera de impresión, pero hay muchas buenos textos de referencias disponibles en la red; tome alguno. Las soluciones "Canned" para los errores de programación existen, tienden a ser un lenguaje independiente, y es muy buenas cosa que hay que tener en cuenta.

Colores Divertidos con Dick y Jane

Una de las cosa que solia hacer, regresando atras en los dias de los BBS y el arte de ASCII, es crear pantallas llamativas que se mueven y parpadean y hacian toda clase de cosas - sin programación grafica ni nada mas complicado que aquellos códigos ASCII y las secuencias de escape ANSI (ellas podian ser lo suficientemente complicadas, muchas gracias), ya que todo esto corria en puro terminales texto. Linux, gracias a los imponentes resultados del trabajo hecho por Jan Hubicka y sus amigos (si Ud no ve el demo "bb" de "aalib", Ud se esta perdiendo de una serio comic acid. Hasta donde conozco, las autoridades aun no lo han capturado, y todavia es legal), han superado a todo aun al más imaginativo artista ASCII que pudiese encontrar ("Quake" y generadores fractales en modo texto, son dos de sus ejemplos).

¿Qué es lo que tenemos que hacer, ya que no estamos haciendo programación basada en "aalib"? Bien, hay veces cuando Ud quiere crear un bonito menu, digamos uno que usará todo los días - y si Ud esta trabajando con texto, necesitará algunas herraminetas especializadas:

1) Manejo del cursor. La habilidad de posicionar este es una de ellas; ser capaz de habiliarla o no, y guardar y restablecer la posición es agradable tenerla.

2) Control de los atributos del texto. Negritas, subrayado, parpadeo, reverso - estas son utiles en la creacion de menues.

3) Color. No nos engañemos, el viejo y simple B&W es bastante aburrido, y aun algo tan simple como un menu de texto puede beneficiarse de un toque de color.

Asi, comenzamos con un simple menu: (text version)

#!/bin/bash
#
# "ho-hum" - a text-mode menu

clear

while [ 1 ]         # Loop `forever'
do
# We're going to do some `display formatting' to lay out the text;
# a `here-document', using "cat", will do the job for us.

cat << !

            M A I N   M E N U

        1. Business auto policies
        2. Private auto policies
        3. Claims
        4. Accounting
        5. Quit

!
echo -n " Enter your choice: "

# Why have I not followed standard indenting practice here? Because
# the `here-doc' format requires the delimiter ('!') to be on a line
# by itself - and spaces or tabs count. Just consider everything as
# being set back one indent level, and it'll all make sense.

read choice

case $choice
in
    1|B|b) bizpol ;;
    2|P|p) perspol ;;
    3|C|c) claims ;;
    4|A|a) acct ;;
    5|Q|q) clear; exit ;;
    *) echo; echo "\"$choice\" is not a valid option."; sleep 2 ;;
esac

clear
done

Si Ud copia y pega lo anterior en un archivo y los corre, se dará cuenta de porque las compañias de seguros son consideradas mortalmente aburridas. Erm, bien, unas de las razones, que yo supongo. ABURRIDAS. ( disculpando a unos de mis antiguos empleados, pero es la verdad...) No existe esa gran emoción por tener un menu texto - pero seguramente existe algo que podamos hacer para mejorar esto! (text version)

#!/bin/bash
#
# "jazz_it_up" - an improved text-mode menu

tput civis        # Turn off the cursor

while [ 1 ]
do
    echo -e '\E[44;38m'    # Set colors: bg=blue, fg=white
    clear                  # Note: colors may be different in xterms
    echo -e '\E[41;38m'    # bg=red

    for n in `seq 6 20`
    do
        tput cup $n 15
        echo " "
    done

    echo -ne '\E[45;38m'    # bg=magenta
    tput cup 8 25 ; echo -n " M A I N   M E N U "
    echo -e '\E[41;38m'     # bg=red

    tput cup 10 25 ; echo -n " 1. Business auto policies "
    tput cup 11 25 ; echo -n " 2. Private auto policies "
    tput cup 12 25 ; echo -n " 3. Claims "
    tput cup 13 25 ; echo -n " 4. Accounting "
    tput cup 14 25 ; echo -n " 5. Quit "

    # I would have really liked to make the cursor invisible here -
    # but my "xterm" does not implement the `civis' option for "tput"
    # which is what does that job. I could experiment and hack it
    # into "terminfo"... but I'm not *that* ambitious.

    echo -ne '\E[44;38m'     # bg=blue
    tput cup 16 28 ; echo -n " Enter your choice: "
    tput cup 16 48

    read choice
    tput cup 18 30

    case $choice
    in
        1|B|b) bizpol ;;
        2|P|p) perspol ;;
        3|C|c) claims ;;
        4|A|a) acct ;;
        5|Q|q) tput sgr0; clear; exit ;;
        *) tput cup 18 26; echo "\"$choice\" is not a valid option.";
            sleep 2 ;;
    esac
done

Este No es, El Mejor Menu Escrito - pero le da una idea de las capacidades básicas de composición y color. Note que los colores pueden no trabajar exactamente bien en su xterm, depende de su hardware y su versión de "terminfo" - hice este como un rápido trabajo para ilustrar las capacidades de los comandos "tput" y "echo -e". Estas cosas pueden ser hechas portables - lasa variables de "tput" son comunes para todos, y los valores de los colores pueden ser configurados basados en los valores de '$TERM' - pero este guión falla en esto. Esos códigos, por la forma, son basicamente los mismos para terminales de textos en DOS, Linux, etc..., - ellos dependen del hardware/firmware mas que del software que estan corriendo. Xterms, como siempre, son una especie diferente...

Así, ¿Qué es este "tput" y "echo -e"? Bien, para "hablar" directamente a nuestro terminal, es decir, dar a este comandos que serán usados para modificar las caracteristicas del terminal, necesitamos un método de enviar códigos de control. Las diferencias entre estos dos métodos es que mientrás "echo -e" acepta códigos de escapes "en bruto" (como '\E[H\E\[2J' - la misma cosa que H2J), "tput" los llama como "capacidades" ("tput clear" hace la misma cosa que "echo -e" con el código anterior) y es (teóricamente) independiente del terminal (este usa los códigos en la base de datos del terminfo de su actual tipo de terminal). El problema con "tput" es que muchos de los códigos para este son tan impenetrables como los códigos de escape que ellos remplazan: cosas como `civis' ("hacer el cursor invisible"), `cup' ("mover el cursor a x y"), y ``smso' ("comenzar en modo de salida") son tan malos de memorizar que los códigos mismos! Peor aún, nunca encontré una referencia que los liste a todos... buieno, recuerde que los dos métodos son basicamente intercambiables, y puede usarlos si estan disponibles. El comando "inforcmp" listará las capacidades y sus códigos equivalentes para un tipo de terminal dado; cuando se corre sin parámetros, este retorna la configuración del tipo de terminal actual .

Colores y atributos para un terminal ISO6429 (ANSI), es decir, un tipico terminal de texto, puede encontrase en la página del manual de "ls", en la sección de "DISPLAY COLORIZATION"; terminales x, por otra parte, varian en muchas de sus interpretaciones de que significa un código de color, lo que basicamente tiene que "tratar y ver" (text version)

#!/bin/bash
#
# "colsel" - a term color selector

for n in `seq 40 47`
do
    for m in `seq 30 37`
    do
        echo -en "\E[$m;${n}m"
        clear
        echo $n $m
        read
    done
done

Este pequeño guión mostrará toda la gama de colores que su tipo de terminal pueda mostrar. Solo recuerde el número de combos que le simpatiza, y use este en su instrución "echo -e '\E[<bg>;<fg>m'".

Note que las posiciones de los números dentro de la instrucción no importan; también note que algunas combinaciones haran que su texto sea incoherentement inleible ("12" parece hacer esto en muchos xterms). No deje que esto lo moleste; solo tipee "reset" o "tput sgr0" y presione "Enter".

Ajuste por hacer

Hmm, parece que tengo que hacer esto a todo lo anterior sin mucho dolor o sufrimiento; sorprendente. :)Si, algunas de las areas de Linux todavia estan en desarrollo... y hasta que alguien realmente le emocione hacerla: ellas estaran cambiando de lugar. Dado la sorprendente diversidad de proyectos de personas que estan trabajando, no me sorprendería que alguien encontrara una elegante solución al desorden de los códigos/atributos del color.

El próximo mes, cubriremos cosas como funciones (un bello y emocionante material - codigos reusables!), algunas realmente ingeniosas y buenas como "eval" y "trap". Hasta entonces -

Feliz Linuxnada a todos

Linux Cita del Mes

"Las palabras 'comunidad' y 'comunicación' tienen la misma raíz. Donde quiera que Ud ponga una red de comunicaciones, Ud pone una buena comunidad. Y siempre que Ud toma cualquier de esas redes - confisca esta, proscribe esta, eleva su precio más alla de lo justo - entonces Ud daña una comunidad.

Las comunidades lucharan para defenderse. Las gente pelearan duro y mas bravamente para defender sus comunidades, que lo que peliarian para defenderse ellos mismos individualmete."
-- Bruce Sterling, "Hacker Crackdown"

Referencias

The "man" pages for 'bash', 'builtins', 'tput', 'infocmp', 'startx'
"Introduction to Shell Scripting - The Basics", LG #53
"Introduction to Shell Scripting", LG #54
"Introduction to Shell Scripting", LG #55
"Introduction to Shell Scripting", LG #56
"Introduction to Shell Scripting", LG #57


Copyright © 2000, Ben Okopnik
Publicado en Issue 57 of Linux Gazette, September 2000