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 EOFEl 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.
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.
i=3; while ((i-2<5)); do echo $i; i=$[i+1]; doneIgualmente, 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.
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.
for f in *.new; do mv $f ${f%new}old doneEl 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" doneO 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
i=1 for f in $(find -type f); do f$i=$f i=$[i+1] donePero 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] doneTambién podría intentar hacer:
i=1 find -type f -printf 'f$i=%p; i=$[i+1]\n' | shEsto 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.
IFS=$'\n' i=1 for f in $(find -type f); do eval f$i=\"$f\" i=$[i+1] doneEn 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.
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.
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 doneO más avanzado:
for nom in ${!uid*}; do eval echo user ${nom/uid_/}, uid \$$nom done
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.
(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' )"
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.5La 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 donepara 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 fiY esto es todo por hoy.