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 cut
jest 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 read
narzę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 echo
i cut
, weź je z szuflady, przywołaj je, umyj, wysusz, włóż z powrotem do szuflady i tak dalej.
Niektóre z tych narzędzi ( read
a echo
) są zbudowane w większości powłok, ale że mało robi różnicę tutaj ponieważ echo
i cut
nadal 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 cut
narzę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ć. read
ma czytać jedną linię. Jeśli odczyta znak nowego wiersza, oznacza to, że następne polecenie, które wykonasz, nie trafi. read
Musi 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 bash
na przykład odczytuje tylko fragmenty 128-bajtowe, co jest wciąż dużo mniej niż narzędzia tekstowe).
To samo po stronie wyjściowej, echo
nie 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 read
pętlą a (podobno) ekwiwalentem cut -c3 < file
w 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.
read
jest 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 line
czy 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 $IFS
i gdzie można użyć odwrotnego ukośnika, aby uciec przed separatorami lub znakiem nowej linii.
Z wartością domyślną $IFS
na wejściu takim jak:
foo\/bar \
baz
biz
read line
zapisze 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
. echo
rozwija sekwencje. Nie można go używać do dowolnych treści, takich jak zawartość losowego pliku. Potrzebujesz printf
tutaj 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
zsh
tego, ż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
yes
zapisuje się do pliku tak szybko?