ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1168
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Принятие аргументов командной строки
Создадим новый проект консольного приложения как обычно с помощью команды cargo new
. Мы назовём проект minigrep
, чтобы различать наше приложение от grep
,
которое возможно уже есть в вашей системе.
Первая задача - заставить minigrep принимать два аргумента командной строки: путь к файлу и строку для поиска. То есть мы хотим иметь возможность запускать нашу программу через cargo run
, с использованием двойного дефиса, чтобы указать, что следующие аргументы предназначены для нашей программы, а не для cargo
, строки для поиска и пути к файлу в котором нужно искать, как описано ниже:
В данный момент программа сгенерированная cargo new не может обрабатывать аргументы, которые мы ей передаём. Некоторые существующие библиотеки на crates.io могут помочь с написанием программы, которая принимает аргументы командной строки, но так как вы просто изучаете эту концепцию, давайте реализуем эту возможность сами.
1 ... 25 26 27 28 29 30 31 32 ... 62
Чтение значений аргументов
Чтобы minigrep мог воспринимать значения аргументов командной строки, которые мы ему передаём, нам понадобится функция std::env::args
, входящая в стандартную библиотеку Rust. Эта функция возвращает итератор аргументов командной строки,
переданных в minigrep
. Мы подробно рассмотрим итераторы в главе 13
. Пока вам достаточно знать две вещи об итераторах: итераторы генерируют серию значений, и мы можем вызвать метод collect у итератора, чтобы создать из него коллекцию, например вектор, который будет содержать все элементы, произведённые итератором.
Код представленный в Листинге 12-1 позволяет вашей программе minigrep читать любые переданные ей аргументы командной строки, а затем собирать значения в вектор.
Файл: src/main.rs
$
cargo new minigrep
Created binary (application) `minigrep` project
$
cd minigrep
$
cargo run -- searchstring example-filename.txt 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] args = [
"target/debug/minigrep",
]
Обратите внимание, что первое значение в векторе "target/debug/minigrep"
является названием нашего двоичного файла. Это соответствует поведению списка аргументов в
Си, позволяя программам использовать название из которой они были вызваны при выполнении. Часто бывает удобно иметь доступ к имени программы, если вы хотите распечатать его в сообщениях или изменить поведение программы в зависимости от того, какой псевдоним командной строки был использован для вызова программы. Но для целей этой главы, мы проигнорируем его и сохраним только два аргумента, которые нам нужны.
Сохранения значений аргументов в переменные
На текущий момент программа может получить доступ к значениям, указанным в качестве аргументов командной строки. Теперь нам требуется сохранять значения этих двух аргументов в переменных, чтобы мы могли использовать их в остальных частях программы. Мы сделаем это в листинге 12-2.
Файл: src/main.rs
Листинг 12-2: Создание переменных для хранения значений аргументов искомой подстроки и пути к файлу
Как видно из распечатки вектора, имя программы занимает первое значение в векторе по адресу args[0]
, значит, аргументы начинаются с индекса
1
. Первый аргумент minigrep
- это строка, которую мы ищем, поэтому мы помещаем ссылку на первый аргумент в переменную query
. Вторым аргументом является путь к файлу, поэтому мы помещаем ссылку на второй аргумент в переменную file_path
$
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] args = [
"target/debug/minigrep",
"needle",
"haystack",
] 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);
}
Для проверки корректности работы нашей программы, значения переменных выводятся в консоль. Далее, запустим нашу программу со следующими аргументами: 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
Чтение файла
Теперь добавим возможность чтения файла, указанного как аргумент командной строки filename
. Во-первых, нам нужен пример файла для тестирования: лучший тип файла для проверки работы minigrep это файл с небольшим количеством текста в несколько строк с несколькими повторяющимися словами. В листинге 12-3 представлено стихотворение
Эмили Дикинсон, которое будет хорошо работать! Создайте файл с именем poem.txt в корне вашего проекта и введите стихотворение "I’m nobody! Who are you?"
Файл: poem.txt
Листинг 12-3: Стихотворение Эмили Дикинсон "I’m nobody! Who are you?"
Текст на месте, отредактируйте src/main.rs и добавьте код для чтения файла, как показано в листинге 12-4.
Файл: src/main.rs
Листинг 12-4: Чтение содержимого файла указанного во втором аргументе
Во-первых, мы добавляем ещё одно объявление use чтобы подключить соответствующую часть стандартной библиотеки: нам нужен std::fs для обработки файлов.
В main мы добавили новый оператор: функция fs::read_to_string принимает filename
, открывает этот файл и возвращает содержимое файла как
Result
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog! use std::env; use std::fs; fn main
() {
// --snip-- println!
(
"In file {}"
, file_path); let contents = fs::read_to_string(file_path)
.expect(
"Should have been able to read the file"
); println!
(
"With text:\n{contents}"
);
}
После этого выражения мы снова добавили временный вывод println!
для печати значения contents после чтения файла, поэтому мы можем проверить, что программа работает.
Давайте запустим этот код с любой строкой в качестве первого аргумента командной строки (потому что мы ещё не реализовали поисковую часть) и файл poem.txt как второй аргумент:
Отлично! Этот код прочитал и затем печатал содержимое файла. Хотя наша программа решает поставленную задачу, она не лишена недостатков. Прежде всего, функция main решает множество задач. Такую функцию неудобно тестировать. Далее, не отслеживаются возможные ошибки ввода данных. Пока наша программа небольшая, то данными недочётами можно пренебречь. При увеличении размеров программы, такую программу будет всё сложнее и сложнее поддерживать. Хорошей практикой программирования является ранний рефакторинг кода по мере усложнения. Поэтому,
далее мы улучшим наш код с помощью улучшения его структуры.
$
cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Рефакторинг для улучшения модульности и обработки
ошибок
Для улучшения программы мы исправим 4 имеющихся проблемы, связанных со структурой программы и тем как обрабатываются потенциальные ошибки.
Во-первых, функция main на данный момент решает две задачи - анализирует переменные командной строки и читает файлы. Для небольшой функции это не является проблемой. Тем не менее, при увеличении функционала внутри main
, количество отдельных обрабатываемых задач в функции main будет расти. Поскольку эта функция получает больше обязанностей, то становится все труднее понимать, труднее тестировать и труднее изменять, не сломав одну из её частей. Лучше всего разделить функциональность, чтобы каждая функция отвечала за одну задачу.
Эта проблема также связана со второй проблемой: хотя переменные query и filename являются переменными конфигурации нашей программы, переменные типа contents используются для выполнения логики программы. Чем длиннее main становится, тем больше переменных нам нужно будет добавить в область видимости; чем больше у нас переменных, тем сложнее будет отслеживать назначение каждой переменной. Лучше всего сгруппировать переменные конфигурации в одну структуру, чтобы сделать их назначение понятным.
Третья проблема заключается в том, что мы используем expect для вывода информации об ошибке при проблеме с чтением файла, но сообщение об ошибке просто выведет текст
Something went wrong reading the file
. Чтение файла может не сработать по разным причинам, например: файл не найден или у нас может не быть разрешения на его чтение. В существующем коде, независимо от ситуации, мы напечатаем
Something went wrong reading the file
, что не даст пользователю никакой информации!
В четвёртых, мы используем expect неоднократно для обработки различных ошибок и если пользователь запускает нашу программу без указания достаточного количества аргументов он получит ошибку index out of bounds из Rust, что не совсем понятно описывает проблему. Было бы лучше, если бы весь код обработки ошибок был в одном месте. Это позволило бы тем, кто будет поддерживать наш код в дальнейшем, при необходимости изменения логики обработки ошибок, вносить нужные изменения только в одном месте. Наличие всего кода обработки ошибок в одном месте гарантирует,
что мы напечатаем сообщения, которые будут иметь смысл для наших конечных пользователей.
Давайте решим эти четыре проблемы путём рефакторинга нашего проекта.
Разделение ответственности для бинарных проектов