Festlegen von IFS für eine einzelne Anweisung


42

Ich weiß, dass ein benutzerdefinierter IFS-Wert für den Bereich eines einzelnen Befehls / integrierten Befehls festgelegt werden kann. Gibt es eine Möglichkeit, einen benutzerdefinierten IFS-Wert für eine einzelne Anweisung festzulegen? Anscheinend nicht, da nach dem unten stehenden der globale IFS-Wert betroffen ist, wenn dies versucht wird

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001

Antworten:


39

In einigen Shells (einschließlich bash):

IFS=: command eval 'p=($PATH)'

(mit bashkönnen Sie die commandif not in sh / POSIX-Emulation weglassen ). Beachten Sie jedoch, dass bei der Verwendung von Variablen ohne Anführungszeichen in der Regel auch dies erforderlich set -fist und in den meisten Shells kein lokaler Bereich dafür vorhanden ist.

Mit zsh können Sie Folgendes tun:

(){ local IFS=:; p=($=PATH); }

$=PATHist zu Wort Spaltung zu zwingen , die nicht standardmäßig in getan wird zsh(Globbing auf variable Expansion erfolgt auch nicht , so dass Sie nicht brauchen , set -fes sei denn in sh - Emulation).

(){...}(oder function {...}) werden als anonyme Funktionen bezeichnet und in der Regel zum Festlegen eines lokalen Bereichs verwendet. Mit anderen Shells, die den lokalen Funktionsumfang unterstützen, können Sie Folgendes tun:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

Um einen lokalen Bereich für Variablen und Optionen in POSIX-Shells zu implementieren, können Sie auch die unter https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh bereitgestellten Funktionen verwenden . Dann können Sie es verwenden als:

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'

(Übrigens ist es ungültig, so zu teilen $PATH, außer in, zshwie in anderen Shells, IFS ist ein Feldtrennzeichen, kein Feldtrennzeichen).

IFS=$'\n' a=($str)

Ist nur zwei Aufgaben, die einer nach dem anderen genau gefallen a=1 b=2.

Eine Erläuterung zu var=value cmd:

Im:

var=value cmd arg

Die Shell wird /path/to/cmdin einem neuen Prozess ausgeführt und passiert cmdund argin argv[]und var=valuein envp[]. Das ist nicht wirklich eine Variablenzuweisung, sondern die Übergabe von Umgebungsvariablen an den ausgeführten Befehl. In der Bourne- oder Korn-Shell set -kkönnen Sie mit sogar schreiben cmd var=value arg.

Dies gilt nicht für integrierte Funktionen oder Funktionen, die nicht ausgeführt werden . In der Bourne - Shell, in var=value some-builtin, varendet danach eingestellt wird, genau wie bei var=valueallein. Das bedeutet zum Beispiel, dass das Verhalten von var=value echo foo(was nicht nützlich ist) abhängig davon variiert, ob echoes eingebaut ist oder nicht.

POSIX und / oder hat dies kshdahingehend geändert, dass das Bourne-Verhalten nur für eine Kategorie von Buildins auftritt, die als spezielle Buildins bezeichnet werden . evalist ein spezielles eingebautes, readnicht. Für nicht spezielle builtin, var=value builtinsetzt varnur für die Ausführung des builtin , die es ähnlich wie wenn ein externer Befehl verhalten macht , ist gestartet wird.

Der commandBefehl kann verwendet werden, um das spezielle Attribut dieser speziellen eingebauten Elemente zu entfernen . Was POSIX jedoch übersehen hat, ist, dass für die evalund .Builtins die Shells einen Variablenstapel implementieren müssten (obwohl sie die localoder typesetbereichsbeschränkenden Befehle nicht angeben ), weil Sie Folgendes tun könnten:

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

Oder auch:

a=1 command eval myfunction

mit myfunctioneiner Funktion, die verwendet oder einstellt $aund möglicherweise aufruft command eval.

Das war wirklich ein Versehen, weil ksh(worauf die Spezifikation hauptsächlich basiert) es nicht implementiert hat (und AT & T kshund zshimmer noch nicht), aber heutzutage, mit Ausnahme dieser beiden, implementieren die meisten Shells es. Das Verhalten der Muscheln variiert jedoch in folgenden Punkten:

a=0; a=1 command eval a=2; echo "$a"

obwohl. Die Verwendung localvon Shells, die dies unterstützen, ist eine zuverlässigere Möglichkeit, den lokalen Bereich zu implementieren.


Seltsamerweise IFS=: command eval …setzt IFSnur für die Dauer der eval, wie von POSIX vorgeschrieben, in Bindestrich, Pdksh und Bash, aber nicht in Ksh 93u. Es ist ungewöhnlich, zu sehen, dass ksh die seltsame Ausnahme ist.
Gilles 'SO- hör auf böse zu sein'

12

Standardmäßiges Speichern und Wiederherstellen aus "The Unix Programming Environment" von Kernighan und Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}

2
Danke und +1. Ja, ich kenne diese Option, aber ich würde gerne wissen, ob es eine "sauberere" Option gibt, wenn Sie wissen, was ich meine
iruvar

Sie könnten es mit Semikolons auf eine Zeile setzen, aber ich denke nicht, dass das sauberer ist. Es wäre schön, wenn alles, was Sie ausdrücken wollten, eine spezielle syntaktische Unterstützung hätte, aber dann müssten wir wahrscheinlich Zimmerer oder Sumptin lernen, anstatt zu codieren;)
msw

9
Die Wiederherstellung schlägt fehl, $IFSwenn sie zuvor deaktiviert wurde.
Stéphane Chazelas

2
Wenn es nicht gesetzt ist, behandelt Bash es wie folgt$'\t\n'' ' : wiki.bash-hackers.org/syntax/expansion/…
davide

2
@ David, das wäre $' \t\n'. Der Raum muss der erste sein, für den er verwendet wird "$*". Beachten Sie, dass dies bei allen Bourne-ähnlichen Shells der Fall ist.
Stéphane Chazelas

8

Fügen Sie Ihr Skript in eine Funktion ein und rufen Sie diese Funktion auf, indem Sie die Befehlszeilenargumente an sie übergeben. Da IFS lokal definiert ist, wirken sich Änderungen daran nicht auf das globale IFS aus.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

Für diesen Befehl:

IFS=$'\n' a=($str)

Es gibt eine alternative Lösung: Um der ersten IFS=$'\n'Anweisung ( ) einen auszuführenden Befehl (eine Funktion) zu geben:

$ split(){ a=( $str ); }
$ IFS=$'\n' split

Das versetzt IFS in die Umgebung, um Split aufzurufen, wird aber in der aktuellen Umgebung nicht beibehalten.

Dies vermeidet auch den immer riskanten Einsatz von eval.


In ksh93 und mksh sowie in bash und zsh im POSIX-Modus bleibt die $IFSEinstellung $'\n'danach immer noch auf POSIX gesetzt.
Stéphane Chazelas

4

Die vorgeschlagene Antwort von @helpermethod ist sicherlich ein interessanter Ansatz. Es ist aber auch eine Art Falle, da der Gültigkeitsbereich der lokalen Variablen in BASH vom Aufrufer bis zur aufgerufenen Funktion reicht. Wenn Sie daher IFS in main () setzen, bleibt dieser Wert für die von main () aufgerufenen Funktionen erhalten. Hier ist ein Beispiel:

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

Und die Ausgabe ...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

Wenn IFS, das in main () deklariert wurde, in func () noch nicht im Gültigkeitsbereich war, wurde das Array in func () B nicht richtig analysiert.

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

Welches ist, was Sie erhalten sollten, wenn IFS aus dem Anwendungsbereich herausgegangen war.

Eine weitaus bessere Lösung ist meiner Meinung nach, auf Änderungen oder das Vertrauen in IFS auf globaler / lokaler Ebene zu verzichten. Erstellen Sie stattdessen eine neue Shell und spielen Sie mit IFS. Wenn Sie beispielsweise func () in main () wie folgt aufrufen, übergeben Sie das Array als Zeichenfolge mit einem umgekehrten Schrägstrich als Feldtrennzeichen:

func $(IFS='\'; echo "${m_args[*]}")

... diese Änderung an IFS wird nicht in func () wiedergegeben. Das Array wird als String übergeben:

ick\blick\flick

... aber innerhalb von func () bleibt das IFS "/" (wie in main () gesetzt), sofern es nicht lokal in func () geändert wird.

Weitere Informationen zum Isolieren von Änderungen an IFS finden Sie unter den folgenden Links:

Wie konvertiere ich eine Bash-Array-Variable in einen mit Zeilenumbrüchen getrennten String?

Bash-String zum Anordnen mit IFS

Hinweise und Tipps zur allgemeinen Programmierung von Shell-Skripten - Siehe "HINWEIS zur Verwendung von Sub-Shells ..."


Interessant in der Tat ...
Iruvar

"Bash string to array with IFS" IFS=$'\n' declare -a astr=(...)perfekt, danke!
Aquarius Power

1

Dieser Ausschnitt aus der Frage:

IFS=$'\n' a=($str)

wird als zwei separate Zuweisungen globaler Variablen interpretiert, die von links nach rechts ausgewertet werden, und ist äquivalent zu:

IFS=$'\n'; a=($str)

oder

IFS=$'\n'
a=($str)

Dies erklärt sowohl, warum das Global IFSgeändert wurde, als auch warum die Wortaufteilung $strin Array-Elemente mit dem neuen Wert von durchgeführt wurde IFS.

Sie könnten versucht sein, eine Subshell zu verwenden, um den Effekt der IFSÄnderung wie folgt einzuschränken :

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"

Sie werden jedoch schnell bemerken, dass die Änderung von aauch auf die Subshell beschränkt ist:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

Als nächstes würden Sie versucht sein, IFS mit der Lösung aus dieser vorherigen Antwort von @msw zu speichern / wiederherzustellen oder local IFSeine von @helpermethod vorgeschlagene Funktion zu verwenden. Aber ziemlich bald bemerken Sie, dass Sie in allerlei Schwierigkeiten geraten, besonders wenn Sie ein Bibliotheksautor sind, der robust gegen schlechtes Benehmen beim Aufrufen von Skripten sein muss:

  • Was ist, wenn IFSanfangs nicht gesetzt war?
  • Was ist, wenn wir mit set -u(aka set -o nounset) laufen ?
  • Was ist, wenn IFSnur lesbar gemacht wurde declare -r IFS?
  • Was ist, wenn der Sicherungs- / Wiederherstellungsmechanismus für die Rekursion und / oder asynchrone Ausführung (z. B. ein trapHandler) erforderlich ist?

Bitte nicht speichern / wiederherstellen IFS. Halten Sie sich stattdessen an temporäre Änderungen:

  • Verwenden Sie, um die Variablenänderung auf einen einzelnen Befehl, einen integrierten Befehl oder einen Funktionsaufruf zu beschränken IFS="value" command.

    • :Verwenden Sie zum Einlesen mehrerer Variablen durch Aufteilen auf ein bestimmtes Zeichen ( im Folgenden als Beispiel verwendet) Folgendes:

      IFS=":" read -r var1 var2 <<< "$str"
    • Verwenden Sie zum Einlesen eines Arrays (anstelle von array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Begrenzen Sie die Auswirkungen des Änderns der Variablen auf eine Unterschale.

    • So geben Sie die durch Komma getrennten Elemente eines Arrays aus:

      (IFS=","; echo "${array[*]}")
    • So erfassen Sie das in einer Zeichenfolge:

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

0

Die einfachste Lösung besteht darin, eine Kopie des Originals zu $IFSerstellen, wie z. B. in der Antwort von msw. Diese Lösung unterscheidet jedoch nicht zwischen einem nicht festgelegten IFSund einem IFSSatz, der der leeren Zeichenfolge entspricht, was für viele Anwendungen wichtig ist. Hier ist eine allgemeinere Lösung, die diese Unterscheidung erfasst:

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.