Sollten Variablen bei der Ausführung in Anführungszeichen gesetzt werden?

18

Die allgemeine Regel in Shell-Skripten lautet, dass Variablen immer in Anführungszeichen gesetzt werden sollten, es sei denn, es gibt einen zwingenden Grund, dies nicht zu tun. Weitere Einzelheiten, als Sie wahrscheinlich wissen möchten, finden Sie in diesem großartigen Q & A: Security-Artikel .

Betrachten Sie jedoch eine Funktion wie die folgende:

run_this(){
    $@
}

Sollte $@dort zitiert werden oder nicht? Ich spielte ein bisschen damit und konnte keinen Fall finden, bei dem der Mangel an Zitaten ein Problem verursachte. Wenn Sie dagegen Anführungszeichen verwenden, wird der Befehl unterbrochen, wenn Sie Leerzeichen als Variable in Anführungszeichen übergeben:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

Das Ausführen des obigen Skripts gibt Folgendes zurück:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Ich kann das umgehen, wenn ich run_that $commstattdessen benutze run_that "$comm", aber da die run_this(nicht zitierte) Funktion mit beiden funktioniert, scheint es die sicherere Wette zu sein.

Sollte also im konkreten Fall der Verwendung $@in einer Funktion, deren Aufgabe es ist, $@als Befehl auszuführen , $@in Anführungszeichen gesetzt werden? Bitte erläutern Sie, warum es zitiert werden sollte / nicht und geben Sie ein Beispiel für Daten an, die es zerstören können.

terdon
quelle
6
run_thatDas Verhalten von ist definitiv das, was ich erwarten würde (was ist, wenn der Pfad zum Befehl ein Leerzeichen enthält?). Wenn Sie das andere Verhalten wollte, sicherlich würden Sie es am Ende des Zitats Anruf -Ort , wo Sie wissen , was die Daten? Ich würde erwarten, diese Funktion als zu bezeichnen run_that ls -l, die in beiden Versionen gleich funktioniert . Gibt es einen Fall, den Sie anders erwarten ließen?
Michael Homer
@MichaelHomer Ich denke, meine Bearbeitung hier hat dies veranlasst: unix.stackexchange.com/a/250985/70524
muru
@MichaelHomer aus irgendeinem Grund (wahrscheinlich, weil ich noch keine zweite Tasse Kaffee getrunken habe) hatte ich keine Leerzeichen in den Argumenten oder im Pfad des Befehls berücksichtigt, sondern nur im Befehl selbst (Optionen). Dies scheint, wie so oft, im Nachhinein sehr offensichtlich zu sein.
terdon
Es gibt einen Grund, warum Shells weiterhin Funktionen unterstützen, anstatt Befehle einfach in ein Array einzufügen und damit auszuführen ${mycmd[@]}.
Chepner

Antworten:

20

Das Problem liegt darin, wie der Befehl an die Funktion übergeben wird:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"sollte in dem allgemeinen Fall verwendet werden, in dem Ihrer run_thisFunktion ein normal geschriebener Befehl vorangestellt wird. run_thisführt zur Hölle zu zitieren:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Ich bin mir nicht sicher, wie ich einen Dateinamen mit Leerzeichen übergeben soll run_this.

muru
quelle
1
Es war in der Tat Ihre Bearbeitung, die dies veranlasste. Aus irgendeinem Grund ist es mir einfach nicht eingefallen, mit einem Dateinamen mit Leerzeichen zu testen. Ich habe absolut keine Ahnung warum nicht, aber los geht's. Sie haben natürlich recht, ich sehe auch keinen Weg, dies richtig zu machen run_this.
terdon
@terdon Quoting ist so sehr zur Gewohnheit geworden, dass ich davon ausgegangen bin, dass Sie es versehentlich $@nicht zitiert haben . Ich hätte ein Beispiel hinterlassen sollen. : D
muru
2
Nein, es ist in der Tat so eine Gewohnheit, dass ich es (falsch) getestet habe und zu dem Schluss gekommen bin, dass "huh, vielleicht braucht dieses hier keine Anführungszeichen". Ein allgemein als Brainfart bezeichnetes Verfahren.
terdon
1
Sie können keinen Dateinamen mit Leerzeichen an übergeben run_this. Dies ist im Grunde das gleiche Problem, mit dem Sie konfrontiert werden, wenn Sie komplexe Befehle in Zeichenfolgen einfügen, wie in Bash FAQ 050 beschrieben .
Etan Reisner
9

Es ist entweder:

interpret_this_shell_code() {
  eval "$1"
}

Oder:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

oder:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Aber:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Macht nicht viel Sinn.

Wenn Sie den ls -lBefehl ausführen möchten (nicht den lsBefehl mit lsund -lals Argumente), tun Sie Folgendes:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Aber wenn es (wahrscheinlicher) der lsBefehl mit lsund -lals Argument ist, würden Sie Folgendes ausführen:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Wenn es sich um mehr als einen einfachen Befehl handelt, den Sie ausführen möchten, reicht es aus, wenn Sie Variablenzuweisungen, Umleitungen, Pipes ... ausführen möchten interpret_this_shell_code:

interpret_this_shell_code 'ls -l 2> /dev/null'

Natürlich können Sie immer Folgendes tun:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Stéphane Chazelas
quelle
5

Betrachtet man es aus der bash / KSH / zsh Perspektive, $*und $@ist ein Spezialfall der allgemeinen Array - Erweiterung. Array-Erweiterungen sind nicht wie normale Variablenerweiterungen:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

Mit den $*/ ${a[*]}-Erweiterungen wird das Array mit dem ersten Wert IFS(standardmäßig Leerzeichen) in einer riesigen Zeichenfolge verbunden. Wenn Sie es nicht zitieren, wird es wie eine normale Zeichenfolge aufgeteilt.

Bei den $@/ ${a[@]}-Erweiterungen hängt das Verhalten davon ab, ob die $@/ ${a[@]}-Erweiterung in Anführungszeichen steht oder nicht:

  1. Wenn es in Anführungszeichen ( "$@"oder "${a[@]}") steht, erhalten Sie das Äquivalent von "$1" "$2" "$3" #... oder"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. Wenn es nicht in Anführungszeichen ( $@oder ${a[@]}) steht, erhalten Sie das Äquivalent von $1 $2 $3 #... oder${a[1]} ${a[2]} ${a[3]} # ...

Zum Umbrechen von Befehlen möchten Sie auf jeden Fall die @ -Erweiterungen in Anführungszeichen (1.).


Weitere nützliche Informationen zu Bash-Arrays (und Bash-ähnlichen Arrays): https://lukeshu.com/blog/bash-arrays.html

PSkocik
quelle
1
Ich habe gerade gemerkt, dass ich mich auf einen Link beziehe, der mit Luke beginnt, während ich eine Vader-Maske trage. Die Kraft ist stark mit diesem Beitrag.
PSkocik
4

Wenn Sie nicht doppelt zitieren $@, haben Sie alle wichtigen Punkte in dem Link belassen, den Sie zu Ihrer Funktion gegeben haben.

Wie können Sie einen Befehl mit dem Namen ausführen *? Du kannst es nicht machen mit run_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

Und Sie sehen, auch wenn ein Fehler aufgetreten ist, run_thathaben Sie eine aussagekräftigere Nachricht erhalten.

Die einzige Möglichkeit, $@einzelne Wörter zu erweitern , besteht darin, sie in Anführungszeichen zu setzen. Wenn Sie es als Befehl ausführen möchten, müssen Sie den Befehl und die Parameter als getrennte Wörter übergeben. Dass das, was Sie auf der Anruferseite getan haben, nicht in Ihrer Funktion.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

ist eine bessere Wahl. Oder wenn Ihre Shell Arrays unterstützt:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Auch wenn die Shell Array überhaupt nicht unterstützt, können Sie mit"$@" Array spielen .

cuonglm
quelle
3

Das Ausführen von Variablen in bashist eine fehleranfällige Technik. Es ist einfach unmöglich, eine run_thisFunktion zu schreiben , die alle Kantenfälle korrekt behandelt, wie:

  • Pipelines (zB ls | grep filename)
  • Eingabe- / Ausgabeumleitungen (zB ls > /dev/null)
  • Shell-Anweisungen wie if whileetc.

Wenn Sie nur die Wiederholung von Code vermeiden möchten, sollten Sie die Funktionen besser nutzen. Zum Beispiel anstelle von:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Du solltest schreiben

command() {
    ls -l
}
...
command

Wenn die Befehle nur zur Laufzeit verfügbar sind, sollten Sie Folgendes verwenden eval, das speziell für die Behandlung aller Macken entwickelt wurde, die zum run_thisFehlschlagen führen:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Beachten Sie, dass evalist bekannt für Sicherheitsfragen, aber wenn Sie Variablen aus nicht vertrauenswürdigen Quellen zu passieren run_this, werden Sie genauso gut der Ausführung beliebigen Codes stellen.

Dmitry Grigoryev
quelle
1

Es ist deine Entscheidung. Wenn Sie $@keinen seiner Werte angeben, werden diese zusätzlich erweitert und interpretiert. Wenn Sie es zitieren, werden alle übergebenen Argumente der Funktion in ihrer Erweiterung wörtlich wiedergegeben. Sie werden niemals in der Lage sein, zuverlässig mit Shell-Syntax-Token wie &>|und usw. umzugehen, ohne die Argumente selbst zu analysieren - und so bleibt Ihnen die vernünftigere Wahl, Ihre Funktion einer der folgenden Möglichkeiten zu übergeben:

  1. Genau die Wörter, die bei der Ausführung eines einzelnen einfachen Befehls mit verwendet werden "$@".

...oder...

  1. Eine weiter erweiterte und interpretierte Version Ihrer Argumente, die erst dann zusammen als einfacher Befehl mit übernommen werden $@.

Keiner der Wege ist falsch, wenn er absichtlich ist und die Auswirkungen Ihrer Wahl gut verstanden werden. Beide Möglichkeiten haben Vorteile gegenüber den anderen, obwohl die Vorteile der zweiten selten besonders nützlich sein dürften. Immer noch...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... es ist nicht nutzlos , nur selten von großem Nutzen . Und in einer bashSchale, weil die bashVoreinstellung nicht hält eine Variablendefinition an seine Umgebung , auch wenn die Definition der Befehlszeile eines speziellen builtin oder zu einer Funktion vorangestellt wird, für den globalen Wert $IFSist nicht betroffen, und ihre Erklärung ist lokal nur zum run_this()anruf.

Ähnlich:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... das Globbing ist auch konfigurierbar. Zitate dienen einem Zweck - sie sind nicht umsonst. Ohne sie wird die Shell-Erweiterung einer zusätzlichen Interpretation unterzogen - konfigurierbare Interpretation. Früher war es - mit einigen sehr alten Shells - das, $IFSwas global auf alle Eingaben angewendet wurde , und nicht nur auf Erweiterungen. Tatsächlich verhielten sich die Shells sehr ähnlich dahingehend, run_this()dass sie alle Eingabewörter auf den Wert von brachen $IFS. Wenn Sie also nach diesem sehr alten Shell-Verhalten suchen, sollten Sie es verwenden run_this().

Ich suche nicht danach und bin im Moment ziemlich gedrängt, ein nützliches Beispiel dafür zu finden. Ich bevorzuge im Allgemeinen, dass die Befehle, die meine Shell ausführt, die sind, die ich tippe. Und wenn ich die Wahl hätte, würde ich es fast immer tun run_that(). Außer dass...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Fast alles kann zitiert werden. Befehle werden in Anführungszeichen ausgeführt. Dies funktioniert, da zum Zeitpunkt der tatsächlichen Ausführung des Befehls alle Eingabewörter bereits in Anführungszeichen gesetzt wurden - dies ist die letzte Stufe des Eingabe-Interpretationsprozesses der Shell. So ist der Unterschied zwischen 'ls'und lskann nur Materie , während die Shell interpretiert - und deshalb zitiert wird lssichergestellt , dass Alias Name lsnicht für mein zitierte ersetzt wird lsBefehlswort. Ansonsten wirken sich die Anführungszeichen nur auf die Abgrenzung von Wörtern aus (das ist, wie und warum das Zitieren von Variablen / Eingabe-Leerzeichen funktioniert) sowie auf die Interpretation von Metazeichen und reservierten Wörtern.

So:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Sie werden das niemals mit run_this()oder tun können run_that().

Aber Funktionsnamen, $PATH'd-Befehle oder integrierte Befehle werden in Anführungszeichen oder ohne Anführungszeichen ausgeführt, und genau so run_this()und überhaupt nicht run_that(). Sie werden mit all diesen Dingen nichts Nützliches $<>|&(){}anfangen können. Kurz evalist.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

Andernfalls sind Sie aufgrund der von Ihnen verwendeten Anführungszeichen an die Grenzen eines einfachen Befehls gebunden (auch wenn dies nicht der $@Fall ist, da der Befehl wie ein Anführungszeichen zu Beginn des Prozesses behandelt wird, wenn er nach Metazeichen analysiert wird) . Dieselbe Einschränkung gilt für Befehlszeilenzuweisungen und -umleitungen, die auf die Befehlszeile der Funktion beschränkt sind. Aber das ist keine große Sache:

(run_that(){ "$@";}; echo hey | run_that cat)

hey

Ich hätte dort genauso leicht <Ein- oder >Ausgänge umleiten können, wie ich die Pipe geöffnet habe.

Auf jeden Fall gibt es hier keinen richtigen oder falschen Weg - jeder Weg hat seinen Nutzen. Sie sollten es nur so schreiben, wie Sie es verwenden möchten, und Sie sollten wissen, was Sie vorhaben. Das Weglassen von Zitaten kann einen Zweck haben - sonst gäbe es keinen sein , überhaupt zitiert - aber wenn man sie weglassen Gründen nicht relevant für Ihre Zwecke, sind Sie nur schlechten Code zu schreiben. Tu was du meinst; Ich versuche es trotzdem.

mikeserv
quelle