Почему Rust — идеальный выбор для CLI-инструментов?
Вы когда-нибудь задумывались, почему все современные DevOps-инструменты — от bat до ripgrep и fd — написаны на Rust? Дело не только в моде. CLI утилиты на Rust получаются невероятно быстрыми, безопасными и, что самое приятное, их можно собрать в один крошечный бинарник без зависимостей. Никаких "а у вас Java установлена?" или "подождите, пока Node.js подтянет node_modules". Просто скачал — и готово.
В этой статье мы создадим CLI утилиту на Rust с нуля. Это будет не абстрактный "Hello, World!", а реальный полезный инструмент — анализатор логов, который ищет ошибки, считает их количество и выводит результаты в красивом цветном формате. И да, мы сделаем это за один вечер.
Что нам понадобится?
Для старта нужно минимум:
- Rust (установите через
rustup, если ещё нет) - Любой редактор кода (VS Code с плагином rust-analyzer — идеально)
- Пару часов свободного времени
Если вы уже писали на Python или Go, то Rust покажется вам необычным, но логичным. Главное — не бояться компилятора. Он не злой, он просто заботливый.
Шаг 1: Создаём проект
Открываем терминал и пишем:
cargo new log-scanner
cd log-scanner
Cargo — это наш швейцарский нож. Он создал базовую структуру с Cargo.toml (файл зависимостей) и src/main.rs (точка входа). Уже можно запустить cargo run и увидеть "Hello, world!". Но мы пойдём дальше.
Открываем Cargo.toml и добавляем зависимости, которые превратят наш код в настоящую CLI утилиту на Rust:
[dependencies]
clap = { version = "4.5", features = ["derive"] }
colored = "2.1"
anyhow = "1.0"
- clap — парсинг аргументов командной строки (без него никуда)
- colored — цветной вывод в терминал
- anyhow — удобная обработка ошибок
Шаг 2: Определяем структуру аргументов
Самое крутое в clap — это derive-макросы. Вы просто описываете структуру, а магия происходит сама. Вот как это выглядит:
use clap::Parser;
#[derive(Parser)]
#[command(name = "log-scanner")]
#[command(about = "Сканирует лог-файлы и ищет ошибки")]
struct Cli {
/// Путь к лог-файлу
file: String,
/// Строка для поиска (по умолчанию "ERROR")
#[arg(short, long, default_value = "ERROR")]
pattern: String,
/// Показать только количество найденных строк
#[arg(short, long)]
count: bool,
}
Обратите внимание: комментарии с тремя слешами /// автоматически становятся подсказками в --help. Запустите cargo run -- --help — и вы увидите аккуратно сгенерированную справку. Это одна из причин, почему создание CLI на Rust так приятно: документация пишется сама собой.
Шаг 3: Пишем логику поиска
Теперь самое интересное — чтение файла и поиск совпадений. Rust славится своей безопасной работой с файлами и памятью, но на практике это просто удобно:
use std::fs::File;
use std::io::{BufRead, BufReader};
use colored::*;
use anyhow::Result;
fn scan_log(file_path: &str, pattern: &str, count_only: bool) -> Result<()> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let mut total_matches = 0;
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
if line.contains(pattern) {
total_matches += 1;
if !count_only {
println!(
"{}:{} {}",
format!("{:>4}", line_num + 1).blue(),
pattern.red().bold(),
line.trim()
);
}
}
}
if count_only {
println!("{}: {}", "Найдено".green(), total_matches);
} else if total_matches == 0 {
println!("{}", "Совпадений не найдено".yellow());
}
Ok(())
}
Что здесь происходит? Мы открываем файл, создаём буферизированный читатель и проходим по строкам. Для каждой строки проверяем, содержит ли она наш паттерн. Если да — выводим с номером строки и подсветкой. anyhow::Result позволяет использовать оператор ? — если файл не откроется, программа вежливо сообщит об ошибке, а не упадёт с паникой.
Шаг 4: Собираем всё вместе в main
Теперь соединяем парсинг аргументов и логику:
fn main() -> Result<()> {
let cli = Cli::parse();
scan_log(&cli.file, &cli.pattern, cli.count)?;
Ok(())
}
Всё! Три строки. Cli::parse() сам разберёт аргументы командной строки и выведет ошибку, если пользователь забыл указать файл. Попробуйте запустить:
cargo run -- test.log
cargo run -- test.log -p "WARNING"
cargo run -- test.log --count
Работает? Отлично! Вы только что создали свою первую CLI утилиту на Rust.
Шаг 5: Добавляем "изюминки" (опционально)
Наш инструмент уже полезен, но давайте сделаем его ещё лучше. Добавим поддержку рекурсивного поиска по директории и прогресс-бар для больших файлов.
Рекурсивный поиск
Добавим флаг --recursive в структуру Cli:
/// Рекурсивно искать во всех файлах директории
#[arg(short, long)]
recursive: bool,
И модифицируем scan_log, чтобы он принимал путь к директории:
use std::path::Path;
fn process_path(path: &str, pattern: &str, count_only: bool, recursive: bool) -> Result<()> {
let p = Path::new(path);
if p.is_file() {
scan_log(path, pattern, count_only)?;
} else if p.is_dir() && recursive {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let sub_path = entry.path();
if sub_path.is_file() {
let file_name = sub_path.to_string_lossy();
println!("{} {}", "Файл:".cyan(), file_name);
scan_log(&file_name, pattern, count_only)?;
}
}
} else {
eprintln!("{}: путь