Échale visio al Shell (bash).

El shell es el programa más usado en un entorno unix. Pero el conocimiento que el usuario tiene de él no corresponde a ese nivel de uso. El resultado es que, procesos (p. e. en forma de scripts) que pueden realizarse en unas pocas líneas, se implementan de forma compleja e ineficiente.
En estas líneas trataré de mostrar algunas técnicas para resolver problemas con el uso eficiente del shell y de algunas aplicaciones como find o sed.

Hacerlo bien.
Hay cosas que se hacen normalmente con poca eficiencia. Por ejemplo:

echo "Código de asignatura: $cod"
echo "              Nombre: $nom"
echo "                Tipo: $tipo"
echo "   Créditos teóricos: $credt"
se realiza mucho mejor así:
cat <<-EOF
Código de asignatura: $cod
              Nombre: $nom
                Tipo: $tipo
   Créditos teóricos: $credt
EOF
El signo "-" permite que todas las líneas hasta el EOF puedan contener tabuladores al principio que será ignorados, de forma que podemos dar al texto una identación adecuada parta la programación.

Anticuallas o modernismos.

El uso de las particularidades modernas del bash, que puede restar portabilidad a un script, nos facilita, en cambio, muchas operaciones básicas. Por ejemplo, podemos prescindir de expr para la evaluación aritmética:
echo $[3+4]
nos da la suma. El operador $[ ... ] es un evaluador recursivo, en el sendido que muestra el ejemplo:
a=3; b="a+5"; c=b; d="c+2"; echo $[4+d]
evalúa las expresiones tantas veces como necesita hasta llegar al final. El operador $(( ... )) es equivalente.
De hecho puede usarse el operador (( ... )) para realizar operaciones matemáticas, devolviendo un valor lógico, que puede usarse en condiciones:
i=3; while ((i-2<5)); do echo $i; i=$[i+1]; done
Igualmente, para realizar comparaciones podemos prescindir de test y de su equivalente [ ... ], usando el operador [[ ... ]], que admite unas expresiones condicionales más acordes con otros lenguajes de programación y con menos dificultades gramaticales (léxicas y sintácticas). Así, por ejemplo, la delicada expresión condicional(paréntesis acotados, espacios en blanco):
[ \( $a = 3 \) -a \( $b = 2 \) ]
se sustituye ventajosamente por la cómoda:
[[($a = 3)&&($b = 2)]]
Otro aspecto del bash es una limitada capacidad de tratamiento de cadenas. Si una cadena está contenida al final de lo almacenado en una variable, la construcción ${variable%cadena} devuelve el valor de variable con la cadena eliminada; si no está contenida, devuelve el valor de la variable inalterado. El operador # causa lo mismo, pero al principio de la cadena. La especificación de * o ? en la cadena representa cualquier cadena o un caracter respectivamente.
Así, por ejemplo:
find -type f | xargs -ifich bash -c 'f=fich; mv $f ${f%??}-x'
cambiará el nombre de todos los archivos del directorio y subdirectorios al que tenían, pero cambiando los dos ultimos caracteres por la terminación -x. No osbstante este es una forma ineficiente de hacerlo, pues implica ejecutar el bash para cada fichero. Vamos a tratar este problema con detalle.

Generando código.

La forma más "poderosa" de trabajar con el shell es generando código y ejecutándolo. Esta propiedad de los lenguajes interpretados nos va a permitir realizar cosas de otro modo impensables.
Supongamos que quiero cambiar todos los ficheros que tengo con extensión .new a .old . Puedo hacer:
for f in *.new; do
  mv $f ${f%new}old
done
El primer problema aparece si los nombrs de los ficheros contienen espacios en blanco. Puedo solventarlo poniendo los nombres entre comillar:
for f in *.new; do
  mv "$f" "${f%new}old"
done
O bien estableciendo el IFS. Los paréntesis evitan que el IFS quede modificado:
(IFS=$'\n'
 for f in *.new; do
   mv $f ${f%new}old
 done
)
No obstante, ¿qué sucede si hay muchísimos ficheros?. El shell usará demasiada memoria. Para resolverlo, observemos la instrucción:
find -type f -maxdepth 1 -name \*new -printf 'f="%p"; mv "$f" "${f%%new}old"\n'
Obsérvese que genera la secuencia de comandos que deseo ejecutar. Obsérvese también que %% significa %. Es también conveniente usar el \n final y no ; para generar multiples líneas y no una sola muy larga. Ahora, basta con alimentar estas líneas al shell:
find -type f -maxdepth 1 -name \*new -printf 'f="%p"; mv "$f" "${f%%new}old"\n' | sh

Pseudo vectores.

Supongamos que deseo almacenar los ficheros del directorio actual en una colección de variables. Si a estas variables las denomino f1, f2, f3, ..., acabo de crear realmente un vector. Basta con hacer:
i=1
for f in $(find -type f); do
  f$i=$f
  i=$[i+1]
done
Pero esto no funciona, pues el shell interpretará f1=nombre como un comando y no lo es. Para ello necesito re-evaluar la línea de comando. Puedo usar el comando eval:
i=1
for f in $(find -type f); do
  eval f$i=$f
  i=$[i+1]
done
También podría intentar hacer:
i=1
find -type f -printf 'f$i=%p; i=$[i+1]\n' | sh
Esto no funciona pues cuando el subshell termina, las variables se pierden. Debo por tanto hacer:
i=1
eval "$(find -type f -printf 'f$i=%p; i=$[i+1]\n')"
Pero me genera de nuevo un error en f$i=%p, pues necesita re-evaluación, por lo que haré:
i=1
eval "$(find -type f -printf 'eval f$i=%p; i=$[i+1]\n')"
Las comillas del eval son convenientes para no generar una única línea de comando, que implicaría mayor consumo de recursos. Aún así esta forma consume más que si se usa el for como antes.
En ambas formas hemos cometido una omisión para tratar ficheros con espacios en blanco. Con el for es necesario retocar el IFS, además de poner comillar escapadas en el eval:
IFS=$'\n'
i=1
for f in $(find -type f); do
  eval f$i=\"$f\"
  i=$[i+1]
done
En la segunda forma, basta con poner comilas:
eval "$(find -type f -printf 'eval f$i=\\"%p\\"; i=$[i+1]\n')"
La doble barra evita un warning en el find.
Otro ejemplo: Deseo almacenar en un par de vectores los nombres y uids de los usuarios del sistema. El comando:
sed /etc/passwd -e 's/^\([^:]*\):[^:]*:\([^:]*\):.*/eval user$i=\1; eval uid$i=\2; i=$[i+1];/'
genera el código que quiero ejecutar. El comando sed ha sustituído "desde el principio: (cualquier cosa salvo :) + : + cualquier cosa salvo : + : + (cualquier cosa salvo :)  + el resto" por los comandos necesarios que referencian a lo indicado entre paréntesis. Basta con evaluar lo generado, según:
i=1; eval "$(sed /etc/passwd -e 's/^\([^:]*\):[^:]*:\([^:]*\):.*/user$i=\1; eval uid$i=\2; eval i=$[i+1];/')"
Es importante observar la posición de las comillas simples y de las dobles y entender porqué se usan unas y otras.

Diccionarios.

La generación de código nos permite también realizar diccionarios. Por ejemplo sen el ejemplo anterior, en lugar de indexar por un número, podemos hacerlo mediante el nombre:
eval "$(sed /etc/passwd -e 's/^\([^:]*\):[^:]*:\([^:]*\):.*/eval uid_\1=\2;/')"
Una forma sencilla de operar con todos los elementos del diccionario es:
for nom in $(set | sed -n -e "s/^uid_\([^=]*\).*/\1/p"); do
  eval echo user $nom, uid \$uid_$nom
done
O más avanzado:
for nom in ${!uid*}; do
  eval echo user ${nom/uid_/}, uid \$$nom
done

Mas ejemplos.

Supongamos que tengo un directorio y quiero generar una página tabla HTML conteniendo cada línea el nombre de un fichero, su tamaño y la fecha de su última modificación. Además quiero que el nombre del fichero sea un hiperenlace al fichero mismo. En un pricipio basta con hacer:
find -type f -printf '<tr align=right><td><a href="%p">%p</a><td> %s bytes <td> %t\n'
El problema llega cuando el nombre del fichero contiene carácteres incompatibles con una URL válida (o que la falsean, como un espacio en blanco). Supongamos que el programa urlencode nos codifica en formato URL-encoded una cadena que toma por la entrada. Si caemos en la tentación de usar $(), podemos llegar a algo como:
find -type f -printf '<tr align=right><td><a href="$(echo %p| urlencode)">%p</a><td> %s bytes <td> %t\n'
lo que es un evidente error, pues el $() se ejecutaría una sola vez y antes del find. Como no puedo ejecutar nada dentro de -printf puedo intentar usar -exec, pero no tendré acceso a la fecha y al tamaño, sólo al nombre.
Puedo usar un for:
(IFS=$'\n'
 for datos in $(find -type f -printf "%p¡%s¡%t\n"); do
  nom=${datos%%¡*}
  datos=${datos#$nom¡}
  tama=${datos%%¡*}
  fecha=${datos#$tama¡}
  echo '<tr align=right><td><a href="'"$(echo "$nom" | urlencode)"'">'"$nom</a><td> $tama bytes <td> $fecha"
done
)
lo que no deja de ser un enredo. Una vez más, puedo optar por generar código y evaluar:
eval "$(
  find -type f \
  -printf 'echo "<tr align=right><td><a href=\\"$(echo -n "%p" | urlencode)\\">%p</a><td> %s bytes <td> %t"\n'
  )"

Bases de datos (o casi).

Supongamos el fichero de nombre asig. Su primera línea es una colección de nombres de campos (compatibles con nombres de variables del shell), separados por ":". El resto de las líneas son registros cuyos campos se separan por ":" y corresponden a los descritos en la primera línea. Su contenido podría ser:
codigo:nombre:tipo:titula:curso:credt:credp
E64:Interconexión de Sistemas Abiertos:Opt:II:5:2.5:2.5
F39:Seguridad y Protección de la Información:Opt:ITIG:3:2.5:2.5
La función creacom genera un comando adecuado para manejar los registros del fichero cuyo nombre se le pasa como parámetro:
function creacom() {
  lin=$(head -1 $1); camp=${lin%%:*}
  comm="sed -e \"s/\([^:]*\)/$camp='\1'; /\""
  while [[ $lin != $camp ]]; do
    lin=${lin#$camp:}
    camp=${lin%%:*}
    comm="$comm -e \"s/:\([^:]*\)/$camp='\1'; /\""
  done
  eval com$1=\$comm
}
Si ejecuto creacom asig obtendré la variable comasig conteniendo:
sed -e "s/\([^:]*\)/codigo='\1'; /" -e "s/:\([^:]*\)/nombre='\1'; /" \
    -e "s/:\([^:]*\)/tipo='\1'; /" -e "s/:\([^:]*\)/titula='\1'; /" \
    -e "s/:\([^:]*\)/curso='\1'; /" -e "s/:\([^:]*\)/credt='\1'; /" \
    -e "s/:\([^:]*\)/credp='\1'; /"
Si tengo varios ficheros, puedo hacer:
for f in asig profes; do
  creacom $f
done
para crear comandos para cada base de datos.

Ahora puedo ejecutar:

echo -n "Dime un cógigo de asignatura: "; read cod
if x=$(grep -i "^$cod:" asig); then
  eval "$(echo "$x" | eval $comasig)"
  cat <<-EOF
    Código de asignatura: $codigo
                  Nombre: $nombre
                    Tipo: $tipo
       Créditos teóricos: $credt
    EOF
else
  cat <<-EOF
    No hay ninguna asginatura con ese código
    EOF
fi
Y esto es todo por hoy.