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>:
| Aspecto | Path | PathBuf |
|---|---|---|
| Propriedade | Emprestado (&Path) | Com propriedade (owned) |
| Mutável | Não | Sim |
| Análogo a | &str, &[T] | String, Vec<T> |
| Criação | Path::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étodo | Retorno | Descriçã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() | Components | Iterador sobre componentes |
ancestors() | Ancestors | Iterador sobre caminhos ancestrais |
starts_with(prefix) | bool | Verifica prefixo |
ends_with(suffix) | bool | Verifica sufixo |
is_absolute() | bool | Caminho é absoluto? |
is_relative() | bool | Caminho é relativo? |
display() | Display | Para 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étodo | Retorno | Descrição |
|---|---|---|
exists() | bool | Arquivo/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étodo | Descriçã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
&Pathem parâmetros de função em vez de&PathBuf— é mais genérico e evita cópias desnecessárias (assim como&strvs&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 novoPathBuf— se estiver construindo caminhos em loop, considere reutilizar umPathBufcompush()/pop().
Veja também
- Módulo std::fs — operações no sistema de arquivos
- File e OpenOptions — abrir arquivos usando caminhos
- Módulo std::env — diretório atual, diretório temporário
- Ler Arquivo em Rust — receita que usa Path na prática
- Documentação oficial:
std::path::Path,std::path::PathBuf