Pular para o conteúdo

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.

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).

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}"),
}
}
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.

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.

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(())
}

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 zero
▶ Experimente você mesmo

std::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.

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.

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 ownership
2. escrever um módulo
3. ler um arquivo
3 itens
▶ Experimente você mesmo

Como 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.

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 ===
iniciado
concluído
▶ Experimente você mesmo

use 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 uma String e devolve um Result.
  • 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 no main, em vez de fazer match em cada chamada.
  • e.kind() diz que tipo de erro aconteceu, como ErrorKind::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

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