Принятие аргументов командной строки

Создадим новый проект консольного приложения как обычно с помощью команды 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 читать любые переданные ей аргументы командной строки, а затем собирать значения в вектор.

Файл: src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}

Листинг 12-1: Собираем аргументы командной строки в вектор и выводим их на печать

Сначала мы вводим модуль std::env в область видимости с помощью инструкции use, чтобы мы могли использовать его функцию args. Обратите внимание, что функция std::env::args вложена в два уровня модулей. Как мы обсуждали в главе 7, в случаях, когда нужная функция оказывается вложенной в более чем один модуль, рекомендуется выносить в область видимости родительский модуль, а не функцию. Таким образом, мы можем легко использовать другие функции из std::env. Это менее двусмысленно, чем добавление use std::env::args и последующий вызов функции только с args, потому что args может быть легко принят за функцию, определённую в текущем модуле.

Функция args и недействительный Юникод символ (Unicode)

Обратите внимание, что std::env::args вызовет панику, если какой-либо аргумент содержит недопустимый символ Юникода. Если вашей программе необходимо принимать аргументы, содержащие недопустимые символы 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 [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 [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" является названием нашего двоичного файла. Это соответствует поведению списка аргументов в Си, позволяя программам использовать название с которым они были вызваны при выполнении. Часто бывает удобно иметь доступ к имени программы, если вы хотите распечатать его в сообщениях или изменить поведение программы в зависимости от того, какой псевдоним командной строки был использован для вызова программы. Но для целей этой главы, мы проигнорируем его и сохраним только два аргумента, которые нам нужны.

Сохранения значений аргументов в переменные

На текущий момент программа может получить доступ к значениям, указанным в качестве аргументов командной строки. Теперь нам требуется сохранять значения этих двух аргументов в переменных, чтобы мы могли использовать их в остальных частях программы. Мы сделаем это в листинге 12-2.

Файл: src/main.rs

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}");
}

Листинг 12-2: Создание переменных для хранения значений аргументов искомой подстроки и пути к файлу

Как видно из распечатки вектора, имя программы занимает первое значение в векторе по адресу 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 [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

Отлично, программа работает! Нам нужно чтобы значения аргументов были сохранены в правильных переменных. Позже мы добавим обработку ошибок с некоторыми потенциальными ошибочными ситуациями, например, когда пользователь не предоставляет аргументы; сейчас мы проигнорируем эту ситуацию и поработаем над добавлением возможности чтения файла.