Zrozumienie „IFS = read -r line”

60

Rozumiem oczywiście, że można dodać wartość do zmiennej separatora pól wewnętrznych. Na przykład:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Rozumiem również, że read -r linezapisze dane stdinw zmiennej o nazwie line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Jak jednak polecenie może przypisać wartość zmiennej? I czy najpierw przechowuje dane od stdindo zmiennej, linea następnie podaje wartość linedo IFS?

Jaskółka oknówka
źródło

Odpowiedzi:

104

Niektórzy ludzie mają błędne pojęcie, którym readjest polecenie odczytania wiersza. To nie jest.

readodczytuje słowa z (ewentualnie kontynuowanego odwrotnego ukośnika) wiersza, w którym słowa są $IFSrozdzielane, a odwrotnego ukośnika można użyć do opuszczenia ograniczników (lub kontynuacji linii).

Ogólna składnia to:

read word1 word2... remaining_words

readodczytuje stdin jeden bajt na raz, aż znajdzie Niecytowany znak nowej linii (lub końcówki wejściowe), dzieli, że według skomplikowanych zasad i zapisuje wynik tego dzielenia się $word1, $word2... $remaining_words.

Na przykład na wejściu takim jak:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

i o wartości domyślnej $IFS, read a b cby przypisać:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Teraz, jeśli przeszedł tylko jeden argument, to się nie stanie read line. Nadal jest read remaining_words. Przetwarzanie odwrotnego ukośnika jest nadal wykonywane, znaki białych znaków IFS są nadal usuwane od początku i końca.

-rOpcja usuwa przetwarzanie ukośnika. Więc to samo polecenie powyżej z -rzamiast tego przypisałoby

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Teraz, dla części dzielącej, ważne jest, aby zdać sobie sprawę, że istnieją dwie klasy znaków dla $IFS: białych znaków IFS (tj. Spacja i tabulator (i nowa linia, choć tutaj nie ma to znaczenia, chyba że użyjesz -d), które również się zdarzają być w wartości domyślnej $IFS) i innych. Traktowanie tych dwóch klas postaci jest inne.

Z IFS=:( :nie będąc biały znak IFS), wejście jak :foo::bar::zostanie podzielony na "", "foo", "", baroraz ""(i dodatkowy ""z niektórych implementacjach jednak, że nie ma znaczenia, z wyjątkiem read -a). Podczas gdy jeśli zastąpimy to :spacją, dzielenie odbywa się tylko na fooi bar. To jest wiodące, a końcowe są ignorowane, a ich sekwencje są traktowane jak jeden. Istnieją dodatkowe zasady łączenia znaków spacji i spacji $IFS. Niektóre implementacje mogą dodawać / usuwać specjalne traktowanie poprzez podwojenie znaków w IFS ( IFS=::lub IFS=' ').

Więc tutaj, jeśli nie chcemy, aby usuwane były wiodące i końcowe znaki bez białych znaków, należy usunąć te znaki IFS z białych znaków z IFS.

Nawet w przypadku znaków IFS spoza białymi znakami, jeśli wiersz wejściowy zawiera jeden (i tylko jeden) z tych znaków i jest to ostatni znak w wierszu (jak IFS=: read -r wordna wejściu jak foo:) z powłokami POSIX (nie ma zshniektórych pdkshwersji), to dane wejściowe jest uważane za jedno foosłowo, ponieważ w tych powłokach znaki $IFSsą uważane za terminatory , więc wordbędą zawierać foo, a nie foo:.

Zatem kanonicznym sposobem odczytu jednego wiersza danych wejściowych za pomocą readwbudowanego jest:

IFS= read -r line

(zauważ, że w przypadku większości readimplementacji działa to tylko w przypadku wierszy tekstu, ponieważ znak NUL nie jest obsługiwany, z wyjątkiem in zsh).

Korzystanie ze var=value cmdskładni powoduje, że IFSczas trwania tej cmdkomendy jest ustawiony inaczej .

Notatka historyczna

readWbudowany został wprowadzony przez Bourne shell i był już czytać słowa , a nie linii. Istnieje kilka ważnych różnic w nowoczesnych powłokach POSIX.

Powłoka Bourne'a readnie obsługiwała -ropcji (która została wprowadzona przez powłokę Korna), więc nie ma sposobu, aby wyłączyć przetwarzanie odwrotnego ukośnika inaczej niż wstępne przetwarzanie danych wejściowych z czymś takim sed 's/\\/&&/g'.

Powłoka Bourne'a nie miała pojęcia dwóch klas postaci (co ponownie zostało wprowadzone przez ksh). W Bourne Shell wszystkie znaki przechodzą takie samo traktowanie jak IFS znaków odstępu zrobić w ksh, czyli IFS=: read a b cna wejściu jak foo::barbyłoby przypisać bardo $b, a nie pusty ciąg.

W powłoce Bourne'a z:

var=value cmd

Jeśli cmdjest wbudowany (jak readjest), varpozostaje ustawiony na valuepo cmdzakończeniu. Jest to szczególnie ważne, $IFSponieważ w powłoce Bourne'a $IFSsłuży do dzielenia wszystkiego, nie tylko rozszerzeń. Ponadto, jeśli usuniesz znak spacji z $IFSpowłoki Bourne'a, "$@"przestanie to działać.

W powłoce Bourne przekierowanie polecenia złożonego powoduje, że działa ono w podpowłoce (w najwcześniejszych wersjach nawet rzeczy takie jak read var < filelub exec 3< file; read var <&3nie działały), więc w powłoce Bourne'a rzadko było używane readdo niczego poza danymi wejściowymi użytkownika na terminalu (tam, gdzie miało to sens obsługa kontynuacji linii)

Niektóre Unices (jak HP / UX, jest też jeden w util-linux) nadal mają linepolecenie odczytu jednego wiersza danych wejściowych (które były standardowym poleceniem UNIX aż do wersji specyfikacji Single UNIX wersja 2 ).

Jest to w zasadzie to samo, head -n 1z wyjątkiem tego, że odczytuje jeden bajt na raz, aby upewnić się, że nie czyta więcej niż jednej linii. W tych systemach możesz:

line=`line`

Oczywiście oznacza to odrodzenie nowego procesu, wykonanie polecenia i odczytanie jego wyniku przez potok, czyli o wiele mniej wydajny niż ksh IFS= read -r line, ale o wiele bardziej intuicyjny.

Stéphane Chazelas
źródło
3
+1 Dziękuję za użyteczne spostrzeżenia na temat różnych zabiegów na spacji / karcie w porównaniu do „innych” w IFS w bash ... Wiedziałem, że byli traktowani inaczej, ale to wyjaśnienie bardzo upraszcza. (A wgląd między bash (i innymi powłokami posix) a regularnymi shróżnicami jest również przydatny do pisania przenośnych skryptów!)
Olivier Dulac
Przynajmniej dla bash-4.4.19, while read -r; do echo "'$REPLY'"; donedziała jak while IFS= read -r line; do echo "'$line'"; done.
x-yuri
To: „... to błędne pojęcie, które czyta, jest poleceniem odczytu linii ...” prowadzi mnie do myślenia, że ​​jeśli użycie readlinii do odczytu linii jest błędne, musi istnieć coś innego. Czym może być to błędne pojęcie? Czy też to pierwsze stwierdzenie jest technicznie poprawne, ale tak naprawdę błędnym pojęciem jest: „read to polecenie do czytania słów z wiersza. Ponieważ jest tak potężne, możesz użyć go do odczytu wierszy z pliku, wykonując: IFS= read -r line
Mike S
8

Teoria

Istnieją tutaj dwie koncepcje:

  • IFSto Separator pól wejściowych, co oznacza, że ​​odczytany ciąg zostanie podzielony na podstawie znaków w IFS. W wierszu polecenia IFSzwykle są dowolne znaki spacji, dlatego wiersz poleceń dzieli się na spacje.
  • Wykonanie czegoś takiego VAR=value commandoznacza „zmodyfikuj środowisko poleceń, aby VARmiało wartość value”. Zasadniczo polecenie commandbędzie VARmiało wartość value, ale każde polecenie wykonane po tym będzie nadal VARmiało poprzednią wartość. Innymi słowy, zmienna ta zostanie zmodyfikowana tylko dla tej instrukcji.

W tym przypadku

Tak więc, robiąc IFS= read -r line, ustawiasz IFSpusty ciąg znaków (żaden znak nie zostanie użyty do podziału, dlatego nie nastąpi podział), aby readodczytać cały wiersz i zobaczyć go jako jedno słowo, które zostanie przypisane do linezmiennej. Zmiany mają IFSwpływ tylko na tę instrukcję, więc zmiana nie będzie miała wpływu na następujące polecenia.

Na marginesie

Chociaż polecenie jest prawidłowe i będzie działać zgodnie z przeznaczeniem, ustawienie IFSw tym przypadku nie jest konieczne 1 może nie być konieczne. Jak napisano na bashstronie man we readwbudowanej sekcji:

Jeden wiersz jest odczytywany ze standardowego wejścia [...], a pierwsze słowo jest przypisywane do imienia, drugie słowo do drugiego imienia, i tak dalej, z pozostałymi słowami i ich separatorami przypisanymi do nazwiska . Jeśli ze strumienia wejściowego odczytanych jest mniej słów niż nazw, do pozostałych nazw przypisywane są puste wartości. Znaki w IFSsą używane do podziału linii na słowa. [...]

Ponieważ masz tylko linezmienną, każde słowo i tak zostanie do niej przypisane, więc jeśli nie potrzebujesz żadnego z poprzedzających i końcowych białych znaków 1, możesz po prostu napisać read -r linei gotowe.

[1] Jako przykład tego, w jaki sposób wartość unsetdomyślna $IFSspowoduje, że spacja będzie readmiała początkowy / końcowy biały znak IFS , możesz spróbować:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Uruchom go, a zobaczysz, że poprzednie i końcowe postacie nie przetrwają, jeśli IFSnie zostanie rozbrojone. Co więcej, mogłyby się zdarzyć dziwne rzeczy, gdyby $IFSzostały zmodyfikowane gdzieś wcześniej w skrypcie.

użytkownik43791
źródło
5

Należy przeczytać, że oświadczenie w dwóch częściach, pierwsza kasuje wartość zmiennej IFS, czyli jest odpowiednikiem bardziej czytelny IFS="", drugi czyta linezmienną z stdin read -r line.

Specyficzne w tej składni jest to, że wpływ na IFS jest przemijający i ważny tylko dla readpolecenia.

O ile mi czegoś nie brakuje, w tym konkretnym przypadku kasowanie IFSnie ma żadnego efektu, ponieważ cokolwiek IFSjest ustawione, cała linia zostanie odczytana w linezmiennej. Nastąpiłaby zmiana zachowania tylko w przypadku, gdy więcej niż jedna zmienna została przekazana jako parametr do readinstrukcji.

Edytować:

Ma -rto na celu umożliwienie \specjalnego przetwarzania zakończenia, którego nie można przetwarzać, tzn. Aby odwrotny ukośnik został uwzględniony w linezmiennej, a nie jako znak kontynuacji umożliwiający wprowadzanie wielu wierszy.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

Wyczyszczenie IFS powoduje efekt uboczny polegający na zapobieganiu czytaniu w celu przycięcia potencjalnych początkowych i końcowych znaków spacji lub tabulatorów, np .:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Dzięki Rici za wskazanie tej różnicy.

jlliagre
źródło
Brakuje tylko tego, że jeśli IFS nie zostanie zmieniony, read -r lineprzycina początkowe i końcowe białe spacje przed przypisaniem danych wejściowych do linezmiennej.
rici
@rici Podejrzewałem coś takiego, ale sprawdzałem tylko znaki IFS między słowami, a nie wiodące / końcowe. Dzięki za wskazanie tego faktu!
jlliagre
wyczyszczenie IFS zapobiegnie również przypisaniu wielu zmiennych (efekt uboczny). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"pokaże-aa bb--
Kyodev