Как вывести список файлов в директории на Rust: пошаговый туториал

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

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

Начать курс

Введение: зачем вам может понадобиться список файлов в Rust?

Представьте: вы пишете на Rust утилиту для резервного копирования, анализатор логов или простой файловый менеджер. И тут встаёт базовая, но критическая задача — получить список всех файлов в указанной директории. Казалось бы, что сложного? Но в Rust, с его строгой типизацией и системой владения, даже такая простая операция таит несколько подводных камней.

Вопрос «How can I list files of a directory in Rust?» — один из самых популярных на Stack Overflow (тысячи просмотров!). И это не случайно: новички часто спотыкаются о std::fs::read_dir, путаются с итераторами и забывают про обработку ошибок. В этом туториале я покажу вам два рабочих подхода: простой (только файлы в одной папке) и продвинутый (рекурсивный обход всех поддиректорий). Поехали!

1. Подготовка: создаём новый проект

Прежде чем писать код, создадим новый бинарный проект. Откройте терминал и выполните:

cargo new list_files_rs
cd list_files_rs

Теперь откройте src/main.rs — именно сюда мы будем писать весь код. Для начала нам не понадобятся внешние зависимости, только стандартная библиотека Rust — std.

2. Самый простой способ: std::fs::read_dir

Функция std::fs::read_dir возвращает итератор по записям в директории. Каждая запись — это DirEntry, которая содержит имя, путь, тип и метаданные файла. Давайте напишем базовый пример:

use std::fs;
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = Path::new("."); // текущая директория

    // Читаем содержимое папки
    let entries = fs::read_dir(path)?;

    for entry in entries {
        let entry = entry?; // распаковываем Result
        let path = entry.path();

        // Проверяем, что это файл (не директория)
        if path.is_file() {
            println!("{}", path.display());
        }
    }

    Ok(())
}

Что здесь происходит?

  • fs::read_dir(path) — открывает директорию и возвращает ReadDir (итератор).
  • Мы проходим по каждой записи. Каждый вызов next() возвращает Result<DirEntry> — поэтому нужен entry?.
  • entry.path() возвращает полный путь к файлу/папке.
  • Метод path.is_file() отсеивает поддиректории.

Запустите код командой cargo run — вы увидите список файлов в корне проекта (например, Cargo.toml, src/main.rs).

2.1. Фильтрация по расширению

Часто нужно получить только определённые файлы, например, только .rs или .txt. Добавим фильтр:

use std::fs;
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = Path::new(".");

    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let path = entry.path();

        // Проверяем, что это файл и расширение .rs
        if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") {
            println!("{}", path.display());
        }
    }

    Ok(())
}

Трюк с map_or нужен, чтобы безопасно обработать случай, когда у файла нет расширения — path.extension() возвращает Option<&OsStr>.

3. Рекурсивный обход: заглядываем во все подпапки

Простой read_dir не заходит в поддиректории. Если вам нужно собрать все файлы рекурсивно (например, для поиска дубликатов), придётся написать свою рекурсивную функцию. Вот элегантное решение:

use std::fs;
use std::path::Path;

fn visit_dirs(dir: &Path, depth: usize) -> Result<(), Box<dyn std::error::Error>> {
    // Пропускаем, если это не директория
    if dir.is_dir() {
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_dir() {
                // Рекурсивно обходим поддиректорию
                visit_dirs(&path, depth + 1)?;
            } else {
                // Печатаем файл с отступом для наглядности
                let indent = "  ".repeat(depth);
                println!("{}{}", indent, path.display());
            }
        }
    }
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let start_path = Path::new(".");
    visit_dirs(start_path, 0)?;
    Ok(())
}

Как это работает:

  • Функция visit_dirs получает путь и глубину вложенности.
  • Если путь — директория, читаем её содержимое.
  • Для каждой записи: если это снова директория — вызываем себя рекурсивно, если файл — печатаем.
  • Параметр depth нужен только для красивых отступов; в реальном проекте вы можете его убрать.

Запустите — и вы увидите полное дерево файлов вашего проекта, включая все файлы в src/, target/ и т.д.

3.1. Избегаем бесконечной рекурсии: симлинки и циклы

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

if path.is_dir() && !path.is_symlink() {
    visit_dirs(&path, depth + 1)?;
}

Метод is_symlink() вернёт true для символических ссылок, и мы их просто пропустим. Если вам нужно следовать по ссылкам — усложняйте логику, отслеживая посещённые inode.

4. Продвинутый вариант: собираем все пути в Vec

Часто список файлов нужен не для печати, а для дальнейшей обработки. Давайте соберём все пути в вектор:

use std::fs;
use std::path::PathBuf;

fn collect_files(dir: &Path) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
    let mut result = Vec::new();

    if dir.is_dir() {
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_dir() && !path.is_symlink() {
                // Рекурсивно собираем файлы из поддиректорий
                let mut sub_files = collect_files(&path)?;
                result.append(&mut sub_files);                

Блоги

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