paramètres de style dd à un script bash

19

Je voudrais passer des paramètres à un script bash, style dd. En gros, je veux

./script a=1 b=43

avoir le même effet que

a=1 b=43 ./script

Je pensais pouvoir y arriver avec:

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

Quelle est la bonne façon de s’assurer que eval est sûr, c'est-à-dire qu'il "$arg"correspond à une affectation de variable statique (pas d'exécution de code)?

Ou existe-t-il une meilleure façon de procéder? (Je voudrais garder cela simple).

PSkocik
la source
C'est tagué avec bash. Voulez-vous une solution compatible Posix, ou accepterez-vous des solutions bash?
rici
Ce que dit le tag, c'est ce que je veux dire :)
PSkocik
Eh bien, vous pouvez simplement l'analyser en tant que modèle avec un =séparateur et effectuer la tâche avec un eval plus soigneusement construit. Juste pour la sécurité, pour un usage privé, je le ferais comme vous l'avez fait.
orion

Réponses:

16

Vous pouvez le faire en bash sans eval (et sans échappement artificiel):

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

Edit: Sur la base d'un commentaire de Stéphane Chazelas, j'ai ajouté des drapeaux à la déclaration pour éviter que la variable assignée ne soit déjà déclarée comme un tableau ou une variable entière, ce qui évitera un certain nombre de cas dans lesquels declareévaluer la partie valeur de l' key=valargument. (Le +aprovoquera une erreur si la variable à définir est déjà déclarée comme une variable de tableau, par exemple.) Toutes ces vulnérabilités sont liées à l'utilisation de cette syntaxe pour réaffecter des variables existantes (tableau ou entier), qui seraient généralement bien connues variables shell.

En fait, ce n'est qu'une instance d'une classe d'attaques par injection qui affectera également les evalsolutions basées sur: il serait vraiment bien mieux de n'autoriser que les noms d'arguments connus que de définir aveuglément la variable qui se trouvait être présente dans la ligne de commande. (Considérez ce qui se passe si la ligne de commande est définie PATH, par exemple. Ou réinitialisePS1 pour inclure une évaluation qui se produira à la prochaine invite.)

Plutôt que d'utiliser des variables bash, je préfère utiliser un tableau associatif d'arguments nommés, qui est à la fois plus facile à définir et beaucoup plus sûr. Alternativement, il pourrait définir des variables bash réelles, mais uniquement si leurs noms se trouvent dans un tableau associatif d'arguments légitimes.

Comme exemple de cette dernière approche:

# 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
la source
1
Le regex semble avoir les crochets mal. Peut-être utiliser ceci à la place ^[[:alpha:]_][[:alnum:]_]*=:?
lcd047
1
@ lcd047: foo=est le seul moyen de définir foo sur la chaîne vide, il doit donc être autorisé (à mon humble avis ). J'ai fixé les supports, merci.
rici
3
declareest à peu près aussi dangereux que eval(on peut même dire pire car il n'est pas aussi évident qu'il est aussi dangereux). Essayez par exemple d'appeler cela avec 'DIRSTACK=($(echo rm -rf ~))'comme argument.
Stéphane Chazelas
1
@PSkocik: +xn'est "pas -x". -a= tableau indexé, -A= tableau associatif, -i= variable entière. Ainsi: pas de tableau indexé, pas de tableau associatif, pas entier.
lcd047
1
Notez qu'avec la prochaine version de bash, vous devrez peut-être ajouter +cpour désactiver les variables composées ou +Fpour désactiver celles variables. J'utiliserais toujours evaloù vous savez où vous en êtes.
Stéphane Chazelas
9

Un POSIX (défini $<prefix>varau lieu de $varpour éviter les problèmes avec des variables spéciales comme IFS/ PATH...):

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

Appelé myscript x=1 PATH=/tmp/evil %=3 blah '=foo' 1=2, il assignerait:

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

La solution de lcd047 a été refactorisée avec un DD_OPT_préfixe codé en dur :

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

frostschutz mérite le crédit pour la plupart de la refactorisation.

Je mets cela dans un fichier source avec comme variable globale:

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

eval "$DD_OPTS_PARSE" fait toute la magie.

Une version pour les fonctions serait:

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

Utilisé:

eval "$DD_OPTS_PARSE_LOCAL"

J'en ai fait un repo , complet avec des tests et un README.md. Ensuite, j'ai utilisé cela dans un wrapper CLI Github API que j'écrivais, et j'ai utilisé le même wrapper pour configurer un clone github dudit dépôt (le bootstrap est amusant).

Passage sécurisé des paramètres pour les scripts bash sur une seule ligne. Prendre plaisir. :)

PSkocik
la source
1
mais vous pouvez vous en débarrasser *=*et arrêter de remplacer clé / val là où il n'y a pas de =. (puisque vous refactorisez): P
frostschutz
1
en fait, vous pouvez vous débarrasser de la boucle for et du if et utiliser à la place à 1 $, car vous êtes en train de changer et tout ...
frostschutz
1
Heh, la preuve que le brainstorming fonctionne. :)
lcd047
1
Récolter les idées du matin: vous pouvez même vous débarrasser de keyet val, et simplement écrire eval "${1%%=*}"=\${1#*=}. Mais c'est à peu près aussi loin que cela va, eval "$1"car dans @ rici declare "$arg"ne fonctionnera pas, évidemment. Méfiez-vous également de définir des choses comme PATHou PS1.
lcd047
1
Merci - je pensais que c'était la variable évaluée. J'apprécie votre patience avec moi - c'est assez évident. Quoi qu'il en soit, non - à part celui imaginé, ça a l'air bien. Vous savez que vous pouvez l'étendre pour qu'il fonctionne dans n'importe quel shell avec case. Cela n'a probablement pas d'importance, mais juste au cas où vous ne le sauriez pas ...
mikeserv
5

Prise en charge du shell Bourne classique et prise en charge du shell Bash et Korn, une -koption. Quand elle est en vigueur, toutes les ddoptions de commande " -like" n'importe où sur la ligne de commande sont converties automatiquement en variables d'environnement passées à la commande:

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

Il est un peu plus difficile de convaincre qu'il s'agit de variables d'environnement; exécuter cela fonctionne pour moi:

$ 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
$

Le premier env | grepne montre aucune variable d'environnement avec une seule lettre minuscule. Le premier bashmontre qu'aucun argument n'est transmis au script exécuté via -cet que l'environnement contient les trois variables à lettre unique. Le set +kannule le -ket montre que la même commande a désormais des arguments passés. (Le a a=1été traité comme $0pour le script; vous pouvez également le prouver avec un écho approprié.)

Ceci réalise ce que la question demande - que la frappe ./script.sh a=1 b=2devrait être la même chose que la frappe a=1 b=2 ./script.sh.

Sachez que vous rencontrez des problèmes si vous essayez des astuces comme celle-ci dans un script:

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

Le "$@"est traité mot pour mot; il n'est pas réanalysé pour trouver des variables de style d'affectation (dans les deux bashet ksh). J'ai essayé:

#!/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

et seule la already_invoked_with_minus_kvariable d'environnement est définie dans le execscript 'd.

Jonathan Leffler
la source
Très belle réponse! Il est intéressant de noter que cela ne changera pas PATH, bien que HOME soit modifiable, il doit donc y avoir quelque chose comme une liste noire (contenant au moins PATH) de vars env qui serait trop dangereux à définir de cette façon. J'adore la façon dont cela est ultra-court et répond à la question, mais j'irai avec la solution sanitize + eval + prefix car elle est toujours plus sûre et donc plus universellement utilisable (dans les environnements où vous ne voulez pas que les utilisateurs gâchent l'environnement) ). Merci et +1.
PSkocik
2

Ma tentative:

#! /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' "$@"

Cela fonctionne avec des ordures (presque) arbitraires sur le 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
la source
shift tuera les mauvaises choses si vous ne rompez pas la boucle au premier paramètre non x = y
frostschutz
@frostschutz Bon point, édité.
lcd047
Beau travail pour le généraliser. Je pense que cela peut être un peu simplifié.
PSkocik
Avez-vous eu la chance de jeter un œil à mon montage?
PSkocik
Veuillez jeter un œil à mon montage. C'est comme ça que je l'aime (+ peut-être juste shiftau lieu de shift 1). Sinon merci!
PSkocik
0

Il y a quelque temps, je me suis installé aliaspour ce genre de travail. Voici une autre de mes réponses:


Cependant, il peut parfois être possible de séparer l'évaluation et l'exécution de ces déclarations. Par exemple, aliaspeut être utilisé pour pré-évaluer une commande. Dans l'exemple suivant, la définition de variable est enregistrée dans un alias qui ne peut être déclaré avec succès que si la $varvariable qu'elle évalue ne contient aucun octet qui ne correspond pas aux caractères alphanumériques ASCII ou _.

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

evalest utilisé ici pour gérer l'appel du nouveau à aliaspartir d'un contexte varname cité - pas pour l'affectation exactement. Et evaln'est appelé du tout que si la aliasdéfinition précédente réussit, et bien que je sache que de nombreuses implémentations différentes accepteront de nombreux types de valeurs différents pour les noms d'alias, je n'ai pas encore trouvé de shell qui acceptera une complètement vide .

La définition au sein de l'alias est _$varcependant pour, et c'est pour s'assurer qu'aucune valeur d'environnement significative n'est écrasée. Je ne connais pas de valeurs d'environnement remarquables commençant par un _ et c'est généralement une valeur sûre pour une déclaration semi-privée.

Quoi qu'il en soit, si la définition de l'alias réussit, elle déclarera un alias nommé pour $varla valeur de. Et evaln'appellera cela que aliass'il ne commence pas non plus par un nombre - sinon, il evaln'obtient qu'un argument nul. Par conséquent, si les deux conditions sont remplies, evalla aliaset la définition de variable enregistrée dans la aliasest effectuée, après quoi le nouvel alias est rapidement supprimé de la table de hachage.


Aussi au sujet utile aliasdans ce contexte est que vous pouvez imprimer votre travail. aliasaffichera une déclaration de réexécution du coffre-fort pour le shell à double guillemet lorsque cela vous sera demandé.

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

PRODUCTION

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