dd-artige Parameter für ein Bash-Skript

19

Ich möchte params an ein Bash-Skript im dd-Stil übergeben. Grundsätzlich möchte ich

./script a=1 b=43

die gleiche Wirkung haben wie

a=1 b=43 ./script

Ich dachte, ich könnte dies erreichen mit:

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

Was ist ein guter Weg, um sicherzustellen, dass der evalsicher ist, dh dass "$arg"eine statische (keine Code-Ausführung), variable Zuweisung entspricht?

Oder gibt es einen besseren Weg, dies zu tun? (Ich möchte das einfach halten).

PSkocik
quelle
Dies ist markiert mit bash. Möchten Sie eine Posix-konforme Lösung oder akzeptieren Sie bash-Lösungen?
rici
Was der Tag sagt, ist was ich meine :)
PSkocik
Nun, Sie könnten es einfach als Muster mit einem =Trennzeichen analysieren und die Zuweisung mit einem sorgfältiger konstruierten Eval durchführen. Nur zur Sicherheit, für den privaten Gebrauch, würde ich es tun, wie Sie es getan haben.
Orion

Antworten:

16

Sie können dies in bash ohne eval (und ohne künstliche Flucht) tun:

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

Bearbeiten: Auf der Grundlage eines Kommentars von Stéphane Chazelas habe ich der Deklaration Flags hinzugefügt, um zu vermeiden, dass die zugewiesene Variable bereits als Array- oder Ganzzahlvariable deklariert ist. In diesem Fall wird eine Reihe von Fällen vermieden, in denen declareder Wertteil des key=valArguments ausgewertet wird . ( +aDies führt zu einem Fehler, wenn die zu setzende Variable beispielsweise bereits als Array-Variable deklariert ist.) Alle diese Sicherheitsanfälligkeiten beziehen sich auf die Verwendung dieser Syntax zum Neuzuweisen vorhandener (Array- oder Ganzzahl-) Variablen, die normalerweise allgemein bekannt sind Shell-Variablen.

Tatsächlich ist dies nur eine Instanz einer Klasse von Injektionsangriffen, die sich gleichermaßen auf evalLösungen auswirkt : Es wäre wirklich viel besser, nur bekannte Argumentnamen zuzulassen, als blind festzulegen, welche Variable zufällig in der Befehlszeile vorhanden war. (Überlegen Sie, was passiert, wenn die Befehlszeile PATHz. B. festgelegt wird. Oder setzen Sie sie zurück PS1, um einige Auswertungen einzuschließen, die bei der nächsten Eingabeaufforderung angezeigt werden.)

Anstatt bash-Variablen zu verwenden, würde ich lieber ein assoziatives Array benannter Argumente verwenden, das sowohl einfacher festzulegen als auch viel sicherer ist. Alternativ können tatsächliche Bash-Variablen festgelegt werden, jedoch nur, wenn sich ihre Namen in einem assoziativen Array legitimer Argumente befinden.

Als Beispiel für den letzteren Ansatz:

# 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
quelle
1
Die Regex scheint die Klammern falsch zu haben. Verwenden Sie stattdessen möglicherweise Folgendes ^[[:alpha:]_][[:alnum:]_]*=:?
lcd047,
1
@ lcd047: foo=ist die einzige Möglichkeit, foo auf den leeren String zu setzen, daher sollte dies erlaubt sein (IMHO). Ich habe die Klammern angebracht, danke.
rici
3
declareist ungefähr so ​​gefährlich wie eval(man kann sogar noch Schlimmeres sagen, da es nicht so offensichtlich ist, dass es so gefährlich ist). Versuchen Sie zum Beispiel, das mit 'DIRSTACK=($(echo rm -rf ~))'als Argument aufzurufen .
Stéphane Chazelas
1
@PSkocik: +xist "nicht -x". -a= indiziertes Array, -A= assoziatives Array, -i= ganzzahlige Variable. Also: kein indiziertes Array, kein assoziatives Array, keine ganze Zahl.
lcd047,
1
Beachten Sie, dass Sie in der nächsten Version von bashmöglicherweise hinzufügen müssen +c, um zusammengesetzte Variablen +Fzu deaktivieren oder um schwebende Variablen zu deaktivieren. Ich würde immer noch verwenden, evalwo Sie wissen, wo Sie stehen.
Stéphane Chazelas
9

Ein POSIX ein (Sets $<prefix>varstatt $varzu vermeiden Probleme mit speziellen Variablen wie IFS/ PATH...):

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

Genannt als myscript x=1 PATH=/tmp/evil %=3 blah '=foo' 1=2, würde es zuweisen:

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

Die Lösung von lcd047 wurde mit einem fest codierten DD_OPT_Präfix überarbeitet :

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

frostschutz verdient die Anerkennung für die meisten Umgestaltungen .

Ich habe dies in eine Quelldatei mit folgender globalen Variable eingefügt:

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

eval "$DD_OPTS_PARSE" macht die ganze Magie.

Eine Version für Funktionen wäre:

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

In Benutzung:

eval "$DD_OPTS_PARSE_LOCAL"

Ich habe ein Repo daraus gemacht, komplett mit Tests und einer README.md. Dann habe ich dies in einem Github API CLI Wrapper Ich schrieb, und ich verwendet , um die gleiche Wrapper für das Einrichten eines Github Klon des Repo (Bootstrapping ist Spaß).

Sichere Parameterübergabe für Bash-Skripte in nur einer Zeile. Genießen. :)

PSkocik
quelle
1
aber Sie können loswerden *=*und aufhören, key / val zu ersetzen, wenn es kein = gibt. (seit du umgestaltet hast): P
frostschutz
1
in der Tat können Sie die for-Schleife und die if loswerden und stattdessen 1 $ verwenden, da Sie verschieben und alle ...
Frostschutz
1
Heh, der Beweis, dass Brainstorming funktioniert. :)
lcd047
1
Morgen Ideen ernten: Sie können sogar loswerden keyund val, und nur schreiben eval "${1%%=*}"=\${1#*=}. Aber das ist so ziemlich so weit wie es geht, eval "$1"denn in @ rici declare "$arg"wird es offensichtlich nicht funktionieren. Achten Sie auch darauf, Dinge wie PATHoder einzustellen PS1.
lcd047,
1
Vielen Dank - ich dachte, das war die bewertete Variable. Ich schätze Ihre Geduld mit mir - das ist ziemlich offensichtlich. Wie auch immer, niemand anders als der, der sich das vorgestellt hat, sieht es gut aus. Sie wissen, obwohl Sie dies erweitern können, um in jeder Shell mit zu arbeiten case. Wahrscheinlich ist es egal, aber nur für den Fall, dass Sie nicht wussten ...
mikeserv
5

Die klassische Bourne-Shell wird unterstützt, und die Bash- und Korn-Shell wird weiterhin unterstützt -k. Wenn dies der ddFall ist, werden alle " ähnlichen" Befehlsoptionen an einer beliebigen Stelle in der Befehlszeile automatisch in Umgebungsvariablen konvertiert, die an den Befehl übergeben werden:

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

Es ist etwas schwieriger, davon zu überzeugen, dass es sich um Umgebungsvariablen handelt. Laufen dies funktioniert für mich:

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

Der erste env | grepzeigt keine Umgebungsvariablen mit einem einzelnen Kleinbuchstaben. Die erste bashzeigt, dass keine Argumente an das Skript übergeben wurden, das über ausgeführt wurde -c, und die Umgebung enthält die drei Variablen mit einem Buchstaben. Der Befehl set +kbricht den -kBefehl ab und zeigt an, dass demselben Befehl jetzt Argumente übergeben wurden. (Die a=1wurde wie $0für das Drehbuch behandelt; das können Sie auch durch entsprechendes Echo beweisen.)

Dies führt zu dem, was die Frage stellt - dass das Tippen ./script.sh a=1 b=2dasselbe sein sollte wie das Tippen a=1 b=2 ./script.sh.

Seien Sie sich bewusst, dass Sie auf Probleme stoßen, wenn Sie Tricks wie diese in einem Skript ausprobieren:

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

Das "$@"wird wörtlich behandelt; Es wird nicht erneut analysiert, um Variablen im Zuweisungsstil zu finden (sowohl in bashals auch ksh). Ich habe es versucht:

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

und nur die already_invoked_with_minus_kUmgebungsvariable wird im exec'd-Skript festgelegt.

Jonathan Leffler
quelle
Sehr nette Antwort! Es ist interessant, dass dies PATH nicht ändert, obwohl HOME änderbar ist, sodass es so etwas wie eine schwarze Liste (zumindest mit PATH) von Umgebungsvariablen geben muss, die zu gefährlich wäre, um auf diese Weise eingestellt zu werden. Ich finde es toll, dass dies ultrakurz ist und die Frage beantwortet, aber ich werde mit der Desinfektions-, Bewertungs- und Präfixlösung fortfahren, da sie immer noch sicherer und damit universeller einsetzbar ist (in Umgebungen, in denen Benutzer nicht mit der Umgebung herumspielen sollen) ). Danke und +1.
PSkocik
2

Mein Versuch:

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

Es funktioniert mit (fast) beliebigem Müll auf der 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
quelle
shift tötet die falschen Dinge, wenn Sie die Schleife nicht beim ersten Nicht-x = y-Parameter
unterbrechen
@frostschutz Guter Punkt, bearbeitet.
lcd047,
Gute Arbeit bei der Verallgemeinerung. Ich denke, es kann ein wenig vereinfacht werden.
PSkocik
Hast du die Gelegenheit bekommen, dir meine Bearbeitung anzuschauen?
PSkocik
Bitte werfen Sie einen Blick auf meine Bearbeitung. So mag ich es (+ vielleicht nur shiftstatt shift 1). Ansonsten danke!
PSkocik
0

Vor einiger Zeit habe ich mich aliasfür diese Art von Arbeit entschieden. Hier ist eine andere Antwort von mir:


Es kann jedoch manchmal möglich sein, die Auswertung und Ausführung solcher Anweisungen zu trennen. Zum Beispiel aliaskann ein Befehl vorab ausgewertet werden. Im folgenden Beispiel wird die Variablendefinition in einem Alias ​​gespeichert, der nur dann erfolgreich deklariert werden kann, wenn die $varauszuwertende Variable keine Bytes enthält, die nicht mit ASCII-Alphanumerik oder _ übereinstimmen.

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

evalwird hier verwendet, um das Aufrufen des Neuen aliasaus einem Kontext mit dem angegebenen Variablennamen heraus zu handhaben - nicht für die genaue Zuweisung. Und evalwird überhaupt nur aufgerufen, wenn die vorherige aliasDefinition erfolgreich ist, und obwohl ich weiß, dass viele verschiedene Implementierungen viele verschiedene Arten von Werten für Aliasnamen akzeptieren, habe ich noch keine Shell gefunden, die eine vollständig leere akzeptiert .

Die Definition innerhalb des Alias ​​ist jedoch für _$var, und dies soll sicherstellen, dass keine signifikanten Umgebungswerte überschrieben werden. Ich kenne keine nennenswerten Umgebungswerte, die mit einem _ beginnen, und dies ist normalerweise eine sichere Option für halbprivate Deklarationen.

Wenn die Aliasdefinition erfolgreich ist, wird ein Alias ​​mit dem Namen des $varWerts deklariert . Und evalruft das nur auf, aliaswenn es auch nicht mit einer Zahl beginnt - sonst evalbekommt man nur ein Nullargument. Wenn also beide Bedingungen erfüllt sind, werden die evalAufrufe aliasund die in der gesicherte Variablendefinition durchgeführt alias, wonach der neue Alias ​​sofort aus der Hash-Tabelle entfernt wird.


aliasIn diesem Zusammenhang ist es auch nützlich, dass Sie Ihre Arbeit drucken können. aliasgibt eine doppelt zitierte Safe-for-Shell-Wiederausführungsanweisung aus , wenn Sie dazu aufgefordert werden.

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

AUSGABE

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