Leggere e scrivere file in Rust
Leggere e scrivere file è uno dei primi momenti in cui tutto quello che hai imparato su Result e sull’operatore ? smette di essere teoria: quasi ogni operazione sui file può fallire (il file potrebbe non esistere, potresti non avere i permessi), quindi la libreria standard ti obbliga a gestirlo in modo diretto.
Leggere un file intero
Sezione intitolata “Leggere un file intero”Il modo più semplice per leggere il contenuto di un file in una String:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("Impossibile leggere il file: {e}"), }}fs::read_to_string restituisce un Result<String, std::io::Error>: Ok con il testo se ha funzionato, Err con un motivo se non ha funzionato (file mancante, permessi negati e così via).
Usare ? per semplificare la gestione degli errori
Sezione intitolata “Usare ? per semplificare la gestione degli errori”Fare match su ogni operazione sui file diventa presto ripetitivo. Se sei dentro una funzione che a sua volta restituisce un Result, l’operatore ? (della pagina su Result) mette molto ordine:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // in caso di errore esce subito Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Errore: {e}"), }}Scrivere su un file
Sezione intitolata “Scrivere su un file”use std::fs;
fn main() { let result = fs::write("output.txt", "Ciao, file!");
match result { Ok(()) => println!("Scrittura riuscita"), Err(e) => println!("Scrittura fallita: {e}"), }}fs::write crea il file se non esiste, e lo sovrascrive completamente se esiste già: tienilo a mente se devi aggiungere qualcosa a un file invece di sostituirlo.
Aggiungere in fondo a un file
Sezione intitolata “Aggiungere in fondo a un file”Per aggiungere a un file esistente invece di sovrascriverlo, usa 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, "Una nuova riga di log")?; Ok(())}Nota che qui è main stesso a restituire un Result: è uno schema che vale la pena conoscere. Quando main restituisce un Result, puoi usare ? direttamente dentro main invece di avvolgere tutto in un match.
Leggere riga per riga
Sezione intitolata “Leggere riga per riga”Con un file grande, leggerlo tutto in memoria in un colpo non è sempre l’ideale. Ecco come leggerlo invece riga per riga:
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(())}Capire quale errore è successo
Sezione intitolata “Capire quale errore è successo”Finora ogni errore è stato gestito allo stesso modo: stampandolo. Ma spesso vuoi reagire in modo diverso a seconda di che cosa è andato storto. Un file mancante di solito non è un problema (magari è la prima esecuzione, e parti semplicemente da zero). «Permesso negato» invece è un problema vero, da segnalare.
Un io::Error può dirti la sua categoria con .kind(), che restituisce un enum ErrorKind (documentazione in inglese) su cui puoi fare match:
use std::fs;use std::io::ErrorKind;
fn main() { // un file nella cartella temporanea del sistema che non esiste let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("Caricate {} righe", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Ancora nessuna nota, si parte da zero"), ErrorKind::PermissionDenied => println!("Non hai il permesso di leggere il file delle note"), _ => println!("Qualcos'altro è andato storto: {e}"), }, }}Output:
Ancora nessuna nota, si parte da zerostd::env::temp_dir() ti dà il percorso della cartella temporanea del sistema (come /tmp su Linux), e .join(...) ci aggiunge il nome di un file. Le funzioni sui file accettano un percorso come questo esattamente come accettano un &str.
Tieni sempre un ramo _ alla fine: ErrorKind ha molte varianti, e col tempo se ne aggiungono di nuove. Il ramo _ prende tutto quello che non hai elencato.
Pratica
Sezione intitolata “Pratica”Entrambi gli esercizi scrivono prima il proprio file nella cartella temporanea, quindi funzionano ovunque, anche nel Playground.
1. Salva e conta le righe
Sezione intitolata “1. Salva e conta le righe”Scrivi tre cose da fare in un file con fs::write, una per riga. Poi rileggi il file con fs::read_to_string e stampa ogni riga con un numero davanti, seguita dal totale.
Suggerimento: metti \n tra gli elementi per avere righe separate. text.lines() ti dà le righe una alla volta. Fai restituire a main un std::io::Result<()>, così puoi usare ?.
Mostra la soluzione
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "imparare l'ownership\nscrivere un modulo\nleggere un file\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("{count} elementi"); Ok(())}Output:
1. imparare l'ownership2. scrivere un modulo3. leggere un file3 elementiVisto che fs::write sostituisce tutto il file, ottieni le stesse tre righe per quante volte tu lo esegua. Ogni ? fa restituire l’errore da main se la scrittura o la lettura falliscono.
2. Aggiungi righe a un log
Sezione intitolata “2. Aggiungi righe a un log”Crea da zero un file di log con una riga di intestazione, poi aggiungi altre due righe con OpenOptions e writeln!. Infine rileggi tutto il file e stampalo.
Suggerimento: usa fs::write per l’intestazione (così ogni esecuzione parte pulita), poi OpenOptions::new().append(true).open(&path)? per le righe in più. print! (non println!) evita una riga vuota in più alla fine, perché il file finisce già con un a capo.
Mostra la soluzione
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")?; // si riparte da zero
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "avviato")?; writeln!(file, "terminato")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Output:
=== log ===avviatoterminatouse std::fs::{self, OpenOptions}; porta nello scope sia il modulo fs stesso sia OpenOptions. Senza il primo fs::write, ogni esecuzione continuerebbe ad aggiungere righe al file vecchio, e il log crescerebbe all’infinito. È esattamente quello che vuoi per un log vero, ma non per un esercizio con un output fisso.
Riepilogo
Sezione intitolata “Riepilogo”fs::read_to_string(percorso)legge un file intero in unaStringe restituisce unResult.fs::write(percorso, contenuto)scrive un file, sovrascrivendolo se esiste già.OpenOptions::new().append(true)ti permette di aggiungere a un file invece di sostituirlo.main() -> std::io::Result<()>ti permette di usare?direttamente inmain, invece di farematchsu ogni chiamata.e.kind()ti dice che tipo di errore è successo, comeErrorKind::NotFound, così puoi gestire ciascuno in modo diverso.- Gli esempi che usano file nella cartella del progetto conviene provarli in locale;
std::env::temp_dir()ti dà una cartella che funziona ovunque.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Che cosa succede se chiami fs::write su un percorso che esiste già?
2. Come aggiungi qualcosa a un file invece di sovrascriverlo?
3. Che cosa ti permette di fare un main che restituisce std::io::Result<()>?
4. Che cosa fa l'operatore ? quando fs::read_to_string(path)? fallisce?
5. Perché leggere un file grande riga per riga con BufReader invece che con fs::read_to_string?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”std::fs— documentazione della libreria standard (in inglese): tutte le funzioni su file e cartelle, comecreate_dir_all,copy,renameeremove_file.- Reading a File — The Rust Book (in inglese): la lettura di un file nel progetto
minigrepdel Book, che unisce questa pagina con gli argomenti da riga di comando. - File I/O — Rust by Example (in inglese): esempi eseguibili su come aprire, creare e leggere file riga per riga.
OpenOptions— documentazione della libreria standard (in inglese): tutti i modi per aprire un file (append, create, truncate e così via).io::ErrorKind— documentazione della libreria standard (in inglese): l’elenco completo delle categorie di errore su cui puoi fare match.