Академический Документы
Профессиональный Документы
Культура Документы
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
Si lo chequeas, probablemente encontrarás que ya estás corriendo bash en este momento. Aún si has
cambiado tu "shell" por defecto, bash está probablemente corriendo aún en algún lugar de tu sistema,
porque es la shell de Linux estándar y es usada para una gran variedad de propósitos. Porque bash ya está
corriendo, cualquier número de scripts adicionales que corras serán intrínsecamente eficientes en cuanto al
uso de memoria porque la comparten con algún proceso de bash que ya se encuentra corriendo. ¿Por qué
cargar un intérprete de 500K si ya estás corriendo algo que puede hacer el trabajo, y hacerlo bien?
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.
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:
Listado de Código 1.2: Usar variables de entorno
$ echo $myvar
This is my environment variable!
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:
Recuerda que también mencionamos que podemos "exportar" variables. Cuando exportamos una variable de
entorno, esta se encuentra automáticamente disponible en el entorno de cualquier script o ejecutable que se
corra subsecuentemente. Los scripts de shell pueden acceder a la variable de entorno usando el soporte que
brinda bash intrínsecamente, mientras que los programas en C pueden usar la función getenv(). Aquí hay un
código en C de ejemplo que deberías escribir y compilar -- nos permitirá entender las variables de entorno
desde la perspectiva de C:
int main(void) {
char *myenvvar=getenv("EDITOR");
printf("The editor environment variable is set to %s\n",myenvvar);
}
Guarda el código anterior en un archivo llamado myenv.c, y luego compílalo usando el siguiente comando:
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:
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
Lo que hicimos arriba se llama sustitución de comando ("command substitution"). Varias cosas merecen ser
notadas en este ejemplo. En la primera línea, simplemente encuadramos el comando que queríamos ejecutar
entre esas comillas especiales. Esas comillas no son las comillas simples estándar, sino que son las que se
imprimen al oprimir una de las teclas del teclado que normalmente se encuentra por encima de la tecla Tab.
Podemos hacer exactamente lo mismo con la sintaxis alternativa para la sustitución de comandos de bash:
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:
La segunda forma de expansión de variable mostrada arriba es casi idéntica a la primera, excepto que usa
sólo un "#" -- y bash realiza un proceso muy muy similar. Chequea las mismas sub-cadenas que chequeó en
nuestro primer ejemplo, sólo que bash ahora quita la más corta de nuestra cadena original, y devuelve el
resultado. Entonces, tan pronto como determina que la subcadena "fo" es lo que buscaba, elimina "fo" de
nuestra cadena y devuelve "odforthought.jpg".
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:
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:
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"
Como la mayoría de los lenguajes, bash tiene su propia implementación de condicionales. Cuando la uses,
ajústate al formato de arriba; esto es, mantén el "if" y el "then" en líneas separadas, y procura que el "else" y
el "fi" final (y requerido) estén alineados horizontalmente con los primeros. Esto hace que el código sea más
fácil de entender y posibilita encontrar los errores más rápidamente. Además de la forma de "if,else", hay
algunas otras formas de instrucciones "if":
Listado de Código 1.24: Forma básica de la instrucción if
if [ condition ]
then
action
fi
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!
Bash by example
Argumentos
Empezaremos con unas nociones básicas sobre el manejo de argumentos en la línea de comandos, para
luego pasar a algunos esquemas básicos de bash.
En el programa de ejemplo en el artículo introductorio, usamos la variable de entorno "$1", que se refería al
primer argumento suministrado en la línea de comandos. Podemos usar "$2", "$3", etc. para referirnos al
segundo y tercer argumentos, respectivamente, y así de forma sucesiva. Aquí tenemos un ejemplo:
A veces resulta útil poder referenciar todos los argumentos en un solo bloque desde el guión. Para este
propósito bash nos ofrece la variable de entorno "$@", que se expande a todos los parámetros
suministrados, concatenados y separados por espacios en blanco. Veremos un ejemplo de su uso al estudiar
los bucles, más adelante en este mismo artículo.
Si alguna vez has programado en un lenguaje procedimental como C, Pascal, Python, or Perl, estarás
familiarizado con los esquemas estándares de la programación, como las sentencias "if", los bucles "for" y
algunos más. Bash tiene sus propias versiones de los mismos. En las próximas secciones introduciré algunos
esquemas de bash y explicaré las diferencias entre estos esquemas y otros similares con los que puede que
te hayas encontrado al usar otros lenguajes de programación. Si no has programado demasiado antes, no te
preocupes. Hay ejemplos e información suficientes para que puedas seguir el texto.
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".
La lista siguiente muestra los operadores de comparación más frecuentemente usados en bash. También
encontrarás un ejemplo de como usar cada opción de forma correcta. El ejemplo podría usarse
inmediatamente después del "if":
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:
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.
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.
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:
salida:
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:
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:
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.
A menudo necesitarás crear variables de entorno dentro de una función. Si bien es posible, hay algo que
deberías saber. En la mayoría de lenguajes compilados (como C), cuando se crea una variable dentro de una
función, ésta es colocada en un contexto separado. Si en C defines una función llamada mifunción, y dentro
defines una variable "x", ninguna otra variable "x" definida en una función diferente se verá afectada,
eliminando efectos colaterales.
Ésto, que es completamente cierto en C, no lo es en bash. En bash, cuando creas una variable de entorno en
cualquier lugar dentro de una función, se añade al contexto global. Esto significa que siempre se corre el
peligro de sobreescribir otra variable, y que la variable trascenderá al lapso de vida de la función:
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
Esta función producirá como resultado "hola" -- la "$mivar" global no se sobreescribe, y "$x" no existirá fuera
de mifunc. En la primera línea de la función creamos "$x", una variable local que se usa después, mientras en
la segunda línea (local mivar="one two three") creamos otra "$mivar", y le asignamos un valor. La primera
forma es ideal para las variables de control de bucles, ya que no podemos hacerlo directamente en la forma
"for local x in $mivar". Esta función no sobreescribe ninguna variable anterior. Se recomienda diseñar de esta
forma todas las funciones, tan solo se deberá omitir la declaración local de las variables cuando se pretenda,
de forma consciente, escribir en una variable global.
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!
Bash by example
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.
Soy el arquitecto líder de Gentoo Linux, un sistema Linux de próxima generación, actualmente en estado
beta. Una de mis principales responsabilidades es asegurarme de que todos los paquetes binarios (similares
a los RPM) se crean correctamente, y funcionan bien en conjunto. Como probablemente sepas, un sistema
Linux estándar no está compuesto de un solo árbol unificado de fuentes, como en el caso de BSD, sino que
está compuesto de más de 25 paquetes críticos que funcionan juntos. Algunos de estos paquetes son:
Paquete Descripción
linux El kernel actual
Una colección de programas variados relacionados con
util-linux
Linux
e2fsprogs Una colección de utilidades relacionadas con ext2
glibc La librería C de GNU
Cada paquete viene en su propio tarball, y es mantenido por desarrolladores o equipos de desarrollo
distintos. Para crear una distribución, cada paquete debe ser descargado por separado, compilado, y
empaquetado, cada vez que el paquete necesita ser reparado, actualizado o mejorado. Todo esto debe ser
repetido (algunos paquetes quedan desfasados realmente rápidot). Para ayudar en este proceso tan
repetitivo, he creado el sistema de ebuilds. Escrito casi por completo en bash. Y para mejorar tu
conocimiento de bash, te enseñaré como implementé las secciones de desempaquetado y compilado del
sistema de ebuilds. Al explicar cada paso, explicaré también por qué se hicieron ciertas decisiones. Al final de
este artículo, no solo tendrás una visión de proyectos en bash a mayor escala, sino que también habrás
implementado una buena porción de un sistama de autocompilado.
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:
$ make
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
Aunque este script de autocompilado funciona, no es muy flexible. Básicamente, el script contiene la lista de
los comandos que han sido escritos anteriormente en línea de comandos. Esta solución funciona, pero sería
mucho mejor tener un script más genérico que pudiera configurar y desempaquetar cualquier paquete,
quizás cambiando solo unas pocas líneas. El trabajo para el mantenedor del paquete se ve así disminuído, y
es más fácil añadir nuevos paquetes a la distribución. Podemos usar variables de entorno para hacer nuestro
script más genérico:
P=sed-3.02
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:
Listado de Código 1.5: sed-3.02.ebuild
#fichero ebuild para sed - ¡simple!
P=sed-3.02
A=${P}.tar.gz
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 [ $# -ne 1 ]
then
echo "se esperaba un argumento."
exit 1
fi
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:
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.
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
Hemos cubierto varias técnicas interesantes de bash, y ahora es el momento de aprender una más. A
menudo es práctico para un programa tener un fichero de configuración global que resida en /etc.
Afortunadamente, esto es fácil cuando se usa bash. Simplemente crea este fichero y guárdalo
como /etc/ebuild.conf:
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.
Y aquí tenemos la versión final de ebuild:
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
/etc/ebuild.conf es interpretado cerca del principio del fichero. Usamos $MAKEOPTS en
nuestra user_compile() prefabricada. Puede que te preguntes como funcionará ésto -- después de todo,
nos referimos a $MAKEOPTS antes de interpretar/etc/ebuild.conf, que es el encargado de
definir $MAKEOPTS. Afortunadamente, ésto no es problema, porque la expansión de variables se produce al
ejecutar user_compile(). Cuando eso sucede, /etc/ebuild.conf ha sido ya incorporado
y $MAKEOPTS tiene un valor correcto.
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:
• Si se especifica, puede empaquetar una aplicación instalada en un tarball (comprimido) de forma que
pueda ser instalada después, en otro ordenador, o durante un proceso de instalación basado en CD
(por ejemplo, si estás contruyendo una distribución basada en dicho medio).
De forma adicional, el sistema ebuild en producción tiene otras opciones globales de configuración, que
permiten al usuario establecer banderas de optimización que se usan en tiempo de compilación, o el soporte
específico que se quiere en ciertas aplicaciones. Por ejemplo, los soportes para GNOME y slang se activan de
forma predeterminada en los paquetes que lo soportan.
Bash puede hacer mucho más de lo que he tocado en esta serie de artículos. Espero que hayas aprendido
mucho sobre esta increíble utilidad, y que estés deseando usar bash para acelerar y mejorar tus proyectos de
desarrollo.