CLI утилиты на Rust: как написать свой первый инструмент за вечер

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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!("{}: путь                

Блоги

Рекомендаций по книгам