"La Gaceta de Linux...haciendo de Linux algo un poco más divertido!"


Los profundos, obscuros secretos de Bash

Por: Ben Okopnik

Traducido al Español por: Javier Cano
el día 21 de Julio de 2000, para La Gaceta de Linux.


"Existen dos productos importantes que salieron de Berkeley: LSD y UNIX. No creemos que esto sea una coincidencia." -- Jeremy Anderson

En la profundidad de las páginas del man de bash acechan cosas terribles no apropiadas para el tímido o el inexperto... Ten cuidado, peregrino: el último incauto que exploraba las cuevas de esta misteriosa región fue hallado, semanas después, murmurando algún tipo de conjuros raros como "nullglob", "dotglob", y "MAILPATH='/usr/spool/mail/bfox?"You have mail":~/shell-mail?"$_ has mail!"'" (El fue contratado inmediatamente por una Compañía Sin Nombre en Silicon Valley por un no mencionado (pero enorme) salario... pero eso no viene al caso.)

<Shrug> Que se le va hacer; Ya estuve navegando y buceando este mes (y muy pronto navegaré las 500 millas de la corriente del Golfo); sigamos viviendo La Vida Loca!

Expansión de Parámetros

La capacidad integrada de análisis gramatical de bash es por cierto mínima comparada con, digamos, perl o awk: en mi opinión, no está pensada para procesamiento serio, solo para el manejo rápido de tareas menores. No obstante, pueden ser muy útiles para ciertos propósitos.

Por ejemplo, digamos que necesitas diferenciar entre nombres de archivos en minúsculas y mayúsculas al procesar un directorio – Yo termine haciéndolo con mis fondos de pantalla para X, desde que algunos se ven mejor embaldosados, y otros estirados para ocupar toda la pantalla (el tamaño del archivo no fue una guía suficiente). Yo "marqué" los nombres de las imágenes de tamaño completo, y "desmarqué" las de los que forman baldosas. Luego, como parte de mi selector aleatorio de fondos de pantalla, "bkgr", escribí lo siguiente:

fn=$(basename $fnm)                   # Solo necesitamos el nombre_del_archivo
[ -z ${fn##[A-Z]*} ] && MAX="-max"    # Colocar el conmutador "-max" si es verdadero
xv -root -quit $MAX $fnm &            # Ejecutar "xv" con|sin "-max"
                                      #   basado en el resultado de esta prueba

Esta cosa confunde, no es así? Bueno, parte de esto ya lo sabemos: la línea [ -z ... ] es una prueba para una cadena de longitud cero. Que hay de la otra parte? Con la intención de ‘proteger’ nuestro resultado de expansión de parámetros del frío, mundo cruel ( esto es, si quieres usar el resultado como parte del nombre de un archivo, necesitas 'protección' para mantenerlo separado de otros caracteres), usamos corchetes para rodear toda la enchilada. $d es lo mismo que ${d} excepto que la segunda variedad puede ser combinada con otras cosas sin perder su identidad – algo así como:

d=Digit
echo ${d}ize        # "Digitize"
echo ${d}al         # "Digital"
echo ${d}s          # "Digits"
echo ${d}alis       # "Digitalis"

Ahora que la hemos aislado del mundo, sin amigos y del todo solitario... oops, lo siento – esto es "_shell_ script", no "el guión de una pelicula de terror" – pierdo el hilo de vez en cuando... De cualquier forma, ahora que hemos separado la variable con los corchetes, podemos aplicar unas cuantas herramientas incorporadas en bash para efectuar algo de análisis gramatical básico. Aquí está la lista: (Para este ejercicio, asumamos que $parameter="amanuensis".)

${#parameter} – retorna la longitud del valor de parameter.
EJEMPLO: ${#parameter} = 10

${parameter#word} – corta la coincidencia más corta desde el inicio de parameter.
EJEMPLO: ${parameter#*n} = uensis

${parameter##word} – corta la coincidencia más larga desde el inicio de parameter.
EJEMPLO: ${parameter##*n} = sis

${parameter%word} – corta la coincidencia más corta desde el final de parameter.
EJEMPLO: ${parameter%n*} = amanue

${parameter%%word} – corta la coincidencia más larga desde el final de parameter.
EJEMPLO: ${parameter%%n*} = ama

${parameter:offset} – retorna el parámetro que empieza en 'offset'.
EJEMPLO: ${parameter:7} = sis

${parameter:offset:length} - retorna 'length' caracteres de parameter empezando en 'offset'.
EJEMPLO: ${parameter:1:3} = man

${parameter/pattern/string} – reemplaza una coincidencia.
EJEMPLO: ${parameter/amanuen/paralip} = paralipsis

${parameter//pattern/string} - reemplaza todas las coincidencias.
EJEMPLO: ${parameter//a/A} = AmAnuensis (para los dos últimas operaciones, si el patrón empieza con #, este coincidirá al inicio de la cadena; si empieza con %, este coincidirá al final. Si la cadena está vacía, las coincidencias serán eliminadas.)

En realidad hay un poco más de esto – cosas como dirección variable, y matrices de análisis gramatical – pero supongo que esa página man tienen que estudiarla ustedes mismos. Consideren esto como un material motivacional.

Así que, ahora que hemos visto las herramientas, veamos de nuevo el código -

[ -z ${fn##[A-Z]*} ]

No es tan difícil, o si? Quizás si; mi proceso racional, al lidiar con búsquedas y coincidencias, tiende a complicar las cosas como un rompecabezas. Lo que hice fue – y se puede hacer de muchas otras maneras, dadas las herramientas – es buscar una coincidencia para el máximo tamaño de la cadena (esto es, el nombre de archivo completo) que empiece con un carácter en mayúsculas. La línea [ -z ... ] retorna ‘verdadero’ si la cadena resultante es de longitud cero (esto es, si coincide con el patrón [A-Z]* ), y $MAX es puesto a "-max".

Noten que, ya que estamos trabajando con la cadena completa, ${fn%%[A-Z]*} debería funcionar igual. Si esto les parece confuso – todo lo anterior – les sugiero mucha, pero mucha practica para familiarizarse con esto. Es fácil: colocar un valor al parámetro, y experimenten, como esto -

Odin:~$ experiment=supercallifragilisticexpialadocious
Odin:~$ echo ${experiment%l*}
supercallifragilisticexpia
Odin:~$ echo ${experiment%%l*}
superca
Odin:~$ echo ${experiment#*l}
lifragilisticexpialadocious
Odin:~$ echo ${experiment##*l}
adocious

...y así sucesivamente. Es la mejor forma de averiguar la utilidad de una herramienta; hay que cogerla, enchufarla, colocarse los lentes de seguridad y apretar el botón de encendido. Tomen todas las medidas de seguridad ya que pueden borrar datos valiosos. Los resultados pueden variar y *usualmente* los sorprenderán.

Estado de Parámetros

Hay veces – digamos, examinando un rango de condiciones de error al setear diferentes variables – cuando necesitamos saber si una variable especifica es seteada (le fue asignado un valor) o no. Es cierto que podemos analizar la longitud, como hice líneas arriba, pero las herramientas provistas por bash nos dan atajos prácticos para ocasiones como esta: (Aquí, asumiremos que nuestra variable - $joe – no está seteada o es nula.)

${parameter:-word} – Si parameter no es seteado, "word" lo sustituye.
EJEMPLO: ${joe:-mary} = mary ($joe sigue sin setear.)

${parameter:=word} - Si parameter no es seteado, setearlo a "word" y retornar ese valor.
EJEMPLO: ${joe:=mary} = mary ($joe="mary".)

${parameter:?word} - Muestra "word" o error si parameter no es seteado.
EJEMPLO:
Odin:~$ echo ${joe:?"Not set"}
bash: joe: Not set
Odin:~$ echo ${joe:?}
bash: joe: parameter null or not set

${parameter:+word} - "word" sustituye a parameter si este no es seteado.
EJEMPLO:
Odin:~$ joe=blahblah
Odin:~$ echo ${joe:+mary}
mary
Odin:~$ echo $joe
blahblah

Manejo de Matrices

Otra ventaja incluida en bash, es un mecanismo básico para el manejo de matrices, lo que nos permite manejar datos que requieran ser indexados, o al menos mantener una estructura que permita el direccionamiento individual de cada uno de sus miembros. Consideren el siguiente escenario: Si tengo una lista con direcciones y teléfonos, y quiero enviar my último "Diario del Navegante" a todos los que están en la categoría de "amigos", como lo hago? Mas aun, digamos que quiero crear una lista con los nombres de los recipientes... o algún otro procesamiento... que implica necesariamente dividir la información en campos por longitud, y las matrices son una de las opciones más viables de hacerlo.

Un vistazo a lo que esto implica. Aquí un fragmento de una agenda a ser usada para el trabajo:

Name                     Category  Address                  e-mail
Jim & Fanny Friends      Business  101101 Digital Dr. LA CA fr@gnarly.com
Fred & Wilma Rocks       friends   12 Cave St. Granite, CT  shale@hill.com
Joe 'Da Fingers' Lucci   Business  45 Caliber Av. B-klyn NY tuff@ny.org
Yoda Leahy-Hu            Friend    1 Peak Fribourg Switz.   warble@sing.ch
Cyndi, Wendi, & Myndi    Friends   5-X Rated St. Holiday FL 3cuties@fl.net

Guau!, esta cosa obviamente debe ser leída por campos – el conteo de palabras no servirá; tampoco la búsqueda de texto. Matrices al rescate!

#!/bin/bash

# 'nlmail' envía el diario mensual a los amigos de la agenda


# bash creará las matrices automáticamente, ya que usamos
# la sintaxis 'name[subscript]' para cargar las variables - 
# pero me gustan las declaraciones explícitas.
 
declare -a name category address email

# Cuenta el número de líneas en la lista y entra en bucle 
# ese número de veces

for x in $(seq $(grep -c $ phonelist))
do
    x=$(($x))                           # Convierte '$x' a número
    line="$(sed -n ${x}p phonelist)"    # Imprime la línea número "$x"
    name[$x]="${line:0:25}"             # Carga la variable 'name' 
    category[$x]="${line:25:10}"        #  Etc.,
    address[$x]="${line:35:25}"         #       etc.,
    email[$x]="${line:60:20}"           #            etc.
done
# Continua abajo ...

Hasta este punto, hemos cargado el archivo "phonelist" en las cuatro matrices que hemos creado, listas para su futuro procesamiento. Cada campo es fácilmente direccionable, convirtiendo el problema planteado – aquel de enviar un e-mail con un archivo a mis amigos – en algo trivial (este fragmento es una continuación del script anterior):

# Viene de arriba ...
for y in $(seq $x)
do
    # Buscaremos coincidencia de "friend" en el campo 'category',  
    # sin importar mayúsculas o minúsculas, y cortamos los caracteres sobrantes.
            
    if [ -z $(echo ${category[$y]##[Ff]riend*}) ]
    then
        mutt -a Newsletter.pdf -s 'S/V Ulysses News, 6/2000' ${email[$y]}
        echo "Mail sent to ${name[$y]}" >> sent_list.txt
    fi
done

Esto debe hacerlo, así como colocar los nombres de los recipientes en un archivo llamado "sent_list.txt" – una simpática opción que me permite ver si olvidé a alguien.

La capacidad de procesar matrices de bash se extiende un poco más allá que en este EJEMPLO. Suficiente con decir que para simples casos de ordenamiento, con archivos menores a un par de cientos de KB, las matrices de bash son la solución. Solo por curiosidad, he creado una lista de nombres que llegaban a los 100KB, usando el EJEMPLO de arriba "phonelist" :

  for n in $(seq 300); do cat phonelist >> ph_list; done

- Esto se ejecutó en mi Pentium 233/64MB en 24 segundos; nada mal para 1500 registros y una herramienta "básica".

Noten que el script anterior puede ser fácilmente generalizado – como un EJEMPLO, puedes agregar la habilidad de especificar diferentes agendas, criterios, o acciones, todo en la línea de comando. Una vez que los datos son divididos en un formato fácilmente direccionable, las posibilidades son interminables...

Empaquetando todo

bash, además de ser muy capaz en su papel de shell/interprete en la línea de comandos, se jacta de disponer de un gran número de sofisticadas herramientas para cualquiera que necesite crear un programa personalizado. En mi opinión, los shell scripting cumplen lo suyo - aquello de ser un simple pero potente lenguaje de programación - perfectamente, encajando entre una utilidad practica de línea de comando y lenguajes completos (C, Tcl/Tk, Python) de programación, deberían ser parte del arsenal de cada usuario de *nix. Linux, específicamente, gusta de alentar esa actitud de "hágalo usted mismo" entre sus usuarios, al darles acceso a herramientas poderosas y la importancia de automatizar su uso: algo que considero una integración sólida (y un mayor "cociente de usabilidad") entre el poder dentro del Sistema Operativo y el entorno del usuario. "El Poder para la Gente!"

Hasta el siguiente mes -
Feliz Linuxing!

Pensamiento del Mes

"...Tan terrible como la adicción a UNIX es, hay peores que ésta. Si UNIX es la heroína de los sistemas operativos, entonces VMS es una adicción a barbitúricos, Mac es MDMA, y MS-DOS es como aspirar pegamento. (Windows llena tus cavidades nasales de plástico y las deja así.)

Le debes a Oracle un programa de 12 pasos."
--The Usenet Oracle

Referencias

Las páginas "man" de bash, builtins

"Introduction to Shell Scripting - The Basics" by Ben Okopnik, LG #52
"Introduction to Shell Scripting" by Ben Okopnik, LG #53
"Introduction to Shell Scripting" by Ben Okopnik, LG #54


Copyright © 2000, Ben Okopnik
Publicado en el número 55 de Linux Gazette, July 2000