Lire et écrire des fichiers en Rust
Lire et écrire des fichiers, c’est l’un des premiers endroits où tout ce que vous avez appris sur Result et l’opérateur ? cesse d’être théorique. Presque toutes les opérations sur les fichiers peuvent échouer (le fichier n’existe peut-être pas, vous n’avez peut-être pas les droits), donc la bibliothèque standard vous oblige à gérer ces cas directement.
Lire un fichier en entier
Section intitulée « Lire un fichier en entier »La façon la plus simple de lire le contenu d’un fichier dans une String :
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("Impossible de lire le fichier : {e}"), }}fs::read_to_string renvoie un Result<String, std::io::Error> : Ok avec le texte si tout s’est bien passé, Err avec une raison sinon (fichier absent, pas les droits, etc.).
Simplifier la gestion des erreurs avec ?
Section intitulée « Simplifier la gestion des erreurs avec ? »Faire un match sur chaque opération de fichier devient vite répétitif. Si vous êtes dans une fonction qui renvoie elle-même un Result, l’opérateur ? (vu sur la page Result) allège beaucoup le code :
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // sort tout de suite en cas d'échec Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Erreur : {e}"), }}Écrire dans un fichier
Section intitulée « Écrire dans un fichier »use std::fs;
fn main() { let result = fs::write("output.txt", "Bonjour, fichier !");
match result { Ok(()) => println!("Écriture réussie"), Err(e) => println!("Échec de l'écriture : {e}"), }}fs::write crée le fichier s’il n’existe pas, et l’écrase complètement s’il existe déjà. Gardez-le en tête si vous voulez ajouter du contenu à un fichier plutôt que le remplacer.
Ajouter à la fin d’un fichier
Section intitulée « Ajouter à la fin d’un fichier »Pour ajouter du contenu à un fichier existant au lieu de l’écraser, utilisez 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, "Une nouvelle ligne de journal")?; Ok(())}Remarquez qu’ici, main renvoie elle-même un Result. C’est un schéma à connaître : quand main renvoie un Result, vous pouvez utiliser ? directement dedans, au lieu de tout envelopper dans un match.
Lire ligne par ligne
Section intitulée « Lire ligne par ligne »Pour un gros fichier, tout charger en mémoire d’un coup n’est pas toujours idéal. Voici comment le lire ligne par ligne :
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(())}Savoir quelle erreur s’est produite
Section intitulée « Savoir quelle erreur s’est produite »Jusqu’ici, toutes les erreurs étaient traitées de la même façon : on les affichait. Mais souvent, vous voulez réagir différemment selon ce qui s’est mal passé. Un fichier absent n’est généralement pas grave (c’est peut-être le premier lancement, et vous partez de zéro). Une « permission refusée », en revanche, est un vrai problème à signaler.
Un io::Error peut vous donner sa catégorie avec .kind(), qui renvoie une enum ErrorKind sur laquelle vous pouvez faire un match :
use std::fs;use std::io::ErrorKind;
fn main() { // un fichier du dossier temporaire du système qui n'existe pas let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("{} lignes chargées", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Pas encore de notes, on part de zéro"), ErrorKind::PermissionDenied => println!("Pas le droit de lire le fichier de notes"), _ => println!("Autre problème : {e}"), }, }}Sortie :
Pas encore de notes, on part de zérostd::env::temp_dir() vous donne le chemin du dossier temporaire du système (par exemple /tmp sous Linux), et .join(...) y ajoute un nom de fichier. Les fonctions sur les fichiers acceptent un chemin comme celui-ci aussi bien qu’un &str.
Gardez toujours une branche _ à la fin : ErrorKind a beaucoup de variantes, et de nouvelles apparaissent au fil du temps. La branche _ attrape tout ce que vous n’avez pas listé.
Exercices
Section intitulée « Exercices »Les deux exercices écrivent d’abord leur propre fichier dans le dossier temporaire : ils fonctionnent donc partout, y compris dans le Playground.
1. Enregistrer et compter les lignes
Section intitulée « 1. Enregistrer et compter les lignes »Écrivez trois tâches dans un fichier avec fs::write, une par ligne. Relisez ensuite le fichier avec fs::read_to_string et affichez chaque ligne précédée d’un numéro, puis le total.
Indice : mettez \n entre les tâches pour obtenir des lignes séparées. text.lines() vous donne les lignes une par une. Faites renvoyer std::io::Result<()> à main pour pouvoir utiliser ?.
Voir la solution
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "apprendre l'ownership\nécrire un module\nlire un fichier\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("{count} tâches"); Ok(())}Sortie :
1. apprendre l'ownership2. écrire un module3. lire un fichier3 tâchesComme fs::write remplace tout le fichier, vous obtenez les trois mêmes lignes quel que soit le nombre de lancements. Chaque ? fait sortir l’erreur de main si l’écriture ou la lecture échoue.
2. Ajouter à un journal
Section intitulée « 2. Ajouter à un journal »Créez un fichier de journal tout neuf avec une ligne d’en-tête, puis ajoutez-y deux lignes avec OpenOptions et writeln!. Enfin, relisez tout le fichier et affichez-le.
Indice : utilisez fs::write pour l’en-tête (ainsi chaque lancement repart de zéro), puis OpenOptions::new().append(true).open(&path)? pour les lignes suivantes. print! (et non println!) évite une ligne vide en trop à la fin, car le fichier se termine déjà par un retour à la ligne.
Voir la solution
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, "=== journal ===\n")?; // on repart de zéro
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "démarrage")?; writeln!(file, "fin")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Sortie :
=== journal ===démarragefinuse std::fs::{self, OpenOptions}; importe à la fois le module fs lui-même et OpenOptions. Sans le premier fs::write, chaque lancement continuerait d’ajouter des lignes à l’ancien fichier, et le journal grossirait à l’infini. C’est exactement ce qu’on veut pour un vrai journal, mais pas pour un exercice dont la sortie doit rester fixe.
Récapitulatif
Section intitulée « Récapitulatif »fs::read_to_string(path)lit un fichier entier dans uneStringet renvoie unResult.fs::write(path, contents)écrit un fichier, en l’écrasant s’il existe déjà.OpenOptions::new().append(true)permet d’ajouter du contenu à un fichier au lieu de le remplacer.main() -> std::io::Result<()>vous permet d’utiliser?directement dansmain, au lieu de faire unmatchsur chaque appel.e.kind()vous indique quel type d’erreur s’est produit, commeErrorKind::NotFound, pour traiter chaque cas différemment.- Les exemples qui utilisent des fichiers du dossier de votre projet sont à essayer en local ;
std::env::temp_dir()vous donne un dossier qui fonctionne partout.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que se passe-t-il si vous appelez fs::write sur un chemin qui existe déjà ?
2. Comment ajouter du contenu à un fichier au lieu de l'écraser ?
3. Qu'est-ce que ça permet quand main renvoie elle-même std::io::Result<()> ?
4. Que fait l'opérateur ? quand fs::read_to_string(path)? échoue ?
5. Pourquoi lire un gros fichier ligne par ligne avec BufReader plutôt qu'avec fs::read_to_string ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »std::fs— documentation de la bibliothèque standard (en anglais) : toutes les fonctions sur les fichiers et les dossiers, commecreate_dir_all,copy,renameetremove_file.- Reading a File — The Rust Book (en anglais) : la lecture d’un fichier dans le projet
minigrepdu Book, qui combine cette page avec les arguments de ligne de commande. - File I/O — Rust by Example (en anglais) : des exemples exécutables pour ouvrir, créer et lire des fichiers ligne par ligne.
OpenOptions— documentation de la bibliothèque standard (en anglais) : toutes les façons d’ouvrir un fichier (ajout, création, troncature, etc.).io::ErrorKind— documentation de la bibliothèque standard (en anglais) : la liste complète des catégories d’erreurs sur lesquelles vous pouvez faire unmatch.