Path e PathBuf em Rust

Referência completa de Path e PathBuf em Rust: criar caminhos, join, parent, extension, exists, canonicalize e caminhos cross-platform.

Path e PathBuf em Rust

Path e PathBuf são os tipos do Rust para representar caminhos no sistema de arquivos de forma segura e portável. Path é uma referência imutável (similar a &str), enquanto PathBuf é a versão com propriedade (owned, similar a String). Esses tipos tratam automaticamente as diferenças entre separadores de diretório do Windows (\) e do Unix (/), e oferecem métodos ricos para manipulação de componentes do caminho.

Visão geral e tipos-chave

Path vs PathBuf

A relação entre Path e PathBuf segue o mesmo padrão de str/String e [T]/Vec<T>:

AspectoPathPathBuf
PropriedadeEmprestado (&Path)Com propriedade (owned)
MutávelNãoSim
Análogo a&str, &[T]String, Vec<T>
CriaçãoPath::new("caminho")PathBuf::from("caminho")

Componentes internos

Um caminho é composto por:

  • Prefixo (Windows): C:, \\servidor\compartilhamento
  • Raiz: / (Unix) ou \ (Windows)
  • Componentes normais: nomes de diretórios e arquivo
  • Nome do arquivo: último componente
  • Extensão: parte após o último . no nome

Padrões comuns com código

Criando caminhos

use std::path::{Path, PathBuf};

fn main() {
    // Path — referência imutável
    let caminho = Path::new("/home/usuario/documentos/relatorio.pdf");
    println!("Path: {}", caminho.display());

    // PathBuf — com propriedade, mutável
    let mut buf = PathBuf::from("/home/usuario");
    buf.push("projetos");
    buf.push("rust");
    buf.push("src");
    buf.push("main.rs");
    println!("PathBuf: {}", buf.display());

    // Conversão entre tipos
    let referencia: &Path = buf.as_path();
    let owned: PathBuf = referencia.to_path_buf();

    // De String/&str
    let de_string = PathBuf::from(String::from("/tmp/arquivo.txt"));
    let de_str = Path::new("/tmp/arquivo.txt");
}

Juntando caminhos com join

use std::path::{Path, PathBuf};

fn main() {
    let base = Path::new("/home/usuario");

    // join() cria um novo PathBuf
    let config = base.join("config").join("app.toml");
    println!("Config: {}", config.display());
    // /home/usuario/config/app.toml

    // join com caminho absoluto substitui completamente
    let absoluto = base.join("/etc/hosts");
    println!("Absoluto: {}", absoluto.display());
    // /etc/hosts (o caminho absoluto substitui a base)

    // Construindo caminhos dinamicamente
    let projeto = "meu_app";
    let arquivo = format!("{}.rs", "main");
    let caminho = Path::new("projetos").join(projeto).join("src").join(arquivo);
    println!("Projeto: {}", caminho.display());
    // projetos/meu_app/src/main.rs
}

Extraindo componentes

use std::path::Path;

fn analisar_caminho(caminho: &str) {
    let p = Path::new(caminho);

    println!("Caminho: {}", p.display());
    println!("  parent():     {:?}", p.parent().map(|p| p.display().to_string()));
    println!("  file_name():  {:?}", p.file_name());
    println!("  file_stem():  {:?}", p.file_stem());
    println!("  extension():  {:?}", p.extension());

    println!("  Componentes:");
    for componente in p.components() {
        println!("    {:?}", componente);
    }
    println!();
}

fn main() {
    analisar_caminho("/home/usuario/projeto/src/main.rs");
    analisar_caminho("../dados/relatorio.csv.gz");
    analisar_caminho("/");
}

Tabela de métodos principais

Métodos de consulta (Path e PathBuf)

MétodoRetornoDescrição
parent()Option<&Path>Diretório pai
file_name()Option<&OsStr>Nome do arquivo (último componente)
file_stem()Option<&OsStr>Nome sem extensão
extension()Option<&OsStr>Extensão do arquivo
components()ComponentsIterador sobre componentes
ancestors()AncestorsIterador sobre caminhos ancestrais
starts_with(prefix)boolVerifica prefixo
ends_with(suffix)boolVerifica sufixo
is_absolute()boolCaminho é absoluto?
is_relative()boolCaminho é relativo?
display()DisplayPara exibição formatada
to_str()Option<&str>Converte para &str (pode falhar)
to_string_lossy()Cow<str>Converte para string (substitui chars inválidos)

Métodos do filesystem (Path)

MétodoRetornoDescrição
exists()boolArquivo/diretório existe?
is_file()boolÉ um arquivo regular?
is_dir()boolÉ um diretório?
is_symlink()boolÉ um link simbólico?
metadata()io::Result<Metadata>Metadados (segue symlinks)
symlink_metadata()io::Result<Metadata>Metadados (não segue symlinks)
canonicalize()io::Result<PathBuf>Caminho absoluto canônico
read_dir()io::Result<ReadDir>Lista entradas do diretório
read_link()io::Result<PathBuf>Destino do symlink

Métodos de mutação (PathBuf)

MétodoDescrição
push(path)Adiciona componente ao final
pop()Remove o último componente
set_file_name(name)Substitui o nome do arquivo
set_extension(ext)Substitui a extensão
with_file_name(name)Retorna novo PathBuf com outro nome
with_extension(ext)Retorna novo PathBuf com outra extensão

Exemplos práticos

Exemplo 1: Renomear arquivos por extensão

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

fn renomear_extensao(diretorio: &str, de: &str, para: &str) -> io::Result<usize> {
    let mut contagem = 0;

    for entrada in fs::read_dir(diretorio)? {
        let entrada = entrada?;
        let caminho = entrada.path();

        if caminho.extension().and_then(|e| e.to_str()) == Some(de) {
            let novo = caminho.with_extension(para);
            fs::rename(&caminho, &novo)?;
            println!(
                "  {} -> {}",
                caminho.file_name().unwrap().to_string_lossy(),
                novo.file_name().unwrap().to_string_lossy()
            );
            contagem += 1;
        }
    }

    Ok(contagem)
}

fn main() -> io::Result<()> {
    let n = renomear_extensao("fotos", "jpeg", "jpg")?;
    println!("Renomeados {} arquivos", n);
    Ok(())
}

Exemplo 2: Resolver caminhos relativos

use std::io;
use std::path::{Path, PathBuf};

fn resolver_caminho(base: &Path, relativo: &str) -> io::Result<PathBuf> {
    let combinado = base.join(relativo);

    // canonicalize resolve .., . e symlinks
    match combinado.canonicalize() {
        Ok(absoluto) => {
            println!(
                "Resolvido: '{}' -> '{}'",
                relativo,
                absoluto.display()
            );
            Ok(absoluto)
        }
        Err(e) => {
            eprintln!(
                "Não foi possível resolver '{}' a partir de '{}': {}",
                relativo,
                base.display(),
                e
            );
            Err(e)
        }
    }
}

fn main() -> io::Result<()> {
    let cwd = std::env::current_dir()?;
    println!("Diretório atual: {}", cwd.display());

    resolver_caminho(&cwd, "src/main.rs")?;
    resolver_caminho(&cwd, "../outro_projeto")?;

    Ok(())
}

Exemplo 3: Construir caminhos cross-platform

use std::path::{Path, PathBuf, MAIN_SEPARATOR};

fn diretorio_config() -> PathBuf {
    // Caminho de configuração baseado no SO
    if cfg!(target_os = "windows") {
        // Windows: C:\Users\<user>\AppData\Roaming\MeuApp
        let appdata = std::env::var("APPDATA")
            .unwrap_or_else(|_| String::from("C:\\Users\\Public"));
        PathBuf::from(appdata).join("MeuApp")
    } else if cfg!(target_os = "macos") {
        // macOS: ~/Library/Application Support/MeuApp
        let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/tmp"));
        PathBuf::from(home)
            .join("Library")
            .join("Application Support")
            .join("MeuApp")
    } else {
        // Linux/outros: ~/.config/meuapp
        let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/tmp"));
        PathBuf::from(home).join(".config").join("meuapp")
    }
}

fn main() {
    let config_dir = diretorio_config();
    println!("Diretório de config: {}", config_dir.display());
    println!("Separador do sistema: '{}'", MAIN_SEPARATOR);

    let config_file = config_dir.join("settings.toml");
    println!("Arquivo de config: {}", config_file.display());

    // Verificar existência
    if config_dir.exists() {
        println!("Diretório já existe");
    } else {
        println!("Diretório precisa ser criado");
    }
}

Exemplo 4: Navegar pela hierarquia com ancestors

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

/// Busca um arquivo subindo na hierarquia de diretórios (como git busca .git)
fn buscar_acima(inicio: &Path, nome_arquivo: &str) -> io::Result<Option<std::path::PathBuf>> {
    let inicio_abs = if inicio.is_absolute() {
        inicio.to_path_buf()
    } else {
        std::env::current_dir()?.join(inicio)
    };

    for ancestral in inicio_abs.ancestors() {
        let candidato = ancestral.join(nome_arquivo);
        if candidato.exists() {
            return Ok(Some(candidato));
        }
    }

    Ok(None)
}

fn main() -> io::Result<()> {
    let cwd = std::env::current_dir()?;
    println!("Procurando Cargo.toml a partir de: {}", cwd.display());

    // ancestors() retorna todos os diretórios pai até a raiz
    println!("\nAncestors:");
    for anc in cwd.ancestors() {
        println!("  {}", anc.display());
    }

    match buscar_acima(&cwd, "Cargo.toml")? {
        Some(encontrado) => println!("\nEncontrado: {}", encontrado.display()),
        None => println!("\nCargo.toml não encontrado na hierarquia"),
    }

    Ok(())
}

Exemplo 5: Manipulação segura de extensões de arquivo

use std::path::{Path, PathBuf};

fn adicionar_sufixo_antes_extensao(caminho: &Path, sufixo: &str) -> PathBuf {
    let stem = caminho
        .file_stem()
        .unwrap_or_default()
        .to_string_lossy();
    let extensao = caminho.extension().and_then(|e| e.to_str());

    let novo_nome = match extensao {
        Some(ext) => format!("{}{}.{}", stem, sufixo, ext),
        None => format!("{}{}", stem, sufixo),
    };

    match caminho.parent() {
        Some(pai) => pai.join(novo_nome),
        None => PathBuf::from(novo_nome),
    }
}

fn gerar_nomes_variantes(caminho: &str) -> Vec<PathBuf> {
    let p = Path::new(caminho);
    vec![
        adicionar_sufixo_antes_extensao(p, "_backup"),
        adicionar_sufixo_antes_extensao(p, "_v2"),
        p.with_extension("bak"),
        p.with_extension("tmp"),
    ]
}

fn main() {
    let variantes = gerar_nomes_variantes("/dados/relatorio.xlsx");
    println!("Variantes de '/dados/relatorio.xlsx':");
    for v in &variantes {
        println!("  {}", v.display());
    }
    // /dados/relatorio_backup.xlsx
    // /dados/relatorio_v2.xlsx
    // /dados/relatorio.bak
    // /dados/relatorio.tmp

    // Lidar com arquivos sem extensão
    let variantes2 = gerar_nomes_variantes("/bin/executavel");
    println!("\nVariantes de '/bin/executavel':");
    for v in &variantes2 {
        println!("  {}", v.display());
    }
}

Caminhos e OsStr: lidando com Unicode

Nem todos os caminhos são UTF-8 válido. Por isso, Path trabalha com OsStr/OsString internamente:

use std::ffi::OsStr;
use std::path::Path;

fn processar_nome_seguro(caminho: &Path) -> String {
    // to_str() pode falhar se o caminho não for UTF-8 válido
    match caminho.file_name().and_then(|n| n.to_str()) {
        Some(nome) => nome.to_string(),
        None => {
            // to_string_lossy() substitui caracteres inválidos com U+FFFD
            caminho
                .file_name()
                .map(|n| n.to_string_lossy().into_owned())
                .unwrap_or_else(|| String::from("<desconhecido>"))
        }
    }
}

fn main() {
    let caminho = Path::new("/home/usuario/relatório.pdf");
    println!("Nome seguro: {}", processar_nome_seguro(caminho));
}

Dicas de desempenho

  • Use &Path em parâmetros de função em vez de &PathBuf — é mais genérico e evita cópias desnecessárias (assim como &str vs &String).
  • exists(), is_file(), is_dir() fazem syscalls — cache o resultado se for chamar múltiplas vezes para o mesmo caminho.
  • canonicalize() é custoso — resolve symlinks e acessa o disco. Use apenas quando precisar do caminho absoluto real.
  • join() aloca um novo PathBuf — se estiver construindo caminhos em loop, considere reutilizar um PathBuf com push()/pop().

Veja também