Dado que hace tiempo que tenía esto en mente y que el miércoles por la mañana dejaré de dar el coñazo por un mes pues os dejo el recuerdo y espero que esto os sirva para algo a todos aquellos que os peleais con BASH y a todos los que quereis aprender BASH scripting y a blablablablablabla....
¡ OJO ! DISCLAIMER: Esto no pretende ser una guía precisa; de hecho seguro que meto la gamba... está basado en mi experiencia y en lo que se aprende de tener que pelearse con situaciones 'raras' en los ebuilds.
Supongo que todo el mundo tiene unas nocioncillas básicas de shell scripting, para los que no las tengan y quieran empezar, antes de seguir leyendo yo recomiendo:
http://www.zonasiete.org/manual/ch07.html
-----------------------------------
Conceptos básicos y ejemplos sobre expansiones de ruta
Las expansiones de ruta son extremadamente útiles cuando programamos en BASH, sin embargo es muy típico abusar de ellas o realmente no conocer cómo funcionan. En lugar de soltar un montón de tecnicismos, lo mejor es ir a lo práctico:
[ $ ~/prueba ] ls *
a aa ab ac b c
[ $ ~/prueba ] echo *
a aa ab ac b c
[ $ ~/prueba ] ls $(echo *)
a aa ab ac b c
[ $ ~/prueba ] ls '*'
ls: *: No such file or directory
Como podemos ver, realmente los comandos normalmente no entienden de 'meta-caracteres' o 'wildcards'. En este caso lo que hace BASH es efectuar la sustitución justo antes de ejecutar el comando; de hecho como vemos
echo ha sacado prácticamente lo mismo.
Si además queremos que la expansión también incluya los 'dotfiles' debemos utilizar una opción del shell llamada
dotglob[ $ ~/prueba ] touch .hey-im-hidden
[ $ ~/prueba ] echo *
a aa ab ac b c
[ $ ~/prueba ] ( shopt -s dotglob ; echo * )
a aa ab ac b c .hey-im-hidden
Como ejemplo 'útil', para ver el número de paquetes que tenemos instalados en una Gentoo podemos hacer algo como:
[ $ ~ ] echo /var/db/pkg/*-*/* | wc -w
921
Expansiones de parámetroHay muchas veces que nos interesa hacer ciertos cambios sobre las variables a la hora de imprimirlas o utilizarlas. BASH nos permite hacer ciertos reemplazos; veremos algunos de ellos:
Reemplazo de patronesUna de las cosas que podemos querer es reemplazar ciertas partes de una cadena; en este ejemplo reemplazaremos los números por 'X'. La sintaxis es:
${variable/[/]patrón[/[reemplazo]]}
Si especificamos
// se reemplazarán todas las ocurrencias de
patrón por
reemplazo en caso de ser este ultimo especificado... si no se especificara, todas las ocurrencias se borrarían. En caso de solo especificarse una
/ entonces solo se efectuaría el reemplazo sobre la primera ocurrencia de
patrón.
Veamos algunos ejemplos para ilustrar todo esto:
[ $ ~/prueba ] ( v=123abc321 ; echo ${v//[0-9]/X} )
XXXabcXXX
[ $ ~/prueba ] ( v=123abc321 ; echo ${v//[0-9]/} )
abc
[ $ ~/prueba ] ( v=123abc321 ; echo ${v//[0-9]} )
abc
[ $ ~/prueba ] ( v=123abc321 ; echo ${v/[0-9]/X} )
X23abc321
Eliminación de patronesAdemás podemos especificar si queremos que el patrón se ajuste al final o al principio de la cadena si el primer caracter de
patrón es
% o
# respectivamente:
[ $ ~/prueba ] ( v=123abc321 ; echo ${v//%[0-9]/X} )
123abc32X
[ $ ~/prueba ] ( v=123abc321 ; echo ${v//#[0-9]/X} )
X23abc321
También podemos especificar conjuntos 'negativos'; por ejemplo, eliminar todo lo que no sea un número de una variable [ creo que este problema se planteó hace un tiempo por aquí ]:
[ $ ~/prueba ] ( v=123abc321 ; echo ${v//[!0-9]} )
123321
Además de esto, podemos utilizar otros dos tipos de reemplazos, pero que en este caso lo que harán será eliminar porciones del contenido de nuestra variable, la sintaxis es:
${variable#[#]patrón}
${variable%[%]patrón}
Esas dos construcciones lo que hacen es eliminar el ajuste de
patrón del principio y del final de
variable respectivamente.
La diferencia entre usar
## (o
%%) y usar
# (o
%) es que el primero usará el ajuste más largo posible de
patrón y el segundo usará el más corto.
Para evitar caer en tecnicismos raros y empezar a perder a la gente, vamos a dar algunos ejemplos:
[ $ ~/prueba ] ( v="esto es una prueba" ; echo ${v#* } )
es una prueba
[ $ ~/prueba ] ( v="esto es una prueba" ; echo ${v##* } )
prueba
[ $ ~/prueba ] ( v="esto es una prueba" ; echo ${v% *} )
esto es una
[ $ ~/prueba ] ( v="esto es una prueba" ; echo ${v%% *} )
esto
Reemplazando 'basename' con built-ins de BASHUn error muy grave es utilizar el comando
basename en shell scripts. Digo que es un error porque
basename lamentablemente no se comporta exactamente igual en todos los Unix (lo que restaría portabilidad a nuestro script) y además notaremos que el rendimiento deja muchísimo que desear si la llamada se hace en algoritmos recursivos o iterativos.
La solución es utilizar BASH para eliminar las partes de la ruta que no queremos:
[ $ ~/prueba ] ( v=/usr/src/linux/COPYING ; echo ${v} ; echo ${v%/*} ; echo ${v##*/} )
/usr/src/linux/COPYING
/usr/src/linux
COPYING
Obtener subcadenas y longitudesTambién es posible obtener subcadenas de una cadena:
${variable:inicio[:fin]}
En este caso los ejemplos son 'autoexplicativos'.
[ $ ~/prueba ] ( v=1234567 ; echo ${v:2} )
34567
[ $ ~/prueba ] ( v=1234567 ; echo ${v:2:4} )
3456
Además podemos saber en cualquier momento la longitud de una cadena:
${#variable}
Por ejemplo:
[ $ ~/prueba ] ( v=01234 ; echo ${#v} )
5
Arrays en BASHEn BASH es posible utilizar arrays como en cualquier otro lenguaje de programación; con la salvedad de que los arrays los tenemos que 'declarar' en algún momento antes de acceder a ellos. Para hacer esto podemos usar:
miarray=()
Y a partir de ahí accederemos a los elementos de
miarray de la siguiente forma:
${miarray[indice]}
Además podemos conocer el número de elementos de un array:
${#miarray[@]}
Veamos un ejemplo:
[ $ ~/prueba ] ( v=() ; v[0]=a ; v[1]=b ; v[2]=c ; echo ${v[@]} ; echo ${#v[@]} )
a b c
3
También se puede inicializar el array con valores desde el principio:
[ $ ~/prueba ] ( v=( "algo" "otra" "cosa" ) ; echo ${v[0]} )
algo
[ vs. [[Es muy típico ver scripts utilizando '[ ]' para hacer comparaciones; yo no voy a decir que esté mal porque estaría mintiendo; de hecho yo también tengo esa mala costumbre. Aquí intentaré explicar y convencer de por qué utilizar '[[ ]]' es mucho mejor:
Para empezar, '[' es un comando por si solo, y si no me creeis, hagamos la prueba:
[ $ ~/prueba ] which [
/usr/bin/[
[ $ ~/prueba ] /usr/bin/[
/usr/bin/[: missing `]'
[ $ ~/prueba ] [
-bash: [: missing `]'
[ $ ~/prueba ] builtin [
-bash: [: missing `]'
Curioso ¿eh?. el caso es que BASH ya implementa un comando 'builtin' que se comporta de forma similar a
/usr/bin/[ pero como podemos ver, no es exactamente lo mismo. BASH lo implementa por pura compatibilidad y para evitar tener que cargar un binario de 25K cada vez que hacemos una comparación.
Pero además tiene otro problemilla muy común; y es que si una variable que vamos a comparar tiene espacios tenemos que tener cuidado de entrecomillarla o tendremos errores de este estilo:
[ $ ~/prueba ] ( v="hola mundo" ; [ ${v} == "hola mundo" ] && echo success || echo fail )
-bash: [: too many arguments
fail
El problema es que BASH está expandiendo la variable antes de llamar al builtin así que la llamada queda así:
[ hola mundo == "hola mundo" ]
Obviamente esto no sigue la sintaxis y obtenemos un error. Si '[' fuera una construcción del lenguaje y no un built-in no tendríamos este problema. Podemos comprobarlo porque '[[' no es un built-in y sin embargo no necesitamos entrecomillar las variables:
[ $ ~/prueba ] builtin [[
-bash: builtin: [[: not a shell builtin
[ $ ~/prueba ] ( v="hola mundo" ; [[ ${v} == "hola mundo" ]] && echo success || echo fail )
success
Además en el caso de bash3 podemos utilizar un operador que no podemos usar en '[ ]':
=~ que nos va a permitir comprobar si una variable se ajusta a una expresión regular POSIX o no.
while's y for's un poco especialesHay veces que un simple
for i in algo o un
while algo no nos sirven para lo que queremos y tenemos que usar algunos truquillos feos.
Kill that 'seq'Es también muy común ver que scripts utilizan
seq para construir secuencias de números o letras; esto está muy desaconsejado ya que este comando se comporta de forma muy distinta dependiendo de la implementación que esté disponible en el sistema. Además de que no es normal verlo en algunos Unix (no-Linux).
Para reemplazarlo podemos utilizar un reemplazo de BASH que tiene la siguiente sintaxis:
{elem1,elem2,elem3,...,elemN}
Esto se expandirá a:
elem1 elem2 elem3 ... elemN
Además en bash3 tenemos la posibilidad de utilizar:
{inicio..fin}
Podemos ver un par de ejemplos sobre esto:
[ $ ~/prueba ] echo {1,2,3,4,5,6,7,8,9,10}
1 2 3 4 5 6 7 8 9 10
[ $ ~/prueba ] echo {a,b,c}{a,b,c}
aa ab ac ba bb bc ca cb cc
[ $ ~/prueba ] echo {0..4}
0 1 2 3 4
[ $ ~/prueba ] echo {a..d}{a..d}
aa ab ac ad ba bb bc bd ca cb cc cd da db dc dd
Además es posible evitar utilizar
seq si utilizamos el for 'a la C':
[ $ ~/prueba ] for (( i=0 ; i<10 ; i++ )) ; do echo -n "${i} "; done ; echo
0 1 2 3 4 5 6 7 8 9
Improve it 'while' you canAhora veremos una forma interesante de tratar con flujos utilizando while's. Por ejemplo si queremos tratar todas y cada una de las líneas de un flujo podemos hacer algo como:
while read f ; do
# hacer algo
done
Un ejemplo de uso me lo encontré hace unos días al intentar arreglar las nuevas manpages-es que se había cargado el nuevo 'maintainer' (upstream) del paquete; aunque en el código aparecen variables propias de los ebuilds creo que se puede entender fácilmente:
# This is needed because manpages-es has broken encodings upstream
for d in {${S1},${S2}} ; do
cd ${d}
file -i man?/* | while read f ; do
iconv -f ${f##*=} \
-t ${toencoding} ${d}/${f%%:*} \
-o ${D}/usr/share/man/es/${f%%:*}
done
done
` ` vs. $( )Es también muy típico ver scripts que utilizan ` ` para ejecutar comandos y asignar su salida a variables, aquí tampoco voy a decir que esto sea una mala práctica; lo que si diré es que utilizar $( ) es muchísimo más limpio y claro; veamos algún ejemplo:
El ejemplo es un poco tonto... pero bien muestra el segundo mayor problema que tiene utilizar ` `:
[ $ ~/prueba ] echo $($(which uname) -m)
i686
[ $ ~/prueba ] echo ``which uname` -m`
-bash: -m: command not found
which uname
¡ BOOOM ! No se pueden anidar 'facilmente'. El otro problema es que es bastante más legible la construcción $( ) que ` `.
-----------------------------------
Gah... seguro que me dejo cosas en el tintero como 'dereferenciación' de variables y tal pero bueno... si hay dudas, críticas, etc etc probad suerte aquí
Saludos,
Ferdy