Ist es ein vernünftiger Ansatz, die $ IFS-Variable zu "sichern"?

18

Ich bin immer sehr zurückhaltend, $IFSweil es ein globales Problem ist.

Aber oft macht es das Laden von Strings in ein Bash-Array nett und prägnant, und für Bash-Skripte ist es schwierig, Prägnanz zu finden.

Also denke ich, es ist vielleicht besser als gar nichts, wenn ich versuche, den Anfangsinhalt einer $IFSanderen Variablen zu "speichern" und ihn dann sofort wiederherzustellen, nachdem ich mit $IFSetwas fertig bin .

Ist das praktisch? Oder ist es im Grunde genommen sinnlos und ich sollte einfach direkt auf das IFSzurückgreifen, was es für seine späteren Verwendungen benötigt?

Steven Lu
quelle
Warum wäre es nicht praktisch?
Bratchley
Weil das Deaktivieren von IFS den Job gut machen würde.
llua
1
Wenn Sie sagen, dass das Deaktivieren von IFS problemlos funktioniert, beachten Sie, dass dies situationsabhängig ist: stackoverflow.com/questions/39545837/… . Nach meiner Erfahrung ist es am besten, IFS manuell auf die Standardeinstellung für Ihren Shell-Interpreter festzulegen, und zwar, $' \t\n'wenn Sie bash verwenden. unset $IFSEs wird einfach nicht immer auf die erwartete Standardeinstellung zurückgesetzt.
Darrel Holt

Antworten:

9

Sie können IFS nach Bedarf speichern und zuweisen. Daran ist nichts auszusetzen. Es ist nicht ungewöhnlich, den Wert für die Wiederherstellung nach einer vorübergehenden, schnellen Änderung zu speichern, wie z. B. in Ihrem Beispiel für die Array-Zuweisung.

Wie @llua in seinem Kommentar zu Ihrer Frage erwähnt, wird durch einfaches Deaktivieren von IFS das Standardverhalten wiederhergestellt, das dem Zuweisen einer Leerzeichen-Tabulator-Zeile entspricht.

Es lohnt sich zu überlegen, wie problematischer es sein kann, IFS nicht explizit zu setzen / zu entfernen, als dies zu tun.

Ab der POSIX 2013 Edition, 2.5.3 Shell-Variablen :

Implementierungen ignorieren möglicherweise den Wert von IFS in der Umgebung oder das Fehlen von IFS in der Umgebung zum Zeitpunkt des Aufrufs der Shell. In diesem Fall setzt die Shell IFS beim Aufruf auf <Leerzeichen> <Tabulator> <Zeilenumbruch> .

Eine POSIX-kompatible, aufgerufene Shell kann IFS von ihrer Umgebung erben oder nicht. Daraus folgt:

  • Ein portierbares Skript kann IFS nicht zuverlässig über die Umgebung erben.
  • Ein Skript, das nur das standardmäßige Aufteilungsverhalten (oder im Fall von Joining "$*") verwenden möchte, jedoch möglicherweise unter einer Shell ausgeführt wird, die IFS aus der Umgebung heraus initialisiert, muss IFS explizit aktivieren / deaktivieren, um sich gegen das Eindringen in die Umgebung zu schützen.

NB Es ist wichtig zu verstehen, dass für diese Diskussion das Wort "angerufen" eine bestimmte Bedeutung hat. Eine Shell wird nur aufgerufen, wenn sie explizit mit ihrem Namen (einschließlich eines #!/path/to/shellShebang) aufgerufen wird . Eine Subshell, wie sie beispielsweise von $(...)oder erstellt wird, cmd1 || cmd2 &ist keine aufgerufene Shell, und ihr IFS ist (zusammen mit dem Großteil ihrer Ausführungsumgebung) mit dem der übergeordneten Shell identisch. Eine aufgerufene Shell setzt den Wert von $auf ihre PID, während Subshells ihn erben.


Dies ist nicht nur eine pedantische Auseinandersetzung; In diesem Bereich gibt es tatsächliche Abweichungen. Hier ist ein kurzes Skript, das das Szenario mit verschiedenen Shells testet. Es exportiert ein modifiziertes IFS (gesetzt auf :) in eine aufgerufene Shell, die dann ihr Standard-IFS druckt.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS ist im Allgemeinen nicht für den Export markiert, aber wenn dies der Fall ist, beachten Sie, dass bash, ksh93 und mksh die Umgebungen ignorieren IFS=:, während dash und busybox dies berücksichtigen.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Einige Versionsinformationen:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Obwohl bash, ksh93 und mksh IFS nicht aus der Umgebung initialisieren, exportieren sie ihr modifiziertes IFS erneut.

Wenn Sie IFS aus irgendeinem Grund portabel über die Umgebung übergeben müssen, können Sie dies nicht mit IFS selbst tun. Sie müssen den Wert einer anderen Variablen zuweisen und diese Variable für den Export markieren. Kinder müssen diesen Wert dann explizit ihrem IFS zuweisen.

Barfuß IO
quelle
Ich verstehe, wenn ich es umschreiben darf, ist es wahrscheinlich besser , den IFSWert in den meisten Situationen, in denen er verwendet werden soll, explizit anzugeben. Daher ist es oft nicht besonders produktiv, auch nur zu versuchen, seinen ursprünglichen Wert "beizubehalten".
Steven Lu
1
Das wichtigste Problem ist, dass wenn Ihr Skript IFS verwendet, es IFS explizit setzen / entfernen sollte, um sicherzustellen, dass sein Wert Ihren Wünschen entspricht. In der Regel hängt das Verhalten Ihres Skripts von IFS ab, wenn Parametererweiterungen, Befehlsersetzungen, arithmetische Erweiterungen readoder doppelte Verweise auf nicht in Anführungszeichen gesetzt sind $*. Diese Liste ist einfach zu übersehen, daher ist sie möglicherweise nicht vollständig (insbesondere wenn man die POSIX-Erweiterungen moderner Shells in Betracht zieht).
Barefoot IO
10

Im Allgemeinen empfiehlt es sich, die Bedingungen auf den Standard zurückzusetzen.

In diesem Fall jedoch nicht so sehr.

Warum?:

Das Speichern des IFS-Werts ist ebenfalls problematisch.
Wenn das ursprüngliche IFS nicht gesetzt wurde, setzt der Code IFS="$OldIFS"das IFS auf "", nicht auf.

Um den Wert von IFS tatsächlich beizubehalten (auch wenn nicht festgelegt), verwenden Sie Folgendes:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.
Gemeinschaft
quelle
IFS kann nicht wirklich enttäuscht sein. Wenn Sie es deaktivieren, wird es von der Shell auf den Standardwert zurückgesetzt. Sie müssen das also beim Speichern nicht wirklich überprüfen.
Filbranden
Beachten Sie, dass in bash, unset IFSnicht zu ungesetzt IFS , wenn es in einem übergeordneten Kontext (Funktionskontext) lokale erklärt worden war und nicht im aktuellen Kontext.
Stéphane Chazelas
5

Sie haben Recht, wenn Sie zögern, ein globales Unternehmen zu bekämpfen. Fürchte dich nicht, es ist möglich, sauberen Arbeitscode zu schreiben, ohne jemals die aktuelle globale IFSVersion zu ändern oder einen umständlichen und fehleranfälligen Save / Restore-Vorgang auszuführen .

Sie können:

  • Setze IFS für einen einzelnen Aufruf:

    IFS=value command_or_function

    oder

  • Setze IFS in eine Subshell:

    (IFS=value; statement)
    $(IFS=value; statement)

Beispiele

  • So erhalten Sie eine durch Kommas getrennte Zeichenfolge aus einem Array:

    str="$(IFS=, ; echo "${array[*]-}")"

    Hinweis: Dies -dient nur zum Schutz eines leeren Arrays, set -uindem ein Standardwert angegeben wird, wenn dieser nicht festgelegt ist (in diesem Fall ist dieser Wert die leere Zeichenfolge) .

    Die IFSÄnderung ist nur innerhalb der durch die $() Befehlsersetzung hervorgerufenen Subshell anwendbar . Dies liegt daran, dass Subshells Kopien der Variablen der aufrufenden Shell haben und daher deren Werte lesen können. Alle von der Subshell vorgenommenen Änderungen wirken sich jedoch nur auf die Kopie der Subshell und nicht auf die übergeordnete Variable aus.

    Vielleicht denken Sie auch: Warum überspringen Sie nicht die Subshell und tun dies einfach:

    IFS=, str="${array[*]-}"  # Don't do this!

    Hier findet kein Befehlsaufruf statt, und diese Zeile wird stattdessen als zwei unabhängige nachfolgende Variablenzuweisungen interpretiert, als wäre es:

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Lassen Sie uns zum Schluss erklären, warum diese Variante nicht funktioniert:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    Der echoBefehl wird zwar mit der IFSVariablen "set to" aufgerufen ,, ist aber echoegal oder wird nicht verwendet IFS. Die Magie des Expandierens "${array[*]}"zu einer Zeichenkette wird von der (Unter-) Shell selbst ausgeführt, bevor sie echoüberhaupt aufgerufen wird.

  • So lesen Sie eine ganze Datei (die keine NULLBytes enthält ) in eine einzelne Variable mit dem Namen ein VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Anmerkung: IFS=ist dasselbe wie IFS=""und IFS='', wobei alle IFS auf die leere Zeichenfolge setzen. Dies unterscheidet sich stark von unset IFS: Wenn dies IFSnicht der Fall ist, ist das Verhalten aller intern verwendeten Bash-Funktionen IFSgenau so, als hätte IFSes den Standardwert von $' \t\n'.

    Durch das Setzen IFSder leeren Zeichenfolge wird sichergestellt, dass führende und nachfolgende Leerzeichen erhalten bleiben.

    Der Befehl -d ''oder -d ""weist read an, nur den aktuellen Aufruf eines NULLBytes anstelle der üblichen Newline zu stoppen.

  • Um $PATHentlang seiner :Begrenzer zu teilen :

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Dieses Beispiel dient nur der Veranschaulichung. In dem allgemeinen Fall, dass Sie entlang eines Trennzeichens aufteilen, ist es möglich, dass die einzelnen Felder dieses Trennzeichen enthalten (eine maskierte Version davon). Denken Sie daran, eine Zeile einer .csvDatei einzulesen, deren Spalten selbst Kommas enthalten können (in irgendeiner Weise mit Escapezeichen versehen oder in Anführungszeichen gesetzt). Das obige Snippet funktioniert in solchen Fällen nicht wie vorgesehen.

    Das heißt, es ist unwahrscheinlich, dass Sie auf solche :Pfade stoßen $PATH. UNIX / Linux-Pfadnamen dürfen zwar ein enthalten :, es scheint jedoch, dass Bash solche Pfade ohnehin nicht verarbeiten kann, wenn Sie versuchen, sie zu Ihren $PATHDateien hinzuzufügen und ausführbare Dateien darin zu speichern, da kein Code zum Parsen von mit Escapezeichen versehenen / zitierten Doppelpunkten vorhanden ist : Quellcode der Bash 4.4 .

    Beachten Sie schließlich, dass das Snippet eine nachgestellte neue Zeile an das letzte Element des resultierenden Arrays anfügt (wie von @ StéphaneChazelas in jetzt gelöschten Kommentaren angegeben). Wenn die Eingabe eine leere Zeichenfolge ist, handelt es sich bei der Ausgabe um ein einzelnes Element Array, in dem das Element aus einer newline ( $'\n') besteht.

Motivation

Der grundlegende old_IFS="${IFS}"; command; IFS="${old_IFS}"Ansatz, der das Globale berührt IFS, funktioniert erwartungsgemäß für die einfachsten Skripte. Sobald Sie jedoch eine Komplexität hinzufügen, kann diese leicht auseinander brechen und subtile Probleme verursachen:

  • Wenn commandes sich um eine Bash-Funktion handelt, die auch die globale Funktion ändert IFS(entweder direkt oder ausgeblendet in einer weiteren Funktion, die sie aufruft), und dabei versehentlich dieselbe globale old_IFSVariable für das Speichern / Wiederherstellen verwendet, tritt ein Fehler auf.
  • Wie in diesem Kommentar von @Gilles hervorgehoben , funktioniertIFS das naive Speichern und Wiederherstellen nicht , wenn der ursprüngliche Status von nicht festgelegt wurde, und führt sogar zu völligen Fehlern, wenn die häufig (falsch) verwendete set -u(aka set -o nounset) Shell-Option verwendet wird ist in Kraft.
  • Es ist möglich, dass einige Shell-Codes asynchron zum Hauptausführungsfluss ausgeführt werden, z. B. mit Signalhandlern (siehe help trap). Wenn dieser Code auch das globale ändert IFSoder davon ausgeht, dass es einen bestimmten Wert hat, können subtile Fehler auftreten.

Sie könnten eine robustere save / restore - Sequenz (wie derjenige in vorgeschlagenen ersinnen dieser andere Antwort einige oder alle dieser Probleme zu vermeiden. Allerdings würde man das Stück laut Standardcode wiederholen müssen , wo immer Sie vorübergehend eine benutzerdefinierte benötigen IFS. Diese Reduziert die Lesbarkeit und Wartbarkeit des Codes.

Zusätzliche Überlegungen zu bibliotheksähnlichen Skripten

IFSIFSDies ist insbesondere ein Problem für Autoren von Shell-Funktionsbibliotheken, die sicherstellen müssen, dass ihr Code unabhängig vom globalen Status ( , Shell-Optionen, ...), der von ihren Aufrufern festgelegt wurde, stabil funktioniert, und auch, ohne diesen Status überhaupt zu stören (die Aufrufer könnten sich darauf verlassen) drauf bleiben immer statisch).

Wenn Sie Bibliothekscode schreiben, können Sie sich nicht darauf verlassen, IFSdass Sie einen bestimmten Wert haben (nicht einmal den Standardwert) oder überhaupt festgelegt sind. Stattdessen müssen Sie explizit IFSfür jedes Snippet festlegen, von dem das Verhalten abhängt IFS.

Wenn IFSin jeder Codezeile explizit der erforderliche Wert festgelegt ist (auch wenn dies der Standardwert ist), bei dem der Wert mithilfe eines der beiden in dieser Antwort beschriebenen Mechanismen von Bedeutung ist, um den Effekt zu lokalisieren, ist der Code beides unabhängig vom Weltstaat und vermeidet es, ihn gänzlich zu plündern. Dieser Ansatz hat den zusätzlichen Vorteil, dass er für eine Person, die das Skript liest, das IFSfür genau diesen einen Befehl / diese Erweiterung von Bedeutung ist, sehr eindeutig ist, und zwar bei minimalem Textaufwand (im Vergleich zu selbst dem grundlegendsten Speichern / Wiederherstellen).

Welcher Code ist überhaupt betroffen IFS?

Glücklicherweise gibt es nicht so viele Szenarien, in denen es darauf IFSankommt (vorausgesetzt, Sie geben immer Ihre Erweiterungen an ):

  • "$*"und "${array[*]}"Erweiterungen
  • Aufrufe der readintegrierten Targeting-Funktion für mehrere Variablen ( read VAR1 VAR2 VAR3) oder eine Array-Variable ( read -a ARRAY_VAR_NAME)
  • Aufrufe des readTargetings einer einzelnen Variablen, wenn führende / nachfolgende Whitespace- oder Nicht-Whitespace-Zeichen in angezeigt werden IFS.
  • Worttrennung (z. B. für nicht zitierte Erweiterungen, die Sie wie die Pest vermeiden möchten )
  • einige andere weniger häufige Szenarien (Siehe: IFS @ Greg's Wiki )
sls
quelle
Ich kann nicht sagen, dass ich das Trennzeichen für $ PATH verstehe , vorausgesetzt, keine der Komponenten enthält einen: yourself- Satz. Wie können die Komponenten enthalten, :wann :der Begrenzer ist?
Stéphane Chazelas
@ StéphaneChazelas Nun, es :ist ein gültiges Zeichen, das in einem Dateinamen auf den meisten UNIX / Linux-Dateisystemen verwendet wird. Es ist also durchaus möglich, ein Verzeichnis mit einem Namen zu haben, der Folgendes enthält :. Vielleicht haben einige Shells die Möglichkeit, :in PATH mit etwas wie zu flüchten \:, und dann werden Spalten angezeigt, die keine tatsächlichen Begrenzer sind (Bash erlaubt anscheinend kein derartiges Flüchten. Die Low-Level-Funktion, die beim Durchlaufen von $PATHnur Suchen :in verwendet wird eine C-Zeichenfolge: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
Sls
Ich überarbeitete die Antwort auf hoffentlich die Aufspaltung machen $PATHentlang Beispiel :klarer.
Sls
1
Willkommen bei SO! Vielen Dank für eine so ausführliche Antwort :)
Steven Lu
1

Ist das praktisch? Oder ist es im Grunde genommen sinnlos und ich sollte IFS einfach direkt auf das zurücksetzen, was es für die nachfolgenden Verwendungen benötigt?

Warum riskieren Sie einen Tippfehler bei der Einstellung von IFS, $' \t\n'wenn Sie nur noch etwas tun müssen?

OIFS=$IFS
do_your_thing
IFS=$OIFS

Alternativ können Sie eine Subshell aufrufen, wenn Sie keine Variablen benötigen, die in den folgenden Bereichen festgelegt / geändert wurden:

( IFS=:; do_your_thing; )
arielCo
quelle
Dies ist gefährlich, da es nicht funktioniert, wenn IFSes anfangs deaktiviert war.
Gilles 'SO- hör auf böse zu sein'