Wie kann ich über die Befehlsersetzung Argumente für einen anderen Befehl generieren?

11

Im Anschluss an: unerwartetes Verhalten bei der Ersetzung von Shell-Befehlen

Ich habe einen Befehl, der eine riesige Liste von Argumenten aufnehmen kann, von denen einige legitimerweise Leerzeichen enthalten können (und wahrscheinlich andere Dinge).

Ich habe ein Skript geschrieben, das diese Argumente mit Anführungszeichen für mich generieren kann, aber ich muss die Ausgabe kopieren und einfügen, z

./somecommand
<output on stdout with quoting>
./othercommand some_args <output from above>

Ich habe versucht, dies durch einfaches Tun zu rationalisieren

./othercommand $(./somecommand)

und stieß auf das unerwartete Verhalten, das oben in Frage gestellt wurde. Die Frage ist: Kann die Befehlssubstitution zuverlässig verwendet werden, um die Argumente zu generieren, othercommandwenn einige Argumente zitiert werden müssen und dies nicht geändert werden kann?

user1207217
quelle
Kommt darauf an, was du mit "zuverlässig" meinst. Wenn Sie möchten, dass der nächste Befehl die Ausgabe genau so übernimmt, wie sie auf dem Bildschirm angezeigt wird, und Shell-Regeln darauf anwendet, kann sie möglicherweise evalverwendet werden, wird jedoch im Allgemeinen nicht empfohlen. xargsist auch etwas zu beachten
Sergiy Kolodyazhnyy
Ich möchte (erwarte), dass die Ausgabe somecommandeiner regelmäßigen Shell-Analyse
unterzogen wird
Wie ich in meiner Antwort sagte, verwenden Sie ein anderes Zeichen für die Feldaufteilung (wie :) ... vorausgesetzt, dass das Zeichen zuverlässig nicht in der Ausgabe enthalten ist.
Olorin
Aber das ist nicht wirklich richtig, weil es nicht den
Zitierregeln entspricht
2
Könnten Sie ein Beispiel aus der Praxis veröffentlichen? Ich meine, die tatsächliche Ausgabe des ersten Befehls und wie Sie möchten, dass er mit dem zweiten Befehl interagiert.
Nxnev

Antworten:

10

Ich habe ein Skript geschrieben, das diese Argumente mit Anführungszeichen für mich generieren kann

Wenn die Ausgabe für die Shell korrekt in Anführungszeichen gesetzt ist und Sie der Ausgabe vertrauen , können Sie sie ausführen eval.

Angenommen, Sie haben eine Shell, die Arrays unterstützt, ist es am besten, eine zu verwenden, um die erhaltenen Argumente zu speichern.

Wenn ./gen_args.sheine Ausgabe wie erzeugt wird 'foo bar' '*' asdf, können wir eval "args=( $(./gen_args.sh) )"ein Array argsmit den Ergebnissen auffüllen. Das wäre die drei Elemente foo bar, *, asdf.

Wir können "${args[@]}"wie gewohnt die Array-Elemente einzeln erweitern:

$ eval "args=( $(./gen_args.sh) )"
$ for var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:

(Beachten Sie die Anführungszeichen. "${array[@]}"Erweitert sich auf alle Elemente als eindeutige Argumente, die nicht geändert wurden. Ohne Anführungszeichen unterliegen die Array-Elemente einer Wortaufteilung . Siehe z. B. die Seite Arrays in BashGuide .)

Jedoch , evalwird gerne weiter Shell Ersetzungen ausgeführt werden , so $HOMEin der Ausgabe zu Ihrem Home - Verzeichnis erweitern würde, und ein Befehl Substitution würde tatsächlich einen Befehl in der Shell - Lauf laufen eval. Eine Ausgabe von "$(date >&2)"würde ein einzelnes leeres Array-Element erstellen und das aktuelle Datum auf stdout drucken. Dies ist ein Problem, wenn gen_args.shdie Daten von einer nicht vertrauenswürdigen Quelle wie einem anderen Host über das Netzwerk abgerufen werden und Dateinamen von anderen Benutzern erstellt werden. Die Ausgabe kann beliebige Befehle enthalten. (Wenn es get_args.shselbst böswillig wäre, müsste es nichts ausgeben, es könnte einfach die böswilligen Befehle direkt ausführen.)


Eine Alternative zu Shell-Zitaten, die ohne Auswertung nur schwer zu analysieren sind, besteht darin, in der Ausgabe Ihres Skripts ein anderes Zeichen als Trennzeichen zu verwenden. Sie müssten eine auswählen, die in den tatsächlichen Argumenten nicht benötigt wird.

Lassen Sie uns wählen #und die Skriptausgabe haben foo bar#*#asdf. Jetzt können wir die Erweiterung des Befehls ohne Anführungszeichen verwenden , um die Ausgabe des Befehls auf die Argumente aufzuteilen.

$ IFS='#'                          # split on '#' signs
$ set -f                           # disable globbing
$ args=( $( ./gen_args3.sh ) )     # assign the values to the arrayfor var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:

Sie müssen IFSspäter zurücksetzen, wenn Sie von der Wortaufteilung an anderer Stelle im Skript abhängig sind ( unset IFSsollte funktionieren, um es zum Standard zu machen), und auch verwenden, set +fwenn Sie später Globbing verwenden möchten.

Wenn Sie Bash oder eine andere Shell mit Arrays nicht verwenden, können Sie die Positionsparameter dafür verwenden. Ersetzen Sie args=( $(...) )durch set -- $(./gen_args.sh)und verwenden Sie "$@"stattdessen "${args[@]}". (Auch hier benötigen Sie Anführungszeichen "$@", da sonst die Positionsparameter einer Wortaufteilung unterliegen.)

ilkkachu
quelle
Beste aus beiden Welten!
Olorin
Würden Sie eine Bemerkung hinzufügen, die die Wichtigkeit des Zitierens zeigt ${args[@]}- das hat bei mir sonst nicht funktioniert
user1207217
@ user1207217, ja, du hast recht. Es ist dasselbe mit Arrays und "${array[@]}"wie mit "$@". Beide müssen in Anführungszeichen gesetzt werden, da sonst die Wortaufteilung die Array-Elemente in Teile zerlegt.
Ilkkachu
6

Das Problem ist , dass , sobald Ihr somecommandSkript die Optionen gibt für othercommanddie Optionen sind wirklich nur Text und auf Gedeih und Verderb des Standard - Parsing Shell (beeinflußt durch , was auch immer $IFSpassiert zu sein , und was Shell - Optionen sind in der Tat, Sie , die in dem allgemeinen Fall würde nicht Kontrolle haben über).

Anstelle der Verwendung von somecommandzu Ausgabe der Optionen, wäre es einfacher, sicherer und robuster , es zu benutzen zu nennen othercommand . Das somecommandSkript würde dann sein , Wrapper - Skript um , othercommandanstatt irgendeine Art von Helfer - Skript , dass Sie in besonderer Weise als Teil der Kommandozeile aufrufen müssten sich daran zu erinnern otherscript. Wrapper-Skripte sind eine sehr verbreitete Methode, um ein Tool bereitzustellen, das nur ein ähnliches Tool mit anderen Optionen aufruft (überprüfen Sie einfach, filewelche Befehle /usr/bintatsächlich Shell-Skript-Wrapper sind).

In bash, kshoder zshkönnten Sie leicht ein Wrapper - Skript , das ein Array verwendet , um die einzelnen Optionen für halten , othercommandetwa so:

options=( "hi there" "nice weather" "here's a star" "*" )
options+=( "bonus bumblebee!" )  # add additional option

Rufen Sie dann auf othercommand(noch im Wrapper-Skript):

othercommand "${options[@]}"

Die Erweiterung von "${options[@]}"würde sicherstellen, dass jedes Element des optionsArrays einzeln in Anführungszeichen gesetzt und othercommandals separate Argumente dargestellt wird.

Der Benutzer des Wrappers würde sich der Tatsache nicht bewusst sein, dass er tatsächlich aufruft othercommand, was nicht wahr wäre , wenn das Skript stattdessen nur die Befehlszeilenoptionen für othercommandals Ausgabe generieren würde .

In /bin/sh, $@um die Optionen zu halten:

set -- "hi there" "nice weather" "here's a star" "*"
set -- "$@" "bonus bumblebee!"  # add additional option

othercommand "$@"

( setDer Befehl zum Einstellen der Positionsparameter verwendet wird $1, $2, $3usw. Diese sind , was die Anordnung macht $@in einem Standard - POSIX - Shell. Die erste --zu signalisieren ist , setdass es keine Möglichkeiten gegeben, nur Argumente. Das --ist wirklich nur dann benötigt , wenn die Der erste Wert beginnt mit -).

Beachten Sie, dass es sich um doppelte Anführungszeichen handelt $@und ${options[@]}dass die Elemente nicht einzeln in Wörter aufgeteilt werden (und der Dateiname nicht markiert ist).

Kusalananda
quelle
Könntest du das erklären set --?
user1207217
@ user1207217 Erklärung zur Antwort hinzugefügt.
Kusalananda
4

Wenn die somecommandAusgabe in einer zuverlässig guten Shell-Syntax vorliegt, können Sie Folgendes verwenden eval:

$ eval sh test.sh $(echo '"hello " "hi and bye"')
hello 
hi and bye

Sie müssen jedoch sicherstellen, dass die Ausgabe ein gültiges Anführungszeichen und dergleichen enthält, da Sie sonst möglicherweise auch Befehle außerhalb des Skripts ausführen:

$ cat test.sh 
for var in "$@"
do
    echo "|$var|"
done
$ ls
bar  baz  test.sh
$ eval sh test.sh $(echo '"hello " "hi and bye"; echo rm *')
|hello |
|hi and bye|
rm bar baz test.sh

Beachten Sie, dass dies echo rm bar baz test.sh(aufgrund von ;) nicht an das Skript übergeben wurde und als separater Befehl ausgeführt wurde. Ich habe die |Umgebung hinzugefügt, um dies $varhervorzuheben.


Im Allgemeinen ist es somecommandnicht möglich, die Ausgabe zuverlässig zum Erstellen einer Befehlszeichenfolge zu verwenden, es sei denn, Sie können der Ausgabe von vollständig vertrauen .

Olorin
quelle