programacion bash

27
Bash by Example Bash con ejemplos. 1. Programación fundamental en la "Bourne again shell" (bash) 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. Confusión con bash 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

Upload: ricardo-sanhueza-aguayo

Post on 03-Jul-2015

147 views

Category:

Documents


2 download

TRANSCRIPT

Page 1: Programacion Bash

Bash by Example

Bash con ejemplos.

1. Programación fundamental en la "Bourne again shell" (bash)

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.

Confusión con bash

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

Page 2: Programacion Bash

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.

La manera estándar de definir una variable de entorno bajo bash es:

Listado de Código 1.1: Definir una variable de entorno$ myvar='This is my environment variable!'

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:

Page 3: Programacion Bash

Listado de Código 1.2: Usar variables de entorno$ echo $myvarThis 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:

Listado de Código 1.3: Primer intento de uso de expansión de una variable$ echo foo$myvarbarfoo

Queríamos que esto imprima "fooThis is my environment variable!bar", pero no funcionó. ¿Cuál fue el error?

En pocas palabras, la facilidad de expansión de variable de bash se encontró confundida. No pudo dilucidar si

queríamos expandir la variable $m, $my, $myvar, $myvarbar, etc. ¿Cómo podemos ser más explícitos y

claramente decirle a bash a qué variables nos estamos refiriendo? Intenta esto:

Listado de Código 1.4: Expansión de variable; segundo intento.$ echo foo${myvar}barfooThis is my environment variable!bar

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).

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:

Listado de Código 1.5: myvar.c -- un programa en C de ejemplo para las variables de entorno#include <stdio.h>#include <stdlib.h>

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:

Listado de Código 1.6: Compilar el código de arriba$ gcc myenv.c -o myenv

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:

Listado de Código 1.7: Correr el programa de arriba$ ./myenvThe editor environment variable is set to (null)

Page 4: Programacion Bash

Hmmm... Como la variable de entorno EDITOR no poseía ningún valor, el programa en C recibe una cadena

nula. Intentemos nuevamente dándole un valor específico:

Listado de Código 1.8: Probar con un valor específico$ EDITOR=xemacs$ ./myenvThe editor environment variable is set to (null)

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:

Listado de Código 1.9: Mismo programa luego de exportar la variable$ export EDITOR$ ./myenvThe editor environment variable is set to xemacs

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:

Listado de Código 1.11: Borrar una variable de entorno$ unset EDITOR$ ./myenvThe editor environment variable is set to (null)Vistazo a los cortes de cadenas

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:

Listado de Código 1.12: Usar basename$ basename /usr/local/share/doc/foo/foo.txtfoo.txt$ basename /usr/home/drobbinsdrobbins

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:

Listado de Código 1.13: Usar dirname$ dirname /usr/local/share/doc/foo/foo.txt/usr/local/share/doc/foo$ dirname /usr/home/drobbins//usr/homeNota: Ambos dirname y basename no miran ningún archivo ni directorio en el disco; son puramente comandos de manipulación de cadenas.

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:

Page 5: Programacion Bash

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:

Listado de Código 1.15: Sintaxis alternativa para la sustitución de comandos$ 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:

Listado de Código 1.16: Tuberías y sustitución de comandos$ MYFILES=$(ls /etc | grep pa)$ echo $MYFILESpam.d passwdCortando cadenas como un profesional

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:

Listado de Código 1.17: Ejemplos de corte de cadenas$ MYVAR=foodforthought.jpg$ echo ${MYVAR##*fo}rthought.jpg$ echo ${MYVAR#*fo}odforthought.jpg

En el primer ejemplo, escribimos ${MYVAR##*fo}. ¿Qué significa esto exactamente? Básicamente, dentro de $

{ }, escribimos el nombre de la variable de entorno, dos ##s, y una "wildcard" (comodín, representación de

caracteres o cadenas de caracteres no explicitados), "*fo". Luego, bash tomó MYVAR, encontró la más larga

sub-cadena desde el comienzo de la cadena "foodforthought.jpg" que encajaba con la "wildcard" "*fo", y

realizó el corte desde el comienzo de la cadena principal. Esto es un poco difícil de asimilar a la primera, así

que para entender cómo funciona esta opción especial de "##", sigamos los pasos que realizó bash para

completar esta expansión. Primero, comenzó buscando las sub-cadenas desde el principio de

"foodforthought.jpg" que encajaran con la "wildcard" "*fo". Aquí están las sub-cadenas que chequeó:

Listado de Código 1.18: Sub-cadenas siendo chequeadasf fo MATCHES *fofoo

Page 6: Programacion Bash

foodfoodf foodfo MATCHES *fofoodforfoodfort foodforthfoodfortho foodforthoufoodforthougfoodforthoughtfoodforthought.jfoodforthought.jpfoodforthought.jpg

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.

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:

Listado de Código 1.19:$ MYFOO="chickensoup.tar.gz"$ echo ${MYFOO%%.*}chickensoup$ echo ${MYFOO%.*}chickensoup.tar

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 finalMYFOOD="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:

Listado de Código 1.21: Seleccionar una sub-cadena específica

Page 7: Programacion Bash

$ EXCLAIM=cowabunga$ echo ${EXCLAIM:0:3}cow$ echo ${EXCLAIM:3:7}abunga

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.

Poniendo en práctica el corte de cadenas

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á:

Listado de Código 1.22: mytar.sh -- un script de ejemplo#!/bin/bash

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:

Listado de Código 1.23: Probar el script$ ./mytar.sh thisfile.tarThis appears to be a tarball.$ ./mytar.sh thatfile.gzAt first glance, this does not appear to be a tarball.

OK, funciona, pero no es muy funcional. Antes de que lo hagamos más útil, echémosle una mirada a la

construcción "if" usada arriba. En ella, tenemos una expresión booleana. En bash, el operador de

comparación "=" chequea la igualdad de cadenas. En bash, todas las expresiones booleanas son encuadradas

en corchetes. ¿Pero qué es lo que la expresión booleana prueba en realidad? Echémosle una mirada a la

parte izquierda. De acuerdo con lo que hemos aprendido sobre corte de cadenas, "${1##*.}" eliminará la

sub-cadena más larga que encaje con "*." del principio de nuestra cadena contenida en la variable de

entorno "1", devolviendo el resultado. Esto causará que se devuelva todo lo escrito luego del último ".".

Obviamente, si el archivo termina en ".tar", tendremos "tar" como resultado, y la condición se cumplirá.

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":

Page 8: Programacion Bash

Listado de Código 1.24: Forma básica de la instrucción ifif [ condition ]then actionfi

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".

Listado de Código 1.25: Chequear condiciones antes de seguir después del fiif [ condition ]then actionelif [ condition2 ]then action2...elif [ condition3 ]then

else actionxfi

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!ñ

Page 9: Programacion Bash

Bash by example

Bash con ejemplos, parte 2

1. Más fundamentos de programación en bash

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:

Listado de Código 1.1: Referente a los argumentos pasados al guión:#!/usr/bin/env bash

echo el nombre del guión es $0echo el primer argumento es $1echo el segundo argumento es $2echo el decimoséptimo argumento es $17echo el número de argumentos es $#

El ejemplo es autoexplicativo, excepto por dos detalles. Primero: "$0" se expande al nombre del propio guión,

tal y como se ha llamado en la línea de comandos; y, segundo: "$#" se expande al número de argumentos

pasados al guión. Juega un poco con el guión anterior, pasándole distintos tipos de argumentos para

familiarizarte con su manera de funcionar.

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.

Esquemas de programación en bash

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.

Page 10: Programacion Bash

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":

Listado de Código 1.2: Operadores de comparación en bashif [ -z "$mivar" ]then echo "mivar no está definida"fi

A veces hay varias formas de realizar una misma comparación, los siguientes ejemplos funcionan de forma

idéntica:

Listado de Código 1.3: Dos formas de hacer una comparaciónif [ "$mivar" -eq 3 ]then echo "mivar igual a 3"fi

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.

Peculiaridades de la comparación de cadenas

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:

Listado de Código 1.4: Ejemplo de comparación defectuosaif [ $mivar = "foo bar oni" ]then echo "si"fi

En el ejemplo de arriba, la condición se cumplirá tan solo si $mivar contiene la cadena "foo". Si contuviera

una cadena con espacios, como "foo var oni", el guión fallará con este error:

Listado de Código 1.5: Error cuando la variable contiene espacios[: too many arguments

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:

Page 11: Programacion Bash

Listado de Código 1.6: Comparación final[ foo bar oni = "foo bar oni" ]

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:

Listado de Código 1.7: Forma correcta de escribir comparacionesif [ "$mivar" = "foo bar oni" ]then echo "si"fi

Este código funcionará como se espera, sin sorpresas desagradables.

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:

Listado de Código 1.8: Ejemplo básico de for#!/usr/bin/env bash

for x in uno dos tres cuatrodo echo número $xdone

Salida:número unonúmero dosnúmero tresnú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:

Listado de Código 1.9: Usando comodines estándar del intérprete#!/usr/bin/env bash

for miarchivo in /etc/r*do if [ -d "$miarchivo" ] then echo "$miarchivo (dir)" else echo "$miarchivo"

Page 12: Programacion Bash

fidone

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.ñ

Podemos usar múltiples comodines o incluso variables de entorno en la lista de palabras:

Listado de Código 1.10: Comodines múltiples y variables de entornofor x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*do cp $x /mnt/mydiradone

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:

Listado de Código 1.11: Usando rutas relativasfor x in ../* miscosas/*do echo $x es un fichero inútildone

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:

Listado de Código 1.12: Recortar ruta antepuesta a un nombre de archivofor x in /var/log/*do echo `basename $x` es un fichero contenido en /var/logdone

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:

Listado de Código 1.13: Ejemplo de uso de la variable $@#!/usr/bin/env bash

for cosa in "$@"

Page 13: Programacion Bash

do echo has escrito ${cosa}.done

salida:

$ allargs hola a todoshas escrito holahas escrito ahas escrito todos

Aritmética del intérprete

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:

Listado de Código 1.14: Contando en bash$ echo $(( 100 / 3 ))33$ mivar="56"$ echo $(( $mivar + 12 ))68$ echo $(( $mivar - $mivar ))0$ mivar=$(( $mivar + 1 ))$ echo $mivar57

Ahora que ya estás familiarizado con las operaciones matemáticas, es el momento de presentar dos nuevos

esquemas iterativos: "while" y "until".

Más esquemas iterativos: "while" y "until"

Una sentencia "while" se ejecutará iterativamente mientras una condición dada sea cierta, y tiene el siguiente

formato:

Listado de Código 1.15: Modelo de sentencia whilewhile [ condición ]do comandosdone

Las sentencias "while" se pueden usar típicamente para ejecutar una tarea un número dado de veces, como

en el ejemplo siguiente:

Listado de Código 1.16: Repetir la sentencia 10 vecesmivar=0while [ $mivar -ne 10 ]do echo $mivar mivar=$(( $mivar + 1 ))done

Como puedes ver, usamos las expansiones matemáticas de bash para asegurarnos de que, eventualmente, la

condición se rompa, y así también el bucle.

Page 14: Programacion Bash

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:

Listado de Código 1.17: Ejemplo de bucle untilmivar=0until [ $mivar -eq 10 ]do echo $mivar mivar=$(( $mivar + 1 ))done

Sentencias case

Las sentencias case "Case" son esquemas condicionales, un ejemplo:

Listado de Código 1.18: Ejemplo de código usando casecase "${x##*.}" in gz) gzunpack ${SROOT}/${x} ;; bz2) bz2unpack ${SROOT}/${x} ;; *) echo "formato de archivo no reconocido." exit ;;esac

En este ejemplo, bash expande "${x##*.}". En el programa, "$x" es el nombre de un archivo, y "${x##*.}"

tiene el efecto de recortar todo el texto excepto el que vaya tras el último punto en el nombre del archivo.

Tras eso bash compara lo que queda con los valores listados en los apartados marcados con ")". El resultado

de "${x##*.}" se compara por tanto con "gz", luego "bz2" y finalmente "*". Si "${x##*.}" coincide con alguno

de dichos patrones o cadenas, las líneas tras el paréntesis ")" se ejecutan, hasta los dos punto y coma

siguientes. En ese punto, bash continúa ejecutando las líneas que haya tras la palabra de cierre "esac". Si

ningún patrón o cadena coincide, ninguna línea de código dentro de case se ejecuta. No obstante, en este

caso concreto, al menos una línea será ejecutada, debido al uso de un comodín "*" como una de las

posibilidades, que coincidirá con todo lo que no coincidan "gz" o "bz2".

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:

Listado de Código 1.19: Ejemplo de definición de funcióntarview() { echo -n "Mostrando contenido de $1 " if [ ${1##*.} = tar ] then echo "(tar descomprimido)" tar tvf $1 elif [ ${1##*.} = gz ] then echo "(tar comprimido con gzip)" tar tzvf $1

Page 15: Programacion Bash

elif [ ${1##*.} = bz2 ] then echo "(tar comprimido con bzip2)" cat $1 | bzip2 -d | tar tvf - fi}Nota: Este mismo ejemplo de arriba podría escribirse usando una sentencia "case". ¿Podrías tú decirnos como?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.

Listado de Código 1.20: Llamando a la función de arriba$ tarview shorten.tar.gzDisplaying contents of shorten.tar.gz (gzip-compressed tar)drwxr-xr-x ajr/abbot 0 1999-02-27 16:17 shorten-2.3a/-rw-r--r-- ajr/abbot 1143 1997-09-04 04:06 shorten-2.3a/Makefile-rw-r--r-- ajr/abbot 1199 1996-02-04 12:24 shorten-2.3a/INSTALL-rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE....

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.

Visibilidad de las variables

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:

Listado de Código 1.21: Tratamiento de variables en bash#!/usr/bin/env bash

mivar="hola"

mifunc() {

mivar="un dos tres" for x in $mivar do echo $x

Page 16: Programacion Bash

done}

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:

Listado de Código 1.22: Asegurándose de no sobreescribir variables globales#!/usr/bin/env bash

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!

Page 17: Programacion Bash

Bash by example

Bash con ejemplos, Parte 3

1. Explorando el sistema de ebuilds

Entra en el sistema de ebuilds

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ónlinux El kernel actual

util-linuxUna colección de programas variados relacionados con Linux

e2fsprogs Una colección de utilidades relacionadas con ext2glibc 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.

¿Por qué bash?

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

Page 18: Programacion Bash

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.

Revista al proceso de construcción

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:

Listado de Código 1.2: Uncompressing sed into a temporary directory$ cd sed-3.02$ ./configure --prefix=/usr(autoconf generará los archivos make adecuados, esto puede tardar)

$ make

(el paquete se compila desde fuente, también tardará un poco)

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:

Listado de Código 1.3: Script bash de ejemplo para desempaquetar y compilar#!/usr/bin/env bash

if [ -d work ]then# remove old work directory if it exists rm -rf workfimkdir workcd worktar xzf /usr/src/distfiles/sed-3.02.tar.gzcd sed-3.02./configure --prefix=/usrmake

Page 19: Programacion Bash

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:

Listado de Código 1.4: Un script nuevo, más genérico#!/usr/bin/env bash

# P es el nombre del paquete

P=sed-3.02

# A es el nombre del archivo comprimido

A=${P}.tar.gz

export ORIGDIR=`pwd`export WORKDIR=${ORIGDIR}/workexport SRCDIR=${WORKDIR}/${P}

if [ -z "$DISTDIR" ]then# DISTDIR es /usr/src/distfiles si no ha sido definido ya DISTDIR=/usr/src/distfilesfiexport 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=/usrmake

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:

Page 20: Programacion Bash

Listado de Código 1.5: sed-3.02.ebuild#fichero ebuild para sed - ¡simple!P=sed-3.02A=${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:

Listado de Código 1.6: The ebuild script#!/usr/bin/env bash

if [ $# -ne 1 ]then echo "se esperaba un argumento." exit 1fi

if [ -e "$1" ]then source $1else echo "ebuild $1 no encontrado." exit 1fi

export ORIGDIR=`pwd`export WORKDIR=${ORIGDIR}/workexport SRCDIR=${WORKDIR}/${P}

if [ -z "$DISTDIR" ]then # DISTDIR será /usr/src/distfiles si no está ya definido DISTDIR=/usr/src/distfilesfiexport 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=/usrmake

Ahora que hemos dividido nuestro sistema en dos ficheros, apuesto a que te estarás preguntando como

funciona. Fácil, para compilar sed, escribe:

Listado de Código 1.7: Probando el ebuild$ ./ebuild sed-3.02.ebuild

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

Page 21: Programacion Bash

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:

Listado de Código 1.8: gzip-1.2.4a.ebuild#Otro script ebuild realmente simpleP=gzip-1.2.4aA=${P}.tar.gz

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í:

Listado de Código 1.9: ebuild, revisión 2#!/usr/bin/env bash

if [ $# -ne 2 ]then echo "Por favor, especifique dos argumentos, el fichero .ebuild y" echo "unpack, compile or all" exit 1fi

if [ -z "$DISTDIR" ]then# DISTDIR será /usr/src/distfiles si no está ya definido DISTDIR=/usr/src/distfilesfiexport 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}."

Page 22: Programacion Bash

#el código fuente está descomprimido}

ebuild_compile() {

#nos aseguramos de estar en el directorio correcto cd ${SRCDIR} if [ ! -d "${SRCDIR}" ] then echo "${SRCDIR} no existe -- por favor, descomprima primero el paquete." exit 1 fi ./configure --prefix=/usr make}

export ORIGDIR=`pwd`export WORKDIR=${ORIGDIR}/work

if [ -e "$1" ]then source $1else echo "Ebuild $1 no encontrado." exit 1fi

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.

También he a adido un test al principio de la funciónñ ebuild_compile(). Ahora comprueba que el

directorio $SRCDIR existe, y, si no, imprime un mensaje de error diciéndole al usuario que desempaquete el

Page 23: Programacion Bash

archivo y sale. Si lo prefieres, puesdes cambiar el comportamiento de forma que, si $SRCDIR no existe,

nuestro ebuild desempaquete automáticamente el archivo. Puedes hacerlo

cambiando ebuild_compile() por esta nueva versión:

Listado de Código 1.10: Nueva versión de ebuild_compile()ebuild_compile() { #nos aseguramos de estar en el directorio correcto if [ ! -d "${SRCDIR}" ] then ebuild_unpack fi cd ${SRCDIR} ./configure --prefix=/usr make}

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:

Listado de Código 1.11: Acción predeterminada$ ebuild sed-3.02.ebuild

Obtendremos un mensaje de error, porque ebuild ahora necesita que le digamos qué hacer, de esta forma:

Listado de Código 1.12: Descomprimir$ ebuild sed-3.02.ebuild unpack

or:

Listado de Código 1.13: Compilar$ ebuild sed-3.02.ebuild compile

or:

Listado de Código 1.14: Descomprimir y compilar$ ebuild sed-3.02.ebuild allImportante: Si se suministra un segundo parámetro distinto de los usados más arriba, se obtiene un mensaje de error (caso *), y el programa termina.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:

1. Si hay un script configure en ${SRCDIR}, ejecutarlo de esta forma: ./configure --prefix=/usr.

De otro modo, saltarse este paso.

2. Ejecutar el comando siguiente: make

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

Page 24: Programacion Bash

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:

Listado de Código 1.15: ebuild_compile() separada en dos funcionesuser_compile() { #estamos en ${SRCDIR} if [ -e configure ] then #run configure script if it exists ./configure --prefix=/usr fi #run make make}

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:

Listado de Código 1.16: e2fsprogs-1.18.ebuild#este fichero ebuild redefine user_compile()P=e2fsprogs-1.18A=${P}.tar.gz

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

Page 25: Programacion Bash

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:

Listado de Código 1.17: /ect/ebuild.conf# /etc/ebuild.conf: configuraciones globales de ebuild

# MAKEOPTS son las opciones pasadas a makeMAKEOPTS="-j2"

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.

Y aquí tenemos la versión final de ebuild:

Listado de Código 1.18: ebuild, la versión final#!/usr/bin/env bash

if [ $# -ne 2 ]then echo "Por favor, especifique fichero ebuild file y unpack, compile u all" exit 1fi

source /etc/ebuild.conf

if [ -z "$DISTDIR" ]then # configura DISTDIR como /usr/src/distfiles si no está configurado ya DISTDIR=/usr/src/distfilesfiexport DISTDIR

ebuild_unpack() { #se asegura de estar en el directorio correcto

Page 26: Programacion Bash

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 $1else echo "Fichero .ebuild $1 no encontrado." exit 1fi

export SRCDIR=${WORKDIR}/${P}

case "${2}" in unpack) ebuild_unpack ;; compile) ebuild_compile ;; all)

Page 27: Programacion Bash

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:

• Descarga las fuentes de forma automática si no están en $DISTDIR

• Verifica que las fuentes no están corruptas, usando sumas MD5

• Si se especifica, instala el programa compilado en un sistema de archivos, en vivo, manteniendo un

listado de los ficheros instalados de forma que el paquete pueda ser fácilmente desinstalado en

cualquier momento.

• 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.