Empleando git y subversion

Emplear dos sistemas de control de versiones parece algo fuera de lugar a la hora de llevar un proyecto. Sin embargo muchas pueden ser las causas en las que emplear subversion y git puede ser muy provechoso. Por ejemplo en el caso de estos dos escenarios el empleo de ambos sistemas de versionado esta justificado:

– Repositorio central con subversion y posibilidad de trabajar offline con git en máquina local. Cuando el desarrollador vuelva a estar online puede comitear los cambios al repositorio central de subversion. Con tu copia de trabajo puedes trabajar offline puedes decir. Si pero no tienes ninguna de las capacidades de un control de versiones disponible.

– Repositorio central con subversion donde no se desea aumentar el número de ramas. Los desarrolladores pueden hacerse con el código en git y hacer ramas y otros repositorios clonados mientras dure el desarrollo de una característica específica luego se reúne el código y se comitea a subversión.

Veamos como puedes mezclar lo mejor de los dos mundos.

Para poder empezar debemos tener instalado subversion git y git-svn. En distros tipos debian/ubuntu podemos ejecutar:

$ sudo apt-get install subversion git git-svn

Para conocer un poco de subversion sigue la serie de artículos sobre subversion.

Para conocer un poco de git visita este tutorial introductorio sobre git

Ok pues partimos de que tenemos la rama principal de desarrollo (trunk) con un repositorio central subversion en esta url

http://svn.midominio.com/miproyecto/trunk

creamos un directorio para nuestra copia de trabajo y entramos en ella

$ mkdir working-copy
$ cd working-copy

A continuación para inicializar el repositorio git ejecutamos:

$ git-svn init http://svn.midominio.com/miproyecto/trunk

Para traernos el código de la revisión HEAD:

$ git-svn fetch -rHEAD

Trabajamos con normalidad con git y comiteamos los cambios primero en este repositorio:

$ git commit -m “Cambios en git”

Si queremos actualizar el repositorio gitr respecto a posibles cambios en el repositorio central ejecutamos

$ git-svn rebase

Y si queremos comitear a subversion

$ git-svn dcommit

Si encontramos un conflicto debemos resolverlo de la siguiente manera

Tras comitear en git

$ git commit -m “Tal vez encontremos conflictos”

Pedimos actualización (si primero ejecutamos un git-svn dcommit nos dará error ‘Transaction is out of date’):

$ git-svn rebase

Esto nos informará de los ficheros con conflictos. Editamos el/los ficheros con conflictos hasta resolverlos. Luego comiteamos en git estos cambios con:

$ git add .

y luego

$ git rebase –continue

para finalmente ejecutar:

$ git-svn dcommit

Que llevará los cambios al repositorio central de subversion.

Subversion y sus mejores recetas (3ª parte)

En la tercera parte de este recetario sobre subversion hablaremos de tags y ramas. Haciendo incapié en su gestión y en como se emplean las ramas en situaciones reales de desarrollo de software.

Puedes encontrar la otras partes de este artículo a continuación:

Subversion y sus mejores recetas (1ª parte)

Subversion y sus mejores recetas (2ª parte)

*Creación de un nuevo tag (etiqueta) del head de trunk*

svn copy file:///var/svn/newrepos/myproject/trunk file:///var/svn/newrepos/myproject/tags/miprimertag -m “Creación de mi primer tag”

*Crear un nuevo tag de tu copia de trabajo*

(siendo micopiadetrabajo el directorio donde está tu copia de trabajo)
svn copy micopiadetrabajo file:///var/svn/newrepos/myproject/tags/misegundotag -m “Tag de mi copia de trabajo”

*Borrar un tag (o una rama)*

svn delete file:///var/svn/newrepos/myproject/tags/misegundotag -m “Eliminando tag obsoleto que ya no se usan”
(Recuerda que siempre puedes recuperar este tag o rama borrado)

*Recuperando un tag (o una rama) borrada*

(suponiendo que el borrado del tag fue en la revisión 215)
svn copy file:///var/svn/newrepos/myproject/tags/[email protected] file:///var/svn/newrepos/myproject/tags/misegundotag -m “Restaurando tag”

Lo habitual al desarrollar software es desarrollar sobre una rama principal (trunk) y cada cierto tiempo (dependiendo de la política del proyecto) se saque una versión nueva del proyecto generando una rama llamada estable, esta rama estable no agregará ninguna nueva característica y sólo se trabajará sobre ella para arreglar bugs. Las nuevas características y mejoras (y también las correcciones de bugs) se aplicarán sobre la rama principal sin que se molesten los dos desarrollos. La rama estable suele tener una vida que depende de las características del proyecto y puede ser de tan sólo unos pocos días a varios años, durante este tiempo se da soporte a esa versión del proyecto.

Subversion y sus mejores recetas (2ª parte)

¿Quien ha dicho que segundas partes nunca fueron buenas?

Continuando con la serie sobre subversión (subversión 1ª parte) seguimos con recetas rápidas para el uso de este sistema de control de versiones. En esta parte explicaremos algunas recetas para poder utilizar ramas de desarrollo paralelas.

*Eliminar bloqueos (locks) que hayan quedado pendientes*

$ svn cleanup
(a veces quedan locks sobre ficheros en tu copia de trabajo. Se retiran con este comando. Si alguna vez subversion se queja de un lock en tu copia de trabajo prueba este comando. Por ejemplo, a veces, svn status puede devolver una L (lock) sobre algún fichero, entonces hay que emplear este comando)

Concepto importante: Revisiones (revisions)

Una operación de commit (svn commit) publica cambios en cualquier cantidad de ficheros y/o directorios en una transacción atómica. Es decir las modificaciones que realices implicadas en un commit se realizan de manera indivisible, no existe posibilidad de que algún otro desarrollador vea o descargue en su copia de trabajo una modificación parcial de tu commit, lo ve todo o nada (si esta viendo o tiene una revisión no actualizada anterior a la tuya). Cada uno de estos commits crean una revisión que tiene asignado un número entero que comienza en cero (repositorio vacío) y que se va incrementando de 1 en 1 por cada commit exitoso. Cada revisión genera un nuevo “sistema de ficheros” que contiene el estado del repositorio después de los cambios efectuados por todos los commits anteriores a esta revisión incluyéndola.

*Obtener la copia de trabajo de una fecha especifica en lugar de un número de revisión*

$ svn checkout -r {“2006-02-17 15:30”}
(Obtiene como copia de trabajo la release más cercana anterior a la fecha 2006-02-17 15:30)

*Crear una nueva rama de desarrollo (branching)*

$ svn copy file:///var/svn/newrepos/myproject/trunk file:///var/svn/newrepos/myproject/branches/myfirstbranch -m “Creada una nueva rama llamada myfistbranch”
(Crea un nueva rama de desarrollo incluyendo un commit para que esta rama ya esté disponible en el repositorio)

*Obtener una copia de trabajo de una rama de desarrollo*

$ svn checkout file:///var/svn/newrepos/myproject/branches/myfirstbranch

*Mantener tu rama de desarrollo en sincronía con la rama principal (trunk)*

(Desde la copia de trabajo de tu rama de desarrollo, asegúrate de que tu copia de trabajo esta limpia svn status limpio)
$ svn commit -m “Llevar los cambios locales a la rama de desarrollo”
(Si creamos el branch en la revisión 9)
$ svn merge -r 9:HEAD ^/trunk
(mezclar los cambios procedentes de la rama principal (trunk) a tu copia de trabajo. Es posible que surjan conflictos que haya que resolver, el acento circunflejo es un alias de la url del repositorio)
$ svn commit -m “Actualizando rama con trunk”
(Se actualiza esta rama de desarrollo)

*Mezclando los cambios de tu rama con la rama principal (trunk)*

$ svn commit -m “Llevar los cambios locales a la rama de desarrollo”
$ svn merge -r 9:HEAD file:///var/svn/newrpos/myproject/trunk
(Volvemos a poner en sincronía la nueva rama)
$ svn commit -m “Actualizando rama con trunk”
$ svn merge –reintegrate ^/branches/myfirstbranch
(la rama es terminada aquí y no se debe trabajar con ella)
$ svn commit -m “Mezclar cambios en nueva rama a la rama principal”

*Borrar rama*

$ svn delete ^/branches/myfirstbranch -m “Eliminando rama myfirstbranch”
(Se borra pero el historial se puede revisar/reutilizar)

*Llevar tu working copy a otra rama de desarrollo*

$ svn switch ^/branches/myfirstbranch

*Información sobre la copia de trabajo actual*

$ svn info

*Obtener información de mezclado de cambios que ya han sido llevados de la rama principal (trunk) a la nueva rama de desarrollo*

(copia de trabajo es la nueva rama de desarrollo, /branches/myfirstbranch)
$ svn mergeinfo file:///var/svn/newrepos/myproject/trunk

*Obtener cambios que aún deben ser mezclados desde la rama principal (trunk) a la nueva rama de desarrollo*

(copia de trabajo es la nueva rama de desarrollo, /branches/myfirstbranch)
$ svn mergeinfo file:///var/svn/newrepos/myproject/trunk –show-revs eligible

*Obtener informe de resultados de una mezcla sin hacerla realmente*

(copia de trabajo es la nueva rama de desarrollo, /branches/myfirstbranch)
$ svn merge file:///var/svn/newrepos/myproject/trunk –dry-run

*Echar atrás cambios ya realizados (comiteados)*

(suponiendo que queremos echar atrás la revisión 101 que contiene graves errores)
$ svn merge -r 101:100 file:///var/svn/newrepos/myproject/trunk

ó

$ svn merge -c -101 file:///var/svn/newrepos/myproject/trunk
(actualizas tu copia de trabajo con lo que esta en la revisión 100)
$ svn diff
(compruebas los cambios)
$ svn commit -m “Deshaciendo los cambios de la revisión 101”

*Recuperar un fichero borrado en una revisión anterior*

(Tenemos un fichero llamado para_borrar.txt que se borró en la revisión 32 y queremos recuperarlo
$ svn copy http://svn.example.com/repos/calc/trunk/[email protected] ./para_borrar.txt
$ svn status
(Verificamos que se va a añadir)
$ svn commit -m “Recuperamos para_borrar.txt borrado en la revisión 32”

Así lo recuperaríamos con historial si por cualquier motivo no quisiéramos el historial de este archivo recuperado tenemos otra opción

*Recuperar un fichero borrado en una revisión anterior eliminando su historial*

$ svn cat http://svn.example.com/repos/calc/trunk/[email protected] > ./para_borrar.txt
$ svn add para_borrar.txt
$ svn commit -m “Re-creado el fichero para_borrar.txt de la revisión 31”

*Recupera un fichero borrado en una revisión anterior sin tener una copia de trabajo*

$ svn copy http://svn.example.com/repos/calc/trunk/[email protected] http://svn.example.com/repos/calc/trunk/para_borrar.txt -m “Recuperamos para_borrar.txt borrado en la revisión 32”

Espero que os haya sido de utilidad.

Como crear un extensión para Firefox

Tutorial paso a paso de como crear una extensión, plugin o addon para firefox. Solamente es necesario tener conocimientos de xul (un lenguaje de marcas xml), javascript y algunas herramientas básicas de edición/compresión de ficheros. Ahora ya no parece tan complicado, ¿verdad?. Con muy poco esfuerzo podrás crear un pequeño plugin para firefox. Adaptarlo para crear el tuyo propio es sencillo pues explico para que sirve cada una de las cosas que haremos durante el tutorial. Espero que te sirva de ayuda pero sólo podrás utilizarlo a condición de que dejes algún pequeño feedback como comentario al final para poder adaptar y mejorar el texto (¡no te olvides de esto!).

Dicho esto comencemos como siempre por el principio:

Para comenzar comentaros que al crear la extensión necesitaremos cambiar de una manera bastante peligrosa nuestro navegador (de hecho, está prácticamente garantizado que echaremos a perder nuestra instalación de firefox). Es por eso que una de las maneras más seguras de actuar es creando un perfil especial para nuestras pruebas. Para generar este perfil debemos primero ejecutar nuestro firefox con la opción –profilemanager

$ firefox –profilemanager

A la hora de arrancar veras que te aparece una ventana donde se muestran los perfiles actuales (default) y te permite crear uno nuevo, borrar perfiles renombrar y utilizar un perfil en concreto. Puedes hacer, además que se lanze esta ventana cada vez que ejecutar firefox o trabajar offline si lo deseas. En este punto vamos a crear un nuevo perfil que llamaremos test además forzaremos que se nos muestre este menu en los próximos arranques de la máquina (eliminando el chekbox de “Don’t ask at startup”). También es posible arrancar firefox con un perfil determinado con la opción -P así si guardamos el perfil test la próxima vez podríamos arrancar utilizando

$ firefox -P test

ó

$ firefox -P default

Empleando directamente el perfil adecuado. Podemos incluso crear accesos directos a cada uno de estos perfiles desde accesos directos en nuestro escritorio

En principio sólo puedes tener en ejecución un tipo de perfil en firefox aunque tengas diversas ventanas independientes. Pero puedes eliminar esta limitación añadiendo el parámetro –no-remote al arrancar.

$ firefox -P test –no-remote

y

$ firefox -P default -no-remote

¡Tendras dos firefox con extensiones, bookmarks, cookies etcereta completamente diferentes!

En este perfil de test es donde desarrollaremos nuestra propia extensión. Es importante acceder a la página about:config (sólo tienes que poner about:config en al barra de direcciones) y modificar algunos parámetros que nos ayudarán en nuestro desarrollo. Estos parámetros suponen una penalización en el rendimiento de firefox para este perfil pero son realmente necesarios para nuestro desarrollo. Estos son:

*javascript.options.showInConsole = true* ok

Muestra los errores de los ficheros del chrome en la consola de errores (luego explico que es el chrome)

*javascript.options.strict = true* ok

Habilita el modo strict para javascript para avisos en la consola.

*extensions.logging.enabled = true* ok

Envía información más detallada a la consola de errores sobre problemas de instalación y actualización.

Para crear un extensión hay que crear en un archivo comprimido mediante zip con la extensión .xpi con un conjunto de ficheros que contienen datos sobre la extensión, código de la extensión, xul y recursos propios a la extensión. Si vas a servir tu extensión desde tu página web asegúrate que tu servidor envia las cabeceras adecuadas al descargar al extensión .xpi. Para ello basta configurar tu apache con

AddType application/x-xpinstall .xpi

Vemos ahora que debe contener este fichero .xpi

En su raiz un fichero llamado install.rdf con la información básica sobre la extensión

Subversion y sus mejores recetas

En este primer artículo haré un repaso a los comandos más comunes de subversion y a su utilización en un escenario real. Empezando desde la creación del repositorio hasta la generación de una nueva release para su salida a producción. En otros artículos extenderé la temática al empleo de ramas paralelas de desarrollo y la configuración de servidores locales y remotos de subversion. Empecemos por el principio

*Crear un nuevo repositorio*

$ sudo mkdir /var/svn
(creamos el directorio para albergar nuestros repositorios)
$ sudo chown $USER.$USER /var/svn
(asignamos este directorio al usuario actual para evitar problemas de permisos)

$ svnadmin create /var/svn/newrepos
(creamos un nuevo repositorio en /var/svn/newrepos
$ svn list file:///var/svn/newrepos
(listamos los ficheros incluidos en este repositorio, vacío de momento)

*Importar ficheros al repositorio*

$ mkdir -p tmp/branches tmp/tags tmp/trunk
(creamos los primeros directorios para introducir en el repositorio)
$ svn import tmp file:///var/svn/newrepos/myproject -m “Importación inicial”
(importamos el contenido del directorio tmp al repositorio dentro de la carpeta myproject con el commentario Importación inicial. myproject no es un directorio real dentro del sistemas de archivos es gestionado internamente por subversion)

*Preparar nuestra copia de trabajo*

$ mkdir myproject
(creamos el directorio donde situaremos nuestra copia de trabajo)
$ svn checkout file:///var/svn/newrepos/myproject/trunk .
(obtenemos los ficheros del directorio trunk del repositorio, en nuestro caso aún está vacío)

*Añadir nuevos ficheros a nuestro proyecto*

$ vi README
(creamos un nuevo fichero dentro de la copia de trabajo)
$ svn add REAME
(añadimos el fichero a los programados para ser añadidos al repositorio)
$ svn commit -m “Añadimos primer fichero README”
(enviamos los cambios al repositorio añadiendo el fichero README al mismo, el texto entrecomillado después del -m es un comentario sobre este envío)

*Sincronizando con el repositorio*

$ svn update
(Sincronizando con el contenido del repositorio por si hay cambios en los ficheros)

*Copiar ficheros*

$ svn copy README README2
$ svn commit -m “README copiado a README2”

*Mover ficheros*

$ svn move README2 README3
$ svn commit -m “Moviendo README2 a README3”

*Borrar un fichero*

$ svn del README3
$ svn commit -m “Borrando README3”

*Creando un nuevo directorio*

$ svn mkdir src
$ svn commit -m “Creando directorio src”

Comandos de apoyo

$ svn log
(registro de cada cambio hasta el último svn update ejecutado)
$ svn list
(Lista de ficheros hasta el último svn update ejecutado)
$ svn status
(Lista de tareas programadas para el siguiente commit basados en los cambios efectuados en la copia actual. Leyenda: A – fichero a añadir, D – fichero a borrar, C – conflictos con respecto al repositorio, M – Modificaciones locales, ? fichero no controlado por el sistema de versionado)

*Modificar ficheros ya existentes y analizar cambios*

$ vi README
(editamos README modificando su contenido)
$ svn diff
(vemos los cambios en ficheros)
$ svn commit -m “Modificaciones en README”

*Deshacer modificaciones en tu copia de trabajo*

$ vi README
(editamos README e introducimos algún error)
$ svn diff
(observamos de nuevo estos cambios en README)
$ svn revert README
(deshacemos las modificaciones sobre README, revert es capaz de deshacer cualquier modificación programada para el siguiente commit)

Tratamiento de conflictos

*Anticipando posibles problemas antes de actualizar tu copia de trabajo*

$ svn update -u

*Actualizando con el repositorios (este retorna coflictos)*

$ svn update
U INSTALL
G README
Conflict discovered in ‘bar.c’.
Select: (p) postpone, (df) diff-full, (e) edit,
(h) help for more options:

Desde la versión 1.5 de subversion (interactive conflict resolution)

U (UPDATED) – fichero actualizado del repositorio (cambios sólo en el repositorio)
G (MERGED) – fichero actualizado del repositorio y con cambios locales combinados automáticamente
Conflict discovered in ‘xxxx’ – fichero actualizado en el repositorio y con cambios locales que se solapan. Hay que proceder a resolver el problema. Atención mientras no se resuelva el conflicto no se puede hacer commit de tus cambios al repositorio.

Las opciones son:

postpone – Deja en la copia local 3 archivos para ayudarte a resolver el conflicto. Suponiendo que el fichero en conflicto se llama filename estos son los 3 ficheros filename.mine (la versión en tu copia de trabajo), filename.r (la versión base de tu copia de trabajo, es decir, la del último update exitoso o en su defecto tu checkout), filename.r (la versión head del repositorio). Además filename incluye ahora el diff entre las dos versiones. La manera de resolver es:

*Si quieres aceptar los cambios procedentes del repositorio y eliminar tus cambios*

$ svn revert

ó

$ svn resolve –accept theirs-full filename

*Si quieres subir tu versión y eliminar los cambios que hayan llegado con el update*

$ svn resolve –accept mine-full filename

Recuerda que svn resolve eliminará las diferentes versiones del fichero conflictivo en tu copia de trabajo

*Si se editan los cambios*

$ vi filename
(se corrigen manualmente los conflictos)
$ svn resolve –accept working filename
(Se resuelve el conflicto)

$ svn commit -m “Conflicto resuelto en filename”
(Tras la resolución se envían los cambios al repositorio)

diff-full – Te devuelve las diferencias
edit – Edita directamente el archivo con las diferencias para resolver el conflicto desde este menu interactivo
resolved – Después de editarlo se declara el conflicto como resuelto
mine-full – Acepta tus cambios y elimina los procedentes del update
theis-full – Elimina tus cambios y acepta los procedentes del update
launch – Ejecuta un programa externo para resolver el conflicto (necesita preparación previa, introducir la variable de entorno SVN_MERGE o definir merge-tool-cmd en los ficheros de configuración de subversion. Al programa se le pasaran 4 ficheros (base, update, working y diff)
help – Muestra la lista de comandos disponibles

*Enviando tus modificaciones*

$ svn commit -m “Modificaciones”
(Envia los cambios de tu copia de trabajo al repositorio, si hay problemas y no se pueden enviar los cambios debes ejecutar un svn update para resolver conflictos tal y como comentaba previamente)

*Ver el contenido de un fichero en una versión en particular*

svn cat -r 3 README
(svn cat -r

*Registro de cambios de una revision*

$ svn log -r 4
(svn log -r )

*Registro de cambios entre revisiones*

$ svn log -r 3:5
(svn log -r :)

*Comparando tu copia de trabajo con el repositorio*

$ svn diff -r 3 README
(svn diff -r )

*Comparando distintas versiones del repositorio*

$ svn diff -r 3:4 README
(svn diff -r :

*Devolviendo fichero del repositorio*

$ svn cat -r 3 README
(svn cat -r

*Crear una copia de trabajo de una versión antigua de tu repositorio*

$ svn checkout -r 3
(svn checkout -r )
Atención, no se pueden enviar cambios desde esta copia de trabajo

*Actualizar tu copia de trabajo a una versión antigua de tu repositorio*

$ svn update -r 3 file:///var/svn/mirepo .
(svn update -r )
Atención, no se pueden enviar cambios desde esta copia de trabajo

*Generar release*

$ svn export file:///var/svn/newrepos/myproject/trunk

*Geneare release de una versión en particular*

$ svn export file:///var/svn/newrepos/myproject/trunk -r 3

Si os ha resultado útil esta artículo aquí tenéis otro con la 2ª parte sobre subversión.

Instalar las claves GPG de launchpad

Hay bastante gente que anda incluyendo las fuentes de launchpad para poder descargarse algunos paquetes no oficiales para Ubuntu. También hay gente que tiene problemas con la validación de los paquetes porque no tienen instalado la clave gpg adecuada para launchpad. Los síntomas del problema son los siguientes:

Abren una consola de root y ejecutan:

# apt-get update

Y tras algunos mensajes relacionados con la descarga de las últimas listas de paquetes actualizados le aparece por consola algo tal que así

W: GPG error: http://ppa.launchpad.net intrepid Release: The following signatures couldn’t be verified because the public key is not available: NO_PUBKEY 28A8205077558DD0

Y te sugiere que vuelvas a darle al apt-get update para solucionar el problema. La solución no esta ahí. En su lugar hay que ver el hash que aparece justo al final del mensaje de aviso en concreto en sus últimos 8 caracteres. En el ejemplo serían 77558DD0 pero en tu caso podrían ser otros (consulta la salida de tu apt-get update)

Para añadir las claves basta con ejecutar lo siguiente pero sustituyendo $key por esos 8 caracteres:

# gpg –keyserver subkeys.pgp.net –recv-key $key && gpg -a –export $key | sudo apt-key add –

El problema/molestia habrá desaparecido

Grabar el historial de la shell sincronamente

A todos los que utilizamos una shell de linux más o menos habitualmente nos ha pasado que hemos perdido información valiosa que creíamos que teníamos a buen recaudo en el historial.

Con el tiempo nos damos cuenta que el histórico de comandos que se graba en ~/.bash_history se actualiza cada vez que cerramos nuestra sesión (logout). Esto trae muchos problemas cuando tenemos abiertas varias sesiones simultáneamente ya que lo que grabamos en una al cerrar sesión se pierde cuando posteriormente cerramos otras sesiones. Las posibilidades de perder la información del histórico son muy grandes. Es raro que no se haya pensado en esto pues es algo muy común. Pues estamos equivocados nuevamente porque el problema se puede solucionar rápidamente con opciones de shell.

Basta con ejecutar

$ shopt -s histappend

Para que cuando se cierren una sesión de shell se añadan los comandos ejecutados en esta sesión al fichero de historial en lugar de sobrescribir su contenido.

Además también podemos hacer que los comandos se graben en el histórico justo después de ser ejecutados y no cuando se cierra la sesión. Basta con fijar esta variable de esta manera:

PROMPT_COMMAND=’history -a’

Hay que tener cuidado aquí si PROMPT_COMMAND ya ha sido inicializada y tiene algún valor podemos perderlo y es mejor hacer esto:

PROMPT_COMMAND=”history -a;$PROMPT_COMMAND”

Por su puesto podemos añadir estas dos lineas

shopt -s histappend
PROMPT_COMMAND=”history -a;$PROMPT_COMMAND”

a nuestro fichero ~/.bashrc para que se activen automáticamente estas opciones al inicio de la sesión. Y si tenemos acceso al root de la máquina y queremos que todos los usuarios tengan las mismas opciones activas podemos escribir las dos líneas en el fichero /etc/bash.bashrc

Bueno creo que este blog se esta convirtiendo poco a poco en un pequeño libro de recetas sobre pequeños problemas que van surgiendo al utilizar linux. Espero que sean de utilidad.

Globbing

La shell bash no es capaz por si sola ni de reconocer ni de analizar expresiones regulares. Sin embargo tiene un proceso para descubrir/detectar nombres de fichero de una manera más o menos automatizada conocida con el nombre de expansión de nombre de fichero o también globbing.

Mediante el globbing y empleando caracteres especiales como \*, ?, ^, -, comas paréntesis, llaves y corchetes; bash puede analizar expresiones que le permiten sustituir dicha expresión por el conjunto de ficheros que verifican dicha expresión.

Veamos cual es el significado de cada uno de estos caracteres y como funcionan con varios ejemplos:

\* Sirve para sustituir cualquier conjunto de caracteres, incluso un conjunto vacío que este presente en el nombre de los ficheros a detectar. No detectará implicitamente ficheros que comiencen por punto

Ejemplos:

$ echo \*
Todos los ficheros que no empiecen por punto

$ echo .\*
Todos los ficheros que empiecen por punto

$ echo a\*
Todos los ficheros que empiecen por ‘a’

$ echo \*a\*
Todos los ficheros que contengan al menos una ‘a’

? Sirve para sustituir 1 caracter de cualquier tipo (excepto el punto al comienzo del nombre del fichero como en el caso del asterisco).

$ echo ???
Todos los ficheros que tengan 3 caracteres por nombre

$ echo ?a\*
Todos los ficheros cuyo segundo caracter sea una ‘a’

[] Para rangos de caracteres separados por guión o un conjunto de caracteres
Ejemplos:

$ echo [a-z]\*
Todos los ficheros que empiecen por una letra minúscula del alfabeto inglés

$ echo \*[0-9]
Todos los ficheros que terminen con un número

$ echo \*[abc]\*
Todos los ficheros que contengan una a, una b o una c minúscula

{} lista separada con comas de expansiones válidas. Permite combinar aditivamente varios expresiones de globbing

Ejemplo:
$ echo {\*[abc]\*,\*[0-9],hola,???}

^ Negación de un rango de caracteres

Ejemplo:
$ echo [^0-9]\*
Ficheros que no comienzan por numero

Globbing admite un modo extendido que habilita mas wildcards y posibilidades. Para tenerlo disponible debemos ejecutar:

$ shopt -s extglob

+, !, @ estarían ahora disponibles como wildcards

Un interesante y sutil punto a tener en cuenta es que el globbing lo realiza directamente el interprete bash y no los programas que lancemos a través de él ni el sistema operativo. Esto quiere decir que el globbing lo realizará el bash antes de ejecutar el programa al que sólo le llegarán los nombres de ficheros que hayan sido detectados correctamente mediante globbing. En la inmensa mayoría de los casos esto carece de importancia pero en otros si que la tiene. Por ejemplo, cuando ejecutamos programas a través de exec() o su familia de funciones amigas. Si no ejecutamos el programa a través de bash no tendremos disponible la expansión de nombre de ficheros.

Existe otro caso bien conocido en el que esta característica se vuelve importante. Imaginemos una carpeta que contiene cientos de miles de ficheros digamos más de cien mil (en realidad con tener más de 65535 nos llega). Por algún motivo (que bien podría ser este) queremos borrar estos ficheros. Entramos en este directorio y escribimos

$ rm -rf \*

Pero rm se niega a ejecutarlo dando este mensaje de error

-bash: /bin/rm: Argument list too long

¿Que es lo que pasa?

Pues que bash ha pasado al programa rm una lista con más de 65535 argumentos que es más de los permitidos para que rm (o cualquier otro programa de bash) pueda ejecutar a causa del globbing. Para solucionar esto podemos ejecutar por ejemplo:

find -type f -exec rm -f {} \;

Cuidado esta instrucción no es equivalente a la anterior aunque si es válida en este escenario. La diferencia radica en que la instrucción con find también borrará ficheros que comiencen por punto que estén en el directorio actual. Sin embargo rm -rf * no los borrará (aunque si los ficheros que comiencen con punto incluidos en subdirectorios del directorio actual).

Llamadas al sistema linux (linux system calls)

A la hora de programar en C hay 3 tipos de funciones a las que podemos recurrir. Las funciones ordinarias internas a tu programa (desarrollo propio al programa), las funciones de librería que son funciones ordinarias que residen en una librería externa a tu programa (desarrollo propio o ajeno al programa). Como por ejemplo las funciones de la librería estandar C (libc). Una llamada a estas funciones es igual a cualquier llamada a una función. Los argumentos son situados en los registros del procesador o en la pila. La ejecución es transferida al comienzo del código de la función, que tipicamente está cargada en una librería compartida. Y, por último, las llamadas al sistema (system call).

Una llamada al sistema está implementada en el núcleo de Linux. Cuando un programa llama a una función del sistema, los argumentos son empaquetados y manejados por el núcleo, el cual toma el control de la ejecución hasta que la llamada se completa. Una llamada al sistema no es una llamada a una función ordinaria, y se requiere un procedimiento especial para transferir el control al núcleo. Sin embargo la librería GNU C encapsula la llamadas al sistema con funciones de manera que pueden ser llamadas de manera sencilla y se confunden con llamadas a funciones ordinarias. Las funciones de entrada/salida como open y read son ejemplos de llamadas al sistema en Linux.

El conjunto de llamadas al sistema Linux forman el interfaz más básico entre los programas y el núcleo de Linux.

Existen llamadas al sistema que sólo pueden ser ejecutados por el superusuario en caso contrario fallarán. Además una función de librería puede invocar una o más funciones de librería o llamadas del sistema como parte de su implementación.

Linux tiene más de 300 diferentes llamadas al sistema. Un listado de las llamadas al sistema de tu versión del kernel de linux se puede encontrar en /usr/include/asm/unistd.h actualmente este fichero incluye a otros dos uno para arquitecturas de 32 bits y otro para arquitecturas de 64 bits (unistd_32.h y unistd_64.h respectivamente)

Para poder introducirnos en las llamadas al sistema es necesario conocer strace. Strace (system trace) es un comando de la shell donde puedes depurar las llamadas al sistema de cualquier programa. strace te informa sobre las llamadas al sistema que realiza un programa además de las señales que recibe. Es por tanto un herramienta muy valiosa de depuración.

Para utilizar strace basta con escribir strace seguido del nombre del programa que quieres analizar, por ejemplo

$ strace hostname

strace basicamente ejecuta el comando especificado hasta que este finaliza. Strace intercepta y registra las llamadas al sistema que el programa ha ejecutado y las señales recibidas por el proceso. strace envia a stderror o a un fichero especificado con la opción -o el nombre de la llamada al sistema, sus argumentos y el valor de retorno de la llamada. Por ejemplo:

execve(“/bin/hostname”, [“hostname”], [/\* 43 vars \*/]) = 0

La llamada al sistema es execve. Los parámetros que se pasan al programa son los que se encuentran entre parámetros “/bin/hostname”, [“hostname”], [/\* 43 vars \*/] y el valor de retorno aparece despues del igual (0)

Para señales se imprime el simbolo de la señal y una cadena explicativa rodeado de ‘—‘ y ‘+++’. Por ejemplo:

— SIGFPE (Floating point exception) @ 0 (0) —
+++ killed by SIGFPE +++

En el caso de SIGKILL podemos ver:

<unfinished …>

Recordemos que esta señal no se puede enmascarar.

A continuación voy a analizar un par de llamadas al sistema bastante comunes que pueden servir como ejemplo. El primero es access que sirve para comprobar los permisos sobre un determinado fichero. Access puede comprobar cualquier combinación de permisos lectura, escritura y ejecución.

La llamada a access tiene 2 argumentos el primero es la ruta al fichero cuyos permisos queremos comprobar el segundo es un campo de bits tipo OR que puede contener R_OK, W_OK, X_OK que corresponden a verificar los permisos de lectura, escritura y ejecución. El valor de retorno es 0 si tiene todos los permisos especificados por el segundo parámetro. Si el fichero existe pero no tiene todos los permisos especificados devuelve -1 y errno cambia a EACCES (o a EROFS si se ha solicitado los permisos de escritura para un fichero de sólo lectura).

Si el segundo argumento es F_OK sólo se comprueba la existencia del fichero. Si el fichero existe devuelve 0 si no existe devuelve -1 y fija errno a NOENT. Si el directorio donde está el fichero es inaccesible errno se fija a EACCESS. Veamoslo con un ejemplo:

#include <error.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc,char** argv)
{
if(argc!=2)
{
printf(“Usage: %s file-path\n”,argv[0]);
return 1;
}
char* path = argv[1];
int sc;
sc = access(path,F_OK);
if(sc == 0) printf(“%s existe\n”, path);
else printf(“%s no existe o es inaccesible\n”, path);
return 0;
}

Otro ejemplo de llamada a sistema es fsync. Fsync permite sincronizar el contenido de un fichero con su copia en disco. Linux utiliza sincronizaciones asincronas entre la copia del fichero en memoria y la copia en disco. Para asegurarse de que ambas copias contienen la misma información. El sistema se encarga de hacer esto automaticamente pero es posible hacerlo manualmente a través de esta llamada. Este es un ejemplo.

#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
const char* journal_filename = “journal.log”;
void write_journal_entry (char* entry)
{
int fd = open (journal_filename, O_WRONLY | O_CREAT | O_APPEND, 0660);
write (fd, entry, strlen (entry));
write (fd, “\n”, 1);
fsync (fd);
close (fd);
}

int main(int argc, int argv)
{
write_journal_entry(“Esto va directo a disco”);
return 0;
}

Si compilas el fichero anterior y al ejecutable lo llamamos sync en el directorio actual podemos ejecutar

$ traceroute sync

Y así ver como traceroute nos muestra la llamada al sistema, sync().