Usar el archivo de configuración para mi script de shell

26

Necesito crear un archivo de configuración para mi propio script: aquí un ejemplo:

guión:

#!/bin/bash
source /home/myuser/test/config
echo "Name=$nam" >&2
echo "Surname=$sur" >&2

Contenido de /home/myuser/test/config:

nam="Mark"
sur="Brown"

eso funciona!

Mi pregunta: ¿es esta la forma correcta de hacer esto o hay otras formas?

Pol Hallen
fuente
Las variables deben estar en la parte superior. Me sorprende que funcione. De todos modos, ¿por qué necesitas un archivo de configuración? ¿Estás planeando usar estas variables en otro lugar?
Faheem Mitha
Faheem, necesito las variables porque mi script tiene muchas opciones: usar un archivo de configuración ejemplificará el script. Gracias
Pol Hallen
55
En mi humilde opinión está bien. Lo haría de esta manera.
Tinti
abcdetambién lo hace de esta manera y ese es un programa bastante grande (para un script de shell). Puedes echarle un vistazo aquí .
Lucas

Respuestas:

19

sourceno es seguro ya que ejecutará código arbitrario. Esto puede no ser una preocupación para usted, pero si los permisos de los archivos son incorrectos, es posible que un atacante con acceso al sistema de archivos ejecute el código como un usuario privilegiado mediante la inyección de código en un archivo de configuración cargado por un script protegido de otra manera, como un guión de inicio.

Hasta ahora, la mejor solución que he podido identificar es la torpe solución de reinventar la rueda:

myscript.conf

password=bar
echo rm -rf /
PROMPT_COMMAND='echo "Sending your last command $(history 1) to my email"'
hostname=localhost; echo rm -rf /

Utilizando source, esto se ejecutaría echo rm -rf /dos veces, así como cambiaría el usuario en ejecución $PROMPT_COMMAND. En cambio, haz esto:

myscript.sh (Bash 4)

#!/bin/bash
typeset -A config # init array
config=( # set default values in config array
    [username]="root"
    [password]=""
    [hostname]="localhost"
)

while read line
do
    if echo $line | grep -F = &>/dev/null
    then
        varname=$(echo "$line" | cut -d '=' -f 1)
        config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
    fi
done < myscript.conf

echo ${config[username]} # should be loaded from defaults
echo ${config[password]} # should be loaded from config file
echo ${config[hostname]} # includes the "injected" code, but it's fine here
echo ${config[PROMPT_COMMAND]} # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

myscript.sh ( compatible con Mac / Bash 3)

#!/bin/bash
config() {
    val=$(grep -E "^$1=" myscript.conf 2>/dev/null || echo "$1=__DEFAULT__" | head -n 1 | cut -d '=' -f 2-)

    if [[ $val == __DEFAULT__ ]]
    then
        case $1 in
            username)
                echo -n "root"
                ;;
            password)
                echo -n ""
                ;;
            hostname)
                echo -n "localhost"
                ;;
        esac
    else
        echo -n $val
    fi
}

echo $(config username) # should be loaded from defaults
echo $(config password) # should be loaded from config file
echo $(config hostname) # includes the "injected" code, but it's fine here
echo $(config PROMPT_COMMAND) # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

Responda si encuentra una vulnerabilidad de seguridad en mi código.

Mikkel
fuente
1
Para su información, esta es una solución Bash versión 4.0 que lamentablemente está sujeta a problemas de licencia locos impuestos por Apple y no está disponible de forma predeterminada en Macs
Sukima
@Sukima Buen punto. He agregado una versión que es compatible con Bash 3. Su debilidad es que no manejará las *entradas correctamente, pero entonces, ¿qué en Bash maneja bien ese carácter?
Mikkel
El primer script falla si la contraseña contiene una barra invertida.
Kusalananda
@Kusalananda ¿Qué pasa si la barra invertida se escapa? my\\password
Mikkel
10

Aquí hay una versión limpia y portátil que es compatible con Bash 3 y superiores, tanto en Mac como en Linux.

Especifica todos los valores predeterminados en un archivo separado, para evitar la necesidad de una función de configuración de "valores predeterminados" enorme, desordenada y duplicada en todos sus scripts de shell. Y le permite elegir entre leer con o sin fallos predeterminados:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (esta es una biblioteca, por lo que no hay shebang-line):

config_read_file() {
    (grep -E "^${2}=" -m 1 "${1}" 2>/dev/null || echo "VAR=__UNDEFINED__") | head -n 1 | cut -d '=' -f 2-;
}

config_get() {
    val="$(config_read_file config.cfg "${1}")";
    if [ "${val}" = "__UNDEFINED__" ]; then
        val="$(config_read_file config.cfg.defaults "${1}")";
    fi
    printf -- "%s" "${val}";
}

test.sh (o cualquier script donde desee leer los valores de configuración) :

#!/usr/bin/env bash
source config.shlib; # load the config library functions
echo "$(config_get myvar)"; # will be found in user-cfg
printf -- "%s\n" "$(config_get myvar)"; # safer way of echoing!
myvar="$(config_get myvar)"; # how to just read a value without echoing
echo "$(config_get othervar)"; # will fall back to defaults
echo "$(config_get bleh)"; # "__UNDEFINED__" since it isn't set anywhere

Explicación del script de prueba:

  • Tenga en cuenta que todos los usos de config_get en test.sh están entre comillas dobles. Al envolver cada config_get entre comillas dobles, nos aseguramos de que el texto en el valor de la variable nunca se malinterprete como banderas. Y garantiza que conservemos los espacios en blanco correctamente, como múltiples espacios en una fila en el valor de configuración.
  • ¿Y cuál es esa printflínea? Bueno, es algo que debes tener en cuenta: echoes un mal comando para imprimir texto sobre el que no tienes control. Incluso si usa comillas dobles, interpretará las banderas. Intente configurar myvar(in config.cfg) -ey verá una línea vacía, porque echopensará que es una bandera. Pero printfno tiene ese problema. El printf --dice "imprime esto y no interpretes nada como banderas", y "%s\n"dice "formatea la salida como una cadena con una nueva línea final, y finalmente el parámetro final es el valor para que printf formatee.
  • Si no va a hacer eco de los valores en la pantalla, simplemente los asignará normalmente, como myvar="$(config_get myvar)";. Si va a imprimirlos en la pantalla, le sugiero que use printf para estar totalmente seguro contra cualquier cadena incompatible con el eco que pueda estar en la configuración del usuario. Pero echo está bien si la variable proporcionada por el usuario no es el primer carácter de la cadena que está repitiendo, ya que esa es la única situación en la que se podrían interpretar "flags", por lo que algo como echo "foo: $(config_get myvar)";es seguro, ya que el "foo" no comienza con un guión y, por lo tanto, le dice a echo que el resto de la cadena tampoco son marcas para él. :-)
gw0
fuente
@ user2993656 Gracias por detectar que mi código original todavía tenía mi nombre de archivo de configuración privado (environment.cfg) en lugar del correcto. En cuanto a la edición "echo -n" que hiciste, eso depende del shell utilizado. En Mac / Linux Bash, "echo -n" significa "echo sin línea nueva", lo que hice para evitar líneas nuevas. Pero parece funcionar exactamente igual sin él, así que gracias por las ediciones.
gw0
En realidad, acabo de leerlo y lo reescribí para usar printf en lugar de echo, lo que garantiza que nos libraremos del riesgo de que el eco malinterprete las "banderas" en los valores de configuración.
gw0
Me gusta mucho esta versión. Dejé el config.cfg.defaultslugar en lugar de definirlos al momento de llamar $(config_get var_name "default_value"). tritarget.org/static/…
Sukima
7

Analiza el archivo de configuración, no lo ejecutes.

Actualmente estoy escribiendo una aplicación en el trabajo que usa una configuración XML extremadamente simple:

<config>
    <username>username-or-email</username>
    <password>the-password</password>
</config>

En el script de shell (la "aplicación"), esto es lo que hago para obtener el nombre de usuario (más o menos, lo he puesto en una función de shell):

username="$( xml sel -t -v '/config/username' "$config_file" )"

El xmlcomando es XMLStarlet , que está disponible para la mayoría de los Unices.

Estoy usando XML ya que otras partes de la aplicación también se ocupan de datos codificados en archivos XML, por lo que fue más fácil.

Si prefiere JSON, existe jqun analizador de JSON de shell fácil de usar.

Mi archivo de configuración se vería así en JSON:

{                                 
  "username": "username-or-email",
  "password": "the-password"      
}                

Y luego obtendría el nombre de usuario en el script:

username="$( jq -r '.username' "$config_file" )"
Kusalananda
fuente
La ejecución del script tiene una serie de ventajas y desventajas. Las principales desventajas son la seguridad, si alguien puede alterar el archivo de configuración, entonces puede ejecutar el código, y es más difícil hacerlo a prueba de idiotas. Las ventajas son la velocidad, en una prueba simple es más de 10,000 veces más rápido obtener un archivo de configuración que ejecutar pq, y flexibilidad, cualquiera a quien le guste el parcheado de mono en Python lo apreciará.
icarus
@icarus ¿Qué tamaño de archivos de configuración encuentra habitualmente y con qué frecuencia necesita analizarlos en una sesión? Observe también que se pueden tener varios valores fuera de XML o JSON de una vez.
Kusalananda
Por lo general, solo unos pocos (1 a 3) valores. Si está utilizando evalpara establecer múltiples valores, entonces está ejecutando partes seleccionadas del archivo de configuración :-).
icarus
1
@icarus Estaba pensando en matrices ... No hay necesidad de evalnada. El impacto en el rendimiento de usar un formato estándar con un analizador existente (aunque sea una utilidad externa) es insignificante en comparación con la robustez, la cantidad de código, la facilidad de uso y la facilidad de mantenimiento.
Kusalananda
1
+1 para "analizar el archivo de configuración, no ejecutarlo"
Iiridayn
4

La forma más común, eficiente y correcta es usar source, o .como forma abreviada. Por ejemplo:

source /home/myuser/test/config

o

. /home/myuser/test/config

Sin embargo, hay que tener en cuenta los problemas de seguridad que puede generar el uso de un archivo de configuración adicional de origen externo, dado que se puede insertar código adicional. Para obtener más información, incluso sobre cómo detectar y resolver este problema, recomendaría echar un vistazo a la sección 'Asegúrelo' de http://wiki.bash-hackers.org/howto/conffile#secure_it

NFarrington
fuente
55
Tenía grandes esperanzas para ese artículo (también apareció en mis resultados de búsqueda), pero la sugerencia del autor de intentar usar expresiones regulares para filtrar el código malicioso es un ejercicio inútil.
Mikkel
El procedimiento con punto, requiere una ruta absoluta? Con el pariente no funciona
Davide
2

Lo uso en mis scripts:

sed_escape() {
  sed -e 's/[]\/$*.^[]/\\&/g'
}

cfg_write() { # path, key, value
  cfg_delete "$1" "$2"
  echo "$2=$3" >> "$1"
}

cfg_read() { # path, key -> value
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" | sed "s/^$(echo "$2" | sed_escape)=//" | tail -1
}

cfg_delete() { # path, key
  test -f "$1" && sed -i "/^$(echo $2 | sed_escape).*$/d" "$1"
}

cfg_haskey() { # path, key
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" > /dev/null
}

Debería admitir todas las combinaciones de caracteres, excepto que las teclas no pueden tener =en ellas, ya que ese es el separador. Cualquier otra cosa funciona.

% cfg_write test.conf mykey myvalue
% cfg_read test.conf mykey
myvalue
% cfg_delete test.conf mykey
% cfg_haskey test.conf mykey || echo "It's not here anymore"
It's not here anymore

Además, esto es completamente seguro ya que no utiliza ningún tipo de source/eval

therealfarfetchd
fuente
0

Para mi escenario, sourceo .estaba bien, pero quería admitir las variables de entorno local (es decir, FOO=bar myscript.sh) teniendo prioridad sobre las variables configuradas. También quería que el archivo de configuración fuera editable por el usuario y cómodo para alguien acostumbrado a obtener archivos de configuración, y mantenerlo lo más pequeño / simple posible, para no distraerme del impulso principal de mi script muy pequeño.

Esto es lo que se me ocurrió:

CONF=${XDG_CONFIG_HOME:-~/config}/myscript.sh
if [ ! -f $CONF ]; then
    cat > $CONF << CONF
VAR1="default value"
CONF
fi
. <(sed 's/^\([^=]\+\) *= *\(.*\)$/\1=${\1:-\2}/' < $CONF)

Esencialmente, comprueba las definiciones de variables (sin ser muy flexible sobre los espacios en blanco) y reescribe esas líneas para que el valor se convierta en un valor predeterminado para esa variable, y la variable no se modifica si se encuentra, como la XDG_CONFIG_HOMEvariable anterior. Obtiene esta versión alterada del archivo de configuración y continúa.

El trabajo futuro podría hacer que el sedscript sea más robusto, filtrar líneas que parecen extrañas o no son definiciones, etc., no interrumpir los comentarios al final de la línea, pero esto es lo suficientemente bueno para mí por ahora.

Iiridayn
fuente
0

Esto es sucinto y seguro:

# Read common vars from common.vars
# the incantation here ensures (by env) that only key=value pairs are present
# then declare-ing the result puts those vars in our environment 
declare $(env -i `cat common.vars`)

Esto -igarantiza que solo obtenga las variables decommon.vars

Marcin
fuente
-2

Puedes hacerlo:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Golfo pérsico
fuente