Zrozumienie IFS

71

Poniższe wątki na tej stronie i StackOverflow były pomocne w zrozumieniu, jak IFSdziała:

Ale wciąż mam krótkie pytania. Postanowiłem zapytać ich w tym samym poście, ponieważ uważam, że może to pomóc przyszłym czytelnikom:

Pytanie 1 IFSjest zwykle omawiane w kontekście „podziału pola”. Czy podział pola jest taki sam jak podział słów ?

Q2: Specyfikacja POSIX mówi :

Jeżeli wartość IFS jest null, podział pola nie będzie wykonywany.

Czy ustawienie jest IFS=takie samo jak ustawienie IFSna null? Czy to też oznacza ustawienie go empty stringrównież na?

P3: W specyfikacji POSIX czytam :

Jeśli IFS nie jest ustawiony, powłoka zachowuje się tak, jakby wartość IFS wynosiła <space>, <tab> and <newline>

Powiedz, że chcę przywrócić domyślną wartość IFS. W jaki sposób mogę to zrobić? (dokładniej, jak mam się odwoływać <tab>i <newline>?)

P4: Wreszcie, w jaki sposób ten kod:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

zachowaj się, jeśli zmienimy pierwszą linię na

while read -r line # Use the default IFS value

lub:

while IFS=' ' read -r line
Amelio Vazquez-Reina
źródło

Odpowiedzi:

28
  1. Tak, są takie same.
  2. Tak.
  3. W bash i podobnych powłokach możesz zrobić coś takiego IFS=$' \t\n'. W przeciwnym razie można wstawić dosłowne kody sterujące za pomocą [space] CTRL+V [tab] CTRL+V [enter]. Jeśli jednak planujesz to zrobić, lepiej użyć innej zmiennej, aby tymczasowo zapisać starą IFSwartość, a następnie przywrócić ją później (lub tymczasowo zastąpić ją dla jednego polecenia za pomocą var=foo commandskładni).
    • Pierwszy fragment kodu spowoduje wstawienie całego wiersza, dosłownie, do $line, ponieważ nie ma separatorów pól, dla których można by dokonać podziału słów. Należy jednak pamiętać, że ponieważ wiele powłok używa ciągów do przechowywania ciągów, pierwsza instancja NUL może nadal powodować przedwczesne zakończenie jej wyświetlania.
    • Drugi fragment kodu może nie zawierać dokładnej kopii danych wejściowych $line. Na przykład, jeśli istnieje wiele kolejnych separatorów pól, zostaną one przekształcone w pojedyncze wystąpienie pierwszego elementu. Jest to często rozpoznawane jako utrata otaczających białych znaków.
    • Trzeci fragment kodu zrobi to samo co drugi, z wyjątkiem tego, że podzieli się tylko na spację (nie na zwykłej spacji, tabulacji lub nowej linii).
Chris Down
źródło
3
Odpowiedź na pytanie 2 jest błędna: puste IFSi rozbrojone IFSsą bardzo różne. Odpowiedź na czwarty kwartał jest częściowo błędna: nie dotykano tutaj wewnętrznych separatorów, tylko wiodące i końcowe.
Gilles
3
@Gilles: W drugim kwartale żaden z trzech podanych nominałów nie odnosi się do rozbrojenia IFS, wszystkie oznaczają IFS=.
Stéphane Gimenez,
@Gilles W drugim kwartale nigdy nie powiedziałem, że są takie same. I wewnętrzne separatory są dotykane, jak pokazano poniżej: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Eee, co? Powinno tam być wiele separatorów spacji, silnik SO ciągle je rozbiera).
Chris Down
2
@ StéphaneGimenez, Chris: Och, tak, przepraszam za Q2, źle odczytałem pytanie. W czwartym kwartale mówimy o read; ostatnia zmienna pobiera wszystko, co zostało, z wyjątkiem ostatniego separatora i pozostawia wewnętrzne separatory w środku.
Gilles,
1
Gilles ma częściowo rację, mówiąc, że spacje nie są usuwane przez odczyt. Przeczytaj moją odpowiedź, aby poznać szczegóły.
22

P1: Tak. „Podział na pola” i „podział na słowa” to dwa terminy dla tej samej koncepcji.

P2: Tak. Jeśli IFSjest nieustawione (tzn. Po unset IFS), to równoważne IFSjest ustawienie $' \t\n'(spacja, tabulator i nowa linia). Jeśli IFSjest ustawiona na pustą wartość (to właśnie oznacza tutaj „null”) (tj. Po IFS=lub IFS=''lub IFS=""), w ogóle nie jest wykonywane dzielenie pól (i $*, który zwykle używa pierwszego znaku $IFS, używa znaku spacji).

P3: Jeśli chcesz mieć domyślne IFSzachowanie, możesz użyć unset IFS. Jeśli chcesz IFSjawnie ustawić tę wartość domyślną, możesz umieścić literalne spacje, tabulatory, znaki nowej linii w pojedynczych cudzysłowach. W ksh93, bash lub zsh możesz użyć IFS=$' \t\n'. Przenośnie, jeśli chcesz uniknąć literowego znaku tabulacji w pliku źródłowym, możesz użyć

IFS=" $(echo t | tr t \\t)
"

P4: Przy IFSustawieniu pustej wartości read -r lineustawia linecałą linię oprócz końcowej nowej linii. Za pomocą IFS=" ", spacje na początku i na końcu linii są przycinane. Przy domyślnej wartości IFStabulatory i spacje są przycinane.

Gilles
źródło
2
Pytanie 2 jest częściowo błędne. Jeśli IFS jest pusty, „$ *” jest łączone bez separatorów. (ponieważ $@istnieją pewne różnice między powłokami w kontekstach innych niż listy, takie jak IFS=; var=$@). Należy zauważyć, że gdy IFS jest pusty, żadne dzielenie słów nie jest wykonywane, ale $ var nadal rozwija się do żadnego argumentu zamiast pustego argumentu, gdy $ var jest pusty, a globowanie nadal obowiązuje, więc nadal musisz cytować zmienne (nawet jeśli wyłączyć globbing)
Stéphane Chazelas
13

Pytanie 1 Podział pola.

Czy podział pola jest taki sam jak podział słów?

Tak, oba wskazują na ten sam pomysł.

P2: Kiedy IFS ma wartość zerową ?

Czy ustawienie jest IFS=''takie samo jak null, tak samo jak pusty ciąg?

Tak, wszystkie trzy oznaczają to samo: Nie należy wykonywać podziału pól / słów. Wpływa to również na drukowanie pól (tak jak w przypadku echo "$*"), wszystkie pola zostaną połączone razem bez spacji.

P3: (część a) Unset IFS.

W specyfikacji POSIX czytam :

Jeśli IFS nie jest ustawiony, powłoka zachowuje się tak, jakby wartość IFS to <space><tab> <newline> .

Co jest dokładnie równoważne z:

W przypadku znaku unset IFSpowłoka powinna zachowywać się tak, jakby IFS był domyślny.

Oznacza to, że „Podział pola” będzie dokładnie taki sam z domyślną wartością IFS, lub zostanie rozbrojony.
To wcale NIE oznacza, że ​​IFS będzie działać tak samo we wszystkich warunkach. Mówiąc dokładniej, wykonanie OldIFS=$IFSustawi zmienną var OldIFSna null , a nie domyślną. A próba przywrócenia IFS w ten sposób IFS=OldIFSspowoduje ustawienie wartości zerowej na IFS, a nie pozostawienie go tak jak wcześniej. Uważaj !!.

P3: (część b) Przywróć IFS.

Jak mogę przywrócić domyślną wartość IFS? Powiedz, że chcę przywrócić domyślną wartość IFS. W jaki sposób mogę to zrobić? (dokładniej, jak mam odwoływać się do <tab> i <newline> ?)

W przypadku zsh, ksh i bash (AFAIK) IFS można ustawić na wartość domyślną jako:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Zrobione, nie musisz nic więcej czytać.

Ale jeśli musisz ponownie ustawić IFS dla sh, może się to skomplikować.

Spójrzmy od najłatwiejszego do wykonania bez żadnych wad (oprócz złożoności).

1. - Unset IFS.

Moglibyśmy unset IFS(Przeczytaj część 3 część A powyżej).

2.- Zamień znaki.

Aby obejść ten problem, zamiana wartości tabulatorów i znaków nowej linii ułatwia ustawienie wartości IFS, a następnie działa w równoważny sposób.

Ustaw IFS na <space><newline> <tab> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- Prosty? rozwiązanie:

Jeśli istnieją skrypty potomne, które wymagają poprawnego ustawienia IFS, zawsze możesz ręcznie napisać:

IFS = „   
„

Gdzie sekwencja została wpisana ręcznie:, IFS='spacetabnewline'sekwencja, która faktycznie została poprawnie wpisana powyżej (Jeśli musisz potwierdzić, edytuj tę odpowiedź). Ale kopiowanie / wklejanie z przeglądarki ulegnie uszkodzeniu, ponieważ przeglądarka wyciska / ukrywa białe znaki. Utrudnia to dzielenie się kodem, jak napisano powyżej.

4.- Kompletne rozwiązanie.

Pisanie kodu, który można bezpiecznie skopiować, zwykle wymaga jednoznacznych znaków specjalnych do wydrukowania.

Potrzebujemy kodu, który „produkuje” oczekiwaną wartość. Ale nawet jeśli jest poprawny pod względem koncepcyjnym, ten kod NIE ustawi końcowego \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

Dzieje się tak, ponieważ pod większością powłok wszystkie końcowe znaki nowej linii $(...)lub `...`podstawienia poleceń są usuwane podczas rozwijania.

Musimy użyć trika dla sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Alternatywnym sposobem może być ustawienie IFS jako wartości środowiskowej z bash (na przykład), a następnie wywołanie sh (wersje, które akceptują ustawienie IFS przez środowisko), ponieważ:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

Krótko mówiąc, sh sprawia, że ​​resetowanie IFS do domyślnych jest dość dziwną przygodą.

P4: W rzeczywistym kodzie:

Wreszcie, w jaki sposób ten kod:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

zachowaj się, jeśli zmienimy pierwszą linię na

while read -r line # Use the default IFS value

lub:

while IFS=' ' read -r line

Po pierwsze: nie wiem, czy echo $line(z cytowanym zmiennym NOT) jest na porpouse, czy nie. Wprowadza drugi poziom „podziału pola”, którego odczytu nie ma. Więc odpowiem na oba. :)

Za pomocą tego kodu (abyś mógł potwierdzić). Będziesz potrzebował przydatnego xxd :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Dostaję:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

Pierwsza wartość to tylko poprawna wartość IFS='spacetabnewline'

Kolejny wiersz to wszystkie wartości szesnastkowe, które $ama var , oraz nowy wiersz „0a” na końcu, gdy zostanie podany każdej komendzie odczytu.

Następny wiersz, dla którego IFS ma wartość null, nie wykonuje „podziału pola”, ale nowa linia jest usuwana (zgodnie z oczekiwaniami).

Następne trzy wiersze, ponieważ IFS zawiera spację, usuwają początkowe spacje i ustawiają linię var na pozostałą saldo.

Ostatnie cztery wiersze pokazują, co zrobi niecytowana zmienna. Wartości zostaną podzielone na (kilka) spacji i zostaną wydrukowane jako:bar,baz,qux,


źródło
4

unset IFS czyści IFS, nawet jeśli później domniemywa się, że będzie to „\ t \ n”:

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Testowane na wersjach bash 4.2.45 i 3.2.25 z tym samym zachowaniem.

derekm
źródło
Pytanie i związana dokumentacja nie mówić o unsetod IFS, jak wyjaśniono w komentarzach zaakceptowanej odpowiedzi tutaj.
ILMostro_7,