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