Wie können wir einen Befehl ausführen, der in einer Variablen gespeichert ist?

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

Ich habe mich gefragt, warum die folgenden Methoden 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
Tim
quelle

Antworten:

54

Dies wurde in einer Reihe von Fragen zu Unix besprochen. In diesem Artikel versuche ich, alle Probleme zu sammeln, die mir einfallen. Referenzen am Ende.


Warum es fehlschlägt

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

Die Fälle in der Frage vorgestellt:

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

Hier $abcwird aufgeteilt und lserhält die beiden Argumente "/tmp/test/myund dir"(mit den Anführungszeichen vor dem ersten und hinter dem zweiten):

$ $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 in Anführungszeichen gesetzt, sodass sie als einzelnes Wort beibehalten wird. Die Shell versucht, ein aufgerufenes Programm mit ls -l "/tmp/test/my dir"Leerzeichen und Anführungszeichen zu finden.

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

Und hier wird nur das erste Wort oder $abcals Argument verwendet -c, sodass Bash nur lsim aktuellen Verzeichnis ausgeführt wird. Die anderen Worten sind Argumente zu bash, und werden verwendet , um zu füllen $0, $1usw.

$ bash -c $abc
'my dir'

Mit bash -c "$abc"und eval "$abc"gibt es einen zusätzlichen Shell-Verarbeitungsschritt, der die Anführungszeichen funktionsfähig macht, aber auch alle Shell-Erweiterungen erneut verarbeitet , sodass die Gefahr besteht, dass versehentlich eine Befehlserweiterung aus vom Benutzer bereitgestellten Daten ausgeführt wird, es sei denn, Sie sind sehr Vorsicht beim Zitieren.


Bessere Möglichkeiten

Die zwei besseren Möglichkeiten zum Speichern eines Befehls sind: a) Verwenden Sie stattdessen eine Funktion, b) Verwenden Sie eine Array-Variable (oder die Positionsparameter).

Verwendung einer Funktion:

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

Verwenden eines Arrays:

Mit Arrays können Variablen mit mehreren Wörtern erstellt werden, bei denen die einzelnen Wörter Leerzeichen enthalten. Hier werden die einzelnen Wörter als unterschiedliche Array-Elemente gespeichert, und die "${array[@]}"Erweiterung 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 mit Arrays können Sie die Befehlszeile auch Stück für Stück erstellen. Beispielsweise:

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

"${mycmd[@]}"

Oder halten Sie Teile der Befehlszeile konstant und verwenden Sie das Array, füllen Sie nur einen Teil davon, 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 kein Standardfeature sind, so dass einfache POSIX-Shells (wie dashder Standard /bin/shin Debian / Ubuntu) sie nicht unterstützen (siehe aber unten). Bash, ksh und zsh tun dies jedoch, sodass Ihr System wahrscheinlich über eine Shell verfügt, die Arrays unterstützt.

Verwenden "$@"

In Shells ohne Unterstützung für benannte Arrays können die Positionsparameter (das Pseudo-Array "$@") weiterhin zum Speichern der Argumente eines Befehls verwendet werden.

Bei den folgenden sollte es sich um portable Skriptbits handeln, die den Codebits im vorherigen Abschnitt entsprechen. Das Array wird durch "$@"die Liste der Positionsparameter ersetzt. Die Einstellung "$@"erfolgt mit setund die doppelten Anführungszeichen "$@"sind wichtig (diese bewirken, dass die Elemente der Liste einzeln in Anführungszeichen gesetzt werden).

Speichern Sie zunächst einfach einen Befehl mit Argumenten "$@"und führen Sie ihn aus:

set -- ls -l "/tmp/test/my dir"
"$@"

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

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

"$@"

Nur "$@"für Optionen und Operanden verwenden:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(Natürlich sind "$@"in der Regel die Argumente für das Skript selbst enthalten, daher müssen Sie sie irgendwo speichern, bevor Sie sie erneut verwenden können "$@".)


Sei vorsichtig mit eval!

Da evaleine zusätzliche Ebene der Angebots- und Erweiterungsverarbeitung eingeführt wird, müssen Sie mit Benutzereingaben vorsichtig sein. Dies funktioniert beispielsweise, solange der Benutzer keine einfachen Anführungszeichen eingibt:

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

Wenn sie jedoch die Eingabe geben '$(uname)'.txt, führt Ihr Skript die Befehlsersetzung problemlos aus.

Eine Version mit Arrays ist dagegen immun, da die Wörter für die gesamte Zeit getrennt bleiben und für den Inhalt von kein Zitat oder eine andere Verarbeitung erforderlich ist filename.

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

Verweise

ilkkachu
quelle
2
Sie können das eval zitieren Ding umgehen, indem Sie tun cmd="ls -l $(printf "%q" "$filename")". nicht hübsch, aber wenn der Benutzer nicht mehr mit einem zufrieden ist eval, hilft es. Es ist auch sehr nützlich für den Befehl , obwohl ähnliche Dinge zu senden, wie ssh foohost "ls -l $(printf "%q" "$filename")"oder in dem sprit dieser Frage: ssh foohost "$cmd".
Patrick
Nicht direkt verwandt, aber haben Sie das Verzeichnis fest codiert? In diesem Fall möchten Sie möglicherweise einen Alias ​​anzeigen. So etwas wie: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny
4

Der sicherste Weg, einen (nicht trivialen) Befehl auszuführen, ist eval. Dann können Sie den Befehl so schreiben, wie Sie es in der Befehlszeile tun würden, und er wird genau so ausgeführt, als ob Sie ihn gerade eingegeben hätten. Aber du musst alles zitieren.

Einfacher Fall:

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

nicht so einfacher Fall:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"
Hauke ​​Laging
quelle
3

Das zweite Anführungszeichen unterbricht den Befehl.

Wenn ich renne:

abc="ls -l '/home/wattana/Desktop'"
$abc

Es gab mir einen Fehler.

Aber wenn ich renne

abc="ls -l /home/wattana/Desktop"
$abc

Es liegt überhaupt kein Fehler vor

Zur Zeit gibt es keine Möglichkeit, dies zu beheben (für mich), aber Sie können den Fehler vermeiden, indem Sie im Verzeichnisnamen kein Leerzeichen verwenden.

Diese Antwort besagt, dass der eval-Befehl verwendet werden kann, um dies zu beheben, aber er funktioniert bei mir nicht :(

Wattana Gaming
quelle
1
Ja, das funktioniert, solange keine Dateinamen mit eingebetteten Leerzeichen (oder mit Glob-Zeichen) erforderlich sind.
ilkkachu
0

Wenn dies nicht funktioniert '', sollten Sie verwenden ``:

abc=`ls -l /tmp/test/my\ dir/`

Besser aktualisieren:

abc=$(ls -l /tmp/test/my\ dir/)
balon
quelle
Dadurch wird das Ergebnis des Befehls in einer Variablen gespeichert. Das OP möchte (seltsamerweise) den Befehl selbst in einer Variablen speichern. Oh, und du solltest anfangen, $( command... )Backticks zu verwenden.
Roaima
Vielen Dank für die Klarstellungen und Tipps!
Balon
0

Wie wäre es mit einem Python3-Einzeiler?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
Marius Mitrofan
quelle