Professional Documents
Culture Documents
Contenido:
Introducción
Te puedes llegar a preguntar por qué deberías aprender programación en Bash. Bueno, aquí
hay un par de razones que te presionan para hacerlo:
Ya la estás corriendo
Ya la estás usando
No sólo ya estás corriendo bash, sino que además estás interectuando con bash diariamente.
Siempre está allí, así que tiene sentido aprender cómo usarla en su máximo potencial.
Hacerlo hará tu experiencia con bash más divertida y productiva. Pero... ¿Por qué deberías
aprender programación en bash? Fácil, porque ya piensas en términos de comandos,
copiando archivos, y usando las tuberías y redirrecionando salidas. ¿No deberías aprender
un lenguaje que te permita trabajar y construir a partir de estas poderosas herramientas que
ya sabes utilizar? Las shells dan libertad al potencial de los sistemas UNIX, y bash es la
shell de Linux. Es el "pegamento" de alto nivel entre tú y la máquina. Crece en tu
conocimiento sobre bash y automáticamente incrementarás tu productividad bajo Linux y
UNIX -- es así de simple.
Aprender bash del modo equivocado puede ser un proceso muy confuso. Muchos usuarios
novatos escriben man bash para ver la página del manual de bash ("man page"), sólo para
ser confrontados con una muy concisa y técnica descripción de la funcionalidad de la shell.
Otros intentan con info bash (para ver la documentación que provee GNU info), causando
que la página del manual sea reimpresa o, si tienen suerte, verán a lo sumo una página de
documentación escasamente más amigable.
Aunque esto puede ser algo entristecedor para los novatos, la documentación estándar de
bash no puede ser todas las cosas para toda la gente, y se orienta hacia aquellos ya
familiarizados con la programación shell en general. Hay definitivamente un montón de
información técnica excelente en la página del manual ("man page"), pero su utilidad para
los principiantes es limitada.
Allí es donde esta serie entra en el juego. En ella, te mostraré cómo usar las construcciones
de bash en realidad, para que te encuentres preparado para escribir tus propios scripts. En
vez de descripciones técnicas, te proveeré explicaciones en tu idioma, para que sepas no
sólo qué es lo que algo hace, sino además cuándo deberías usarlo. Hacia el final esta serie
de tres partes, serás capaz de escribir tus propios scripts complejos para bash, y de estar al
nivel en el que podrás usar bash confortablemente y aumentar tus conocimientos leyendo (y
entendiendo!) la documentación estándar de bash. Comencemos.
Variables de entorno
Bajo bash y bajo casi cualquier shell, el usuario puede definir variables de entorno, que son
guardadas internamente como cadenas de caracteres ASCII. Una de las cosas más prácticas
acerca de las variables de entorno es que son una parte estándar del modelo de proceso de
UNIX. Esto significa que las variables de entorno no son exclusivas de los scripts de shell,
sino que pueden ser usadas por programas compilados de manera estándar también. Cuando
"exportamos" una variable de entorno bajo bash, cualquier programa subsecuente que
corramos podrá leer lo que le asignamos, sea un script de shell o no. Un buen ejemplo es el
comando vipw, que normalmente permite al superusuario root editar el archivo con la clave
("password") del sistema. Ajustando la variable de entorno EDITOR con el nombre de tu
editor de texto favorito, puedes configurar a vipw para que lo use en lugar de vi, algo
bastante práctico si estás acostumbrado a xemacs y realmente no te gusta vi.
El comando de arriba definió una variable de entorno llamada "myvar" que contiene la
cadena "This is my environment variable!". Hay algunas cosas a las que es necesario
prestarle atención en lo anterior: primero, no hay ningún espacio rodeando al signo "=";
cualquier espacio allí resultaría en un error (pruébalo y confírmalo). La segunda cosa a
tener en cuenta es que, aunque pudimos haber omitido las comillas si se tratase de una sola
palabra, son necesarias cuando el valor de la variable de entorno es más de una palabra
(contiene espacios o tabs).
Nota: Para información extremadamente detallada sobre cómo deben ser usadas las
comillas en bash, es probable que la sección "QUOTING" de la "man page" de bash te
resulte útil. La existencia de secuencias especiales de caracteres que son "expandidas"
(reemplazadas) por otros valores complica el modo en que las cadenas son manejadas en
bash. Sólo cubriremos las funciones más usadas/importantes de las comillas en esta serie.
En tercer lugar, mientras que normalmente podemos usar comillas dobles en vez de
comillas simples, hacerlo en el ejemplo anterior hubiera causado un error. ¿Por qué? Porque
el usar comillas simples desactiva una de las características de bash llamada "expansión",
donde caracteres y secuencias de caracteres especiales son reemplazados por valores. Por
ejemplo, el caracter "!" es el caracter de expansión del historial, que bash normalmente
reemplaza por un comando previamente escrito. (No cubriremos la expansión del historial
en esta serie de artículos, porque no es usada frecuentemente en la programación en bash.
Para más información sobre eso, mira la sección "HISTORY EXPANSION" en la página
del manual ("man page") de bash.) Aunque este comportamiento al estilo "macro" puede
ser muy práctico, en esta ocasión queremos un signo de exclamación literal al final del
valor de nuestra variable de entorno, en vez de un "macro".
Ahora, echémosle una mirada a cómo uno usa en realidad una variable de entorno. Aquí
hay un ejemplo:
Precediendo el nombre de nuestra variable de entorno con un $, podemos hacer que bash la
reemplace por el valor de myvar. En la terminología de bash, esto se llama "expansión de
variable" ("variable expansion"). Pero, si probamos lo siguiente:
Como puedes ver, podemos encuadrar el nombre de nuestra variable de entorno entre llaves
cuando no se encuentra claramente separada del texto que la rodea. Mientras que $myvar es
más rápido de escribir y funcionará en la mayoría de las ocasiones, ${myvar} puede ser
comprendida correctamente en casi cualquier situación. Más allá de eso, ambas hacen lo
mismo, y verás ambas formas de expansión de variable en el resto de la serie. Querrás
recordar que tienes que usar la forma más explícita (con las llaves) cuando tu variable de
entorno no se encuentre aislada del texto que la rodea mediante algún espacio en blanco
(espacio o tabs).
int main(void) {
char *myenvvar=getenv("EDITOR");
printf("The editor environment variable is set to %s\n",myenvvar);
}
Ahora habrá un programa ejecutable en tu directorio que, al ser corrido, imprimirá el valor
de la variable de entorno EDITOR, si es que lo tiene. Esto es lo que pasa cuando lo corro en
mi máquina:
Aunque pudiste haber esperado que myenv imprima el valor "xemacs", no funcionó, porque
no exportamos la variable de entorno EDITOR. Esta vez lo haremos funcionar:
Así, has visto con tus propios ojos que otro proceso (en este caso nuestro programa en C)
no puede ver la variable de entorno hasta que esta es exportada. Incidentalmente, si tú
quieres, puedes definir y exportar una variable de entorno usando una sola línea, del
siguiente modo:
Listado de Código 1.10: Definir y exportar una variable de entorno en un solo comando
$ export EDITOR=xemacs
Funciona idénticamente a la versión de dos líneas. Este sería un buen momento para
enseñar cómo borrar una variable de entorno usando unset:
Cortar cadenas -- esto es, separar una cadena original en más pequeños y separados trozos
-- es una de esas tareas que es llevada a cabo diariamente por tu script de shell tipo. Muchas
veces, los scripts de shell necesitan tomar una ruta completa, y encontrar el archivo o
directorio final. Aunque es posible (y divertido!) programar esto en bash, el ejecutable
estándar de UNIX basename hace esto extremadamente bien:
basename es una muy práctica herramienta para cortar cadenas. Su acompañante, llamado
dirname, devuelve la "otra" parte de la ruta que basename desecha:
Sustitución de comandos
Una cosa bastante práctica de saber es cómo crear una variable de entorno que contenga el
resultado de un comando ejecutable. Esto es muy fácil de hacer:
Listado de Código 1.14: Crear una variable de entorno con el resultado de un comando
$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo
Como puedes ver, bash provee múltiples maneras de realizar exactamentela misma cosa.
Usando la sustitución de comandos, podemos ubicar cualquier comando o tuberías con
comandos entre ` ` o $( ) y asignar su resultado a una variable de entorno. ¡Qué cosa
práctica! Aquí hay un ejemplo de cómo usar tuberías con la sustitución de comandos:
Si bien basename y dirname son herramientas grandiosas, hay momentos en los que
podemos necesitar realizar operaciones de corte de cadenas de una manera más avanzada
que sólo manipulaciones estándar con las rutas. En esos casos, podemos aprovechar la
característica intrínseca avanzada de bash de expansión de variable. Ya hemos usado la
manera estándar de expansión de variable, que se ve como esto: ${MYVAR}. Pero bash
puede también realizar cortes de cadenas prácticos por sí mismo. Échale una mirada a estos
ejemplos:
Luego de buscar sub-cadenas que encajaran (puedes ver que bash encontró dos) seleccionó
la más larga, la eliminó del inicio de la cadena original, y devolvió el resultado.
Esto puede parecer extremadamente críptico, así que te mostraré una manera fácil de
recordar estas herramientas. Cuando buscamos la sub-cadena más larga, usamos ## (porque
## es más largo que #). Cuando buscamos la más corta, usamos #. ¿Ves? ¡No es tan difícil
de recordar! Espera. ¿Cómo recordamos que debemos usar el caracter '#' para eliminar
desde el "principio" de una cadena? ¡Simple! Notarás que en un teclado americano, shift-4
es "$", que es el caracter de expansión de variable de bash. En el teclado, inmediatamente a
la izquierda de "$" está "#". Entonces, puedes ver que "#" está "al principio" de "$", y así
(de acuerdo a nuestra regla mnemotécnica), "#" elimina caracteres desde el principio de la
cadena. Te puedes llegar a preguntar cómo eliminamos caracteres ubicados al final de la
cadena. Si adivinaste que usamos el caracter inmediatamente a la derecha de "$" en el
teclado americano ("%"), estás en lo cierto! Aquí hay algunos ejemplos rápidos sobre cómo
cortar porciones finales de cadenas:
Como puedes ver, las opciones de expansión de variable % y %% funcionan del mismo
modo que # y ##, excepto que eliminan la "wildcard" del final de la cadena. Nota que no
estás obligado a usar el caracter "*" si quieres eliminar una sub-cadena específica del final:
Listado de Código 1.20: Cortar sub-cadenas del final
MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken
En este ejemplo, no importa si usas "%%" o "%", ya que sólo una sub-cadena puede
encajar. Y recuerda, si te olvidas si debes usar "#" o "%", mira las teclas 3, 4, y 5 en tu
teclado y te darás cuenta.
Podemos usar otra forma de expansión de variable para seleccionar una sub-cadena
específica, basándonos en un punto de inicio y una longitud. Intenta escribir las siguienes
líneas en bash:
Esta forma de corte de cadena puede sernos muy útil; simplemente especifica el caracter a
partir del cual iniciar y la longitud de la sub-cadena, todo separado por dos puntos.
Ahora que hemos aprendido todo acerca del corte de cadenas, escribamos un pequeño y
simple script de shell. Nuestro script aceptará un sólo archivo como argumento, e imprimirá
si parece ser un "tarball" o no. Para determinar si se trata de un "tarball", se fijará en el
patrón ".tar" al final del archivo. Aquí está:
if [ "${1##*.}" = "tar" ]
then
echo This appears to be a tarball.
else
echo At first glance, this does not appear to be a tarball.
fi
Para correr este script, transcríbelo dentro a un archivo llamado mytar.sh, y luego escribe
chmod 755 mytar.sh para hacerlo ejecutable. Luego, pruébalo con un "tarball" del siguiente
modo:
Te puedes estar preguntando qué representa la variable de entorno "1". Muy simple -- $1 es
el primer argumento recibido por el script desde la línea de comandos, $2 es el segundo,
etc. OK, ahora que hemos repasado la función, podemos echar una primera mirada a las
instrucciones "if".
Instrucciones "if"
Esta realiza una acción sólo si la condición es verdadera; caso contrario, no realiza ninguna
acción y continúa ejecutando cualquier línea luego del "fi".
else
actionx
fi
La forma "elif" de arriba probará consecutivamente cada condición y ejecutará la acción
correspondiente a la primera condición verdadera. Si ninguna de las condiciones es
verdadera, ejecutará la acción del "else", si la hay, y luego continuará ejecutando las líneas
que sigan a la instrucción entera de"if,elif,else".
La próxima vez
Ahora que hemos cubierto lo más básico de bash, es tiempo de poner manos a la obra y
escribir algunos scripts reales. En el próximo artículo, cubriré las construcciones de bucles
(iteraciones), funciones, "namespace" (ámbito de variables), y otros temas esenciales.
Luego, estaremos listos para escribir algunos scripts más complicados. En el tercer artículo,
nos enfocaremos casi exclusivamente en scripts y funciones más complejos, como también
en varias opciones de diseño de scripts en bash. Nos vemos en el siguiente!
2. Recursos
Argumentos
Amor condicional
Si alguna vez has programado algo, relacionado con archivos, en C, sabrás que se requiere
un esfuerzo significativo para saber si un fichero dado es más nuevo que otro. Eso es
porque C no tiene una sintaxis interna para realizar dicha comparación; en lugar de eso dos
llamadas a stat() y dos estructuras stat son necesarias para poder realizar dicha comparación
"a mano". En contraste, bash puede realizar esta operación mediante operadores estándar
que posee. Por eso, determinar si "/tmp/miarchivo es legible" es tan sencillo como
comprobar si "$mivar es mayor que cuatro".
A veces hay varias formas de realizar una misma comparación, los siguientes ejemplos
funcionan de forma idéntica:
if [ "$mivar" = "3" ]
then
echo "mivar igual a 3"
fi
En el ejemplo de arriba, ambas comparaciones hacen lo mismo, pero, mientras que la
primera una un operador de comparación aritmético, la segunda usa un operador de
comparación de cadenas de texto.
Si bien la mayoría de las veces se pueden omitir las comillas dobles alrededor de las
cadenas y las variables que las contienen, no se considera una buena práctica de
programación. ¿Por qué? Todo funcionará perfectamente mientras la variable no contenga
un espacio o un carácter de tabulación. En ese caso bash se confundirá. Aquí hay un
ejemplo de comparación que fallará por este motivo:
En este caso, los espacios en "$mivar" (que contiene "foo var oni") confunden al intérprete
bash. Tras expandir "$mivar", la comparación queda de esta forma:
Si la variable de entorno no se ha puesto entre comillas dobles, bash piensa que se han
colocado más argumentos de la cuenta entre los corchetes. La solución obvia a este
problema pasa por encerrar el argumento entre comillas dobles. Recuerda: si tomas el buen
hábito de poner siempre tus variables entre comillas dobles, eliminarás de raíz muchos
errores similares de programación. Así es como esta comparación debería haberse escrito:
Nota: Si quieres que tus variables de entorno se expandan, debes usar comillas dobles.
Recuerda que las comillas simples desactivan la expansión de variables y del historial.
Esquemas de bucle: "for"
Ahora que hemos comentado las sentencias de bifurcación condicional "if" empezaremos
con los bucles. El bucle "for" estándar. Aquí hay un ejemplo básico:
Salida:
número uno
número dos
número tres
número cuatro
¿Qué ha pasado exactamente? La parte "for x" del bucle define una variable que
llamaremos variable de control del bucle, que se llama "$x", y a la cual se le asignan de
forma sucesiva los valores "uno", "dos", "tres" y "cuatro". Tras cada asignación, el cuerpo
del bucle (la parte entre "do" y "done") se ejecuta una vez. En el cuerpo nos referimos a la
variable de control del bucle "$x" usando la sintaxis estándar de bash para la expansión de
variables, como se haría con cualquier otra variable de entorno. For siempre acepta una lista
de palabras tras "in". En este caso hemos usado cuatro palabras en castellano, pero la lista
de palabras puede contener también nombres de archivo e incluso comodines. El siguiente
ejemplo ilustra como usar los comodines estándar de bash en un bucle for:
salida:
/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc
Este código itera sobre cada archivo en /etc que empiece con una "r". Para ello, bash,
primero expande el comodín en /etc/r*, reemplazando esa ruta con la cadena /etc/rc.d
/etc/resolv.conf /etc/resolv.conf~ /etc/rpc antes de iterar. Una vez dentro del bucle, el
operador condicional "-d" se usa en dos líneas que hacen dos cosas distintas, dependiendo
de si "miarchivo" es un directorio o no. Si lo es, entonces la cadena " (dir)" se añade al final
de la línea.
Bash ejecutará todas las expansiones de comodines y de variables que sean posibles,
creando -potencialmente- una muy larga lista de palabras.
Si bien todos los ejemplos de expansión de comodines se han realizado con rutas absolutas,
también se pueden usar rutas relativas, como estas:
En el anterior ejemplo, bash expande los comodines en relación al directorio actual. Tal y
como se usarían rutas relativas en la línea de comandos. Juega un poco con la expansión de
comodines. Ten en cuenta que, si usas rutas absolutas con tus comodines, bash expandirá el
comodín a una lista de rutas absolutas. De cualquier otra forma, bash usará rutas relativas
en la lista de palabras resultante de la expansión. Si solo quieres referirte a los archivos en
el directorio activo (por ejemplo, cuando escribas for x in *), la lista resultante no tendrá
ningún tipo de prefijo de ruta añadido. Recuerda que la información de ruta precedente se
podría eliminar, de todas formas, con basename, tal y como en este ejemplo:
Muchas veces puede ser útil realizar bucles que operen sobre los parámetros suministrados
en la línea de comandos. El siguiente es un ejemplo sobre como usar la variable "$@", que
se introdujo al principio de este mismo artículo:
Antes de aprender un segundo tipo de esquema de bucle, es una buena idea aprender algo
sobre la aritmética de bash. Si, es cierto, bash puede realizar operaciones simples con
enteros. Tan solo es necesario encerrar la operación entre estos dos pares: "$((" y "))", y
bash evaluará la expresión. Aquí hay algunos ejemplos:
Una sentencia "while" se ejecutará iterativamente mientras una condición dada sea cierta, y
tiene el siguiente formato:
Las sentencias "while" se pueden usar típicamente para ejecutar una tarea un número dado
de veces, como en el ejemplo siguiente:
Las sentencias "until" nos proveen con la funcionalidad inversa de "while". Repiten el
bucle mientras la condición sea falsa, aquí hay un bucle "until" que funciona de forma
idéntica al ejemplo "while" anterior:
Sentencias case
Funciones y contexto
En bash, puedes definir funciones, de forma similar a como se definen en los lenguajes
procedimentales como Pascal, C y otros. Dichas funciones pueden aceptar argumentos de
forma similar a como los aceptan los guiones. Aquí tenemos una definición de función de
ejemplo:
Arriba definimos una función llamada "tarview" que acepta un argumento, un tarball de
alguna clase. Cuando la función se ejecuta, identifica el tipo de tarball al que el argumento
apunta, (si es un tarball descomprimido, o comprimido con bzip2 o gzip), imprime una
línea informativa, y muestra el contenido del tarball. Así es como debemos llamar a esta
función (da igual que sea desde un guión o desde la misma línea de comandos, tras ser
escrita en el mismo intérprete, pegada, o volcada usando source.
Como puedes ver, los argumentos usan el mismo mecanismo de referenciación dentro de la
función que los usados en un guión para referenciar a los argumentos de línea de comandos.
La macro "$#" se expande también al número de argumentos. La única cosa que puede no
funcionar completamente como se espera es la variable "$0", que, o bien se expande a la
cadena "bash" (si la función se llamó desde la línea de comandos, de forma interactiva, o
bien al nombre del guión que la llamó.
Nota: Úsalas de forma interactiva: No olvides que las funciones, como la de arriba, pueden
ser incluídas en tu ~/.bashrc o tu ~/.bash_profile para que estén disponibles siempre que
estés usando bash.
mivar="hola"
mifunc() {
mifunc
echo $mivar $x
Cuando este guión se ejecuta, produce la salida "un dos tres tres", mostrando como la
variable "$mivar" definida en la función ha sobreescrito a la global "$mivar", y como la
variable de control "$x" continúa existiendo incluso tras salir de la función en la que fue
definida, y a su vez sobreescribiendo cualquier otra posible "$x" que estuviesa ya definida.
En este simple ejemplo, el error es fácil de ver y se puede compensar tan solo cambiando
los nombres de las variables. Sin embargo, la mejor forma de acometer el problema pasa
por prevenir la posibilidad de que ninguna variable pueda sobreescribir a una definida de
forma global, mediante el uso del comando "local". Cuando se usa el comando "local" para
crear variables dentro de una función, las mismas se mantendrán en un entorno local a la
función, y no interferirán con ninguna otra variable global. A continuación, un ejemplo de
como implementar dicha definición, para que ninguna variable global sea sobreescrita:
mivar="hola"
mifunc() {
local x
local mivar="un dos tres"
for x in $mivar
do
echo $x
done
}
mifunc
echo $mivar $x
Resumiendo
Ahora que hemos cubierto lo más esencial de la funcionalidad de bash, es hora de ver como
desarrollar una aplicación entera en bash. En el próximo capítulo veremos como hacer eso.
¡Hasta entonces!
2. Recursos
He estado deseando que llegara este capítulo final de Bash con ejemplos, porque ahora que
hemos cubierto los conceptos básicos de la programación en bash, Parte 1 y Parte 2,
podemos centrarnos en temas más avanzados, como el desarrollo de aplicaciones en bash y
diseño de programas. Te daré una buena dosis de programación práctica, del mundo real,
presentando un proyecto en cuya codificación y refinamiento he invertido muchas horas: El
sistema de Ebuilds de Gentoo.
Paquete Descripción
linux El kernel actual
util-linux Una colección de programas variados relacionados con Linux
e2fsprogs Una colección de utilidades relacionadas con ext2
glibc La librería C de GNU
Bash es un componente esencial del sistema de ebuilds de Gentoo Linux. Fue elegido como
el lenguaje primario para los ebuilds por varias razones. En primer lugar, posee una sintaxis
familiar y asequible, muy apropiada para el uso de programas externos. Un sistema de
autocompilado es un código intermedio que automatiza la llamada a programas externos, y
bash es un lenguaje particularmente apropiado para este tipo de aplicación. Segundo, el
soporte de bash para funciones permite al sistema de ebuilds adoptar un diseño modular,
fácil de entender. Tercero, el sistema de ebuilds saca provecho del soporte de bash para las
variables de entorno, permitiendo a los mantenedores de paquetes y a los desarrolladores
reconfigurarlo al vuelo.
Antes de entrar en el sistema de ebuilds de lleno, tendremos que conocer los pasos
necesarios para compilar e instalar un paquete. Para nuestro ejemplo usaremos el paquete
"sed", un editor estándar de flujos GNU que es parte integrante de todas las distribuciones
de Linux. Primero descarga el archivo con las fuentes (sed-3.02.tar.gz) (ver Recursos).
Almacenaremos este archivo en /usr/src/distfiles, un directorio al que nos referiremos
usando la variable de entorno $DISTDIR. $DISTDIR es el directorio donde se guardarán
todos los tarball de código fuente, será un gran almacén de código fuente.
Nuestro siguiente paso será crear un directorio temporal work, que aloje los fuentes
descomprimidos. Nos referiremos a este directorio usando la variable $WORKDIR. Para
ésto, cambia a un directorio sobre el que tengas permiso de escritura y escribe lo siguiente:
Listado de Código 1.1: Descomprimiendo sed en un directorio temporal
$ mkdir work
$ cd work
$ tar xzf /usr/src/distfiles/sed-3.02.tar.gz
Ahora el tarball está descomprimido, habrá creado un directorio llamado sed-3.02, que
contiene las fuentes de sed. Nos referiremos a dicho directorio sed-3.02 más tarde usando la
variable de entorno $SRCDIR. Para compilar el programa teclea lo siguiente:
$ make
Vamos a saltarnos el paso "make install", ya que solo estamos cubriendo los pasos de
desempaquetado y compilación en este artículo. Si quisiéramos usar un script de bash para
realizar todos estos pasos por nosotros haríamos algo como:
if [ -d work ]
then
# remove old work directory if it exists
rm -rf work
fi
mkdir work
cd work
tar xzf /usr/src/distfiles/sed-3.02.tar.gz
cd sed-3.02
./configure --prefix=/usr
make
Generalizando el código
A=${P}.tar.gz
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}
if [ -z "$DISTDIR" ]
then
# DISTDIR es /usr/src/distfiles si no ha sido definido ya
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
if [ -d ${WORKDIR} ]
then
# borra el directorio de trabajo antiguo si es que existe
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make
Hemos añadido muchas variables al nuevo código, pero, básicamente, todavía hace lo
mismo. Sin embargo, ahora podemos compilar cualquier paquete basado en GNU autoconf.
Simplemente copiando este archivo con un nuevo nombre que refleje el nombre del
paquete, y cambiando los valores de $A y $P, compilará. Las demás variables se ajustarán
automáticamente. Si bien es útil, hay aún mejoras que podemos introducir en este código.
Este código es bastante más largo que el original. Ya que una de las tareas principales de
cualquier proyecto de programación es reducir la complejidad de cara al usuario, estaría
bien reducir un poco la longitud del cógido, o, al menos, organizarlo un poco mejor.
Podemos hacer esto con un ingenioso truco -- separaremos el código en dos ficheros
separados, guarda lo siguiente como sed-3.02.ebuild:
Nuestro primer fichero es trivial, y contiene solo variables de entorno, que han de ser
configuradas paquete por paquete, el segundo fichero contiene el cerebro de la operación.
Guárdalo como "ebuild" y hazlo ejecutable:
if [ -e "$1" ]
then
source $1
else
echo "ebuild $1 no encontrado."
exit 1
fi
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}
if [ -z "$DISTDIR" ]
then
# DISTDIR será /usr/src/distfiles si no está ya definido
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
if [ -d ${WORKDIR} ]
then
# borra directorio antiguo si ya existía
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make
Ahora que hemos dividido nuestro sistema en dos ficheros, apuesto a que te estarás
preguntando como funciona. Fácil, para compilar sed, escribe:
Cuando "ebuild" se ejecuta, primero intenta interpretar $1. ¿Que significa esto? Recuerda
de mi anterior artículo, que $1 es el primer argumento de línea de comandos, en este caso
sed-3.02.ebuild. En bash, el comando "source" lee instrucciones bash de un archivo y las
ejecuta como si estuvieran dentro del fichero desde donde se usa el comando "source". Así
que "source" ${1}" causa que el script "ebuild" ejecute los comandos contenidos en sed-
3.02.ebuild, de este modo, $P y $A son definidos. Este cambio de diseño es realmente
conveniente, porque si queremos compilar otro programa, en lugar de sed, tan solo
necesitamos un nuevo fichero .ebuild que pasar a nuestro script "ebuild". De este modo, los
ficheros .ebuild son realmente simples, mientras la parte complicada del sistema se
almacena en el script "ebuild". De esta forma, también se puede mejorar o actualizar el
sistema ebuild simplemente editando el script, manteniendo los detalles de la
implementación fuera de los ficheros ebuild. Aquí hay un fichero ebuild de ejemplo para
gzip:
Añadiendo funcionalidad
Bien, ya hemos hecho algún progreso, pero hay funcionalidades adicionales que me
gustaría añadir. Me gustaría que el script ebuild aceptara un segundo parámetro que será
compile, unpack, o all. Este segundo parámetro dirá al ebuild la operación que debe
realizar. De esta forma, puedo decirle a ebuild que desempaquete el archivo pero sin
compilarlo (por si necesito inspeccionar el código fuente antes de la compilación). Para
hacer esto usaremos una estructura case, que comprobará la variable $2, y actuará de
acuerdo con su valor. El código sería algo así:
if [ $# -ne 2 ]
then
echo "Por favor, especifique dos argumentos, el fichero .ebuild
y"
echo "unpack, compile or all"
exit 1
fi
if [ -z "$DISTDIR" ]
then
# DISTDIR será /usr/src/distfiles si no está ya definido
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
ebuild_unpack() {
#nos aseguramos de estar en el directorio correcto
cd ${ORIGDIR}
if [ -d ${WORKDIR} ]
then
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
if [ ! -e ${DISTDIR}/${A} ]
then
echo "${DISTDIR}/${A} no existe. Por favor, descárguelo
primero."
exit 1
fi
tar xzf ${DISTDIR}/${A}
echo "Desempaquetado ${DISTDIR}/${A}."
#el código fuente está descomprimido
}
ebuild_compile() {
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
if [ -e "$1" ]
then
source $1
else
echo "Ebuild $1 no encontrado."
exit 1
fi
export SRCDIR=${WORKDIR}/${P}
case "${2}" in
unpack)
ebuild_unpack
;;
compile)
ebuild_compile
;;
all)
ebuild_unpack
ebuild_compile
;;
*)
echo "Por favor, especifique unpack, compile o All como
segundo argumento"
exit 1
;;
esac
Hemos hecho varios cambios, así que revisémoslos. Primero, hemos puesto las órdenes
para desempaquetar y compilar los paquetes en su propia función. Las hemos llamado
ebuild_compile() y ebuild_unpack(), respectivamente. Ha sido un buen movimiento, ya que
el código se está complicando, y las funciones lo dotan de algo más de modularidad, lo que
nos ayudará a mantener el script ordenado. En la primera línea de cada función, se cambia
de forma explícita, con cd, al directorio al que se quiere ir. Al complicarse nuestro código
es muy probably que terminemos ejecutando algo en un directorio distinto del correcto, así,
nos aseguramos de estar en el lugar correcto antes de hacer nada, con cd, y nos ahorraremos
posible errores más adelante. Ésto es un paso importante, sobre todo, si se borran ficheros
dentro de una función.
Uno de los cambios más obvios en nuestro script ebuild es la estructura case añadida al
final del mismo. Dicha estructura simplemente chequea el segundo argumento de línea de
comandos, y, en base al valor del mismo, decide la acción a realizar. Si ahora ejecutamos
esto:
Obtendremos un mensaje de error, porque ebuild ahora necesita que le digamos qué hacer,
de esta forma:
or:
Modularizando el código
Ahora que el código es más avanzado y funcional, puede que estés pensando en crear varios
ebuilds para desempaquetar y compilar tus programas favoritos. Si lo hicieras, tarde o
temprano comprobarías que algunas fuentes no usan autoconf (./configure), sino que se
valen de otros procesos de compilación no estándar. Tenemos que modificar el sistema de
ebuilds para que se acomode a estos programas. Pero antes de hacerlo, es bueno pararse a
pensar como conseguiremos esto.
Una de las grandes ventajas de usar siempre ./configure --prefix=/usr; make en la fase de
compilación, es que, la mayoría de las veces funciona. Pero también debemos hacer que el
sistema de ebuilds funcione con aquellos fuentes que no usan autoconf, o fichero Make
normales. Propongo lo siguiente, como solución a este problema:
Los ebuilds solo ejecutarán configure si dicho script existe. Así hacemos que ebuild
funcione con programas que no usan autoconf, y tienen un fichero Make estándar. Pero, ¿y
si un simple "make" no funciona con algunos fuentes? Necesitamos una forma de saltarse
esta funcionalidad predefinida, usando un código alternativo para manejar situaciones
específicas. Para esto, convertiremos nuestra función ebuild_compile() en dos funciones.
La primera de dichas funciones puede ser vista como "padre" de la segunda, y se llamará
ebuild_compile(). La nueva función, llamada user_compile(), contendrá nuestras acciones
predeterminadas:
ebuild_compile() {
if [ ! -d "${SRCDIR}" ]
then
echo "${SRCDIR} no existe -- por favor, descomprima
primero."
exit 1
fi
#se asegura de que estamos en el directorio correcto
cd ${SRCDIR}
user_compile
}
Puede que no parezca obvio el por qué de todo esto ahora mismo. Así que, por ahora,
sigamos. Si bien el código de arriba funciona de forma idéntica a la anterior versión de
ebuild, ahora podemos hacer algo que no podiamos hacer antes. Podemos redefinir la
función user_compile() en sed-3.02.ebuild. Así, si la predeterminada user_compile() no
sirve a nuestras necesidades, podemos redefinirla por completo en nuestro fichero .ebuild.
Como ejemplo, un fichero .ebuild para e2fsprogs-1.18, que requiere una línea ./configure
ligeramente modificada:
user_compile() {
./configure --enable-elf-shlibs
make
}
Ahora, e2fsprogs será compilado de la forma correcta. Para la mayoría de los paquetes, esto
no es necesario. Simplemente omitiendo la definición de user_compile() en nuestro fichero
.ebuild, conseguiremos que se use la función user_compile() predeterminada.
¿Como sabe el script ebuild que función user_compile() debe usar? Muy sencillo: en el
script ebuild, la función user_compile() es definida antes de que el fichero .ebuild
e2fsprogs-1.18.ebuild sea leído. Si hay una función user_compile() en e2fsprogs-
1.18.ebuild, dicha función sobreescribe a la versión predeterminada, definida previamente.
Si no, la primera versión es usada.
Hemos añadido una gran funcionalidad sin requerir ningún tipo de codificación compleja.
No lo explicaré aquí, pero se podría hacer algo similar con la función ebuild_unpack(), de
forma que podamos reescribir el proceso de desempaquetado predeterminado. Esto podría
ser práctico si se tiene que hacer algún tipo de parcheo o si los ficheros están contenido en
múltiples archivos comprimidos. También sería una buena idea modificar el código de
desempaquetado de forma que reconozca tarballs comprimidos con bzip2 por defecto.
Ficheros de configuración
En este ejemplo he incluído una sola opción de configuración, pero se podrían incluír
muchas más. Una de las cosas interesantes de bash es que el fichero se puede interpretar
simplemente usando el comando "source" sobre el mismo. Éste es un truco de diseño que
funciona con la mayoría de lenguajes interpretados. Después de que /etc/ebuild.conf haya
sido interpretado, $MAKEOPTS está definido en nuestro script .ebuild, y le permite al
usuario pasar dichas opciones a make. En este caso, la opción le dice al ebuild que lance
una instancia paralela de make.
Nota: ¿Qué es una instancia paralela de make? Las instancias paralelas pueden servir
para agilizar el proceso en sistema con varios procesadores. Make soporta la compilación
en paralelo. Esto significa que, en lugar de compilar un fichero fuente en un momento dado,
make puede compilar un número de ficheros (dado por el usuario) al mismo tiempo. En un
sistema multiprocesador esto hace que se usen estos procesadores extra. Make en paralelo
se activa al interpretar la opción -j # pasada a make, de esta forma: make -j4 MAKE="make
-j4". Esto instruye a make para compilar cuatro programas de forma simultánea. El
argumento MAKE="make -j4" le dice a make que pase la opción -j4 a cualquier proceso
hijo que lance.
if [ $# -ne 2 ]
then
echo "Por favor, especifique fichero ebuild file y unpack,
compile u all"
exit 1
fi
source /etc/ebuild.conf
if [ -z "$DISTDIR" ]
then
# configura DISTDIR como /usr/src/distfiles si no está
configurado ya
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
ebuild_unpack() {
#se asegura de estar en el directorio correcto
cd ${ORIGDIR}
if [ -d ${WORKDIR} ]
then
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
if [ ! -e ${DISTDIR}/${A} ]
then
echo "${DISTDIR}/${A} no existe. Por favor, descargue
primero."
exit 1
fi
tar xzf ${DISTDIR}/${A}
echo "Unpacked ${DISTDIR}/${A}."
#las fuentes han sido descomprimidas
}
user_compile() {
#ya estamos en ${SRCDIR}
if [ -e configure ]
then
#ejecuta el script configure si existe
./configure --prefix=/usr
fi
#ejecuta make
make $MAKEOPTS MAKE="make $MAKEOPTS"
}
ebuild_compile() {
if [ ! -d "${SRCDIR}" ]
then
echo "${SRCDIR} no existe -- por favor, descomprima
primero."
exit 1
fi
#se asegura de estar en el directorio correcto
cd ${SRCDIR}
user_compile
}
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
if [ -e "$1" ]
then
source $1
else
echo "Fichero .ebuild $1 no encontrado."
exit 1
fi
export SRCDIR=${WORKDIR}/${P}
case "${2}" in
unpack)
ebuild_unpack
;;
compile)
ebuild_compile
;;
all)
ebuild_unpack
ebuild_compile
;;
*)
echo "Por favor, especifique unpack, compile u all como
segundo argumento"
exit 1
;;
esac
Resumiendo
Hemos cubierto muchas técnicas de programación en bash en este artículo, pero en realidad
solo hemos arañado la superficie de lo que el poder auténtico de bash representa. Por
ejemplo, el sistema de ebuilds de Gentoo no solo puede desempaquetar y compilar de
forma automática, sino que también:
2. Recursos