Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Leer ficheros de configuración INI desde nuestros scripts en BASH


Una de las tareas que debemos hacer como programadores es la de facilitar al usuario la configuración de nuestros programas. Haciéndolos más flexibles y adaptables a las necesidades de cada individuo.
Y una forma muy fácil de definir configuración para nuestros programas es en ficheros con formato INI. Este formato se introdujo en los años 90 en versiones de Windows como la 3.1. En aquella época, teníamos en varios archivos con extensión INI la configuración de nuestro sistema Windows y teníamos que modificar los archivos y reiniciar Windows para que los cambios tuvieran efecto. Windows, por aquel entonces era un programa más que se ejecutaba en el ordenador, que trabajaba con MS-DOS.

¿Qué tienen esos archivos?

El contenido de los archivos es sencillo. No es más que un montón de líneas con claves y valores, con esta forma:

clave=valor

Es decir, la clave será una palabra (preferiblemente sin espacios ni símbolos, sólo letras y un guión bajo, teóricamente no debe contener ni punto y coma (;) ni almohadilla (#) porque están reservados para los comentarios; ni corchetes ([]) porque son parte de las secciones.
Eso sí, a partir del primer igual, hasta el final de la línea cualquier carácter formará parte del valor. Ya puede ser un número, letra, punto y coma, corchete.
Además, las claves pueden estar encerradas en una categoría o sección, para que sea sencillo, tanto para usuarios como programadores trabajar con muchas claves. Las secciones vendrán especificadas de la siguiente forma:

[seccion]

Estos ficheros también podrán contener comentarios, como dije antes, con ; o #. Y se usan por muchos programas para configurar ciertas partes o componentes de los mismos porque son archivos muy fáciles de leer por una máquina y muy fáciles de editar por un ser humano. No son perfectos, pero en muchos casos es lo único que necesitamos.

¿Por qué en BASH?

Normalmente las personas que trabajamos con scripts para Bash tenemos muchas formas de incluir configuración de nuestros scripts:

  • Dentro del propio script. Dentro del mismo script puedes incluir algunas líneas al principio con algunas definiciones. En muchos casos está muy bien.
  • En otro script. En Bash, podemos poner un punto, espacio y el nombre del fichero que queremos incluir. De esa forma, las variables declaradas en ese otro fichero serán visibles en el actual.
  • En un archivo no ejecutable. Lo malo de incluir archivos de Bash es que pueden contener código ejecutable. Y no deberíamos dejar que un usuario pueda ejecutar código donde no debe. Así que una buena opción es la de crear un archivo que debamos leer y parsear. Ya sea un archivo XML, JSON, YAML o INI. Tal vez sea la opción más lenta y más larga, pero será la más segura.

Ahora bien, Bash, por su forma de trabajar, presenta varios problemas en este aspecto. En el transcurso de nuestro programa deberemos obtener los valores para varias claves y, Bash se caracteriza por ejecutar todos los programas a los que llamamos de forma secuencial. Es decir, aunque está muy optimizado, si tenemos que llamar repetidas veces a sed, awk, grep, cut o cualquier otro programa, el tiempo de ejecución se va a resentir. Cada vez hay más órdenes nativas de Bash que nos evitan tener que cargar un programa nuevo en memoria y su consiguiente ejecución, destrucción, cambios de contexto y demás cosas que hacen los sistemas operativos modernos. Algunos ejemplos los podemos encontrar en este post: manejo de cadenas en Bash.

Múltiples opciones

En este post voy a poner varias formas de hacer las cosas. Nuestra gran responsabilidad será utilizar la que creamos conveniente en cada momento. Depende de nuestras necesidades en cada momento. Por ejemplo, si vamos a leer solo dos líneas de configuración, y no necesitamos secciones ni nada, podríamos utilizar una forma que es muy corta, y un poco lenta (total, para dos lecturas tampoco vamos a perder una eternidad). Pero por ejemplo, si nuestro fichero de configuración tiene 100 líneas, secciones y algunas partes inseguras (pedazo de script), seguro que nos conviene más utilizar un parseo del fichero de configuración más rápido y fijarnos un poco en la seguridad del sistema.

Todo esto lo iré explicando detalladamente.

Evaluando el código en Bash

Esto al final es como si incluimos el fichero en Bash, pero hacemos una pequeña transformación para que a Bash le guste un poco lo que le vamos a meter. Personalmente no me gusta esta opción porque no soluciona muchos problemas, nos permite ejecutar código desde el fichero de configuración, nos permite sobreescribir variables que ya tengamos en el código y algunas cosas más que lo hacen tremendamente inseguro, aunque es muy rápida.

Imaginemos que tenemos un fichero ini sencillo como este (simple.ini):

1
2
3
4
servidor=db.dominio.com
puerto=3306
usuario=armandoguerra
password=arreugodnamra32

Ahora, en nuestro código podemos hacer esto:

1
2
3
source (grep servidor simple.ini)

echo "Servidor: "$servidor""

Con este código capturaríamos la variable servidor dentro del fichero ini. En realidad, hacemos que se evalúe el contenido del fichero ini como si fuera de Bash. Si queremos evaluar el fichero completo para extraer todos los elementos podríamos hacer esto:

1
2
3
4
5
6
source (grep = simple.ini | sed -e 's/\s*=\s*/=/g' -e 's/^;/#/g')

echo "Servidor: "$servidor""
echo "Puerto: "$puerto""
echo "Usuario: "$usuario""
echo "Password: "$password""

Aquí extraemos todas las líneas que tengan un signo igual (=), luego con sed filtramos con dos expresiones, la primera elimina los espacios alrededor del igual (que a Bash no le gusta eso), y la segunda cambiará los ; por # sólo cuando una línea empiece por ; Todo eso se evaluará para extraer las variables.

Con awk leyendo cada línea

Podemos coger el mismo fichero simple.ini del ejemplo anterior.

Desde nuestro script para Bash queremos poder acceder al valor de servidor, puerto, usuario y password de una forma más o menos sencilla. Podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
CFG_FILE=simple.ini

SERVER=$(awk -F "=" '/servidor/ {print $2}' "$CFG_FILE")
PORT=$(awk -F "=" '/puerto/ {print $2}' "$CFG_FILE")
USER=$(awk -F "=" '/usuario/ {print $2}' "$CFG_FILE")
PASS=$(awk -F "=" '/password/ {print $2}' "$CFG_FILE")

echo "Servidor: "$SERVER""
echo "Puerto: "$PORT""
echo "Usuario: "$USER""
echo "Password: "$PASS""

Como no hay muchos elementos en la configuración podemos hacerlo llamando a awk y será rápido. Si lo preferimos, podemos crear una pequeña función que haga la lectura, para no tener que poner la línea de awk todo el rato:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CFG_FILE=simple.ini

function read_ini()
{
    local KEY="$1"
    local FILE="$2"
    awk -F "=" '/'"$KEY"'/ {print $2}' "$FILE"
}

SERVER=$(read_ini "servidor" "$CFG_FILE")
PORT=$(read_ini "puerto" "$CFG_FILE")
USER=$(read_ini "usuario" "$CFG_FILE")
PASS=$(read_ini "password" "$CFG_FILE")

echo "Servidor: "$SERVER""
echo "Puerto: "$PORT""
echo "Usuario: "$USER""
echo "Password: "$PASS""

Eso sí, se nos presentan algunos problemas:

  • Tenemos que saber que el fichero vamos a leerlo por completo 4 veces (tantas veces como lecturas hagamos) y las búsquedas de las palabras las haremos en todo el archivo. Lo que no es muy óptimo si tenemos muchas definiciones en la configuración.
  • Si tenemos varias veces la misma clave, veremos el valor completo de las dos claves. Es decir, si ponemos usuario dos veces, veremos los dos nombres de usuario seguidos al ver la variable (podemos solucionar esto con un exit dentro de awk, y aumentaremos algo el rendimiento).
  • Aunque el parseo es rápido no es exacto, si creamos una configuración en el INI llamada “nombre_usuario=test” ésta también se leerá como usuario. Y si comentamos un nombre de usuario, éste seguirá apareciendo.
  • Si ponemos espacios entre la clave y el igual o entre el igual y el valor, estos espacios figurarán en el valor obtenido. Deberíamos filtrarlos.
  • No tenemos secciones. Así que sólo servirá para cosas sencillas.

Vamos a completar un poco la llamada a awk en la función para solucionar algún problema, aunque el rendimiento bajará un 33% más o menos, aún así, sigue siendo rápido, pero realizaremos mejor el parseo:

1
2
3
4
5
6
function read_ini()
{
    local KEY="$1"
    local FILE="$2"
    awk -F "=" '/^\s*'"$KEY"'\s*/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}' "$FILE"
}

Ampliemos un poco más el script, para soportar secciones. Ahora tendremos un fichero ini así (secciones.ini):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[principal]
servidor=db.dominio.com
puerto=3306
; Este no vale
nombre_usuario = aksjddd;
;usuario = test
usuario = armandoguerra
password=arreugodnamra32

[secundario]
servidor=database.dom.com
puerto=4417
usuario=zacariaslabasura
password=arusabalsairacaz

Y nuestro fichero para realizar la lectura sería así:

1
2
3
4
5
6
7
function read_ini()
{
        local SECTION="$1"
    local KEY="$2"
    local FILE="$3"
    sed -n '/^\['$SECTION'\]/,/^\[.*\]/p' "$FILE" | awk -F "=" '/^\s*'"$KEY"'\s*/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}'
}

Podríamos probarlo con estas llamadas:

1
2
3
4
5
6
7
8
9
10
11
12
13
SERVER=$(read_ini "principal" "servidor" "$CFG_FILE")
PORT=$(read_ini "principal" "puerto" "$CFG_FILE")
SERVER2=$(read_ini "secundario" "servidor" "$CFG_FILE")
PORT2=$(read_ini "secundario" "puerto" "$CFG_FILE")
USER=$(read_ini "principal" "usuario" "$CFG_FILE")
PASS=$(read_ini "principal" "password" "$CFG_FILE")

echo "Servidor: "$SERVER""
echo "Puerto: "$PORT""
echo "Servidor 2: "$SERVER2""
echo "Puerto 2: "$PORT2""
echo "Usuario: "$USER""
echo "Password: "$PASS""

El script, lógicamente tarda más del doble de tiempo, aunque todavía sigue siendo razonable (también depende mucho del tamaño del archivo, de los comentarios que tenga, etc). Además, seguimos haciendo una lectura por cada variable que queremos leer.

Parseo una vez, recopilacion de variables

Una de las cosas que no me gustan del primer método, además de la ejecución de código es que se declaran directamente las variables generadas para todo el script. Eso puede dar lugar a sobreescritura de variables que estemos utilizando (por ejemplo si encontramos en el .ini una variable del mismo nombre que una variable existente de nuestro script).
Así que una opción muy interesante sería poder incluirlas en un array. Y como Bash no soporta arrays multidimensionales podríamos hacer las claves del array con la forma SECCION_CLAVE.

Podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
CFG_FILE=secciones.ini
declare -A CONFIG

function read_ini()
{
        # Miramos la extensión extglob que nos permitirá utilizar
        # expresiones de sustitución complejas en Bash
        shopt -p extglob &> /dev/null
        local CHANGE_EXTGLOB=$?
        if [ $CHANGE_EXTGLOB = 1 ]; then
                # Establece la extensión
                shopt -s extglob
        fi

        local FILE="$1"
        # Nombre por defecto cuando no hay sección
        local CURRENT_SECTION="_default"

        local ini="$("

        # Quitamos los \r usados en la nueva línea en formato DOS
        ini=${ini//$'\r'/}
        # Convertimos a un array
        IFS=$'\n' && ini=(${ini})
        # Borra espacios al principio y al final (trim)
        ini=(${ini[*]/#+([[:space:]])/})
        ini=(${ini[*]/%+([[:space:]])/})
        # Borra comentarios, con ; y con #
        ini=(${ini[*]//;*/})
        ini=(${ini[*]//\#*/})

        for l in ${ini[*]}; do
                if [[ "$l" =~ ^\[(.*)\]$ ]]; then
                        #echo "SECCION ${BASH_REMATCH[1]}"
                        CURRENT_SECTION="${BASH_REMATCH[1]}"
                # Los comentarios los podemos quitar antes
                # elif [[ "$l" =~ ^\; || "$l" =~ ^\# ]]; then
                #       echo "COMENTARIO $l"
                elif [[ "$l" =~ ^(.*)=(.*)$ ]]; then
                        local KEY="${CURRENT_SECTION}_"${BASH_REMATCH[1]%%+([[:space:]])}
                        local VALUE=${BASH_REMATCH[2]##+([[:space:]])}
                        CONFIG[$KEY]="$VALUE"
                        # echo "EVALUA ""$KEY"" = ""$VALUE"""

                else
                        #echo "ERROR EN $l"
                        false
                fi
        done

        if [ $CHANGE_EXTGLOB = 1 ]; then
                # Si tuvimos que meter la extensión, la quitamos
                shopt -u extglob
        fi
}

read_ini "$CFG_FILE"

echo "Servidor principal: "${CONFIG["principal_servidor"]}
echo "Servidor secundario: "${CONFIG["secundario_servidor"]}
echo "Puerto principal: "${CONFIG["principal_puerto"]}
echo "Puerto secundario: "${CONFIG["secundario_puerto"]}

Con este script, llamando a la función read_ini() y pasándole el nombre de archivo de configuración, rellenará el array CONFIG con la información del archivo. Para este script me he basado en este proyecto, basado a su vez en este otro. Sólo que este script no depende de eval, ni de source como ejemplos anteriores.
En mis pruebas, este método tiene un rendimiento algo superior al método de awk del principio. Si os fijáis, no recurro a herramientas externas a Bash. Además, sólo se hace una lectura del fichero, se almacena en un buffer y la función read_ini() se encarga de poner todo en el array, por lo tanto, cada vez que necesitemos conseguir un valor de configuración, sólo leemos del array. Eso lo hará todo mucho más rápido.

¿Qué sistema utilizas para la configuración de tus scripts?

Dejo esta pregunta abierta para vuestros comentarios. ¿Usas archivos Json? ¿Utilizas un script en Python que haga de puente? ¿Lees de una base de datos?
Foto principal: unsplash-logoChris Kristiansen

The post Leer ficheros de configuración INI desde nuestros scripts en BASH appeared first on Poesía Binaria.



This post first appeared on Poesía Binaria - Programación, Tecnología Y Sof, please read the originial post: here

Share the post

Leer ficheros de configuración INI desde nuestros scripts en BASH

×

Subscribe to Poesía Binaria - Programación, Tecnología Y Sof

Get updates delivered right to your inbox!

Thank you for your subscription

×