Warum verschluckt sich mein Shell-Skript an Leerzeichen oder anderen Sonderzeichen?

284

Oder eine Einführung in die robuste Handhabung von Dateinamen und andere Zeichenfolgen, die in Shell-Skripten übergeben werden.

Ich habe ein Shell-Skript geschrieben, das die meiste Zeit gut funktioniert. Aber es drosselt bei einigen Eingaben (z. B. bei einigen Dateinamen).

Ich habe ein Problem wie das folgende festgestellt:

  • Ich habe einen Dateinamen, der ein Leerzeichen enthält hello world, und es wurde als zwei separate Dateien behandelt hellound world.
  • Ich habe eine Eingabezeile mit zwei aufeinanderfolgenden Leerzeichen und sie sind in der Eingabe auf eins geschrumpft.
  • Führende und nachfolgende Leerzeichen verschwinden in den Eingabezeilen.
  • Wenn die Eingabe eines der Zeichen enthält \[*?, werden diese manchmal durch Text ersetzt, der eigentlich der Name der Dateien ist.
  • Es gibt ein Apostroph '(oder ein doppeltes Anführungszeichen ") in der Eingabe und die Dinge wurden nach diesem Punkt seltsam.
  • Die Eingabe enthält einen Backslash (oder: Ich verwende Cygwin und einige meiner Dateinamen haben Windows-ähnliche \Trennzeichen).

Was ist los und wie behebe ich das?

Gilles
quelle
16
shellcheckIhnen helfen, die Qualität Ihrer Programme zu verbessern.
Aurelien
3
Abgesehen von den in den Antworten beschriebenen Schutztechniken, und obwohl dies für die meisten Leser wahrscheinlich offensichtlich ist, ist es meines Erachtens erwähnenswert, darauf hinzuweisen, dass es bei der Verarbeitung von Dateien mit Befehlszeilentools empfehlenswert ist, ausgefallene Zeichen im zu vermeiden Namen an erster Stelle, wenn möglich.
Dienstag,
1
@bli Nein, das lässt nur Bugs länger auftauchen. Es versteckt heute Wanzen. Und jetzt kennen Sie nicht alle Dateinamen, die später in Ihrem Code verwendet wurden.
Volker Siegel
Wenn Ihre Parameter Leerzeichen enthalten, müssen sie zunächst in Anführungszeichen gesetzt werden (in der Befehlszeile). Sie können jedoch die gesamte Befehlszeile abrufen und selbst analysieren. Zwei Leerzeichen werden nicht zu einem Leerzeichen. Beliebig viel Platz sagt Ihrem Skript, dass es die nächste Variable ist. Wenn Sie also so etwas wie "echo $ 1 $ 2" machen, ist es Ihr Skript, das ein Leerzeichen dazwischen setzt. Verwenden Sie auch "find (-exec)", um Dateien mit Leerzeichen anstatt einer for-Schleife zu durchlaufen. Sie können leichter mit den Räumen umgehen.
Patrick Taylor

Antworten:

352

Verwenden Sie immer in doppelte Anführungszeichen Variablenersetzungen und Befehlsersetzungen: "$foo","$(foo)"

Wenn Sie $foonicht in Anführungszeichen setzen, wird Ihr Skript bei Eingaben oder Parametern (oder Befehlsausgaben $(foo)) mit Leerzeichen oder blockiert \[*?.

Dort können Sie aufhören zu lesen. Na gut, hier noch ein paar mehr:

  • read- Um die Eingabe zeilenweise mit dem readeingebauten Code zu lesen , verwenden Siewhile IFS= read -r line; do …
    Plain , umread Backslashes und Whitespace speziell zu behandeln.
  • xargs- Vermeidenxargs . Wenn Sie verwenden müssen xargs, machen Sie das xargs -0. Statt find … | xargs, bevorzugtfind … -exec … .
    xargsbehandelt Whitespace und die Zeichen \"'speziell.

Diese Antwort gilt für Bourne / POSIX-Stil Schalen ( sh, ash, dash, bash, ksh, mksh, yash...). Zsh-Benutzer sollten es überspringen und das Ende von lesen. Wann ist eine doppelte Anführungszeichen erforderlich? stattdessen. Wenn Sie alles genau wissen wollen, lesen Sie den Standard oder das Handbuch Ihrer Shell.


Beachten Sie, dass die folgenden Erläuterungen einige Näherungswerte enthalten (Aussagen, die in den meisten Fällen zutreffen, jedoch vom umgebenden Kontext oder von der Konfiguration beeinflusst werden können).

Warum muss ich schreiben "$foo"? Was passiert ohne die Anführungszeichen?

$foobedeutet nicht "den Wert der Variablen nehmen foo". Es bedeutet etwas viel komplexeres:

  • Nehmen Sie zunächst den Wert der Variablen.
  • Feldaufteilung: Behandeln Sie diesen Wert als eine durch Leerzeichen getrennte Liste von Feldern und erstellen Sie die resultierende Liste. Wenn beispielsweise die Variable enthält foo * bar ​das Ergebnis dieses Schritt dann die 3-Element - Liste foo, *, bar.
  • Generierung von Dateinamen: Behandeln Sie jedes Feld als Glob, dh als Platzhalter, und ersetzen Sie es durch die Liste der Dateinamen, die diesem Muster entsprechen. Wenn das Muster nicht mit Dateien übereinstimmt, bleibt es unverändert. In unserem Beispiel ergibt dies die Liste mit foo, gefolgt von der Liste der Dateien im aktuellen Verzeichnis und schließlich bar. Wenn das aktuelle Verzeichnis leer ist, ist das Ergebnis foo, *, bar.

Beachten Sie, dass das Ergebnis eine Liste von Zeichenfolgen ist. In der Shell-Syntax gibt es zwei Kontexte: Listenkontext und String-Kontext. Feldaufteilung und Dateinamengenerierung erfolgen nur im Listenkontext, dies ist jedoch die meiste Zeit der Fall. Doppelte Anführungszeichen begrenzen einen Zeichenfolgenkontext: Die gesamte Zeichenfolge mit doppelten Anführungszeichen ist eine einzelne Zeichenfolge, die nicht geteilt werden darf. (Ausnahme: "$@"zum Erweitern der Liste der Positionsparameter, entspricht z. B. dem "$@"Vorhandensein von "$1" "$2" "$3"drei Positionsparametern. Siehe Was ist der Unterschied zwischen $ * und $ @? )

Gleiches gilt für die Befehlsersetzung mit $(foo)oder mit `foo`. Verwenden Sie auf keinen Fall `foo`: Die Angebotsregeln sind seltsam und nicht portierbar, und alle modernen Shells unterstützen $(foo)nur intuitive Angebotsregeln, die absolut gleichwertig sind.

Die Ausgabe der arithmetischen Substitution wird ebenfalls erweitert, dies ist jedoch normalerweise kein Problem, da sie nur nicht erweiterbare Zeichen enthält (vorausgesetzt, sie IFSenthält keine Ziffern oder -).

Siehe Wann sind doppelte Anführungszeichen erforderlich? Weitere Informationen zu den Fällen, in denen Sie die Anführungszeichen weglassen können.

Denken Sie daran, Variablen- und Befehlssubstitutionen immer in Anführungszeichen zu setzen, es sei denn, Sie wollen damit einverstanden sein. Seien Sie vorsichtig: Das Auslassen von Anführungszeichen kann nicht nur zu Fehlern, sondern auch zu Sicherheitslücken führen .

Wie verarbeite ich eine Liste von Dateinamen?

Wenn Sie myfiles="file1 file2"mit Leerzeichen schreiben , um die Dateien zu trennen, funktioniert dies nicht mit Dateinamen, die Leerzeichen enthalten. Unix-Dateinamen können andere Zeichen als /(das ist immer ein Verzeichnisseparator) und Null-Bytes enthalten (die Sie in Shellskripten mit den meisten Shells nicht verwenden können).

Gleiches Problem mit myfiles=*.txt; … process $myfiles. Wenn Sie dies tun, myfilesenthält die Variable die 5-stellige Zeichenfolge *.txt, und wenn Sie schreiben $myfiles, wird der Platzhalter erweitert. Dieses Beispiel wird tatsächlich funktionieren, bis Sie Ihr Skript ändern myfiles="$someprefix*.txt"; … process $myfiles. Wenn auf eingestellt someprefixist final report, funktioniert dies nicht.

Um eine Liste beliebiger Art (z. B. Dateinamen) zu verarbeiten, fügen Sie sie in ein Array ein. Dies erfordert mksh, ksh93, yash oder bash (oder zsh, das nicht alle diese Anführungszeichen enthält); Eine einfache POSIX-Shell (z. B. ash oder dash) hat keine Array-Variablen.

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88 verfügt über Array-Variablen mit einer anderen Zuweisungssyntax set -A myfiles "someprefix"*.txt(siehe Zuweisungsvariable unter verschiedenen ksh-Umgebungen, wenn Sie die Portierbarkeit von ksh88 / bash benötigen). Bourne / POSIX-Shells haben ein einziges Array, das Array der Positionsparameter, mit "$@"denen Sie festlegen, setund das für eine Funktion lokal ist:

set -- "$someprefix"*.txt
process -- "$@"

Was ist mit Dateinamen, die mit beginnen -?

Beachten Sie in diesem Zusammenhang, dass Dateinamen mit einem -(Bindestrich / Minus) beginnen können, was von den meisten Befehlen als Kennzeichnung einer Option interpretiert wird. Wenn Sie einen Dateinamen haben, der mit einem variablen Teil beginnt, stellen Sie sicher, dass Sie diesen voranstellen --, wie im obigen Snippet. Dies zeigt dem Befehl an, dass das Ende der Optionen erreicht ist. Danach ist alles ein Dateiname, auch wenn es mit beginnt -.

Alternativ können Sie sicherstellen, dass Ihre Dateinamen mit einem anderen Zeichen als beginnen -. Absolute Dateinamen beginnen mit /und können ./am Anfang von relativen Namen hinzugefügt werden . Das folgende Snippet verwandelt den Inhalt der Variablen fin eine "sichere" Art und Weise, auf dieselbe Datei zu verweisen, von der garantiert wird, dass sie nicht anfängt -.

case "$f" in -*) "f=./$f";; esac

Beachten Sie abschließend, dass einige Befehle auch nachträglich -als Standardeingabe oder Standardausgabe interpretiert werden --. Wenn Sie auf eine tatsächliche Datei mit dem Namen verweisen -müssen oder ein solches Programm aufrufen und nicht von stdin lesen oder in stdout schreiben möchten, müssen Sie die oben beschriebenen Schritte ausführen -. Siehe Was ist der Unterschied zwischen „du -sh *“ und „du -sh ./*“? zur weiteren Diskussion.

Wie speichere ich einen Befehl in einer Variablen?

"Befehl" kann drei Dinge bedeuten: einen Befehlsnamen (der Name als ausführbare Datei mit oder ohne vollständigen Pfad oder der Name einer Funktion, eines eingebauten oder eines Alias), einen Befehlsnamen mit Argumenten oder einen Teil des Shell-Codes. Dementsprechend gibt es verschiedene Möglichkeiten, sie in einer Variablen zu speichern.

Wenn Sie einen Befehlsnamen haben, speichern Sie ihn einfach und verwenden Sie die Variable wie gewohnt in doppelten Anführungszeichen.

command_path="$1"

"$command_path" --option --message="hello world"

Wenn Sie einen Befehl mit Argumenten haben, ist das Problem dasselbe wie bei einer Liste der oben genannten Dateinamen: Dies ist eine Liste von Zeichenfolgen, keine Zeichenfolge. Sie können die Argumente nicht einfach in eine einzelne Zeichenfolge mit Leerzeichen dazwischen einfügen, da Sie sonst den Unterschied zwischen Leerzeichen, die Teil von Argumenten sind, und Leerzeichen, die Argumente trennen, nicht erkennen können. Wenn Ihre Shell über Arrays verfügt, können Sie diese verwenden.

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

Was ist, wenn Sie eine Shell ohne Arrays verwenden? Sie können die Positionsparameter weiterhin verwenden, wenn Sie nichts dagegen haben, sie zu ändern.

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

Was ist, wenn Sie einen komplexen Shell-Befehl speichern müssen, z. B. mit Umleitungen, Pipes usw.? Oder wenn Sie die Positionsparameter nicht ändern möchten? Anschließend können Sie eine Zeichenfolge mit dem Befehl evalerstellen und die integrierte Zeichenfolge verwenden.

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

Beachten Sie die geschachtelten Anführungszeichen in der Definition von code: Die einfachen Anführungszeichen '…'begrenzen ein Zeichenfolgenliteral, sodass der Wert der Variablen codedie Zeichenfolge ist /path/to/executable --option --message="hello world" -- /path/to/file1. Das evaleingebaute Kommando weist die Shell an, die als Argument übergebene Zeichenfolge so zu analysieren, als ob sie im Skript enthalten wäre. An diesem Punkt werden also die Anführungszeichen und die Pipe analysiert usw.

Verwenden evalist schwierig. Überlegen Sie genau, was wann analysiert wird. Insbesondere können Sie nicht einfach einen Dateinamen in den Code einfügen: Sie müssen ihn in Anführungszeichen setzen, genau wie in einer Quellcodedatei. Es gibt keinen direkten Weg, das zu tun. So etwas code="$code $filename"bricht , wenn der Dateiname enthält eine beliebige Shell - Sonderzeichen (Leerzeichen, $, ;, |, <, >, etc.). code="$code \"$filename\""bricht immer noch auf "$\`. Gerade code="$code '$filename'"bricht, wenn der Dateiname a enthält '. Es gibt zwei Lösungen.

  • Fügen Sie dem Dateinamen eine Anführungszeichen-Ebene hinzu. Der einfachste Weg, dies zu tun, ist das Hinzufügen von einfachen Anführungszeichen und das Ersetzen einzelner Anführungszeichen durch '\''.

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
    
  • Behalten Sie die Variablenerweiterung im Code bei, damit sie nachgeschlagen wird, wenn der Code ausgewertet wird, und nicht, wenn das Codefragment erstellt wird. Dies ist einfacher, funktioniert aber nur, wenn die Variable zum Zeitpunkt der Codeausführung immer noch denselben Wert aufweist, nicht z. B., wenn der Code in einer Schleife erstellt wird.

    code="$code \"\$filename\""

Benötigen Sie wirklich eine Variable mit Code? Die natürlichste Art, einem Codeblock einen Namen zu geben, besteht darin, eine Funktion zu definieren.

Was ist readlos mit ?

Ohne -r, readerlaubt Fortsetzungszeilen - dies ist eine einzige logische Eingabezeile:

hello \
world

readTeilt die Eingabezeile in Felder auf, die durch Zeichen in $IFS(ohne -rBackslash) getrennt sind. Wenn zum Beispiel der Eingabe eine Zeile mit drei Worten ist, dann read first second thirdsetzt firstauf das erste Wort der Eingabe, der secondauf das zweite Wort und thirdmit dem dritten Wort. Wenn es mehr Wörter gibt, enthält die letzte Variable alles, was nach dem Setzen der vorhergehenden übrig bleibt. Führende und nachfolgende Leerzeichen werden abgeschnitten.

Das Setzen IFSauf die leere Zeichenkette vermeidet jegliches Beschneiden. Siehe Warum wird `while IFS = read` so oft verwendet, anstatt` IFS =; während gelesen..`? für eine längere Erklärung.

Was ist los mit xargs?

Das Eingabeformat xargsbesteht aus durch Leerzeichen getrennten Zeichenfolgen, die wahlweise in einfache oder doppelte Anführungszeichen gesetzt werden können. Kein Standardwerkzeug gibt dieses Format aus.

Die Eingabe in xargs -L1oder xargs -list fast eine Liste von Zeilen, aber nicht ganz - wenn am Ende einer Zeile ein Leerzeichen steht, ist die folgende Zeile eine Fortsetzungszeile.

Sie können verwenden, xargs -0wo zutreffend (und wo verfügbar: GNU (Linux, Cygwin), BusyBox, BSD, OSX, aber es ist nicht in POSIX). Das ist sicher, da Null-Bytes in den meisten Daten, insbesondere in Dateinamen, nicht vorkommen können. Verwenden find … -print0Sie zum Erstellen einer durch Nullen getrennten Liste von Dateinamen (oder find … -exec …wie unten erläutert).

Wie verarbeite ich Dateien, die von gefunden wurden find?

find  -exec some_command a_parameter another_parameter {} +

some_commandmuss ein externer Befehl sein, es kann keine Shell-Funktion oder ein Alias ​​sein. Wenn Sie eine Shell aufrufen müssen, um die Dateien zu verarbeiten, rufen Sie sie shexplizit auf.

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

Ich habe noch eine andere Frage

Durchsuchen Sie das auf dieser Website oder die oder das . (Klicken Sie auf "Weitere Informationen ...", um einige allgemeine Tipps und eine handverlesene Liste häufig gestellter Fragen anzuzeigen.) Wenn Sie gesucht haben und keine Antwort finden, fragen Sie nach .

Gilles
quelle
6
@ John1024 Da es sich nur um eine GNU-Funktion handelt, bleibe ich bei „no standard tool“.
Gilles
2
Sie brauchen auch Anführungszeichen $(( ... ))(auch $[...]in einigen Shells), außer in zsh(sogar in der Sh-Emulation) und mksh.
Stéphane Chazelas
3
Beachten Sie, dass dies xargs -0kein POSIX ist. Mit Ausnahme von FreeBSD xargsmöchten Sie im Allgemeinen xargs -r0statt xargs -0.
Stéphane Chazelas
2
@ John1024, nein, ls --quoting-style=shell-alwaysist nicht kompatibel mit xargs. Versuchen Sietouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas
3
Eine weitere nette Funktion (nur GNU) ist xargs -d "\n", dass Sie beispielsweise locate PATTERN1 |xargs -d "\n" grep PATTERN2nach Dateinamen suchen können, die mit PATTERN1 und mit PATTERN2 übereinstimmen . Ohne GNU kann man das zB so machenlocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz
26

Während Gilles Antwort ausgezeichnet ist, gehe ich auf seinen Hauptpunkt ein

Verwenden Sie immer doppelte Anführungszeichen um Variablensubstitutionen und Befehlssubstitutionen: "$ foo", "$ (foo)"

Wenn Sie mit einer Bash-ähnlichen Shell beginnen, die Worttrennung ausführt, ist der sichere Rat natürlich immer die Verwendung von Anführungszeichen. Die Wortteilung wird jedoch nicht immer durchgeführt

§ Wortteilung

Diese Befehle können fehlerfrei ausgeführt werden

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

Ich ermutige Benutzer nicht, dieses Verhalten zu übernehmen, aber wenn jemand genau versteht, wann es zu Wortspaltungen kommt, sollte er selbst entscheiden können, wann er Anführungszeichen verwendet.

Steven Penny
quelle
19
Wie ich in meiner Antwort erwähne , siehe unix.stackexchange.com/questions/68694/… für Details. Beachten Sie die Frage "Warum erstickt mein Shell-Skript?". Das häufigste Problem (aufgrund jahrelanger Erfahrung auf dieser Website und anderswo) sind fehlende Anführungszeichen. "Immer doppelte Anführungszeichen verwenden" ist leichter zu merken als "Immer doppelte Anführungszeichen verwenden, außer in den Fällen, in denen sie nicht erforderlich sind".
Gilles
14
Regeln sind für Anfänger schwer zu verstehen. Zum Beispiel foo=$barist OK, aber export foo=$baroder env foo=$varnicht (zumindest in einigen Shells). Ein Tipp für Anfänger: Geben Sie immer Ihre Variablen an, es sei denn, Sie wissen, was Sie tun, und Sie haben einen guten Grund, dies nicht zu tun .
Stéphane Chazelas
5
@StevenPenny Ist es wirklich korrekter? Gibt es vernünftige Fälle, in denen Anführungszeichen das Skript brechen würden? In Situationen, in denen in halben Fällen Anführungszeichen verwendet werden müssen und in anderen Fällen Anführungszeichen optional verwendet werden können, sollte die Empfehlung "Immer Anführungszeichen verwenden, nur für den Fall" in Betracht gezogen werden, da dies wahr, einfach und weniger riskant ist. Das Unterrichten solcher Ausnahmelisten für Anfänger ist bekanntermaßen ineffektiv (ohne Kontext, an den sie sich nicht erinnern können) und kontraproduktiv, da sie benötigte / nicht benötigte Zitate verwirren, ihre Skripte brechen und sie demotivieren, um weiter zu lernen.
Peteris
6
Meine $ 0.02 wären, dass es ein guter Rat ist, alles zu zitieren. Irrtümlich etwas zu zitieren, das es nicht braucht, ist harmlos. Irrtümlich etwas zu zitieren, das es braucht, ist schädlich. Für die Mehrheit der Autoren von Shell-Skripten, die die Feinheiten der genauen Wortteilung nie verstehen werden, ist es daher viel sicherer, alles zu zitieren, als nur zu zitieren, wenn dies erforderlich ist.
Godlygeek
5
@Peteris und godlygeek: "Gibt es vernünftige Fälle, in denen Anführungszeichen das Drehbuch brechen würden?" Dies hängt von Ihrer Definition von "angemessen" ab. Wenn ein Skript gesetzt ist criteria="-type f", find . $criteriafunktioniert es, find . "$criteria"aber nicht.
G-Man
22

Soweit ich weiß, gibt es nur zwei Fälle, in denen Erweiterungen in doppelte Anführungszeichen gesetzt werden müssen. In diesen Fällen handelt es sich um die beiden speziellen Shell-Parameter "$@"und "$*"-, die angegeben werden, um in doppelte Anführungszeichen eingeschlossen unterschiedlich zu expandieren. In allen anderen Fällen (möglicherweise mit Ausnahme von Shell-spezifischen Array-Implementierungen) ist das Verhalten einer Erweiterung konfigurierbar - dafür gibt es Optionen.

Dies soll natürlich nicht heißen, dass doppelte Anführungszeichen vermieden werden sollten - im Gegenteil, es ist wahrscheinlich die bequemste und robusteste Methode zur Begrenzung einer Erweiterung, die die Shell zu bieten hat. Aber ich denke, da Alternativen bereits fachmännisch erläutert wurden, ist dies ein ausgezeichneter Ort, um zu diskutieren, was passiert, wenn die Shell einen Wert erweitert.

Die Shell in ihrem Herzen und in ihrer Seele (für diejenigen, die solche haben) ist ein Befehlsinterpreter - sie ist ein Parser, wie ein großer, interaktiver sed. Wenn Ihre Shell-Anweisung an Leerzeichen oder Ähnlichem erstickt , ist es sehr wahrscheinlich, dass Sie den Interpretationsprozess der Shell nicht vollständig verstanden haben - insbesondere, wie und warum sie eine Eingabeanweisung in einen ausführbaren Befehl übersetzt. Die Aufgabe der Shell ist es:

  1. Eingabe akzeptieren

  2. interpretieren und spalten sie richtig in Tokens übersetzten Eingangsworte

    • Eingabewörter sind die Shell - Syntax Elemente wie $wordoderecho $words 3 4* 5

    • Wörter werden immer in Leerzeichen aufgeteilt - das ist nur die Syntax -, aber nur die literalen Leerzeichen, die in der Eingabedatei der Shell zur Verfügung gestellt werden

  3. Erweitern Sie diese gegebenenfalls in mehrere Felder

    • Felder ergeben sich aus Wort Erweiterungen - sie die endgültige ausführbare Befehl bilden

    • ausgenommen "$@", $IFS Feldaufspaltung und Pfadnamenserweiterung ein Eingangswort zu einem einzelnen immer auswerten muß Feld .

  4. und dann den resultierenden Befehl auszuführen

    • In den meisten Fällen geht es darum, die Ergebnisse der Interpretation in irgendeiner Form weiterzugeben

Die Leute sagen oft, die Hülle sei ein Klebstoff , und wenn dies zutrifft, dann haften Listen von Argumenten - oder Feldern - an dem einen oder anderen Prozess , wenn dies der Fall ist exec. Die meisten Shells behandeln das NULByte nicht gut - wenn überhaupt - und das liegt daran, dass sie sich bereits darauf aufteilen. Die Shell hat exec viel zu tun und muss dies mit einem NULbegrenzten Array von Argumenten tun , die sie dem Systemkern zur execZeit übergibt . Wenn Sie den Begrenzer der Shell mit den begrenzten Daten vermischen würden, würde die Shell dies wahrscheinlich vermasseln. Seine internen Datenstrukturen basieren - wie die meisten Programme - auf diesem Begrenzer. zshInsbesondere vermasselt dies nicht.

Und genau hier $IFSkommt ins $IFSSpiel. Dies ist ein immer vorhandener und ebenfalls einstellbarer Shell-Parameter, der definiert, wie die Shell die Shell-Erweiterungen von Wort zu Feld aufteilen soll - insbesondere, welche Werte durch diese Felder begrenzt werden sollen. $IFSteilt Shell-Erweiterungen auf andere Trennzeichen als NUL- oder mit anderen Worten, die Shell ersetzt Bytes, die aus einer Erweiterung resultieren, die mit dem Wert von $IFSwith NULin ihren internen Datenarrays übereinstimmt . Wenn man es aussehen , dass Sie damit beginnen könnten , um zu sehen , dass jede Feld-split Shell Erweiterung eine ist $IFSseparierten Datenarray.

Es ist wichtig zu verstehen, dass $IFSnur Erweiterungen begrenzt werden , die nicht bereits anderweitig begrenzt sind - was Sie mit "doppelten Anführungszeichen tun können . Wenn Sie eine Erweiterung zitieren, begrenzen Sie sie am Anfang und mindestens am Ende ihres Werts. In diesen Fällen $IFStrifft dies nicht zu, da keine zu trennenden Felder vorhanden sind. In der Tat, eine doppelte Anführungszeichen Expansion zeigt identisches Feldaufteilungsverhalten zu einer nicht notierten Expansion , wenn IFS=auf einen leeren Wert eingestellt ist.

Sofern nicht anders angegeben, $IFShandelt es sich um eine $IFSbegrenzte Shell-Erweiterung. Der Standardwert ist <space><tab><newline>- alle drei weisen spezielle Eigenschaften auf, wenn sie in enthalten sind $IFS. Wohingegen jedes andere Wert $IFSwird auf einen einzelnen bewerten spezifizierten Feld pro Expansions Auftreten , $IFS Leerzeichen - jede dieser drei - angegeben wird auf ein einzelnes Feld pro Expansion elide Sequenz und Vorder- / Hinter Sequenzen sind vollständig elided. Dies ist wahrscheinlich am einfachsten anhand eines Beispiels zu verstehen.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Aber das ist nur $IFS- nur die Worttrennung oder das Leerzeichen, wie gefragt, also was ist mit den Sonderzeichen ?

Die Shell erweitert - standardmäßig - auch bestimmte nicht zitierte Token (wie ?*[hier an anderer Stelle erwähnt) in mehrere Felder, wenn sie in einer Liste vorkommen. Dies wird als Pfadnamenerweiterung oder Globbing bezeichnet . Es ist ein unglaublich nützliches Tool, und da es nach dem Aufteilen von Feldern in der Syntaxreihenfolge der Shell nicht von $ IFS betroffen ist - Felder, die durch eine Pfadnamenerweiterung generiert werden, werden am Kopf / Ende der Dateinamen selbst abgegrenzt, unabhängig davon, ob Ihr Inhalt enthält alle Zeichen, die sich derzeit in befinden $IFS. Dieses Verhalten ist standardmäßig aktiviert, kann aber sehr einfach anders konfiguriert werden.

set -f

Das weist die Shell an, nicht zu globieren . Die Erweiterung des Pfadnamens erfolgt erst, wenn diese Einstellung auf irgendeine Weise rückgängig gemacht wird - beispielsweise, wenn die aktuelle Shell durch einen anderen neuen Shell-Prozess ersetzt wird oder ...

set +f

... wird an die Shell ausgegeben. Doppelte Anführungszeichen - wie auch beim $IFS Aufteilen von Feldern - machen diese globale Einstellung pro Erweiterung überflüssig. Damit:

echo "*" *

... wenn die Pfadnamenerweiterung derzeit aktiviert ist, werden wahrscheinlich sehr unterschiedliche Ergebnisse pro Argument erzielt - da das erste nur bis zu seinem Literalwert (das einzige Sternchen, das heißt, überhaupt nicht) und das zweite nur bis zu demselben Wert erweitert wird wenn das aktuelle Arbeitsverzeichnis keine Dateinamen enthält, die möglicherweise übereinstimmen (und es stimmt mit fast allen überein) . Wenn Sie jedoch Folgendes tun:

set -f; echo "*" *

... die Ergebnisse für beide Argumente sind identisch - das erweitert *sich dann nicht.

mikeserv
quelle
Eigentlich stimme ich @ StéphaneChazelas zu, dass es die Dinge (meistens) mehr verwirrt als hilft ... aber ich fand es persönlich hilfreich, also habe ich mich dafür ausgesprochen. Ich habe jetzt eine bessere Vorstellung (und einige Beispiele) davon, wie es IFStatsächlich funktioniert. Was ich nicht verstehe , ist, warum es jemals eine gute Idee wäre IFS, etwas anderes als die Standardeinstellung zu wählen.
Wildcard
1
@Wildcard - ist ein Feldbegrenzer. Wenn Sie einen Wert in einer Variablen haben, den Sie auf mehrere Felder erweitern möchten, teilen Sie ihn auf $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; donedruckt \ndann usr\ndann bin\n. Das erste Feld echoist leer, da /es sich um ein Nullfeld handelt. Die path_components können Zeilenumbrüche oder Leerzeichen oder was auch immer enthalten - das wäre egal, da die Komponenten aufgeteilt wurden /und nicht der Standardwert. Leute machen es awksowieso die ganze Zeit. Ihre Muschel macht es auch
mikeserv
3

Ich hatte ein großes Videoprojekt mit Leerzeichen in Dateinamen und Leerzeichen in Verzeichnisnamen. Während find -type f -print0 | xargs -0Arbeiten für verschiedene Zwecke und in verschiedenen Schalen, finde ich , dass eine benutzerdefinierte IFS (Eingabefeld Separator) gibt Ihnen mehr Flexibilität zu verwenden , wenn Sie bash verwenden. Das folgende Snippet verwendet bash und setzt IFS nur auf eine neue Zeile. vorausgesetzt, Ihre Dateinamen enthalten keine Zeilenumbrüche:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

Beachten Sie die Verwendung von Parens, um die Neudefinition von IFS zu isolieren. Ich habe andere Beiträge darüber gelesen, wie man IFS wiederherstellt, aber das ist nur einfacher.

Wenn Sie IFS auf newline setzen, können Sie außerdem Shell-Variablen im Voraus festlegen und diese problemlos ausdrucken. Zum Beispiel kann ich eine Variable V inkrementell vergrößern, indem ich Zeilenumbrüche als Trennzeichen verwende:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

und entsprechend:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

Jetzt kann ich die Einstellung von V mit echo "$V"doppelten Anführungszeichen "auflisten", um die Zeilenumbrüche auszugeben. (Wir danken diesem Thread für die $'\n'Erklärung.)

Russ
quelle
3
Aber dann haben Sie immer noch Probleme mit Dateinamen, die Zeilenvorschub- oder Glob-Zeichen enthalten. Siehe auch: Warum ist das Schleifen über Finds Ausgabe eine schlechte Übung? . Wenn Sie verwenden zsh, können Sie verwenden IFS=$'\0'und verwenden -print0( zshbei Erweiterungen wird kein Globbing ausgeführt, sodass Glob-Zeichen dort kein Problem darstellen).
Stéphane Chazelas
1
Dies funktioniert mit Dateinamen, die Leerzeichen enthalten, jedoch nicht mit potenziell feindlichen Dateinamen oder versehentlich "unsinnigen" Dateinamen. Sie können das Problem von Dateinamen, die Platzhalterzeichen enthalten, einfach beheben, indem Sie sie hinzufügen set -f. Andererseits schlägt Ihr Ansatz bei Dateinamen, die Zeilenumbrüche enthalten, grundsätzlich fehl. Wenn es sich um andere Daten als Dateinamen handelt, schlägt dies auch bei leeren Elementen fehl.
Gilles
Richtig, meine Einschränkung ist, dass es mit Zeilenumbrüchen in Dateinamen nicht funktioniert. Ich glaube jedoch, wir müssen die Linie nur schüchtern vom Wahnsinn ziehen ;-)
Russ
Und ich bin nicht sicher, warum dies eine Ablehnung erhalten hat. Dies ist eine durchaus sinnvolle Methode, um Dateinamen mit Leerzeichen zu durchlaufen. Die Verwendung von -print0 erfordert xargs, und es gibt Dinge, die bei der Verwendung dieser Kette schwierig sind. Es tut mir leid, dass jemand meiner Antwort nicht zustimmt, aber das ist kein Grund, sie abzulehnen.
Russ
0

Wenn Sie alle oben genannten Sicherheitsaspekte berücksichtigen und davon ausgehen, dass Sie den Variablen vertrauen und diese steuern, können Sie mehrere Pfade mit Leerzeichen verwenden eval. Aber sei vorsichtig!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
Mattias Wadman
quelle