Введение: зачем вам может понадобиться список файлов в 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);