Akceptowanie argumentów wiersza poleceń
Utwórzmy nowy projekt, jak zawsze, za pomocą cargo new. Nasz projekt nazwiemy minigrep, aby odróżnić go od narzędzia grep, które możesz już mieć w swoim systemie:
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
Pierwszym zadaniem jest sprawienie, aby minigrep akceptował dwa argumenty wiersza poleceń: ścieżkę do pliku i ciąg znaków do wyszukania. Oznacza to, że chcemy móc uruchamiać nasz program za pomocą cargo run, dwóch myślników wskazujących, że następujące argumenty są przeznaczone dla naszego programu, a nie dla cargo, ciągu znaków do wyszukania i ścieżki do pliku, w którym ma się odbywać wyszukiwanie, w następujący sposób:
$ cargo run -- searchstring example-filename.txt
Obecnie program wygenerowany przez cargo new nie może przetwarzać podanych mu argumentów. Istniejące biblioteki na crates.io mogą pomóc w pisaniu programu, który akceptuje argumenty wiersza poleceń, ale ponieważ dopiero uczysz się tej koncepcji, zaimplementujmy tę funkcjonalność samodzielnie.
Odczytywanie wartości argumentów
Aby minigrep mógł odczytywać wartości argumentów wiersza poleceń, które do niego przekazujemy, będziemy potrzebować funkcji std::env::args udostępnionej w standardowej bibliotece Rust. Funkcja ta zwraca iterator argumentów wiersza poleceń przekazanych do minigrep. Wyczerpująco omówimy iteratory w Rozdziale 13. Na razie musisz znać tylko dwa szczegóły dotyczące iteratorów: iteratory produkują serię wartości, a metodę collect możemy wywołać na iteratorze, aby zamienić go w kolekcję, taką jak wektor, która zawiera wszystkie elementy produkowane przez iterator.
Kod z Listingu 12-1 pozwala programowi minigrep odczytać wszelkie argumenty wiersza poleceń, które do niego przekazano, a następnie zebrać wartości w wektor.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
Najpierw wprowadzamy moduł std::env do zasięgu za pomocą instrukcji use, abyśmy mogli używać jego funkcji args. Zauważ, że funkcja std::env::args jest zagnieżdżona na dwóch poziomach modułów. Jak omówiliśmy w Rozdziale 7, w przypadkach, gdy pożądana funkcja jest zagnieżdżona w więcej niż jednym module, zdecydowaliśmy się wprowadzić moduł nadrzędny do zasięgu, a nie funkcję. Dzięki temu możemy łatwo używać innych funkcji z std::env. Jest to również mniej dwuznaczne niż dodawanie use std::env::args, a następnie wywoływanie funkcji tylko za pomocą args, ponieważ args można by łatwo pomylić z funkcją zdefiniowaną w bieżącym module.
W pierwszym wierszu main wywołujemy env::args i natychmiast używamy collect, aby zamienić iterator w wektor zawierający wszystkie wartości wyprodukowane przez iterator. Możemy użyć funkcji collect do tworzenia wielu rodzajów kolekcji, więc jawnie adnotujemy typ args, aby określić, że chcemy wektora ciągów znaków. Chociaż w Rust bardzo rzadko trzeba adnotować typy, collect jest jedną z funkcji, którą często trzeba adnotować, ponieważ Rust nie jest w stanie wywnioskować, jakiego rodzaju kolekcję chcemy.
Na koniec wypisujemy wektor za pomocą makra debugowania. Spróbujmy uruchomić kod najpierw bez argumentów, a następnie z dwoma argumentami:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
Zauważ, że pierwszą wartością w wektorze jest "target/debug/minigrep", co jest nazwą naszego pliku binarnego. Odpowiada to zachowaniu listy argumentów w C, pozwalając programom używać nazwy, za pomocą której zostały wywołane, w swoim wykonaniu. Często wygodnie jest mieć dostęp do nazwy programu w przypadku, gdy chcesz ją wypisać w komunikatach lub zmienić zachowanie programu na podstawie aliasu wiersza poleceń, który został użyty do wywołania programu. Ale dla celów tego rozdziału zignorujemy to i zapiszemy tylko dwa argumenty, których potrzebujemy.
Zapisywanie wartości argumentów w zmiennych
Program jest obecnie w stanie uzyskać dostęp do wartości określonych jako argumenty wiersza poleceń. Teraz musimy zapisać wartości tych dwóch argumentów w zmiennych, abyśmy mogli używać ich w pozostałej części programu. Robimy to w Listingu 12-2.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
Jak widzieliśmy, wypisując wektor, nazwa programu zajmuje pierwszą wartość w wektorze pod args[0], więc zaczynamy argumenty od indeksu 1. Pierwszym argumentem, który przyjmuje minigrep, jest ciąg znaków, którego szukamy, więc umieszczamy referencję do pierwszego argumentu w zmiennej query. Drugim argumentem będzie ścieżka do pliku, więc umieszczamy referencję do drugiego argumentu w zmiennej file_path.
Tymczasowo wypisujemy wartości tych zmiennych, aby udowodnić, że kod działa zgodnie z naszym zamierzeniem. Uruchommy ten program ponownie z argumentami test i sample.txt:
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
Świetnie, program działa! Potrzebne nam wartości argumentów są zapisywane w odpowiednich zmiennych. Później dodamy obsługę błędów, aby poradzić sobie z pewnymi potencjalnie błędnymi sytuacjami, takimi jak brak argumentów podanych przez użytkownika; na razie zignorujemy tę sytuację i zajmiemy się dodaniem możliwości odczytu plików.