Czy rozsądnym podejściem jest „tworzenie kopii zapasowej” zmiennej $ IFS?

19

Zawsze jestem bardzo niezdecydowany, żeby się z $IFStym pogodzić, ponieważ jest to globalny problem.

Ale często sprawia, że ​​ładowanie ciągów do tablicy bash jest przyjemne i zwięzłe, a dla skryptów bash trudno jest uzyskać zwięzłość.

Myślę więc, że może być lepsze niż nic, jeśli spróbuję „zapisać” początkową zawartość $IFSinnej zmiennej, a następnie przywrócę ją natychmiast po tym, jak skończę używać $IFSczegoś.

Czy to jest praktyczne? A może jest to w zasadzie bezcelowe i powinienem po prostu bezpośrednio IFScofnąć się do tego, co trzeba, do jego kolejnych zastosowań?

Steven Lu
źródło
Dlaczego to nie byłoby praktyczne?
Bratchley,
Ponieważ rozbrojenie IFS wykona zadanie dobrze.
llua
1
Dla tych, którzy mówią, że kasowaniu IFS będzie działać poprawnie, należy pamiętać, że jest sytuacyjne: stackoverflow.com/questions/39545837/... . Z mojego doświadczenia wynika, że ​​najlepiej ustawić ręcznie IFS na domyślny interpreter powłoki, a mianowicie $' \t\n'jeśli używasz bash. unset $IFSpo prostu nie zawsze przywraca go do wartości domyślnej.
Darrel Holt

Odpowiedzi:

9

W razie potrzeby możesz zapisać i przypisać do IFS. Nie ma w tym nic złego. Często zdarza się, aby zapisać jego wartość do przywrócenia po tymczasowej, szybkiej modyfikacji, takiej jak przykład przypisania tablicy.

Jak @llua wspomina w swoim komentarzu do twojego pytania, po prostu rozbrojenie IFS przywróci domyślne zachowanie, równoważne przypisaniu spacji-tab-nowej linii.

Warto zastanowić się, w jaki sposób może być bardziej problematyczne nie jawnie ustawiać / wyłączać IFS, niż to robić.

Od wersji POSIX 2013, 2.5.3 Shell Variables :

Implementacje mogą ignorować wartość IFS w środowisku lub brak IFS ze środowiska podczas wywoływania powłoki, w którym to przypadku powłoka powinna ustawić IFS na <space> <tab> <newline>, gdy jest wywoływana .

Wywołana powłoka zgodna z POSIX może, ale nie musi, dziedziczyć IFS ze swojego środowiska. Z tego wynika:

  • Skrypt przenośny nie może w sposób niezawodny dziedziczyć systemu IFS przez środowisko.
  • Skrypt, który zamierza używać tylko domyślnego zachowania dzielącego (lub łączenia, w przypadku "$*"), ale który może działać pod powłoką inicjującą IFS ze środowiska, musi jawnie ustawić / rozbroić IFS, aby bronić się przed ingerencją w środowisko.

Uwaga: Należy zrozumieć, że w tej dyskusji słowo „przywołane” ma szczególne znaczenie. Powłoka jest wywoływana tylko wtedy, gdy jest jawnie wywoływana przy użyciu jej nazwy (w tym #!/path/to/shellshebang). Podpowłoka - taka, która może być utworzona przez $(...)lub cmd1 || cmd2 &- nie jest wywoływaną powłoką, a jej IFS (wraz z większością środowiska wykonawczego) jest identyczny z nadrzędnym. Wywołana powłoka ustawia wartość $swojego pid, a podpowłoki ją dziedziczą.


To nie jest tylko pedantyczna wyprawa; w tej dziedzinie istnieje rzeczywista rozbieżność. Oto krótki skrypt, który testuje scenariusz przy użyciu kilku różnych powłok. Eksportuje zmodyfikowany IFS (ustawiony na :) do wywoływanej powłoki, która następnie drukuje domyślny IFS.

$ 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 nie jest generalnie oznaczony do eksportu, ale jeśli tak, zwróć uwagę, jak bash, ksh93 i mksh ignorują środowisko IFS=:, podczas gdy dash i busybox honorują to.

$ 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

Niektóre informacje o wersji:

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

Mimo że bash, ksh93 i mksh nie inicjują IFS ze środowiska, reeksportują zmodyfikowany IFS.

Jeśli z jakiegokolwiek powodu musisz przenośnie przekazać IFS przez środowisko, nie możesz tego zrobić za pomocą samego IFS; musisz przypisać wartość do innej zmiennej i oznaczyć tę zmienną do eksportu. Dzieci będą wówczas musiały wyraźnie przypisać tę wartość do swojego IFS.

Boso IO
źródło
Rozumiem, więc jeśli mogę sparafrazować, to jest bardziej przenośne, aby wyraźnie określić IFSwartość w większości sytuacji, w których ma być używana, i dlatego często nie jest zbyt produktywna nawet próba „zachowania” pierwotnej wartości.
Steven Lu
1
Najważniejszym problemem jest to, że jeśli twój skrypt korzysta z IFS, powinien jawnie ustawić / rozbroić IFS, aby upewnić się, że jego wartość jest taka, jakiej chcesz. Zazwyczaj zachowanie skryptu zależy od IFS, jeśli występują jakieś niecytowane rozszerzenia parametrów, niecytowane podstawienia poleceń, niecytowane rozszerzenia arytmetyczne, reads lub podwójne cudzysłowy $*. Ta lista jest tuż nad moją głową, więc może nie być wyczerpująca (szczególnie jeśli weźmiemy pod uwagę rozszerzenia POSIX współczesnych powłok).
Boso IO
10

Ogólnie dobrą praktyką jest przywracanie domyślnych warunków.

Jednak w tym przypadku nie tak bardzo.

Dlaczego?:

Problemem jest także przechowywanie wartości IFS.
Jeśli oryginalny IFS został rozbrojony, kod IFS="$OldIFS"ustawi IFS na "", a nie rozbroi go.

Aby faktycznie zachować wartość IFS (nawet jeśli jest rozbrojona), użyj tego:

${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.
Społeczność
źródło
IFS nie da się tak naprawdę rozbroić. Jeśli ją rozbroisz, powłoka przywróci ją do wartości domyślnej. Więc tak naprawdę nie musisz tego sprawdzać podczas zapisywania.
filbranden
Uważaj, że in bash, unset IFSnie rozbroi IFS, jeśli został zadeklarowany lokalnie w kontekście nadrzędnym (kontekst funkcji), a nie w bieżącym kontekście.
Stéphane Chazelas
5

Masz rację, że wahasz się przed zrzucaniem globalnego świata. Nie obawiaj się, możliwe jest napisanie czystego działającego kodu bez modyfikowania rzeczywistego globalnegoIFS lub wykonywania uciążliwego i podatnego na błędy tańca zapisu / przywracania.

Możesz:

  • ustaw IFS dla pojedynczego wywołania:

    IFS=value command_or_function

    lub

  • ustaw IFS w podpowłoce:

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

Przykłady

  • Aby uzyskać ciąg rozdzielany przecinkami z tablicy:

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

    Uwaga: -służy wyłącznie do ochrony pustej tablicy przed set -upodaniem wartości domyślnej, gdy nie jest ustawiona (w tym przypadku jest to pusty ciąg) .

    The IFSModyfikacja dotyczy tylko wewnątrz podpowłoce wywoływanych przez $() podstawienie poleceń . Wynika to z tego, że podpowłoki mają kopie zmiennych wywołującej powłoki i dlatego mogą odczytywać ich wartości, ale wszelkie modyfikacje wykonywane przez podpowłokę wpływają tylko na kopię podpowłoki, a nie na zmienną rodzica.

    Być może zastanawiasz się: dlaczego nie pominąć podpowłoki i po prostu zrobić to:

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

    Nie ma tu wywołania polecenia, a ten wiersz jest interpretowany jako dwa kolejne niezależne przypisania zmiennych, tak jakby to było:

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

    Na koniec wyjaśnijmy, dlaczego ten wariant nie działa:

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

    echoPolecenie rzeczywiście nazwać swoją IFSzmienną do zestawu ,, ale echonie obchodzi lub wykorzystanie IFS. Magia rozwijania "${array[*]}"do łańcucha jest wykonywana przez samą (pod-) powłokę, zanim echojeszcze zostanie wywołana.

  • Aby wczytać cały plik (który nie zawiera NULLbajtów) do jednej zmiennej o nazwie VAR:

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

    Uwaga: IFS=jest taki sam jak IFS=""i IFS='', z których wszystkie ustawiają IFS na pusty ciąg, który jest bardzo różny od unset IFS: jeśli IFSnie jest ustawiony, zachowanie wszystkich funkcji bash, które wewnętrznie używają IFSjest dokładnie takie samo, jak gdybyIFS miał domyślną wartość$' \t\n' .

    Oprawa IFS pustego ciągu gwarantuje zachowanie początkowych i końcowych białych znaków.

    Polecenie -d ''lub -d ""informuje, aby zatrzymać tylko bieżące wywołanie naNULL bajcie zamiast zwykłej nowej linii.

  • Aby podzielić $PATHwzdłuż :ograniczników:

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

    Ten przykład jest wyłącznie ilustracyjny. W ogólnym przypadku, gdy dzielisz wzdłuż separatora, poszczególne pola mogą zawierać (separowaną wersję) tego separatora. Pomyśl o próbie wczytania wiersza.csv pliku, którego kolumny mogą same zawierać przecinki (znaki specjalne lub cudzysłowy). Powyższy fragment kodu nie będzie działał zgodnie z przeznaczeniem w takich przypadkach.

    To powiedziawszy, jest mało prawdopodobne, abyś spotkał w sobie takie :zawierające ścieżki $PATH. Chociaż ścieżki UNIX / Linux mogą zawierać a :, wydaje się , że bash i tak nie będzie w stanie obsłużyć takich ścieżek, jeśli spróbujesz je dodać do swoich $PATHplików i zapisać w nich pliki wykonywalne, ponieważ nie ma kodu do analizy dwukropków : kod źródłowy bash 4.4 .

    Na koniec zauważ, że fragment dołącza końcowy znak nowej linii do ostatniego elementu wynikowej tablicy (jak wywołano @ StéphaneChazelas w teraz usuniętych komentarzach) i że jeśli wejście jest pustym ciągiem, wynikiem będzie pojedynczy element tablica, w której element będzie się składał z nowej linii ( $'\n').

Motywacja

Podstawowe old_IFS="${IFS}"; command; IFS="${old_IFS}"podejście, które dotyka globalnego, IFSbędzie działać zgodnie z oczekiwaniami w przypadku najprostszych skryptów. Jednak natychmiast po dodaniu złożoności może łatwo się rozdzielić i spowodować subtelne problemy:

  • Jeśli commandjest to funkcja bash, która również modyfikuje globalną IFS(albo bezpośrednio, albo ukryta, wewnątrz jeszcze jednej funkcji, którą wywołuje) i robiąc to błędnie używa tej samej old_IFSzmiennej globalnej do zapisywania / przywracania, otrzymujesz błąd.
  • Jak wskazano w tym komentarzu @Gilles , jeśli pierwotny stan IFSbył nieuzbrojony, naiwne zapisywanie i przywracanie nie zadziała, a nawet spowoduje całkowite awarie, jeśli powszechnie używana set -u( błędnie) (aka set -o nounset) opcja powłoki obowiązuje.
  • Możliwe jest, że niektóre kody powłoki wykonują się asynchronicznie z głównym przepływem wykonywania, na przykład za pomocą procedur obsługi sygnałów (patrz help trap). Jeśli ten kod również modyfikuje globalny IFSlub zakłada, że ​​ma określoną wartość, możesz uzyskać subtelne błędy.

Można opracować bardziej niezawodną sekwencję zapisywania / przywracania (taką jak ta zaproponowana w tej drugiej odpowiedzi, aby uniknąć niektórych lub wszystkich tych problemów. Jednak trzeba będzie powtarzać ten fragment głośnego kodu płyty kotłowej wszędzie tam, gdzie tymczasowo potrzebujesz niestandardowegoIFS . zmniejsza czytelność kodu i łatwość konserwacji.

Dodatkowe uwagi na temat skryptów podobnych do bibliotek

IFS jest szczególnie niepokojący dla autorów bibliotek funkcji powłoki, którzy muszą zapewnić niezawodne działanie swojego kodu bez względu na stan globalny (IFS niezawodne działanie , opcji powłoki, ...) narzuconych przez ich wywołujących, a także bez zakłócania tego stanu (wywołujący mogą polegać na nim, aby zawsze pozostawał statyczny).

Pisząc kod biblioteki, nie możesz polegać na IFSżadnej konkretnej wartości (nawet domyślnej) lub nawet na ustawieniu w ogóle. Zamiast tego musisz jawnie ustawić IFSdla każdego fragmentu kodu, którego zachowanie zależy od IFS.

Jeśli IFSjest jawnie ustawione na niezbędną wartość (nawet jeśli jest to wartość domyślna) w każdym wierszu kodu, w którym wartość ma znaczenie przy użyciu dowolnego z dwóch mechanizmów opisanych w tej odpowiedzi, odpowiednich do zlokalizowania efektu, wówczas kod jest zarówno niezależny od państwa globalnego i całkowicie nie dopuszcza do jego zatkania. Podejście to ma dodatkową zaletę polegającą na tym, że jest bardzo wyraźne dla osoby czytającej skrypt, która ma IFSznaczenie dla tego jednego polecenia / rozszerzenia przy minimalnym koszcie tekstu (w porównaniu z nawet najbardziej podstawowym zapisaniem / przywróceniem).

Na jaki kod ma to jednak wpływ IFS?

Na szczęście nie ma tak wielu scenariuszy, w których liczy się to IFS(zakładając, że zawsze podajesz swoje rozszerzenia ):

  • "$*"i "${array[*]}"rozszerzenia
  • wywołania readwbudowanego targetowania wielu zmiennych ( read VAR1 VAR2 VAR3) lub zmiennej tablicowej (read -a ARRAY_VAR_NAME )
  • wywołania readkierowania na jedną zmienną, jeśli chodzi o wiodące / końcowe białe znaki lub znaki niebiałe IFS.
  • dzielenie słów (np. w przypadku niecytowanych rozszerzeń, których można by uniknąć jak zarazy )
  • kilka innych mniej popularnych scenariuszy (patrz: IFS @ Greg's Wiki )
sls
źródło
Nie mogę powiedzieć, że rozumiem, aby podzielić ścieżkę $ PATH wzdłuż: ograniczniki, zakładając, że żaden ze składników nie zawiera zdania : same . Jak składniki mogą zawierać, :kiedy :jest separator?
Stéphane Chazelas
@ StéphaneChazelas Cóż, :jest poprawnym znakiem do użycia w nazwie pliku w większości systemów plików UNIX / Linux, więc jest całkowicie możliwe, aby katalog zawierał nazwę :. Być może niektóre powłoki mają opcję ucieczki :w PATH za pomocą czegoś podobnego \:, a wtedy zobaczysz kolumny, które nie są rzeczywistymi ogranicznikami (Wygląda na to, że bash nie pozwala na takie ucieczkę. Funkcja niskiego poziomu używana podczas iteracji po $PATHprostu szuka :w ciąg C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls
Poprawiłem odpowiedź, aby, mam nadzieję, wyjaśnić $PATHprzykład podziału :.
sls
1
Witamy w SO! Dzięki za tak dogłębną odpowiedź :)
Steven Lu
1

Czy to jest praktyczne? Czy też jest to w zasadzie bezcelowe i powinienem po prostu przywrócić IFS z powrotem do tego, co musi być dla jego kolejnych zastosowań?

Po co ryzykować literówkę, ustawiając IFS, $' \t\n'gdy wszystko, co musisz zrobić, to

OIFS=$IFS
do_your_thing
IFS=$OIFS

Alternatywnie możesz wywołać podpowłokę, jeśli nie potrzebujesz żadnych zmiennych ustawionych / zmodyfikowanych w:

( IFS=:; do_your_thing; )
arielCo
źródło
Jest to niebezpieczne, ponieważ nie działa, jeśli IFSpoczątkowo było rozbrojone.
Gilles „SO- przestań być zły”