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

RustBeginner
Практиковаться сейчас

Введение

Программа командной строки получает значения после имени исполняемого файла, записывает обычные результаты в стандартный вывод, а об ошибках сообщает через стандартный поток ошибок и ненулевой код завершения. Благодаря этим границам процесса пользователи и скрипты могут отличать успешное выполнение от ошибки.

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

Разбор аргументов командной строки

На этом шаге вы проверите и извлечёте поисковый запрос и путь к файлу из аргументов процесса.

Проект находится в /home/labex/project/mini-search. Файл src/main.rs содержит бинарную программу командной строки, а в src/lib.rs находится подготовленная многократно используемая логика поиска для следующего шага. Перейдите в каталог проекта и откройте исходный код бинарного файла:

cd /home/labex/project/mini-search
nano src/main.rs

env::args() возвращает каждый аргумент как String, включая путь к исполняемому файлу с индексом 0. Подготовленная функция main собирает эти значения в вектор. Затем она передаёт &args параметру args: &[String].

Тип &[String] — это срез значений String: доступное только для чтения заимствованное представление элементов вектора. Он использует ту же идею заимствования, что и &str, но представляет последовательность элементов String, а не байты текста. Вектор по-прежнему принадлежит функции main, пока run читает его элементы.

Поток данных выглядит так:

shell words → env::args() → Vec<String> in main → borrowed &[String] in run

Замените заполнитель Err(...) внутри run следующим кодом:

    if args.len() != 3 {
        return Err(String::from("usage: mini-search <query> <file>"));
    }
    let query = args[1].clone();
    let path = args[2].clone();
    Ok(vec![format!("Query: {query}"), format!("File: {path}")])

Ровно три элемента означают имя исполняемого файла и два аргумента пользователя. Явный return Err(...) немедленно завершает выполнение run, если количество аргументов не соответствует этому формату. Это досрочный возврат, в отличие от возврата через последнее выражение, который использовался в функциях из лабораторной работы: код ниже выполняется только при допустимом количестве аргументов.

Индексация заимствованного среза возвращает заимствованные строки. Два вызова clone намеренно создают собственные копии только запроса и пути, чтобы далее run могла работать с ними как с локальными значениями, которыми она владеет. Это осознанный выбор модели владения, а не универсальное решение ошибок перемещения. Затем макрос vec![first, second] создаёт вектор из двух элементов, а Ok временно возвращает эти две строки для проверки.

Сохраните файл с помощью Ctrl+O, нажмите Enter, затем выйдите с помощью Ctrl+X. Запустите команду с двумя аргументами:

cargo run --quiet -- rust data/notes.txt

Первый --quiet уменьшает количество сообщений самого Cargo. Отдельный -- сообщает Cargo, что нужно прекратить обработку параметров; всё после него передаётся вашей программе.

Query: rust
File: data/notes.txt

Это подтверждает, что аргументы поступили в ожидаемые позиции.

Чтение файла и вызов логики библиотеки

На этом шаге вы замените временный результат для проверки настоящим чтением файла и выводом результатов поиска.

Откройте исходный код бинарного файла:

nano src/main.rs

Добавьте следующие импорты после use std::env;:

use std::fs;
use mini_search::find_lines;

Имя пакета mini-search преобразуется в имя крейта Rust mini_search. Подготовленный модификатор pub у find_lines делает эту функцию доступной за пределами крейта библиотеки. Импорт функции позволяет бинарной программе вызвать общедоступную функцию из src/lib.rs; размещение логики поиска в библиотеке отделяет повторно используемую обработку данных от работы с аргументами, специфичной для процесса. Это небольшой пример модели «библиотека/бинарный файл» и модели видимости, которые подробно рассматриваются в последующих модулях лабораторной работы.

Замените временную строку Ok(vec![...]) следующим кодом:

    let contents = fs::read_to_string(&path)
        .map_err(|error| format!("could not read {path}: {error}"))?;
    Ok(find_lines(&query, &contents))

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

Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet -- rust data/notes.txt
Rust makes ownership explicit.
Cargo builds Rust packages.
Rust tools help beginners.

Подготовленная ветвь Ok в main выводит обычные результаты поиска в стандартный вывод.

Превращение пустого результата поиска в понятную ошибку

На этом шаге вы различите успешный поиск с результатами и корректный поиск, при котором ничего не найдено.

Откройте исходный код:

nano src/main.rs

Замените Ok(find_lines(&query, &contents)) следующим кодом:

    let matches = find_lines(&query, &contents);
    if matches.is_empty() {
        return Err(format!("no lines matched '{query}'"));
    }
    Ok(matches)

Пустой вектор можно обнаружить программно, но команда будет полезнее, если явно сообщит об этом результате. Сохраните файл и выйдите из редактора, затем выполните поиск отсутствующего термина:

cargo run --quiet -- python data/notes.txt
no lines matched 'python'

На этом промежуточном этапе подготовленная ветвь Err всё ещё выводит сообщение через стандартный вывод и завершает программу успешно. На следующем шаге вы настроите для ошибок правильное поведение процесса.

Вывод ошибок в stderr и ненулевой код завершения

На этом шаге вы завершите границу командной строки, отделив обычный вывод от сообщений об ошибках.

Стандартный вывод, или stdout, содержит запрошенные результаты. Стандартный поток ошибок, или stderr, независимо передаёт диагностические сообщения. Нулевой код завершения означает успех, а ненулевой — ошибку. Откройте исходный код бинарного файла:

nano src/main.rs

Добавьте этот импорт после уже существующих импортов стандартной библиотеки:

use std::process;

Замените однострочную ветвь Err(error) следующим кодом:

        Err(error) => {
            eprintln!("{error}");
            process::exit(1);
        }

eprintln! записывает строку в stderr. process::exit(1) немедленно завершает процесс с кодом 1. Сохраните файл и выйдите из редактора, затем один раз соберите программу, чтобы запускать бинарный файл напрямую и не получать дополнительное сообщение об ошибке от Cargo:

cargo build --quiet
./target/debug/mini-search python data/notes.txt

Диагностическое сообщение останется прежним:

no lines matched 'python'

Сразу выведите код завершения предыдущей команды:

echo $?

echo выводит свои аргументы, а оболочка подставляет в $? код завершения последней команды:

1

Теперь та же граница процесса обрабатывает некорректное использование и отсутствующие файлы. Запуск ./target/debug/mini-search выводит сообщение об использовании; путь, например data/missing.txt, вызывает сообщение об ошибке чтения. В обоих случаях данные записываются только в stderr, а процесс завершается с кодом 1. При совпадении результат поиска записывается в stdout, а процесс завершается с кодом 0.

Итоги

Вы собрали и проверили аргументы командной строки, использовали разделитель -- в Cargo, оставили логику поиска в подготовленной границе библиотеки, загрузили запрошенный файл и настроили стандартное поведение stdout, stderr и кода завершения для успешного выполнения и трёх случаев ошибки.