GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Wie führen wir einen in einer Variablen gespeicherten Befehl aus?

$ ls -l /tmp/test/my dir/
total 0

Ich habe mich gefragt, warum die folgenden Möglichkeiten zum Ausführen des obigen Befehls fehlschlagen oder erfolgreich sind?

$ abc='ls -l "/tmp/test/my dir"'

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0

Akzeptierte Antwort:

Dies wurde in einer Reihe von Fragen zu unix.SE diskutiert, ich werde versuchen, alle Probleme zu sammeln, die mir hier einfallen. Referenzen am Ende.

Warum es fehlschlägt

Der Grund für diese Probleme ist die Worttrennung und die Tatsache, dass aus Variablen erweiterte Anführungszeichen nicht als Anführungszeichen fungieren, sondern nur gewöhnliche Zeichen sind.

Die in der Frage vorgestellten Fälle:

Die Zuweisung hier weist den einzelnen String ls -l "/tmp/test/my dir" zu zu abc :

$ abc='ls -l "/tmp/test/my dir"'

Darunter $abc wird auf Whitespace aufgeteilt, und ls erhält die drei Argumente -l , "/tmp/test/my und dir" (mit einem Zitat vor dem zweiten und einem weiteren am Ende des dritten). Die Option funktioniert, aber der Pfad wird falsch verarbeitet:

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Hier wird die Erweiterung zitiert, also als einzelnes Wort beibehalten. Die Shell versucht, ein Programm namens ls -l "/tmp/test/my dir" zu finden , Leerzeichen und Anführungszeichen enthalten.

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

Und hier, $abc wird aufgeteilt, und nur das erste resultierende Wort wird als Argument für -c genommen , also führt Bash einfach ls aus im aktuellen Verzeichnis. Die anderen Wörter sind Argumente für Bash und werden verwendet, um $0 zu füllen , $1 usw.

$ bash -c $abc
'my dir'

Mit bash -c "$abc" , und eval "$abc" , gibt es einen zusätzlichen Shell-Verarbeitungsschritt, der dafür sorgt, dass die Anführungszeichen funktionieren, aber auch bewirkt, dass alle Shell-Erweiterungen erneut verarbeitet werden , daher besteht die Gefahr, dass Sie z. eine Befehlsersetzung aus vom Benutzer bereitgestellten Daten, es sei denn, Sie sind beim Zitieren sehr vorsichtig.

Bessere Methoden

Die beiden besseren Möglichkeiten, einen Befehl zu speichern, sind a) stattdessen eine Funktion zu verwenden, b) eine Array-Variable (oder die Positionsparameter) zu verwenden.

Eine Funktion verwenden:

Deklarieren Sie einfach eine Funktion mit dem darin enthaltenen Befehl und führen Sie die Funktion aus, als wäre es ein Befehl. Erweiterungen in Befehlen innerhalb der Funktion werden nur verarbeitet, wenn der Befehl ausgeführt wird, nicht wenn er definiert ist, und Sie müssen die einzelnen Befehle nicht in Anführungszeichen setzen.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

Ein Array verwenden:

Arrays ermöglichen das Erstellen von Mehrwortvariablen, bei denen die einzelnen Wörter Leerzeichen enthalten. Hier werden die einzelnen Wörter als eigenständige Array-Elemente gespeichert, und der "${array[@]}" expansion erweitert jedes Element als separate Shell-Wörter:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

Die Syntax ist etwas schrecklich, aber Arrays ermöglichen es Ihnen auch, die Befehlszeile Stück für Stück aufzubauen. Zum Beispiel:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

oder lassen Sie Teile der Befehlszeile konstant und verwenden Sie das Array, um nur einen Teil davon zu füllen, wie Optionen oder Dateinamen:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

Der Nachteil von Arrays ist, dass sie keine Standardfunktion sind, sodass einfache POSIX-Shells (wie dash , der Standard /bin/sh in Debian/Ubuntu) unterstützen sie nicht (aber siehe unten). Bash, ksh und zsh tun dies jedoch, daher ist es wahrscheinlich, dass Ihr System über eine Shell verfügt, die Arrays unterstützt.

Verwandter:der „eval“-Befehl in der Bash?

Mit "[email protected]"

In Shells ohne Unterstützung für benannte Arrays kann man immer noch die Positionsparameter verwenden (das Pseudo-Array "[email protected]" ) um die Argumente eines Befehls aufzunehmen.

Das Folgende sollten portierbare Skriptbits sein, die den Codebits im vorherigen Abschnitt entsprechen. Das Array wird durch "[email protected]" ersetzt , die Liste der Positionsparameter. Einstellen von "[email protected]" erfolgt mit set , und die doppelten Anführungszeichen um "[email protected]" wichtig sind (diese bewirken, dass die Elemente der Liste einzeln zitiert werden).

Speichern Sie zunächst einfach einen Befehl mit Argumenten in "[email protected]" und es ausführen:

set -- ls -l "/tmp/test/my dir"
"[email protected]"

Bedingtes Setzen von Teilen der Befehlszeilenoptionen für einen Befehl:

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"

"[email protected]"

Nur mit "[email protected]" für Optionen und Operanden:

set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir

transmutate "[email protected]"

(Natürlich "[email protected]" ist normalerweise mit den Argumenten für das Skript selbst gefüllt, sodass Sie sie irgendwo speichern müssen, bevor Sie "[email protected]" wiederverwenden .)

Mit eval (Vorsicht hier!)

eval nimmt einen String und führt ihn als Befehl aus, so als ob er in der Shell-Befehlszeile eingegeben worden wäre. Dies schließt die gesamte Kurs- und Erweiterungsverarbeitung ein, die sowohl nützlich als auch gefährlich ist.

Im einfachen Fall erlaubt es uns, genau das zu tun, was wir wollen:

cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"

Mit eval , die Anführungszeichen werden verarbeitet, also ls sieht schließlich nur die beiden Argumente -l und /tmp/test/my dir , wie wir wollen. eval ist auch intelligent genug, um alle erhaltenen Argumente zu verketten, also eval $cmd könnte in einigen Fällen auch funktionieren, aber z. Alle Leerzeichen werden in einzelne Leerzeichen geändert. Es ist immer noch besser, die Variable dort in Anführungszeichen zu setzen, da dies sicherstellt, dass sie unverändert zu eval wird .

Es ist jedoch gefährlich, Benutzereingaben in die Befehlszeichenfolge für eval aufzunehmen . Zum Beispiel scheint dies zu funktionieren:

read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";

Aber wenn der Benutzer eine Eingabe macht, die einfache Anführungszeichen enthält, kann er aus der Anführungszeichen ausbrechen und beliebige Befehle ausführen! Z.B. mit der Eingabe '$(whatever)'.txt , führt Ihr Skript die Befehlsersetzung problemlos aus. Dass es rm -rf gewesen sein könnte (oder schlimmer) statt.

Das Problem dabei ist, dass der Wert von $filename in die Befehlszeile eingebettet wurde, dass eval läuft. Es wurde vor eval erweitert , die z.B. den Befehl ls -l ''$(whatever)'.txt' . Sie müssten die Eingabe vorverarbeiten, um sicher zu sein.

Wenn wir es anders machen, behalten wir den Dateinamen in der Variablen und lassen den eval Befehl erweitern, es ist wieder sicherer:

read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";

Beachten Sie, dass die äußeren Anführungszeichen jetzt einfache Anführungszeichen sind, sodass Erweiterungen innerhalb nicht stattfinden. Daher eval sieht den Befehl ls -l "$filename" und erweitert den Dateinamen sicher selbst.

Aber das ist nicht viel anders, als den Befehl einfach in einer Funktion oder einem Array zu speichern. Bei Funktionen oder Arrays gibt es kein solches Problem, da die Wörter die ganze Zeit getrennt gehalten werden und es keine Anführungszeichen oder andere Verarbeitung für den Inhalt von filename gibt .

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

So ziemlich der einzige Grund, eval zu verwenden ist einer, bei dem der variierende Teil Shell-Syntaxelemente umfasst, die nicht über Variablen (Pipelines, Umleitungen usw.) eingebracht werden können. Achten Sie auch dann darauf, keine Benutzereingaben in eval einzubetten Befehl!

Verwandt:Hinzufügen eines Umgebungspfads zur Visual Studio-Eingabeaufforderung?

Referenzen

  • Wortaufteilung in BashGuide
  • BashFAQ/050 oder „Ich versuche, einen Befehl in eine Variable zu schreiben, aber die komplexen Fälle schlagen immer fehl!“
  • Die Frage Warum verschluckt sich mein Shell-Skript an Leerzeichen oder anderen Sonderzeichen?, die eine Reihe von Problemen im Zusammenhang mit Anführungszeichen und Leerzeichen behandelt, einschließlich des Speicherns von Befehlen.

Linux
  1. Wie weist man die Ausgabe eines Befehls einer Shell-Variablen zu?

  2. Wie führe ich einen Befehl ohne Root-Eigenschaften aus?

  3. Wie führe ich den Vim-Befehl von der Shell aus?

  4. Wie führe ich den Top-Befehl richtig über SSH aus?

  5. wie man die Umgebungsvariable an sudo su übergibt

So führen Sie einen Linux-Shell-Befehl / ein Skript im Hintergrund aus

So führen Sie den Sudo-Befehl ohne Passwort aus

So führen Sie Linux-Befehle im Hintergrund aus

So führen Sie einen Befehl für eine bestimmte Zeit in Linux aus

So speichern Sie einen Linux-Befehl als Variable im Shell-Skript

So führen Sie einen Befehl regelmäßig unter Linux mit Watch aus