Dlaczego tak często używa się `while IFS = read` zamiast` IFS =; podczas czytania ...

81

Wydaje się, że normalna praktyka wystawiałaby ustawienie IFS poza pętlę while, aby nie powtarzać ustawiania go dla każdej iteracji ... Czy jest to zwykły styl „małpa patrz, małpa robi”, jak to miało miejsce w przypadku tej małpy do Czytam mężczyznę czytającego , czy brakuje mi tutaj jakiejś subtelnej (lub rażąco oczywistej) pułapki?

Peter.O
źródło

Odpowiedzi:

82

Pułapka jest taka

IFS=; while read..

ustawia IFSdla całego środowiska powłoki poza pętlą, natomiast

while IFS= read

redefiniuje to tylko dla readwywołania (z wyjątkiem powłoki Bourne'a). Możesz to sprawdzić wykonując pętlę

while IFS= read xxx; ... done

następnie po takiej pętli echo "blabalbla $IFS ooooooo"drukuje

blabalbla
 ooooooo

podczas gdy po

IFS=; read xxx; ... done

na IFS pobyty przedefiniowane: teraz echo "blabalbla $IFS ooooooo"drukuje

blabalbla  ooooooo

Więc jeśli użyć drugiego formularza, trzeba pamiętać, aby zresetować: IFS=$' \t\n'.


Druga część tego pytania została tutaj scalona , więc usunąłem stąd powiązaną odpowiedź.

rozcietrzewiacz
źródło
Okej, wydaje się, że potencjalną „pułapką” jest zaniedbanie resetowania zewnętrznego IFS… Ale zastanawiam się, czy jest jeszcze coś innego… Testuję tutaj rzeczy, dość gorączkowo, i mam zwróć uwagę, że ustawienie IFS w liście poleceń while zachowuje qute inaczej, w zależności od tego, czy po nim następuje dwukropek. Nie rozumiem tego zachowania (jeszcze) i teraz zastanawiam się, czy na tym poziomie są jakieś specjalne względy ... np. while IFS=X readnie dzieli się na X, ale while IFS=X; read...
Peter.O
(Ty oznaczało semi dwukropek, prawda?) Drugi whilenie ma sensu - stan na while końcach w tym średnikiem, więc nie ma rzeczywistego pętla ... readstaje się właśnie pierwsze polecenie wewnątrz pętli jednego elementu ... Albo nie ? Co dowtedy ...?
rozcietrzewiacz
1
Nie, czekaj - masz rację, możesz mieć kilka poleceń w whilestanie (wcześniej do).
rozcietrzewiacz
Och ... zdecydowanie możesz je mieć ... jak sobie uświadomiłeś ... ale wydaje się, że nie lubią średnika ... (a pętla będzie zapętlać ad-infinitum, dopóki ostatnia komenda nie zwróci -zerowy kod wyjścia) ... Zastanawiam się teraz, czy pułapka leży całkowicie w innym sektorze; zrozumienie, jak działa lista poleceń while , np. dlaczego nie IFS=działa, ale IFS=Xnie ... (a może ja przedawkowała na to za jakiś czas .. przerwa kawowa potrzebne :)
Peter.O
1
$ rozcietrzewiacz .. Ups ... Nie zauważyłem twojej aktualizacji, kiedy przeniosłem aktualizację (jak wspomniano w poprzednim komentarzu) .. Wygląda interesująco i zaczyna mieć sens ... ale nawet na noc- Ptak taki jak ja, jest bardzo późno ... (Właśnie usłyszałem poranne ptaki:) ... To powiedziawszy, zebrałem trochę i przeczytałem twoje przykłady ... Myślę, że mam, właściwie to jestem pewien, że masz, ale muszę spać :) ... To prawie Eureka! chwila ... dzięki
Peter.O
45

Spójrzmy na przykład z starannie przygotowanym tekstem wejściowym:

text=' hello  world\
foo\bar'

To dwie linie, pierwsza zaczynająca się spacją i kończąca się odwrotnym ukośnikiem. Po pierwsze, spójrzmy na to, co się dzieje bez żadnych środków ostrożności read(ale używając printf '%s\n' "$text"do ostrożnego drukowania $textbez ryzyka ekspansji). (Poniżej $ ‌znajduje się monit powłoki).

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

readzjadł ukośniki odwrotne: ukośnik-nowa linia powoduje zignorowanie nowej linii, a ukośnik-cokolwiek ignoruje ten pierwszy ukośnik. Aby uniknąć specjalnego traktowania ukośników odwrotnych, używamy read -r.

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

Tak lepiej, mamy dwie linie zgodnie z oczekiwaniami. Dwie linie prawie zawierają pożądaną treść: podwójna spacja między helloi worldzostała zachowana, ponieważ znajduje się w linezmiennej. Z drugiej strony początkowa przestrzeń została zjedzona. Dzieje się tak, ponieważ readodczytuje tyle słów, ile przekazujesz, zmienne, z tą różnicą, że ostatnia zmienna zawiera resztę wiersza - ale wciąż zaczyna się od pierwszego słowa, tzn. Początkowe spacje są odrzucane.

Tak więc, aby odczytać każdą linię dosłownie, musimy upewnić się, że nie dochodzi do podziału słów . Robimy to, ustawiając IFSzmienną na pustą wartość.

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

Zwróć uwagę, jak ustawiliśmy IFS specjalnie na czas trwania readwbudowanego . W IFS= read -r lineustawia zmienne środowiska IFS(na pusty wartość) specjalnie dla realizacji read. Jest to przykład ogólnej składni komend prostych : (być może pusta) sekwencja przypisań zmiennych, po której następuje nazwa komendy i jej argumenty (można także przekierowywać w dowolnym momencie). Ponieważ readjest to funkcja wbudowana, zmienna nigdy nie kończy się w środowisku zewnętrznego procesu; niemniej jednak wartość $IFSjest przypisywana tak długo, jak długo readjest wykonywana¹. Pamiętaj, że readnie jest to specjalne wbudowane , więc zadanie trwa tylko przez czas jego trwania.

Dlatego staramy się nie zmieniać wartości IFSinnych instrukcji, które mogą na nim polegać. Ten kod będzie działał bez względu na to, co IFSpoczątkowo ustawił otaczający kod i nie spowoduje żadnych problemów, jeśli kod w pętli będzie polegał IFS.

Porównaj z tym fragmentem kodu, który wyszukuje pliki w ścieżce oddzielonej dwukropkami. Lista nazw plików jest odczytywana z pliku, jedna nazwa pliku w wierszu.

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

Jeśli pętla była while IFS=; read -r name; do …, to for dir in $PATHnie podzieliłaby się $PATHna składniki oddzielone dwukropkami. Gdyby kod był IFS=; while read …, byłoby jeszcze bardziej oczywiste, że IFSnie jest ustawione :w treści pętli.

Oczywiście możliwe byłoby przywrócenie wartości IFSpo wykonaniu read. Wymagałoby to jednak znajomości poprzedniej wartości, która stanowi dodatkowy wysiłek. IFS= readto prosty sposób (i dogodnie także najkrótszy sposób).

¹ A jeśli readzostanie przerwany przez sygnał pułapki, być może podczas działania pułapki - nie jest to określone przez POSIX i zależy od powłoki w praktyce.

Gilles
źródło
4
Dzięki Gilles .. bardzo ładna wycieczka z przewodnikiem .. (Czy chodziło Ci o „set-f”?) .... Teraz, dla czytelnika, aby powtórzyć to, co już powiedziano, chciałbym podkreślić problem, który miał ja patrzę na to w niewłaściwy sposób. Przede wszystkim fakt, że konstrukt while IFS= read(bez średnika po =) nie jest specjalną postacią whileani z IFSlub z read.. Konstrukt jest ogólny: tj. anyvar=anyvalue anycommand. Brak ;ustawienia po anyvarpowoduje, że zakres pętli anyvar lokalnej do anycommand... Pętla while - do / done jest w 100% niezwiązana z zakresem lokalnym any_var.
Peter.O,
3

Oprócz (już wyjaśnionego) IFSróżnic zakresu między i while IFS='' read, IFS=''; while reada while IFS=''; readidiomami ( zakres na polecenie vs IFSzakres / zmienna obejmująca całą powłokę ), lekcją wstępną jest to, że tracisz początkowe i końcowe spacje linii wejściowej, jeśli zmienna IFS jest ustawiony na (zawiera a) spację.

Może to mieć dość poważne konsekwencje, jeśli przetwarzane są ścieżki plików.

Dlatego ustawienie zmiennej IFS na pusty ciąg nie jest złym pomysłem, ponieważ zapewnia, że ​​początkowe i końcowe białe znaki linii nie zostaną usunięte.

Zobacz także: Bash, czytać linia po linii z pliku, za pomocą IFS

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
jon
źródło
+1 doskonała demonstracja, czyszczenie po pliku „rm * * z * spacjami *”
amdn
0

Zainspirowany odpowiedzią Yuzema

Jeśli chcesz ustawić IFSprawdziwą postać, działało to dla mnie

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
Steven Penny
źródło