W programach działających w trybie wiersza poleceń, kluczowe jest prawidłowe przetwarzanie argumentów oraz efektywne zarządzanie komunikatami o błędach. Kiedy piszemy programy w takim środowisku, jednym z najważniejszych aspektów jest zachowanie porządku między standardowym wyjściem (STDOUT) a wyjściem błędów (STDERR). Dobrym przykładem jest sytuacja, w której program przetwarza argumenty i napotyka błędy. Wiadomo, że błędy mogą występować na różnych etapach działania programu i ich obsługa powinna być uzależniona od ich powagi.
Kiedy uruchamiamy program, który nie otrzymał wymaganych argumentów, nie powinien on nic wypisywać na STDOUT, ale błędy powinny zostać zapisane do pliku błędów (stderr). Typowy komunikat błędu może wyglądać następująco:
Widzimy tutaj, że klasyczne, dobrze zaprojektowane programy CLI powinny wypisywać standardowy wynik na STDOUT, a komunikaty o błędach na STDERR. Ważne jest, aby rozróżniać te dwa strumienie i odpowiednio je obsługiwać, ponieważ nie każdy błąd powinien kończyć działanie programu. Czasami błędy mogą być tylko ostrzeżeniami, które nie wymagają natychmiastowego zakończenia pracy. Na przykład, w rozdziale 3 będziemy pisać program przetwarzający pliki wejściowe, z których niektóre mogą być celowo niedostępne lub uszkodzone. W takich przypadkach można użyć STDERR do wypisania ostrzeżenia o błędnym pliku, ale kontynuować przetwarzanie innych plików.
Kiedy już przetworzymy argumenty programu, możemy przejść do generowania wyników, które przypominają działanie komendy echo. Jednym z ważniejszych elementów w tym przypadku jest sposób obsługi tekstu wejściowego. Aby przechować argumenty tekstowe (mogące składać się z jednej lub kilku fraz), można wykorzystać typ wektora (Vec). Wartości te są reprezentowane jako String w języku Rust, a ich przetwarzanie może przebiegać w kilku krokach.
W pierwszym kroku musimy wyciągnąć wartość argumentu z przekazanych danych. Możemy to zrobić za pomocą metody ArgMatches::get_many, która pozwala na pobranie wielu wartości argumentu w postaci iteratora. Ponieważ nie zawsze możemy mieć pewność, że wartości te będą dostępne, wynikiem tej metody jest typ Option, który może przyjąć jedną z dwóch wartości: None (brak wartości) lub Some (wartość obecna). W przypadku, gdy użytkownik musi dostarczyć co najmniej jedną wartość, możemy bezpiecznie wywołać metodę unwrap, aby wyciągnąć wartość z opcji:
Należy jednak pamiętać, że wywołanie unwrap na opcji None spowoduje panikę w programie, co może doprowadzić do jego awarii. Dlatego ważne jest, aby w takich przypadkach zachować ostrożność i unikać niepotrzebnych ryzykownych operacji.
Kolejnym istotnym zagadnieniem jest zarządzanie pamięcią, szczególnie rozróżnienie między kopiowaniem a klonowaniem danych. W przypadku typów takich jak liczby całkowite, Rust używa mechanizmu kopiowania, ponieważ dane te mają stałą, znaną wielkość. Z kolei w przypadku typów dynamicznych, jak ciągi znaków (String), musimy używać klonowania, ponieważ ich rozmiar może się zmieniać w trakcie działania programu. Rust wyróżnia dwa obszary pamięci: stos (stack) i stertę (heap). Dane o stałej wielkości przechowywane są na stosie, natomiast dane o zmiennej wielkości – jak w przypadku wektora zawierającego ciągi znaków – przechowywane są na stercie. Dla takich typów danych konieczne jest klonowanie, co wiąże się z kopią całych danych w nowym obszarze pamięci.
Kiedy mamy już przygotowane dane, kolejnym krokiem jest ich wypisanie na ekranie. Dla tego celu możemy wykorzystać metodę join typu Vec, która łączy wszystkie elementy wektora w jeden ciąg znaków, oddzielony określonym separatorem:
Jest to jedno z częściej używanych podejść w Rust, ponieważ pozwala na łączenie tekstów bez konieczności manualnego iterowania po każdym elemencie tablicy.
W sytuacji, gdy program powinien wypisać tekst bez dodawania nowej linii na końcu, możemy użyć makra print!, które nie dodaje znaku nowej linii, w przeciwieństwie do println!. W tym przypadku można dostosować zachowanie programu, aby w zależności od flagi (np. -n), nowa linia była dodawana lub nie:
Problem pojawia się, gdy próbujemy zmienić wartość zmiennej, która jest zdefiniowana jako niemutowalna. Rust domyślnie zakłada, że zmienne są niemutowalne, dlatego w przypadku konieczności zmiany wartości zmiennej, należy zadeklarować ją jako mutowalną, używając słowa kluczowego mut:
Jest to jedno z wielu podejść w języku Rust, które wymusza na programiście świadome podejmowanie decyzji o tym, kiedy i w jaki sposób zmienne mogą być zmieniane.
Warto również zauważyć, że Rust bardzo silnie kontroluje typy danych, co oznacza, że wektory w Rust mogą przechowywać tylko dane tego samego typu. Oznacza to, że w językach dynamicznych, które pozwalają na mieszanie typów w listach, w Rust musimy upewnić się, że wszystkie elementy mają ten sam typ, co może być przyczyną wielu błędów podczas pisania programu.
Należy pamiętać, że niektóre operacje w Rust, takie jak manipulacja pamięcią lub obsługa błędów, mogą różnić się od tych w innych językach, co może wymagać od programisty większej ostrożności i znajomości narzędzi oferowanych przez ten język.
Jak działa program wc i jak go zaimplementować w Rust?
W tym rozdziale stawiasz czoła zadaniu stworzenia programu wc, który jest jednym z klasycznych narzędzi systemu Unix. Program ten służy do liczenia liczby linii, słów oraz bajtów w plikach tekstowych lub w danych wejściowych ze standardowego wejścia (STDIN). Narzędzie to było obecne już w pierwszej wersji systemu AT&T Unix i stało się powszechnie używane w pracy z tekstem. Wiedza na temat jego działania jest nieoceniona, zwłaszcza w kontekście programowania w języku Rust, który charakteryzuje się doskonałą kontrolą nad pamięcią oraz dbałością o bezpieczeństwo typów.
Program wc działa na zasadzie analizy wierszy tekstu, gdzie linia to ciąg znaków zakończony znakiem nowej linii (\n). Słowa są zdefiniowane jako ciągi znaków oddzielone białymi znakami, natomiast bajty odpowiadają wszystkim jednostkom danych, w tym także wielobajtowym znakom w systemach z kodowaniem Unicode. Funkcjonalność wc jest dość rozbudowana i pozwala na szczegółową kontrolę nad tym, co dokładnie ma być liczone.
Kluczowe aspekty wc
Program wc może przyjmować różne opcje, które pozwalają na wybór rodzaju informacji, które mają być wyświetlane:
-
-l – liczba linii,
-
-w – liczba słów,
-
-c – liczba bajtów,
-
-m – liczba znaków (w przypadku Unicode może różnić się od liczby bajtów).
Bez dodatkowych opcji, wc zlicza wszystkie te elementy i wyświetla je w określonym porządku: liczba linii, słów, bajtów, a następnie nazwa pliku. Dodatkowo, wc potrafi obsługiwać dane wejściowe z wielu plików, a po zakończeniu przetwarzania plików wyświetli sumę dla wszystkich z nich.
Implementacja wc w Rust
W rozdziale tym nauczyłeś się, jak zaimplementować ten program w języku Rust. Kluczowym elementem, który należy zrozumieć, jest użycie funkcji iterujących oraz manipulacja danymi wejściowymi w celu uzyskania dokładnych wyników. Wszystkie argumenty wiersza poleceń są traktowane jako ciągi znaków, a ich konwersja na odpowiednie typy (liczby całkowite, bajty, znaki) jest fundamentalna dla prawidłowego działania programu.
Manipulacja danymi wejściowymi
Pierwszym krokiem w realizacji zadania jest prawidłowe odczytanie danych wejściowych. Rust oferuje funkcję BufRead::read_line, która pozwala na odczyt każdej linii z pliku lub standardowego wejścia, zachowując znaki końca linii. Funkcja ta może być używana razem z metodą Iterator::all, która pozwala na przetwarzanie wszystkich linii, aby np. zliczyć słowa lub bajty.
Do rozdzielania linii na słowa, bajty lub znaki można wykorzystać funkcje iterujące, takie jak split_whitespace (dzieli na słowa na podstawie białych znaków) oraz bytes (dzieli na bajty). Również ważnym narzędziem w tej operacji jest funkcja take, która pozwala na ograniczenie liczby przetwarzanych elementów, co może okazać się pomocne w trakcie testowania.
Testowanie i debugowanie
Rust umożliwia również tworzenie modułów testowych, które są nieocenioną pomocą w procesie tworzenia aplikacji. Możesz łatwo zasymulować dane wejściowe, wykorzystując np. std::io::Cursor jako uchwyt do wirtualnych plików, co pozwala na testowanie funkcjonalności wc bez konieczności posiadania fizycznych plików.
Przykłady działania programu wc
Podstawowy przykład działania wc polega na uruchomieniu programu z jednym lub kilkoma plikami tekstowymi. Jeśli plik jest pusty, wynik będzie wskazywał na zerową liczbę linii, słów i bajtów. W przypadku pliku zawierającego jedną linię tekstu, liczba linii wyniesie 1, liczba słów będzie zależała od tego, ile jest słów w tej linii, a liczba bajtów będzie odpowiadała liczbie znaków w tej linii, uwzględniając także białe znaki i tabulatory.
Przykład:
Jeśli plik zawiera tekst w wielu językach, w tym z wielobajtowymi znakami Unicode, warto pamiętać, że liczba bajtów może się różnić od liczby znaków. W przypadku plików zawierających tylko znaki ASCII, liczba bajtów będzie równa liczbie znaków, natomiast w przypadku znaków wielobajtowych, jak w przypadku niektórych znaków Unicode, liczba bajtów może być większa.
Optymalizacja i poprawność
Podczas implementacji programu wc warto również zwrócić uwagę na optymalizację kodu. Rust daje możliwość precyzyjnej kontroli nad pamięcią oraz bezpiecznymi operacjami na danych, dzięki czemu program działa szybko i efektywnie. Dzięki systemowi typów w Rust możliwe jest również uniknięcie wielu typowych błędów związanych z nieprawidłową konwersją danych.
Przygotowując swój program, należy również zadbać o odpowiednie zarządzanie pamięcią i unikać nadmiernego kopiowania danych, co może wpływać na wydajność, zwłaszcza przy dużych plikach tekstowych.
Co warto jeszcze wiedzieć?
Program wc to nie tylko przykład pracy z danymi wejściowymi, ale także świetna okazja do zrozumienia podstawowych zasad pracy z systemem plików i manipulacji tekstem w Rust. Ważne jest, aby pamiętać o różnych typach kodowania (ASCII vs Unicode), co może wpłynąć na różnicę między liczbą bajtów a liczbą znaków. Ponadto warto rozumieć, że wc ma różne wersje, np. wersja GNU i BSD różnią się nieco zachowaniem, szczególnie w kontekście obsługi znaków i długości wyświetlanych wyników.
Jak stworzyć program liczący linie, słowa, znaki i bajty w plikach w Rust?
Program wc w systemach Unixowych to narzędzie, które liczy linie, słowa, znaki i bajty w plikach tekstowych. W tej części omawiamy, jak zaimplementować jego wersję w języku Rust, wykorzystując pakiet clap do przetwarzania argumentów wiersza poleceń i anyhow do obsługi błędów.
Najpierw przedstawimy opcje, które będą obsługiwane przez nasz program. Program będzie umożliwiał użytkownikowi wybór, które z liczb mają być wyświetlane: liczba linii, słów, znaków (wielkość w bajtach) oraz długość najdłuższej linii w pliku. Poniżej przedstawiono opcje, które będą obsługiwane przez nasz program:
-
-l, --lines: wypisuje liczbę linii w pliku. -
-w, --words: wypisuje liczbę słów w pliku. -
-c, --bytes: wypisuje liczbę bajtów w pliku. -
-m, --chars: wypisuje liczbę znaków w pliku (bez uwzględnienia kodowania). -
--max-line-length: wypisuje długość najdłuższej linii w pliku.
Program ma również możliwość przyjmowania nazw plików, które będą przetwarzane, lub odczytu danych z wejścia standardowego (STDIN), jeśli nie zostaną podane żadne pliki.
Obsługa wielu plików i błędów
Jeśli program otrzyma więcej niż jeden plik, na końcu wyniku zostanie dodana linia podsumowująca, która wyświetli sumy wszystkich liczb dla linii, słów i bajtów z wszystkich plików. Program będzie również obsługiwał błędy związane z plikami, które nie istnieją. W przypadku błędów, jak brak pliku, program wypisze odpowiedni komunikat o błędzie na standardowe wyjście błędów (STDERR), co będzie można przekierować do pliku.
Przykład:
Przekierowanie komunikatów o błędach do pliku err pozwala na zapisanie ostrzeżeń do osobnego pliku:
W ten sposób użytkownik może łatwo monitorować błędy.
Przetwarzanie argumentów w wierszu poleceń
Do przetwarzania argumentów wiersza poleceń używamy biblioteki clap. Program, który będziemy pisać, wymaga kilku opcji:
-
Lista plików wejściowych.
-
Określenie, czy mają być liczone linie, słowa, bajty, czy znaki.
Przykład kodu, który ustawia strukturę do przechowywania tych informacji:
Domyślne wartości
Program powinien domyślnie liczyć linie, słowa i bajty z wejścia standardowego (STDIN), jeżeli nie podano plików lub żadnych opcji. Używając odpowiednich flag, użytkownik może wymusić liczenie tylko jednej z kategorii: linii, słów, bajtów lub znaków. Program będzie także obsługiwał przypadki, w których użytkownik wybiera niekompatybilne opcje, na przykład flagi -m i -c:
Testowanie programu
Warto dodać testy, które będą weryfikować poprawność działania programu. W tym celu można wykorzystać bibliotekę assert_cmd do testowania, czy program poprawnie reaguje na różne przypadki wejściowe. Testy powinny obejmować:
-
Sprawdzenie, czy program działa poprawnie z plikami.
-
Testowanie różnych opcji wiersza poleceń.
-
Obsługę błędów związanych z brakiem pliku.
Początkowo można stworzyć testy w folderze tests/ i uruchamiać je za pomocą cargo test. Każdy test powinien sprawdzać, czy program wykonuje się poprawnie w różnych warunkach.
Obsługa błędów i wyjątków
Należy pamiętać, że program musi obsługiwać błędy w sposób przyjazny dla użytkownika. Korzystanie z biblioteki anyhow do obsługi błędów pozwala na łatwiejsze zarządzanie błędami i ich raportowanie. Jeśli program napotka na problem, na przykład z plikiem, który nie istnieje, powinien wypisać komunikat o błędzie i zakończyć działanie.
Program powinien także obsługiwać przypadki, w których użytkownik nie poda żadnych argumentów. Wtedy domyślne wartości zostaną użyte, a program wykona odpowiednie akcje na standardowym wejściu.
Jak badać mikroskopowo życie wodne i mikroorganizmy w jamie ustnej?
Jak odkrycia archeologiczne w Nebrasce zmieniają nasze rozumienie historii rdzennej Ameryki?
Jakie są najskuteczniejsze metody leczenia zapalenia twardówki i jakie terapie warto rozważyć w przypadku scleritis?
Jak media wpływają na demokrację i politykę: analiza roli prasy i jej relacji z władzą

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский