Консольное приложение
Первая задача - заставить minigrep принимать два аргумента командной строки: путь к файлу и строку для поиска.
cargo run -- searchstring example-filename.txt
итераторы генерируют серию значений, и мы можем вызвать метод collect у итератора, чтобы создать из него коллекцию, например вектор, который будет содержать все элементы, произведённые итератором.
Первый шаг программы
- Подключили библиотеку для работы с окружением и аргументами Описание библиотеки ENV
- Подключили библиотеку для работы с файлами Описание библиотеки FS
use std::env; use std::fs; fn main() { let args: Vec<String> = env::args().collect(); // dbg!(args); let query = &args[1]; let file_path = &args[2]; println!("Поисковый запрос {query}"); println!("В файле {file_path}"); let contents = fs::read_to_string(file_path) .expect("Файл должен быть доступен для чтения"); println!("Файл содержит текст:\n{contents}"); }
Рефакторинг программы
- Разделите код программы на два файла main.rs и lib.rs. Перенесите всю логику работы программы в файл lib.rs.
- Функциональные обязанности, которые остаются в функции main:
- Вызов логики разбора командной строки со значениями аргументов
- Настройка любой другой конфигурации
- Вызов функции run в lib.rs
- Обработка ошибки, если run возвращает ошибку
Выделение структуры
#![allow(unused)] fn main() { struct Config { query: String, file_path: String, } }
Создание конструктора new
#![allow(unused)] fn main() { impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } } } }
Изменение вызова
#![allow(unused)] fn main() { let config = Config::new(&args); }
Обработка ошибок
Поставить проверку и panic
#![allow(unused)] fn main() { impl Config { fn new(args: &[String]) -> Config { if args.len() < 3 { panic!("Не достаточно аргументов: -- 1:строка поиска 2:имя файла"); } }
Обработать через Result
#![allow(unused)] fn main() { impl Config { fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("Не достаточно аргументов: -- 1:строка поиска 2:имя файла"); } let query = args[1].clone(); let file_path = args[2].clone(); Ok(Config { query, file_path }) } } }
в main вызываем:
#![allow(unused)] fn main() { let config = Config::build(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {err}"); process::exit(1); }); }
Разделение на lib.rs и main.rs
src/main.rs
use std::env; //для чтения переменных окружения use std::process; //для завершения приложения use minigrep::Config; // подключаем lib fn main() { let args: Vec<String> = env::args().collect();//считываем аргументы из командной строки let config = Config::new(&args).unwrap_or_else(|err| { //инициализируем открытие файла с проверкой на ошибки eprintln!("Проблема распознавания аргументов: {err}");//выводим ошибки в поток ошибок process::exit(1); //закрываем программу если ошибка }); // dbg!(args); println!("Поисковый запрос: [{}]", config.query); println!("В файле: [{}]", config.file_path); let key = "PATH"; //это вывод содержимого переменной окружения match env::var(key) { Ok(val) => println!("Значение переменной {key}: {val:?}"), Err(e) => println!("couldn't interpret {key}: {e}"), } if let Err(e) = minigrep::run(config) { //выполняем поиск и проверяем возможные ошибки eprintln!("Приложение содержит ошибку {e}"); //если ошибка, то выводим информацию в поток ошибок process::exit(1); } }
src/lib.rs
#![allow(unused)] fn main() { use std::error::Error; use std::fs; use std::env; pub struct Config { //структура для работы с ключами pub query: String, //структура и все ее компоненты pub pub file_path: String, pub ignore_case: bool, } impl Config { //инициализация структуры по Config::new pub fn new(args: &[String]) -> Result<Config, &'static str> {//метод инициализации pub if args.len() < 3 { return Err("Не достаточно аргументов: -- 1:строка поиска 2:имя файла"); } let query = args[1].clone();//создаю копии элементов для владения let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> {//запуск run с параметрами Config let contents = fs::read_to_string(config.file_path)?;// если ошибка передаем в main let results = if config.ignore_case {//проверяем наличие переменной среды search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {//поиск чувствительный к регистру let mut result = Vec::new(); for line in contents.lines() { if line.contains(query) { result.push(line); } } result } fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {//поиск нечувствительный к регистру let mut result = Vec::new(); let query = query.to_lowercase(); for line in contents.lines() { if line.to_lowercase().contains(&query) { result.push(line); } } result } //тесты библиотеки #[cfg(test)] mod tests { use super::*; #[test] fn one_result() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. uduct mabuct"; assert_eq!( vec!["safe, fast, productive.", "uduct mabuct"], search(query, contents) ); } } #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }