Dateien in Rust lesen und schreiben
Beim Lesen und Schreiben von Dateien wird alles, was du über Result und den ?-Operator gelernt hast, auf einmal ganz praktisch. Fast jede Dateioperation kann schiefgehen: Die Datei gibt es vielleicht nicht, oder dir fehlen die Rechte. Deshalb zwingt dich die Standardbibliothek, dich direkt darum zu kümmern.
Eine ganze Datei lesen
Abschnitt betitelt „Eine ganze Datei lesen“Der einfachste Weg, den Inhalt einer Datei in einen String zu lesen:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("Datei konnte nicht gelesen werden: {e}"), }}fs::read_to_string gibt ein Result<String, std::io::Error> zurück: Ok mit dem Text, wenn es geklappt hat, und Err mit dem Grund, wenn nicht (Datei fehlt, keine Berechtigung und so weiter).
Fehlerbehandlung mit ? vereinfachen
Abschnitt betitelt „Fehlerbehandlung mit ? vereinfachen“Jede Dateioperation mit match auszuwerten, wird schnell eintönig. Wenn du dich in einer Funktion befindest, die selbst ein Result zurückgibt, räumt der ?-Operator (von der Seite Result) hier ordentlich auf:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // kehrt bei einem Fehler sofort zurück Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Fehler: {e}"), }}In eine Datei schreiben
Abschnitt betitelt „In eine Datei schreiben“use std::fs;
fn main() { let result = fs::write("output.txt", "Hallo, Datei!");
match result { Ok(()) => println!("Erfolgreich geschrieben"), Err(e) => println!("Schreiben fehlgeschlagen: {e}"), }}fs::write legt die Datei an, wenn es sie noch nicht gibt. Gibt es sie schon, wird sie komplett überschrieben. Denk daran, wenn du etwas an eine Datei anhängen willst, statt sie zu ersetzen.
An eine Datei anhängen
Abschnitt betitelt „An eine Datei anhängen“Um etwas an eine bestehende Datei anzuhängen, statt sie zu überschreiben, benutzt du 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, "Eine neue Zeile im Log")?; Ok(())}Hier gibt main selbst ein Result zurück. Dieses Muster solltest du kennen: Wenn main ein Result zurückgibt, kannst du ? direkt darin benutzen, statt alles in ein match zu packen.
Zeile für Zeile lesen
Abschnitt betitelt „Zeile für Zeile lesen“Bei einer großen Datei ist es nicht immer ideal, alles auf einmal in den Speicher zu laden. So liest du sie stattdessen Zeile für Zeile:
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(())}Prüfen, welcher Fehler passiert ist
Abschnitt betitelt „Prüfen, welcher Fehler passiert ist“Bisher wurde jeder Fehler gleich behandelt: ausgeben. Oft willst du aber unterschiedlich reagieren, je nachdem, was schiefgegangen ist. Eine fehlende Datei ist meist kein Problem (vielleicht ist es der erste Start, und du fängst einfach leer an). „Permission denied“ (Zugriff verweigert) dagegen ist ein echtes Problem, das du melden solltest.
Ein io::Error verrät dir mit .kind() seine Kategorie. Das gibt ein Enum ErrorKind (auf Englisch) zurück, auf das du match anwenden kannst:
use std::fs;use std::io::ErrorKind;
fn main() { // eine Datei im temporären Ordner des Systems, die es nicht gibt let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("{} Zeilen geladen", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Noch keine Notizen, fange leer an"), ErrorKind::PermissionDenied => println!("Keine Berechtigung, die Notizdatei zu lesen"), _ => println!("Etwas anderes ist schiefgegangen: {e}"), }, }}Ausgabe:
Noch keine Notizen, fange leer anstd::env::temp_dir() liefert dir den Pfad zum temporären Ordner des Systems (unter Linux etwa /tmp), und .join(...) hängt einen Dateinamen daran. Dateifunktionen nehmen so einen Pfad genauso gern wie einen &str.
Schreib ans Ende immer einen _-Zweig: ErrorKind hat viele Varianten, und mit der Zeit kommen neue dazu. Der _-Zweig fängt alles ab, was du nicht aufgeführt hast.
Übungen
Abschnitt betitelt „Übungen“Beide Übungen schreiben zuerst ihre eigene Datei in den temporären Ordner. Deshalb laufen sie überall, auch im Playground.
1. Speichern und Zeilen zählen
Abschnitt betitelt „1. Speichern und Zeilen zählen“Schreib mit fs::write drei To-dos in eine Datei, eins pro Zeile. Lies die Datei dann mit fs::read_to_string wieder ein und gib jede Zeile mit einer Nummer davor aus, danach die Gesamtzahl.
Tipp: Setz \n zwischen die Einträge, damit es getrennte Zeilen werden. text.lines() liefert dir die Zeilen einzeln. Lass main ein std::io::Result<()> zurückgeben, damit du ? benutzen kannst.
Lösung anzeigen
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "Ownership lernen\nein Modul schreiben\neine Datei lesen\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("{count} Einträge"); Ok(())}Ausgabe:
1. Ownership lernen2. ein Modul schreiben3. eine Datei lesen3 EinträgeWeil fs::write die ganze Datei ersetzt, bekommst du dieselben drei Zeilen, egal wie oft du das Programm startest. Jedes ? gibt den Fehler aus main zurück, falls Schreiben oder Lesen fehlschlägt.
2. An ein Log anhängen
Abschnitt betitelt „2. An ein Log anhängen“Beginne eine Log-Datei frisch mit einer Kopfzeile und hänge dann mit OpenOptions und writeln! zwei weitere Zeilen an. Lies zum Schluss die ganze Datei wieder ein und gib sie aus.
Tipp: Nimm fs::write für die Kopfzeile (damit jeder Durchlauf sauber beginnt) und dann OpenOptions::new().append(true).open(&path)? für die weiteren Zeilen. print! (nicht println!) vermeidet eine zusätzliche Leerzeile am Ende, weil die Datei schon mit einem Zeilenumbruch endet.
Lösung anzeigen
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")?; // frisch beginnen
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "gestartet")?; writeln!(file, "beendet")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Ausgabe:
=== Log ===gestartetbeendetuse std::fs::{self, OpenOptions}; holt sowohl das Modul fs selbst als auch OpenOptions herein. Ohne das erste fs::write würde jeder Durchlauf weiter an die alte Datei anhängen, und das Log würde endlos wachsen. Genau das willst du bei einem echten Log – aber nicht bei einer Übung mit fester Ausgabe.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“fs::read_to_string(path)liest eine ganze Datei in einenStringund gibt einResultzurück.fs::write(path, contents)schreibt eine Datei und überschreibt sie, falls es sie schon gibt.- Mit
OpenOptions::new().append(true)hängst du etwas an eine Datei an, statt sie zu ersetzen. - Mit
main() -> std::io::Result<()>kannst du?direkt inmainbenutzen, statt jeden Aufruf mitmatchauszuwerten. e.kind()sagt dir, welche Art von Fehler passiert ist, etwaErrorKind::NotFound. So kannst du jeden Fall anders behandeln.- Beispiele mit Dateien in deinem Projektordner probierst du am besten lokal aus.
std::env::temp_dir()liefert dir einen Ordner, der überall funktioniert.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was passiert, wenn du fs::write mit einem Pfad aufrufst, den es schon gibt?
2. Wie hängst du etwas an eine Datei an, statt sie zu überschreiben?
3. Was ermöglicht es dir, wenn main selbst std::io::Result<()> zurückgibt?
4. Was macht der ?-Operator, wenn fs::read_to_string(path)? fehlschlägt?
5. Warum liest man eine große Datei mit BufReader Zeile für Zeile statt mit fs::read_to_string?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“std::fs– Standardbibliothek (auf Englisch): alle Funktionen für Dateien und Ordner, etwacreate_dir_all,copy,renameundremove_file.- Eine Datei einlesen – Das Rust-Buch: Dateien lesen im Projekt
minigrepaus dem Buch, das diese Seite mit Kommandozeilenargumenten verbindet. - File I/O – Rust by Example (auf Englisch): ausführbare Beispiele zum Öffnen, Anlegen und zeilenweisen Lesen von Dateien.
OpenOptions– Standardbibliothek (auf Englisch): alle Arten, eine Datei zu öffnen (anhängen, anlegen, leeren und so weiter).io::ErrorKind– Standardbibliothek (auf Englisch): die vollständige Liste der Fehlerkategorien, auf die dumatchanwenden kannst.