Warum schlagen Optionen in einer Variablen in Anführungszeichen fehl, funktionieren aber, wenn sie nicht in Anführungszeichen stehen?

18

Ich habe darüber gelesen, dass ich Variablen in bash zitieren sollte, zB "$ foo" anstelle von $ foo. Beim Schreiben eines Skripts stieß ich jedoch auf einen Fall, in dem es ohne Anführungszeichen, jedoch nicht mit diesen funktioniert:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Dieser funktioniert:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Dies ist nicht der Fall (beachten Sie die doppelten Anführungszeichen um $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • Was ist der Grund dafür?

  • Ist die erste Zeile die gute Version? oder sollte ich vermuten, dass irgendwo ein versteckter Fehler vorliegt, der dieses Verhalten verursacht?

  • Wo finde ich im Allgemeinen eine gute Dokumentation, um zu verstehen, wie Bash und seine Zitate funktionieren? Während ich dieses Skript schreibe, habe ich das Gefühl, dass ich angefangen habe, auf einer Trial-and-Error-Basis zu arbeiten, anstatt die Regeln zu verstehen.

z32a7ul
quelle
3
Ihre Frage wird hier beantwortet: mywiki.wooledge.org/BashFAQ/050
glenn jackman
3
Gehen Sie zur Quelle für die Regeln: das Bash-Handbuch . Beachten Sie Abschnitt 3.5 "Shell-Erweiterungen", insbesondere Wortteilung und Dateinamenerweiterung - diese beiden Faktoren steuern Sie in Anführungszeichen.
Glenn Jackman
4
Ich denke, es hilft zu verstehen, wie Befehlszeilenargumente auf einer niedrigen Ebene funktionieren. Wenn ein Programm ausgeführt wird, erhält es Argumente als Liste von Zeichenlisten (nahe genug). Jede innere Liste ist das, was wir als "Argument" bezeichnen. Die meisten Programme hängen von der logischen Trennung zwischen Argumenten ab. Hier sehen Sie, dass wgetdas nicht weiß, was --mirror --no-host-directoriesbedeutet (als ein Argument), aber es behandelt es, wenn es in zwei Argumente aufgeteilt ist. Sehr wenige Programme behandeln Leerzeichen und Anführungszeichen besonders, wenn sie sich innerhalb des Argumentvektors befinden. Das Problem ist, dass bash, und andere Muscheln, sollen>
HTNW
2
> vom Menschen benutzt. Es wäre ärgerlich, die Grenzen zwischen Argumenten manuell zu definieren, also teilen Sie Shells in Leerzeichen auf, um eine Zeile (eine Liste von Zeichen) in einen Argumentvektor (eine Liste von Listen von Zeichen) umzuwandeln. Die variable Erweiterung ist eine der ersten Erweiterungen. bashSie können sich also vorstellen, dass $adies dem direkten Schreiben des Inhalts entspricht. Jetzt ist das Problem offensichtlich: a="-a -b"; cmd "$a"Erweitert sich zu cmd "-a -b", cmdweiß aber wahrscheinlich nicht, was das bedeutet. cmd $aerweitert sich cmd -a -b, was wahrscheinlich funktioniert .
HTNW

Antworten:

28

Grundsätzlich sollten Sie Variablenerweiterungen in doppelte Anführungszeichen setzen, um sie vor dem Teilen von Wörtern (und der Generierung von Dateinamen) zu schützen. In Ihrem Beispiel jedoch

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Worttrennung ist genau das, was Sie wollen .

Mit "$wget_options"(zitiert), wgetweiß nicht, was mit dem einzelnen Argument zu tun ist --mirror --no-host-directoriesund beschwert sich

wget: unknown option -- mirror --no-host-directories

Damit wgetdie beiden Optionen angezeigt --mirrorund --no-host-directoriesgetrennt werden können, muss eine Wortteilung erfolgen.

Es gibt robustere Möglichkeiten, dies zu tun. Wenn Sie basheine andere Shell verwenden, die Arrays wie bashdo verwendet, lesen Sie die Antwort von Glenn Jackman . Die Antwort von Gilles beschreibt zusätzlich eine alternative Lösung für einfachere Schalen wie den Standard /bin/sh. Beide speichern im Wesentlichen jede Option als separates Element in einem Array.

Verwandte Frage mit guten Antworten: Warum verschluckt sich mein Shell-Skript an Leerzeichen oder anderen Sonderzeichen?


Variable Erweiterungen in doppelten Anführungszeichen sind eine gute Faustregel. Mach das . Dann sei dir der wenigen Fälle bewusst, in denen du das nicht tun solltest. Diese werden Ihnen anhand von Diagnosemeldungen wie der obigen Fehlermeldung angezeigt.

Darüber hinaus gibt es einige Fälle , in denen Sie nicht brauchen variable Erweiterungen zu zitieren. Es ist jedoch einfacher, weiterhin doppelte Anführungszeichen zu verwenden, da dies keinen großen Unterschied macht. Ein solcher Fall ist

variable=$other_variable

Ein anderer ist

case $variable in
    ...) ... ;;
esac
Kusalananda
quelle
2
Bevor Sie diesen split + glob-Operator verwenden können, müssen Sie möglicherweise sicherstellen, dass er $IFSden richtigen Wert enthält. Hier müssen Sie nach Leerzeichen aufteilen, und der Text enthält zufällig keine Tabulatoren oder Zeilenumbrüche. Der Standardwert von $IFSwürde dies jedoch tun, wenn dieser Code in einer Funktion verwendet werden soll, die möglicherweise in einem Kontext aufgerufen wird, in dem $IFSÄnderungen möglich gewesen wären , möchten Sie im $IFSVoraus festlegen (und möglicherweise später wiederherstellen oder einen lokalen Bereich dafür verwenden, wenn der Rest des Codes eine unveränderte annimmt $IFS)
Stéphane Chazelas
32

Die robusteste Methode zum Codieren ist die Verwendung eines Arrays:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
Glenn Jackman
quelle
Das ist die richtige Antwort. Referenz
l0b0
2
Es ist eine gute Antwort, also habe ich sie hochgestuft, aber Kusalanda hat mir mehr geholfen zu verstehen, warum mein Code falsch war und ich kann nur einen akzeptieren.
z32a7ul
Ich geriet in eine Welt voller Schwierigkeiten, bis mir jemand auf der Rsync-Liste dieses Konstrukt zeigte. Es ist besonders hilfreich, wenn einige der Elemente leere Zeichenfolgen sind. Dadurch verschwinden leere Zeichenfolgen. Einige Befehle mögen cpund rsyncwerden unerwartete Dinge tun, wenn Ihr Befehl zu etwas erweitert wird rsync '' rest of parameters. Dies ist ideal, um einen Befehl Stück für Stück unter bestimmten Bedingungen zu erstellen und ihn dann nur einmal an einer Stelle auszuführen.
Joe
17

Sie versuchen, eine Liste von Zeichenfolgen in einer Zeichenfolgenvariablen zu speichern. Es passt nicht. Egal wie Sie auf die Variable zugreifen, etwas ist kaputt.

wget_options='--mirror --no-host-directories'Setzt die Variable wget_optionsauf eine Zeichenfolge, die ein Leerzeichen enthält. An diesem Punkt ist es nicht möglich zu wissen, ob der Raum Teil einer Option oder ein Trennzeichen zwischen Optionen sein soll.

Wenn Sie mit einer Ersetzung in Anführungszeichen auf die Variable zugreifen wget "$wget_options", wird der Wert der Variablen als Zeichenfolge verwendet. Dies bedeutet, dass es als einzelner Parameter an übergeben wird wget, es ist also eine einzelne Option. Dies bricht in Ihrem Fall ab, weil Sie damit mehrere Optionen gemeint haben.

Wenn Sie eine nicht in Anführungszeichen gesetzte Ersetzung verwenden wget $wget_options, wird der Wert der Zeichenfolgenvariablen einem Erweiterungsprozess mit dem Spitznamen "split + glob" unterzogen:

  1. Nehmen Sie den Wert der Variablen und teilen Sie ihn in durch Leerzeichen getrennte Teile auf (vorausgesetzt, Sie haben die $IFSVariable nicht geändert ). Dies führt zu einer Zwischenliste von Zeichenfolgen.
  2. Wenn es sich bei jedem Element der Zwischenliste um ein Platzhaltermuster handelt, das mit einer oder mehreren Dateien übereinstimmt, ersetzen Sie dieses Element durch die Liste der übereinstimmenden Dateien.

Dies funktioniert in Ihrem Beispiel, weil der Aufteilungsprozess das Leerzeichen in ein Trennzeichen verwandelt, aber im Allgemeinen nicht funktioniert, da eine Option Leerzeichen und Platzhalterzeichen enthalten kann.

In ksh, bash, yash und zsh können Sie eine Arrayvariable verwenden. Ein Array in der Shell-Terminologie ist eine Liste von Zeichenfolgen, sodass kein Informationsverlust auftritt. Um eine Array-Variable zu erstellen, setzen Sie Klammern um die Array-Elemente, wenn Sie der Variablen einen Wert zuweisen. Um auf alle Elemente des Arrays zuzugreifen, verwenden Sie - dies ist eine Verallgemeinerung von , die aus den Elementen des Arrays eine Liste bildet. Beachten Sie, dass Sie auch hier die doppelten Anführungszeichen benötigen, da ansonsten jedes Element split + glob durchläuft."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

In plain sh gibt es keine Arrayvariablen. Wenn es Ihnen nichts ausmacht, die Positionsargumente zu verlieren, können Sie sie zum Speichern einer Liste von Zeichenfolgen verwenden.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Weitere Informationen finden Sie unter Warum verschluckt sich mein Shell-Skript an Leerzeichen oder anderen Sonderzeichen?

Gilles 'SO - hör auf böse zu sein'
quelle
Für Ebene sh wäre eine Subshell die Positionsargumente erhalten: (set -- ...; exec wget "$@" ...).
John Kugelman unterstützt Monica