Parámetros de estilo dd a un script bash

19

Me gustaría pasar los parámetros a un script bash, dd-style. Básicamente quiero

./script a=1 b=43

tener el mismo efecto que

a=1 b=43 ./script

Pensé que podría lograr esto con:

for arg in "$@"; do
   eval "$arg";
done

¿Cuál es una buena manera de garantizar que evalsea ​​seguro, es decir, que "$arg"coincida con una asignación de variables estática (sin ejecución de código)?

¿O hay una mejor manera de hacer esto? (Me gustaría mantener esto simple).

PSkocik
fuente
Esto está etiquetado con bash. ¿Desea una solución compatible con Posix o aceptará soluciones bash?
rici
Lo que dice la etiqueta es lo que quiero decir :)
PSkocik
Bueno, podría analizarlo como un patrón con un =separador y hacer la asignación con una evaluación más cuidadosamente construida. Solo por seguridad, para uso privado, lo haría como lo hiciste.
orion

Respuestas:

16

Puede hacer esto en bash sin eval (y sin escape artificial):

for arg in "$@"; do
  if [[ $arg =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
    declare +i +a +A "$arg"
  fi
done

Editar: Basado en un comentario de Stéphane Chazelas, agregué banderas a la declaración para evitar que la variable asignada ya se declare como una matriz o variable entera, lo que evitará una serie de casos en los que declarese evaluará la parte del valor del key=valargumento. (Esto +aprovocará un error si la variable a establecer ya se declara como una variable de matriz, por ejemplo). Todas estas vulnerabilidades se relacionan con el uso de esta sintaxis para reasignar variables existentes (matriz o entero), que normalmente serían bien conocidas. variables de shell

De hecho, esta es solo una instancia de una clase de ataques de inyección que afectarán igualmente las evalsoluciones basadas en: sería realmente mucho mejor permitir solo nombres de argumentos conocidos que establecer ciegamente cualquier variable que esté presente en la línea de comandos. (Considere lo que sucede si se establece la línea de comando PATH, por ejemplo. O se restablece PS1para incluir alguna evaluación que ocurrirá en la siguiente pantalla de aviso).

En lugar de usar variables bash, preferiría usar una matriz asociativa de argumentos con nombre, que es más fácil de configurar y mucho más seguro. Alternativamente, podría establecer variables bash reales, pero solo si sus nombres están en una matriz asociativa de argumentos legítimos.

Como ejemplo del último enfoque:

# Could use this array for default values, too.
declare -A options=([bs]= [if]= [of]=)
for arg in "$@"; do
  # Make sure that it is an assignment.
  # -v is not an option for many bash versions
  if [[ $arg =~ ^[[:alpha:]_][[:alnum:]_]*= &&
        ${options[${arg%%=*}]+ok} == ok ]]; then
    declare "$arg"
    # or, to put it into the options array
    # options[${arg%%=*}]=${arg#*=}
  fi
done
rici
fuente
1
La expresión regular parece tener los corchetes incorrectos. Quizás use esto en su lugar ^[[:alpha:]_][[:alnum:]_]*=:?
lcd047
1
@ lcd047: foo=es la única forma de establecer foo en la cadena vacía, por lo que debería permitirse (en mi humilde opinión). Arreglé los soportes, gracias.
rici
3
declarees casi tan peligroso como eval(incluso se puede decir peor, ya que no es tan aparente que sea tan peligroso). Intenta, por ejemplo, llamar a eso 'DIRSTACK=($(echo rm -rf ~))'como argumento.
Stéphane Chazelas
1
@PSkocik: +xes "no -x". -a= matriz indexada, -A= matriz asociativa, -i= variable entera. Por lo tanto: matriz no indexada, no matriz asociativa, no entero.
lcd047
1
Tenga en cuenta que con la próxima versión de bash, es posible que deba agregar +cpara deshabilitar variables compuestas o +Fdeshabilitar las variables flotantes. Todavía usaría evaldonde sabes dónde estás parado.
Stéphane Chazelas
9

Una POSIX (se establece en $<prefix>varlugar de $varevitar problemas con variables especiales como IFS/ PATH...):

prefix=my_prefix_
for var do
  case $var in
    (*=*)
       case ${var%%=*} in
         "" | *[!abcdefghijiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_]*) ;;
         (*) eval "$prefix${var%%=*}="'${var#*=}'
       esac
  esac
done

Llamado como myscript x=1 PATH=/tmp/evil %=3 blah '=foo' 1=2, asignaría:

my_prefix_x <= 1
my_prefix_PATH <= /tmp/evil
my_prefix_1 <= 2
Stéphane Chazelas
fuente
6

La solución de lcd047 se refactorizó con un DD_OPT_prefijo codificado :

while [[ $1 =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; do
  eval "DD_OPT_${1%%=*}"='${1#*=}'; shift;
done

Frostschutz merece el crédito por la mayor parte de la refactorización.

Puse esto en un archivo fuente con una variable global:

DD_OPTS_PARSE=$(cat <<'EOF'
  while [[ $1 =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; do
    eval "DD_OPT_${1%%=*}"='${1#*=}'; shift;
  done
EOF
)

eval "$DD_OPTS_PARSE" hace toda la magia

Una versión para funciones sería:

DD_OPTS_PARSE_LOCAL="${PARSE_AND_REMOVE_DD_OPTS/DD_OPT_/local DD_OPT_}"

En uso:

eval "$DD_OPTS_PARSE_LOCAL"

Hice un repositorio de esto, completo con pruebas y un archivo README.md. Luego usé esto en un contenedor CLI de API de Github que estaba escribiendo, y utilicé el mismo contenedor para configurar un clon de github de dicho repositorio (el arranque es divertido).

Paso seguro de parámetros para scripts de bash en una sola línea. Disfrutar. :)

PSkocik
fuente
1
pero puede deshacerse de *=*y dejar de sustituir key / val donde no hay =. (desde que estabas refactorizando): P
frostschutz
1
de hecho, puede deshacerse del bucle for y el if y usar while $ 1 en su lugar, ya que está cambiando y todo ...
frostschutz
1
Je, la prueba de que la lluvia de ideas funciona. :)
lcd047
1
Cosechando ideas matutinas: incluso puede deshacerse de keyy val, y simplemente escribir eval "${1%%=*}"=\${1#*=}. Pero eso es más o menos, eval "$1"ya que en @ rici's declare "$arg"no funcionará, obviamente. También tenga cuidado de configurar cosas como PATHo PS1.
lcd047
1
Gracias, pensé que esa era la variable evaluada. Aprecio tu paciencia conmigo, eso es muy obvio. De todos modos, no, aparte de lo imaginado, se ve bien. Sin embargo, puede extender esto para que funcione en cualquier shell case. Probablemente no importa, pero por si no lo sabías ...
mikeserv
5

El shell Bourne clásico es compatible, y los shell Bash y Korn siguen siendo compatibles, una -kopción. Cuando está en vigor, cualquier ddopción de comando ' similar' en cualquier lugar de la línea de comando se convierte automáticamente en variables de entorno que se pasan al comando:

$ set -k
$ echo a=1 b=2 c=3
$ 

Es un poco más difícil convencer de que son variables de entorno; ejecutar esto funciona para mí:

$ set -k
$ env | grep '^[a-z]='   # No environment a, b, c
$ bash -c 'echo "Args: $*" >&2; env' a=1 b=2 c=3 | grep '^[a-z]='
Args: 
a=1
b=2
c=3
$ set +k
$ bash -c 'echo "Args: $*" >&2; env' a=1 b=2 c=3 | grep '^[a-z]='
Args: b=2 c=3
$

El primero env | grepno muestra variables de entorno con una sola letra minúscula. El primero bashmuestra que no se pasan argumentos al script ejecutado mediante -c, y el entorno contiene las tres variables de una letra. La set +kcancela -ky muestra que el mismo comando ahora tiene argumentos pasados. (Se a=1trató como $0para el guión; también puede probarlo con el eco adecuado).

Esto logra lo que la pregunta pregunta: que escribir ./script.sh a=1 b=2debería ser lo mismo que escribir a=1 b=2 ./script.sh.

Tenga en cuenta que tiene problemas si intenta trucos como este dentro de un script:

if [ -z "$already_invoked_with_minus_k" ]
then set -k; exec "$0" "$@" already_invoked_with_minus_k=1
fi

El "$@"es tratado literalmente; no se vuelve a analizar para encontrar variables de estilo de asignación (en ambos bashy ksh). Lo intenté:

#!/bin/bash

echo "BEFORE"
echo "Arguments:"
al "$@"
echo "Environment:"
env | grep -E '^([a-z]|already_invoked_with_minus_k)='
if [ -z "$already_invoked_with_minus_k" ]
then set -k; exec "$0" "$@" already_invoked_with_minus_k=1
fi

echo "AFTER"
echo "Arguments:"
al "$@"
echo "Environment:"
env | grep -E '^([a-z]|already_invoked_with_minus_k)='

unset already_invoked_with_minus_k

y solo la already_invoked_with_minus_kvariable de entorno se establece en el execscript 'd.

Jonathan Leffler
fuente
Muy buena respuesta! Es interesante que esto no cambie PATH, aunque HOME se puede cambiar, por lo que debe haber algo así como una lista negra (que contiene al menos PATH) de entornos que serían demasiado peligrosos para establecer de esta manera. Me encanta cómo esto es ultracorto y respondo la pregunta, pero seguiré con la solución sanitize + eval + prefix ya que aún es más segura y, por lo tanto, más universalmente utilizable (en entornos donde no desea que los usuarios se metan con el entorno) ) Gracias y +1.
PSkocik
2

Mi intento:

#! /usr/bin/env bash
name='^[a-zA-Z][a-zA-Z0-9_]*$'
count=0
for arg in "$@"; do
    case "$arg" in
        *=*)
            key=${arg%%=*}
            val=${arg#*=}

            [[ "$key" =~ $name ]] && { let count++; eval "$key"=\$val; } || break

            # show time
            if [[ "$key" =~ $name ]]; then
                eval "out=\${$key}"
                printf '|%s| <-- |%s|\n' "$key" "$out"
            fi
            ;;
        *)
            break
            ;;
    esac
done
shift $count

# show time again   
printf 'arg: |%s|\n' "$@"

Funciona con basura (casi) arbitraria en el RHS:

$ ./assign.sh Foo_Bar33='1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0' '1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0=33'
|Foo_Bar33| <-- |1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0|
arg: |1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0=33|

$ ./assign.sh a=1 b=2 c d=4
|a| <-- |1|
|b| <-- |2|
arg: |c|
arg: |d=4|
lcd047
fuente
shift matará las cosas incorrectas si no se rompe el ciclo en el primer parámetro que no sea x = y
frostschutz
@frostschutz Buen punto, editado.
lcd047
Buen trabajo en generalizarlo. Creo que se puede simplificar un poco.
PSkocik
¿Tuviste la oportunidad de echar un vistazo a mi edición?
PSkocik
Por favor, eche un vistazo a mi edición. Así es como me gusta (+ quizás solo en shiftlugar de shift 1). De lo contrario, gracias!
PSkocik
0

Hace algún tiempo me decidí aliaspor este tipo de trabajo. Aquí hay alguna otra respuesta mía:


Sin embargo, a veces puede ser posible separar la evaluación y la ejecución de tales declaraciones. Por ejemplo, aliasse puede usar para evaluar previamente un comando. En el siguiente ejemplo, la definición de la variable se guarda en un alias que solo puede declararse con éxito si la $varvariable que está evaluando no contiene bytes que no coincidan con los caracteres alfanuméricos ASCII o _.

LC_OLD=$LC_ALL LC_ALL=C
for var do    val=${var#*=} var=${var%%=*}
    alias  "${var##*[!_A-Z0-9a-z]*}=_$var=\$val" &&
    eval   "${var##[0-9]*}" && unalias "$var"
done;       LC_ALL=$LC_OLD

evalse usa aquí para manejar la invocación de lo nuevo aliasdesde un contexto de nombre de variable entre comillas, no para la asignación exactamente. Y evalsolo se llama si la aliasdefinición anterior es exitosa, y aunque sé que muchas implementaciones diferentes aceptarán muchos tipos diferentes de valores para nombres de alias, aún no he encontrado un shell que acepte uno completamente vacío .

_$varSin embargo, la definición dentro del alias es para , y esto es para garantizar que no se sobrescriban valores de entorno significativos. No conozco ningún valor de entorno notable que comience con un _ y, por lo general, es una apuesta segura para la declaración semiprivada.

De todos modos, si la definición de alias es exitosa, declarará un alias con el nombre del $varvalor. Y evalsolo llamará a eso aliassi tampoco comienza con un número; de lo contrario, evalsolo obtiene un argumento nulo. Entonces, si se cumplen ambas condiciones, se evalllama a la aliasdefinición de variable y se guarda en elalias , después de lo cual el nuevo alias se elimina rápidamente de la tabla hash.


También útil aliasen este contexto es que puede imprimir su trabajo. aliasimprimirá una declaración de ejecución segura de cotización doblemente citada cuando se le solicite .

sh -c "IFS=\'
    alias q=\"\$*\" q" -- \
    some args which alias \
    will print back at us

SALIDA

q='some'"'"'args'"'"'which'"'"'alias'"'"'will'"'"'print'"'"'back'"'"'at'"'"'us'
mikeserv
fuente