Прием аргументов командной строки
Создадим новый проект, как обычно, с помощью 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 запаникует, если какой-либо аргумент
содержит недопустимый 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
Отлично, программа работает! Значения нужных нам аргументов сохраняются в правильные переменные. Позже мы добавим обработку ошибок для некоторых потенциально ошибочных ситуаций, например когда пользователь не передал аргументы; пока проигнорируем эту ситуацию и вместо этого перейдем к добавлению возможности чтения файла.