Dlaczego używanie pętli powłoki do przetwarzania tekstu jest uważane za złą praktykę?

196

Czy używanie pętli while do przetwarzania tekstu jest ogólnie uważane za złą praktykę w powłokach POSIX?

Jak zauważył Stéphane Chazelas , niektóre z powodów nieużywania pętli powłoki są koncepcyjne , niezawodność , czytelność , wydajność i bezpieczeństwo .

Ta odpowiedź wyjaśnia aspekty niezawodności i czytelności :

while IFS= read -r line <&3; do
  printf '%s\n' "$line"
done 3< "$InputFile"

Dla wydajności The whilepętla i odczytu są ogromnie powolny podczas odczytu z pliku lub potoku, ponieważ odczyt shell wbudowaną czyta jeden znak naraz.

A co z aspektami koncepcyjnymi i bezpieczeństwa ?

Cuonglm
źródło
Powiązane (druga strona medalu): Jak yeszapisuje się do pliku tak szybko?
Wildcard,
1
Wbudowana powłoka odczytu nie odczytuje pojedynczego znaku na raz, odczytuje pojedynczą linię na raz. wiki.bash-hackers.org/commands/builtin/read
A.Danischewski
@ A.Danischewski: To zależy od twojej powłoki. W bashodczytuje jeden rozmiar bufora w czasie, spróbuj dashna przykład. Zobacz także unix.stackexchange.com/q/209123/38906
cuonglm,

Odpowiedzi:

256

Tak, widzimy wiele rzeczy, takich jak:

while read line; do
  echo $line | cut -c3
done

Albo gorzej:

for line in `cat file`; do
  foo=`echo $line | awk '{print $2}'`
  echo whatever $foo
done

(nie śmiej się, widziałem wiele z nich).

Ogólnie od początkujących skryptów powłoki. Są to naiwne dosłowne tłumaczenia tego, co zrobiłbyś w imperatywnych językach, takich jak C lub python, ale nie tak robisz rzeczy w powłokach, a te przykłady są bardzo nieefektywne, całkowicie niewiarygodne (potencjalnie prowadzące do problemów związanych z bezpieczeństwem) i jeśli kiedykolwiek zarządzasz aby naprawić większość błędów, kod staje się nieczytelny.

Koncepcyjnie

W języku C lub w większości innych języków bloki konstrukcyjne znajdują się tylko jeden poziom powyżej instrukcji komputerowych. Mówisz procesorowi, co robić, a następnie co robić dalej. Bierzesz procesor za rękę i zarządzasz nim mikro: otwierasz ten plik, czytasz tyle bajtów, robisz to, robisz to z nim.

Muszle są językiem wyższego poziomu. Można powiedzieć, że to nawet nie język. Są przed wszystkimi interpretatorami wiersza poleceń. Zadanie jest wykonywane przez te polecenia, które uruchamiasz, a powłoka służy wyłącznie do ich uporządkowania.

Jedną z wielkich rzeczy, które wprowadził Unix, był potok i te domyślne strumienie stdin / stdout / stderr, które domyślnie obsługują wszystkie polecenia.

Przez 45 lat nie znaleźliśmy lepszego niż ten interfejs API, aby wykorzystać moc poleceń i zmusić je do współpracy przy zadaniu. To prawdopodobnie główny powód, dla którego ludzie nadal używają dziś powłok.

Masz narzędzie tnące i transliteracyjne i możesz po prostu:

cut -c4-5 < in | tr a b > out

Powłoka zajmuje się tylko instalacją wodną (otwieranie plików, konfigurowanie rur, wywoływanie poleceń), a gdy wszystko jest gotowe, po prostu przepływa bez powłoki. Narzędzia wykonują swoją pracę jednocześnie, skutecznie we własnym tempie, z wystarczającą ilością buforowania, aby żadne z nich nie blokowało drugiego, jest po prostu piękne, a jednocześnie takie proste.

Wywołanie narzędzia ma jednak swój koszt (a my opracujemy to w punkcie wydajności). Narzędzia te można napisać z tysiącami instrukcji w C. Należy stworzyć proces, narzędzie należy załadować, zainicjować, a następnie wyczyścić, zniszczyć proces i poczekać.

Inwokowanie cutjest jak otwieranie szuflady kuchennej, weź nóż, użyj go, umyj, wysusz, włóż z powrotem do szuflady. Kiedy to zrobisz:

while read line; do
  echo $line | cut -c3
done < file

To jest tak, jak w przypadku każdej linii pliku, pobieranie readnarzędzia z szuflady kuchennej (bardzo niezdarnej, ponieważ nie jest do tego przeznaczone ), czytanie linii, mycie narzędzia do odczytu, wkładanie go z powrotem do szuflady. Następnie zaplanuj spotkanie dla narzędzia echoi cut, weź je z szuflady, przywołaj je, umyj, wysusz, włóż z powrotem do szuflady i tak dalej.

Niektóre z tych narzędzi ( reada echo) są zbudowane w większości powłok, ale że mało robi różnicę tutaj ponieważ echoi cutnadal muszą być prowadzone w osobnych procesach.

To jak krojenie cebuli, ale mycie noża i wkładanie go z powrotem do szuflady kuchennej między każdym plasterkiem.

Tutaj oczywistym sposobem jest wyciągnięcie cutnarzędzia z szuflady, pokrojenie całej cebuli i włożenie jej z powrotem do szuflady po zakończeniu całej pracy.

IOW, w powłokach, szczególnie do przetwarzania tekstu, wywołujesz jak najmniej narzędzi i pozwalasz im współpracować z zadaniem, a nie uruchamiasz tysiące narzędzi w kolejności, czekając na uruchomienie, uruchomienie i oczyszczenie każdego z nich przed uruchomieniem następnego.

Dalsze czytanie w pięknej odpowiedzi Bruce'a . Wewnętrzne narzędzia do przetwarzania tekstu niskiego poziomu w powłokach (z wyjątkiem może zsh) są ograniczone, uciążliwe i zasadniczo nie nadają się do ogólnego przetwarzania tekstu.

Występ

Jak powiedziano wcześniej, uruchomienie jednego polecenia ma swój koszt. Ogromny koszt, jeśli to polecenie nie jest wbudowane, ale nawet jeśli są wbudowane, koszt jest duży.

Powłoki nie zostały zaprojektowane do takiego działania, nie mają pretensji do bycia wydajnymi językami programowania. Nie są, są tylko interpretatorami wiersza poleceń. Tak więc na tym froncie dokonano niewielkiej optymalizacji.

Ponadto powłoki wykonują polecenia w osobnych procesach. Te bloki konstrukcyjne nie mają wspólnej pamięci ani stanu. Kiedy robisz a fgets()lub fputs()w C, jest to funkcja in stdio. stdio przechowuje wewnętrzne bufory wejściowe i wyjściowe dla wszystkich funkcji stdio, aby uniknąć zbyt częstego wykonywania kosztownych wywołań systemowych.

Odpowiednie nawet wbudowane narzędzia powłoki ( read, echo, printf) nie może zrobić. readma czytać jedną linię. Jeśli odczyta znak nowego wiersza, oznacza to, że następne polecenie, które wykonasz, nie trafi. readMusi więc czytać dane wejściowe jeden bajt na raz (niektóre implementacje mają optymalizację, jeśli dane wejściowe są zwykłym plikiem, ponieważ odczytują fragmenty i szukają wstecz, ale działa to tylko dla zwykłych plików i bashna przykład odczytuje tylko fragmenty 128-bajtowe, co jest wciąż dużo mniej niż narzędzia tekstowe).

To samo po stronie wyjściowej, echonie może po prostu buforować swoich danych wyjściowych, musi je natychmiast wydrukować, ponieważ następne uruchomione polecenie nie udostępni tego bufora.

Oczywiście uruchamianie poleceń sekwencyjnie oznacza, że ​​musisz na nie poczekać, to mały taniec harmonogramu, który daje kontrolę z powłoki i narzędzi iz powrotem. Oznacza to również (w przeciwieństwie do używania długo działających instancji narzędzi w potoku), że nie można wykorzystać kilku procesorów jednocześnie, jeśli są one dostępne.

Pomiędzy tą while readpętlą a (podobno) ekwiwalentem cut -c3 < filew moim szybkim teście współczynnik czasu procesora wynosi około 40000 w moich testach (jedna sekunda w porównaniu do pół dnia). Ale nawet jeśli używasz tylko wbudowanych powłok:

while read line; do
  echo ${line:2:1}
done

(tutaj z bash), to wciąż około 1: 600 (jedna sekunda vs 10 minut).

Wiarygodność / czytelność

Bardzo trudno jest poprawnie ustawić ten kod. Podane przeze mnie przykłady są zbyt często spotykane na wolności, ale zawierają wiele błędów.

readjest poręcznym narzędziem, które może robić wiele różnych rzeczy. Może odczytywać dane wejściowe od użytkownika, dzielić je na słowa, aby przechowywać je w różnych zmiennych. read lineczy nie czytać linię wejścia, a może to czyta wiersz w bardzo szczególny sposób. W rzeczywistości odczytuje słowa z danych wejściowych, te słowa oddzielone $IFSi gdzie można użyć odwrotnego ukośnika, aby uciec przed separatorami lub znakiem nowej linii.

Z wartością domyślną $IFSna wejściu takim jak:

   foo\/bar \
baz
biz

read linezapisze się "foo/bar baz"w $line, nie " foo\/bar \"tak jak można się spodziewać.

Aby odczytać wiersz, potrzebujesz:

IFS= read -r line

To nie jest bardzo intuicyjne, ale tak właśnie jest, pamiętaj, że muszle nie były przeznaczone do takiego użycia.

To samo dotyczy echo. echorozwija sekwencje. Nie można go używać do dowolnych treści, takich jak zawartość losowego pliku. Potrzebujesz printftutaj zamiast tego.

I oczywiście jest typowe zapominanie o cytowaniu zmiennej, do której wszyscy wpadają. Więc to więcej:

while IFS= read -r line; do
  printf '%s\n' "$line" | cut -c3
done < file

Teraz jeszcze kilka ostrzeżeń:

  • z wyjątkiem zshtego, że to nie działa, jeśli wejście zawiera znaki NUL, podczas gdy przynajmniej narzędzia tekstowe GNU nie miałyby problemu.
  • jeśli po ostatniej nowej linii są dane, zostaną one pominięte
  • wewnątrz pętli stdin jest przekierowywany, dlatego należy zwrócić uwagę, aby zawarte w nim polecenia nie odczytywały stdin.
  • w przypadku poleceń w pętli nie zwracamy uwagi na to, czy im się uda, czy nie. Zwykle warunki błędów (dysk pełny, błędy odczytu ...) będą źle obsługiwane, zwykle gorsze niż przy odpowiednim odpowiedniku.

Jeśli chcemy rozwiązać niektóre z powyższych problemów, staje się to:

while IFS= read -r line <&3; do
  {
    printf '%s\n' "$line" | cut -c3 || exit
  } 3<&-
done 3< file
if [ -n "$line" ]; then
    printf '%s' "$line" | cut -c3 || exit
fi

To staje się coraz mniej czytelne.

Istnieje wiele innych problemów z przekazywaniem danych do poleceń za pomocą argumentów lub odzyskiwaniem ich danych wyjściowych w zmiennych:

  • ograniczenie wielkości argumentów (niektóre implementacje narzędzi tekstowych również tam mają ograniczenia, chociaż efekt tych osiąganych jest na ogół mniej problematyczny)
  • znak NUL (również problem z narzędziami tekstowymi).
  • argumenty brane jako opcje, gdy zaczynają się -(lub +czasami)
  • różne dziwactwa różnych poleceń zwykle stosowanych w tych pętlach jak expr, test...
  • (ograniczone) operatory tekstowe różnych powłok, które w niespójny sposób obsługują znaki wielobajtowe.
  • ...

Względy bezpieczeństwa

Kiedy zaczynasz pracę ze zmiennymi powłoki i argumentami poleceń , wpisujesz pole minowe.

Jeśli zapomnisz zacytować zmienne , zapomnisz znacznika końca opcji , będziesz pracować w ustawieniach regionalnych ze znakami wielobajtowymi (obecnie jest to norma), na pewno wprowadzisz błędy, które wcześniej czy później staną się podatne na atak.

Kiedy możesz użyć pętli.

TBD

Stéphane Chazelas
źródło
24
Jasne (żywo), czytelne i niezwykle pomocne. Dziękuję raz jeszcze. To jest właściwie najlepsze wytłumaczenie, jakie widziałem w Internecie na temat podstawowej różnicy między skryptowaniem powłoki a programowaniem.
Wildcard
2
Takie posty pomagają początkującym zapoznać się ze skryptami powłoki i zobaczyć, jakie są subtelne różnice. Należy dodać zmienną odniesienia jako $ {VAR: -default_value}, aby upewnić się, że nie otrzymasz wartości null. i ustaw -o rzeczownik, aby krzyczał na ciebie, gdy odwołujesz się do niezdefiniowanej wartości.
unsignedzero,
6
@ A.Danischewski, myślę, że nie rozumiesz sedna sprawy. Tak, cutna przykład jest wydajny. cut -f1 < a-very-big-filejest wydajny, tak wydajny, jak byś napisał go w C. To, co jest strasznie nieefektywne i podatne na błędy, wywołuje cutkażdą linię a-very-big-filew pętli powłoki, o co właśnie chodzi w tej odpowiedzi. To zgadza się z twoim ostatnim stwierdzeniem o pisaniu niepotrzebnego kodu, co sprawia, że ​​myślę, że może nie rozumiem twojego komentarza.
Stéphane Chazelas
5
„Przez 45 lat nie znaleźliśmy lepszego niż ten interfejs API, aby wykorzystać moc poleceń i zmusić je do współpracy przy zadaniu”. - w rzeczywistości PowerShell, na przykład, rozwiązał przerażający problem z analizą, przekazując uporządkowane dane zamiast strumieni bajtów. Jedynym powodem, dla którego powłoki jeszcze go nie używają (pomysł był obecny już od dłuższego czasu i zasadniczo skrystalizował się kiedyś w Javie, kiedy standardowe typy list i słowników zostały włączone do głównego nurtu) jest to, że ich opiekunowie nie mogli jeszcze zgodzić się na wspólny format danych strukturalnych do użycia (.
ivan_pozdeev
6
@OlivierDulac Myślę, że to trochę humoru. Ta sekcja będzie na zawsze TBD.
muru
43

Jeśli chodzi o koncepcję i czytelność, powłoki zazwyczaj są zainteresowane plikami. Ich „jednostką adresowalną” jest plik, a „adres” to nazwa pliku. Powłoki mają wszelkiego rodzaju metody testowania na obecność pliku, typ pliku, formatowanie nazwy pliku (zaczynając od globowania). Powłoki mają bardzo mało prymitywów do radzenia sobie z zawartością plików. Programiści powłoki muszą wywołać inny program do obsługi zawartości pliku.

Z uwagi na orientację pliku i nazwy pliku manipulowanie tekstem w powłoce jest bardzo powolne, jak zauważyłeś, ale wymaga również niejasnego i zniekształconego stylu programowania.

Bruce Ediger
źródło
25

Istnieje kilka skomplikowanych odpowiedzi, podających wiele ciekawych szczegółów dla maniaków wśród nas, ale to naprawdę bardzo proste - przetwarzanie dużego pliku w pętli powłoki jest po prostu zbyt wolne.

Myślę, że pytający jest interesujący w typowym rodzaju skryptu powłoki, który może rozpocząć się od analizy wiersza poleceń, ustawienia środowiska, sprawdzania plików i katalogów oraz nieco większej inicjalizacji, zanim przejdzie do swojego głównego zadania: przejścia przez duże plik tekstowy zorientowany liniowo.

W przypadku pierwszych części ( initialization) zwykle nie ma znaczenia, że ​​polecenia powłoki są powolne - uruchamia tylko kilkadziesiąt poleceń, może z kilkoma krótkimi pętlami. Nawet jeśli piszemy tę część nieefektywnie, zwykle zajmie to mniej niż sekundę, aby wykonać całą tę inicjalizację, i to dobrze - dzieje się to tylko raz.

Ale gdy mamy do przetwarzania duży plik, który może mieć tysiące lub miliony linii, to jest nie w porządku dla skrypt powłoki podjąć znaczny ułamek sekundy (nawet jeśli jest to tylko kilkadziesiąt milisekund) dla każdej linii, ponieważ to może zsumować godziny.

Właśnie wtedy musimy użyć innych narzędzi, a piękno skryptów powłoki Unix polega na tym, że ułatwiają nam to.

Zamiast używać pętli do patrzenia na każdą linię, musimy przekazać cały plik przez potok poleceń . Oznacza to, że zamiast wywoływać polecenia tysiące lub miliony razy, powłoka wywołuje je tylko raz. To prawda, że ​​te polecenia będą miały pętle do przetwarzania pliku wiersz po wierszu, ale nie są to skrypty powłoki i zostały zaprojektowane tak, aby były szybkie i wydajne.

Unix ma wiele wspaniałych wbudowanych narzędzi, od prostych po kompleksowe, których możemy użyć do budowy naszych potoków. Zwykle zaczynałem od prostych i tylko w razie potrzeby korzystałem z bardziej złożonych.

Spróbowałbym też trzymać się standardowych narzędzi, które są dostępne w większości systemów, i starać się, aby moje użycie było przenośne, chociaż nie zawsze jest to możliwe. A jeśli twoim ulubionym językiem jest Python lub Ruby, być może nie będziesz miał nic przeciwko dodatkowemu wysiłkowi, aby upewnić się, że jest zainstalowany na każdej platformie, na której oprogramowanie musi działać :-)

Proste narzędzia obejmują head, tail, grep, sort, cut, tr, sed, join(gdy łączenie 2 pliki) i awkjednej wkładki, wśród wielu innych. To niesamowite, co niektórzy ludzie mogą zrobić dzięki dopasowaniu wzorców i sedpoleceniom.

Kiedy staje się bardziej skomplikowana i naprawdę musisz zastosować logikę do każdej linii, awkjest dobrą opcją - albo jednowierszowa (niektórzy ludzie umieszczają całe skrypty awk w „jednej linii”, chociaż nie jest to zbyt czytelne) lub w krótki skrypt zewnętrzny.

Ponieważ awkjest to język interpretowany (jak twoja powłoka), to niesamowite, że potrafi tak wydajnie przetwarzać wiersz po wierszu, ale jest specjalnie do tego przeznaczony i jest naprawdę bardzo szybki.

I jest Perljeszcze wiele innych języków skryptowych, które są bardzo dobre w przetwarzaniu plików tekstowych, a także zawierają wiele przydatnych bibliotek.

I wreszcie, jest dobry stary C, jeśli potrzebujesz maksymalnej prędkości i dużej elastyczności (chociaż przetwarzanie tekstu jest nieco nudne). Ale prawdopodobnie jest to bardzo złe wykorzystanie twojego czasu na napisanie nowego programu C dla każdego innego zadania przetwarzania plików, na jakie napotkasz. Dużo pracuję z plikami CSV, więc napisałem kilka ogólnych narzędzi w C, które mogę ponownie wykorzystać w wielu różnych projektach. W efekcie rozszerza to zakres „prostych, szybkich narzędzi uniksowych”, które mogę wywoływać ze swoich skryptów powłoki, dzięki czemu mogę obsługiwać większość projektów, pisząc tylko skrypty, co jest znacznie szybsze niż pisanie i debugowanie kodu C na zamówienie!

Kilka ostatecznych wskazówek:

  • nie zapomnij uruchomić głównego skryptu powłoki export LANG=C, w przeciwnym razie wiele narzędzi potraktuje zwykłe pliki ASCII jako Unicode, dzięki czemu będą one znacznie wolniejsze
  • rozważ także ustawienie, export LC_ALL=Cjeśli chcesz sortprodukować spójne zamówienia, niezależnie od środowiska!
  • jeśli potrzebujesz sortswoich danych, prawdopodobnie zajmie to więcej czasu (i zasobów: procesor, pamięć, dysk) niż wszystko inne, więc spróbuj zminimalizować liczbę sortpoleceń i rozmiar sortowanych plików
  • pojedynczy potok, gdy jest to możliwe, jest zwykle najbardziej wydajny - uruchamianie wielu potoków w sekwencji, z plikami pośrednimi, może być bardziej czytelne i debugowane, ale wydłuży czas, jaki zajmuje Twój program
Laurence Renshaw
źródło
6
Rurociągi wielu prostych narzędzi (w szczególności wspomnianych, takich jak głowa, ogon, grep, sortowanie, cięcie, tr, sed, ...) są często używane niepotrzebnie, szczególnie jeśli masz już instancję awk w tym potoku, która może to zrobić zadania tych prostych narzędzi. Inną kwestią, którą należy wziąć pod uwagę, jest to, że w potokach nie można w prosty i niezawodny sposób przekazywać informacji o stanie z procesów na przedniej stronie potoku do procesów, które pojawiają się na tylnej stronie. Jeśli używasz dla takich potoków prostych programów programu awk, masz pojedynczą przestrzeń stanów.
Janis
14

Tak ale...

Poprawna odpowiedź Stéphane Chazelas opiera się na pojęcia delegowania każdej operacji tekstu do określonych plików binarnych, jak grep, awk, sedi innych.

Ponieważ jest w stanie samodzielnie robić wiele rzeczy, upuszczanie widelców może stać się szybsze (nawet niż uruchamianie innego tłumacza do wykonywania wszystkich zadań).

Na przykład, spójrz na ten post:

https://stackoverflow.com/a/38790442/1765658

i

https://stackoverflow.com/a/7180078/1765658

przetestuj i porównaj ...

Oczywiście

Nie bierze się pod uwagę wkładu użytkownika i bezpieczeństwa !

Nie pisz aplikacji internetowej pod !!

Ale w przypadku wielu zadań związanych z administrowaniem serwerem, gdzie może być użyty zamiast , użycie wbudowanego bash może być bardzo wydajne.

Moje znaczenie:

Pisanie narzędzi takich jak bin utils to nie to samo, co administracja systemem.

Więc nie ci sami ludzie!

Tam, gdzie sysadmini muszą wiedzieć shell, mogą pisać prototypy , korzystając z jego preferowanego (i najlepiej znanego) narzędzia.

Jeśli to nowe narzędzie (prototyp) jest naprawdę przydatne, inne osoby mogłyby opracować dedykowane narzędzie, używając bardziej odpowiedniego języka.

F. Hauri
źródło
1
Dobry przykład. Twoje podejście jest z pewnością bardziej wydajne niż Lololux, ale zwróć uwagę, że odpowiedź tensibai (właściwy sposób na wykonanie tego IMO, to znaczy bez użycia pętli powłoki) jest o rząd wielkości szybsza niż twoja. A twój jest o wiele szybszy, jeśli go nie używasz bash. (ponad 3 razy szybszy z ksh93 w moim teście na moim systemie). bashjest ogólnie najwolniejszą powłoką. Nawet zshjest dwa razy szybszy na tym skrypcie. Masz również kilka problemów z niecytowanymi zmiennymi i użyciem read. W rzeczywistości ilustrujesz tutaj wiele moich punktów.
Stéphane Chazelas,
@ StéphaneChazelas Zgadzam się, bash jest prawdopodobnie najwolniejszą powłoką, z której ludzie mogą dziś korzystać, ale i tak najczęściej używaną.
F. Hauri,
@ StéphaneChazelas Zamieściłem wersję perla na mojej odpowiedzi
F. Hauri
1
@Tensibai znajdziesz POSIXsh , Awk , Sed , grep, ed, ex, cut, sort, join... wszystko z większą niezawodność niż bash lub Perl.
Wildcard
1
@Tensibai, ze wszystkich systemów, których dotyczy U&L, większość z nich (Solaris, FreeBSD, HP / UX, AIX, większość wbudowanych systemów Linux ...) nie jest bashdomyślnie instalowana. bashjest przeważnie tylko na Apple MacOS i systemów GNU (Przypuszczam, że to, co nazywasz główne dystrybucje ), choć wiele systemów mają również go jako opcjonalny pakiet (jak zsh, tcl, python...)
Stéphane Chazelas