Arquivos em Rust
Ler e escrever arquivos é um dos primeiros momentos em que tudo o que você aprendeu sobre Result e o operador ? deixa de ser teoria. Quase toda operação com arquivos pode dar errado (o arquivo pode não existir, você pode não ter permissão), então a biblioteca padrão obriga você a lidar com isso diretamente.
Lendo um arquivo inteiro
Seção intitulada “Lendo um arquivo inteiro”O jeito mais simples de ler o conteúdo de um arquivo para uma String:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("Não foi possível ler o arquivo: {e}"), }}fs::read_to_string devolve um Result<String, std::io::Error>: Ok com o texto, se deu certo, ou Err com o motivo, se deu errado (arquivo inexistente, falta de permissão e assim por diante).
Usando ? para simplificar o tratamento de erros
Seção intitulada “Usando ? para simplificar o tratamento de erros”Fazer match em cada operação com arquivo fica repetitivo rapidinho. Se você está dentro de uma função que também devolve um Result, o operador ? (da página de Result) deixa tudo bem mais limpo:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // retorna mais cedo se falhar Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Erro: {e}"), }}Escrevendo em um arquivo
Seção intitulada “Escrevendo em um arquivo”use std::fs;
fn main() { let result = fs::write("output.txt", "Olá, arquivo!");
match result { Ok(()) => println!("Arquivo gravado com sucesso"), Err(e) => println!("Falha ao gravar: {e}"), }}fs::write cria o arquivo se ele não existir e o sobrescreve por completo se ele já existir. Lembre-se disso quando precisar acrescentar algo a um arquivo em vez de substituí-lo.
Acrescentando ao final de um arquivo
Seção intitulada “Acrescentando ao final de um arquivo”Para adicionar conteúdo a um arquivo que já existe, sem sobrescrevê-lo, use OpenOptions:
use std::fs::OpenOptions;use std::io::Write;
fn main() -> std::io::Result<()> { let mut file = OpenOptions::new() .append(true) .create(true) .open("log.txt")?;
writeln!(file, "Uma nova linha de log")?; Ok(())}Repare que aqui o próprio main devolve um Result. Vale a pena conhecer esse padrão: quando main devolve um Result, você pode usar ? diretamente dentro dele, em vez de colocar tudo dentro de um match.
Lendo linha por linha
Seção intitulada “Lendo linha por linha”Com um arquivo grande, carregar tudo na memória de uma vez nem sempre é uma boa ideia. Em vez disso, você pode ler linha por linha:
use std::fs::File;use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> { let file = File::open("hello.txt")?; let reader = BufReader::new(file);
for line in reader.lines() { println!("{}", line?); }
Ok(())}Verificando qual erro aconteceu
Seção intitulada “Verificando qual erro aconteceu”Até aqui, todo erro foi tratado do mesmo jeito: imprimindo. Mas muitas vezes você quer reagir de um jeito diferente dependendo de o que deu errado. Um arquivo que não existe costuma ser normal (talvez seja a primeira execução, e você simplesmente começa do zero). Já “permissão negada” é um problema de verdade, que você deveria informar.
Um io::Error diz a categoria dele com .kind(), que devolve um enum ErrorKind (em inglês) em que você pode fazer match:
use std::fs;use std::io::ErrorKind;
fn main() { // um arquivo na pasta temporária do sistema que não existe let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("{} linhas carregadas", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Ainda não há anotações, começando do zero"), ErrorKind::PermissionDenied => println!("Sem permissão para ler o arquivo de anotações"), _ => println!("Outra coisa deu errado: {e}"), }, }}Saída:
Ainda não há anotações, começando do zerostd::env::temp_dir() devolve o caminho da pasta temporária do sistema (como /tmp no Linux), e .join(...) acrescenta um nome de arquivo a ele. As funções de arquivo aceitam um caminho assim tão bem quanto um &str.
Sempre deixe um braço _ no final: o ErrorKind tem muitas variantes, e novas são adicionadas com o tempo. O braço _ pega tudo o que você não listou.
Prática
Seção intitulada “Prática”Os dois exercícios gravam o próprio arquivo na pasta temporária antes de tudo, então rodam em qualquer lugar, inclusive no Playground.
1. Salve e conte as linhas
Seção intitulada “1. Salve e conte as linhas”Grave três tarefas em um arquivo com fs::write, uma por linha. Depois, leia o arquivo de volta com fs::read_to_string e imprima cada linha com um número na frente, seguida do total.
Dica: coloque \n entre os itens para separar as linhas. text.lines() entrega as linhas uma de cada vez. Faça o main devolver std::io::Result<()> para poder usar ?.
Ver solução
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "aprender ownership\nescrever um módulo\nler um arquivo\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("{count} itens"); Ok(())}Saída:
1. aprender ownership2. escrever um módulo3. ler um arquivo3 itensComo o fs::write substitui o arquivo inteiro, você recebe as mesmas três linhas não importa quantas vezes rode. Cada ? devolve o erro a partir do main se a gravação ou a leitura falhar.
2. Acrescente a um log
Seção intitulada “2. Acrescente a um log”Comece um arquivo de log do zero com uma linha de cabeçalho e depois acrescente mais duas linhas com OpenOptions e writeln!. Por fim, leia o arquivo inteiro de volta e imprima.
Dica: use fs::write para o cabeçalho (assim cada execução começa limpa) e depois OpenOptions::new().append(true).open(&path)? para as linhas extras. Use print! (não println!) para evitar uma linha em branco a mais no fim, porque o arquivo já termina com uma quebra de linha.
Ver solução
use std::fs::{self, OpenOptions};use std::io::Write;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-log.txt");
fs::write(&path, "=== log ===\n")?; // começa do zero
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "iniciado")?; writeln!(file, "concluído")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Saída:
=== log ===iniciadoconcluídouse std::fs::{self, OpenOptions}; traz tanto o próprio módulo fs quanto o OpenOptions. Sem o primeiro fs::write, cada execução continuaria acrescentando ao arquivo antigo, e o log cresceria para sempre. É exatamente isso que você quer em um log de verdade, mas não em um exercício com saída fixa.
fs::read_to_string(path)lê um arquivo inteiro para umaStringe devolve umResult.fs::write(path, contents)grava um arquivo e o sobrescreve se ele já existir.OpenOptions::new().append(true)permite acrescentar conteúdo a um arquivo em vez de substituí-lo.main() -> std::io::Result<()>permite usar?diretamente nomain, em vez de fazermatchem cada chamada.e.kind()diz que tipo de erro aconteceu, comoErrorKind::NotFound, para você tratar cada um de um jeito.- Os exemplos que usam arquivos da pasta do projeto ficam melhores no seu computador;
std::env::temp_dir()dá uma pasta que funciona em qualquer lugar.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. O que acontece se você chamar fs::write com o caminho de um arquivo que já existe?
2. Como você acrescenta conteúdo a um arquivo em vez de sobrescrevê-lo?
3. O que você ganha quando o próprio main devolve std::io::Result<()>?
4. O que o operador ? faz quando fs::read_to_string(path)? falha?
5. Por que ler um arquivo grande linha por linha com BufReader em vez de usar fs::read_to_string?
Pontuação: 0 / 5
Para saber mais
Seção intitulada “Para saber mais”std::fs— documentação da std (em inglês): todas as funções de arquivos e pastas, comocreate_dir_all,copy,renameeremove_file.- Reading a File — The Rust Book (em inglês): a leitura de um arquivo dentro do projeto
minigrepdo livro, que junta esta página com argumentos de linha de comando. - File I/O — Rust by Example (em inglês): exemplos executáveis de abrir, criar e ler arquivos linha por linha.
OpenOptions— documentação da std (em inglês): todas as formas de abrir um arquivo (append, create, truncate e assim por diante).io::ErrorKind— documentação da std (em inglês): a lista completa de categorias de erro em que você pode fazer match.