Приём аргументов командной строки
Давайте создадим новый проект с помощью, как всегда, cargo new. Назовём наш проект minigrep, чтобы отличать его от утилиты grep, которая может уже присутствовать в вашей системе.
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
Первая задача — заставить minigrep принимать два аргумента командной строки: путь к файлу и строку для поиска. То есть мы хотим иметь возможность запускать нашу программу с помощью cargo run, двух дефисов, указывающих, что последующие аргументы предназначены для нашей программы, а не для cargo, строки для поиска и пути к файлу, в котором нужно искать, например так:
$ cargo run -- searchstring example-filename.txt
Сейчас программа, сгенерированная cargo new, не может обрабатывать передаваемые ей аргументы. Существуют готовые библиотеки на crates.io, которые могут помочь в написании программы, принимающей аргументы командной строки, но поскольку вы только начинаете изучать эту концепцию, давайте реализуем эту возможность самостоятельно.
Чтение значений аргументов
Чтобы minigrep мог читать значения передаваемых ему аргументов командной строки, нам понадобится функция std::env::args из стандартной библиотеки Rust. Эта функция возвращает итератор по аргументам командной строки, переданным minigrep. Мы полностью разберём итераторы в Главе 13. Пока вам нужно знать только две детали об итераторах: итераторы производят серию значений, и мы можем вызвать метод collect у итератора, чтобы превратить его в коллекцию, такую как вектор, содержащую все элементы, которые производит итератор.
Код в Листинге 12-1 позволяет вашей программе minigrep читать любые переданные ей аргументы командной строки, а затем собирать значения в вектор.
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
Сначала мы подключаем модуль std::env в область видимости с помощью оператора use, чтобы использовать его функцию args. Обратите внимание, что функция std::env::args вложена в два уровня модулей. Как мы обсуждали в Главе 7, в случаях, когда нужная функция вложена более чем в один модуль, мы предпочитаем подключать в область видимости родительский модуль, а не саму функцию. Делая это, мы можем легко использовать другие функции из std::env. Это также менее двусмысленно, чем добавление use std::env::args и последующий вызов функции просто как args, потому что args можно легко принять за функцию, определённую в текущем модуле.
Функция args и невалидный Unicode
Обратите внимание, что std::env::args вызовет панику (panic), если любой аргумент содержит невалидный Unicode. Если вашей программе нужно принимать аргументы, содержащие невалидный Unicode, используйте вместо этого std::env::args_os. Эта функция возвращает итератор, производящий значения OsString вместо значений String. Мы выбрали использование std::env::args здесь для простоты, потому что значения OsString различаются в зависимости от платформы и сложнее в работе, чем значения String.
В первой строке main мы вызываем env::args и сразу используем collect, чтобы превратить итератор в вектор, содержащий все значения, производимые итератором. Мы можем использовать функцию collect для создания многих видов коллекций, поэтому мы явно аннотируем тип args, чтобы указать, что хотим получить вектор строк. Хотя вы очень редко нуждаетесь в аннотации типов в Rust, collect — это одна из функций, которую часто нужно аннотировать, потому что Rust не может вывести вид коллекции, который вы хотите.
Наконец, мы выводим вектор с помощью макроса отладки. Давайте попробуем запустить код сначала без аргументов, а затем с двумя аргументами:
$ 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",
]
Обратите внимание, что первое значение в векторе — "target/debug/minigrep", это имя нашего бинарного файла. Это соответствует поведению списка аргументов в C, позволяя программам использовать имя, под которым они были вызваны, в своём выполнении. Часто удобно иметь доступ к имени программы на случай, если вы хотите вывести его в сообщениях или изменить поведение программы в зависимости от того, каким псевдонимом командной строки она была вызвана. Но для целей этой главы мы проигнорируем его и сохраним только два нужных нам аргумента.
Сохранение значений аргументов в переменные
Программа сейчас может получать доступ к значениям, указанным в качестве аргументов командной строки. Теперь нам нужно сохранить значения двух аргументов в переменные, чтобы мы могли использовать эти значения в остальной части программы. Мы делаем это в Листинге 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}");
}
Как мы видели, когда выводили вектор, имя программы занимает первое значение в векторе по индексу args[0], поэтому мы начинаем аргументы с индекса 1. Первый аргумент, который принимает minigrep, — это строка, которую мы ищем, поэтому мы помещаем ссылку на первый аргумент в переменную query. Второй аргумент будет путём к файлу, поэтому мы помещаем ссылку на второй аргумент в переменную file_path.
Мы временно выводим значения этих переменных, чтобы убедиться, что код работает так, как мы задумали. Давайте запустим эту программу снова с аргументами test и 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
Отлично, программа работает! Значения нужных нам аргументов сохраняются в правильных переменных. Позже мы добавим обработку ошибок для работы с некоторыми потенциальными ошибочными ситуациями, например, когда пользователь не предоставляет аргументы; пока мы проигнорируем эту ситуацию и займёмся добавлением возможностей чтения файлов.