Projekt: To-do-Liste in Rust
In diesem Projekt baust du eine kleine To-do-Liste, die in deinem Terminal lebt. Du tippst todo add Milch kaufen, und sie merkt es sich. Du schließt das Terminal, kommst morgen wieder, tippst todo list – und deine Aufgaben sind noch da.
Das ist ein tolles zweites Projekt, weil es sich wie ein echtes Werkzeug anfühlt. Du brauchst dafür Structs, Enums, Vektoren, Kommandozeilenargumente, Dateien lesen und schreiben und Result mit ?. Wir bauen jedes Teil einzeln, testen es im Playground und setzen am Ende alles zusammen.
Was du baust
Abschnitt betitelt „Was du baust“So sieht es aus, wenn du das fertige Werkzeug benutzt:
$ cargo run -- add Milch kaufenAufgabe 1 hinzugefügt.$ cargo run -- add Oma anrufenAufgabe 2 hinzugefügt.$ cargo run -- done 1Aufgabe 1 als erledigt markiert.$ cargo run -- list[x] 1: Milch kaufen[ ] 2: Oma anrufen$ cargo run -- remove 1Aufgabe 1 entfernt: Milch kaufenAlles nach -- geht an dein Programm, nicht an Cargo. Die Befehlsnamen (add, list, done und remove) lassen wir auf Englisch, wie bei den meisten Kommandozeilen-Tools. Wenn du lieber neu oder liste tippen willst, änderst du sie einfach in parse_command (Schritt 2).
Die Aufgaben werden in einer Datei namens tasks.txt gespeichert, eine Aufgabe pro Zeile:
2|0|Oma anrufenJede Zeile hat die Form id|done|text. Dabei steht done auf 1, wenn die Aufgabe erledigt ist, und auf 0, wenn nicht.
Leg ein neues Projekt an:
cargo new todocd todoSchritt 1: Das Struct Task
Abschnitt betitelt „Schritt 1: Das Struct Task“Jede Aufgabe hat eine Nummer (damit du sie ansprechen kannst), eine Markierung für „erledigt“ und einen Text. Ein Struct passt dafür perfekt. Wir geben ihm außerdem zwei Methoden: Die eine macht aus einer Aufgabe eine Textzeile für die Datei, die andere liest eine Aufgabe aus so einer Zeile wieder ein.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { // Macht aus einer Aufgabe eine Zeile, z. B. "1|0|Milch kaufen". fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
// Liest eine Aufgabe aus einer Zeile. Gibt None zurück, wenn die Zeile kaputt ist. fn from_line(line: &str) -> Option<Task> { let mut parts = line.splitn(3, '|'); let id = parts.next()?.parse().ok()?; let done = parts.next()? == "1"; let text = parts.next()?.to_string(); Some(Task { id, done, text }) }}
fn main() { let task = Task { id: 1, done: false, text: String::from("Milch kaufen") }; let line = task.to_line(); println!("{line}");
println!("{:?}", Task::from_line("2|1|Oma anrufen")); println!("{:?}", Task::from_line("3|0|Über A|B-Tests lesen")); println!("{:?}", Task::from_line("diese Zeile ist kaputt"));}Ausgabe:
1|0|Milch kaufenSome(Task { id: 2, done: true, text: "Oma anrufen" })Some(Task { id: 3, done: false, text: "Über A|B-Tests lesen" })NoneZwei Details lohnen einen genaueren Blick:
splitn(3, '|')teilt in höchstens 3 Stücke. Die ersten beiden|trennen die Felder, und alles danach ist der Text – auch wenn darin selbst ein|vorkommt. Mit einem einfachensplit('|')würde die Aufgabe „Über A|B-Tests lesen“ die Hälfte ihres Textes verlieren.from_linegibt eineOptionzurück und verwendet bei jedem Schritt?. Fehlt ein Teil oder ist die Id keine Zahl, gibt die ganze FunktionNonezurück. Das.ok()macht aus demResultvonparse()eineOption, damit?damit arbeiten kann.
Schritt 2: Das Enum Command
Abschnitt betitelt „Schritt 2: Das Enum Command“Das Werkzeug versteht vier Befehle. Jeder ist eine Variante eines Enums, und manche Varianten tragen Daten mit sich: Add braucht den Text, Done und Remove brauchen eine Aufgabennummer.
#[derive(Debug)]enum Command { Add(String), List, Done(u32), Remove(u32),}
fn parse_id(arg: Option<&String>) -> Result<u32, String> { let arg = arg.ok_or(String::from("bitte gib eine Aufgabennummer an"))?; arg.parse().map_err(|_| format!("'{arg}' ist keine Aufgabennummer"))}
fn parse_command(args: &[String]) -> Result<Command, String> { match args.first().map(|s| s.as_str()) { None | Some("list") => Ok(Command::List), Some("add") => { let text = args[1..].join(" "); if text.is_empty() { Err(String::from("Verwendung: todo add <Aufgabentext>")) } else { Ok(Command::Add(text)) } } Some("done") => parse_id(args.get(1)).map(Command::Done), Some("remove") => parse_id(args.get(1)).map(Command::Remove), Some(other) => Err(format!("unbekannter Befehl '{other}'")), }}
fn main() { let examples = [ vec!["add", "Milch", "kaufen"], vec!["list"], vec![], vec!["done", "2"], vec!["done", "zwei"], vec!["remove"], vec!["tanzen"], ];
for words in examples { let args: Vec<String> = words.iter().map(|w| w.to_string()).collect(); println!("{:?} -> {:?}", words, parse_command(&args)); }}Ausgabe:
["add", "Milch", "kaufen"] -> Ok(Add("Milch kaufen"))["list"] -> Ok(List)[] -> Ok(List)["done", "2"] -> Ok(Done(2))["done", "zwei"] -> Err("'zwei' ist keine Aufgabennummer")["remove"] -> Err("bitte gib eine Aufgabennummer an")["tanzen"] -> Err("unbekannter Befehl 'tanzen'")Im echten Programm kommt args von der Kommandozeile. Wie du auf der Seite Kommandozeilenargumente gesehen hast, ist das erste Element von std::env::args() der Name des Programms selbst. Deshalb überspringen wir es:
let args: Vec<String> = std::env::args().skip(1).collect();Warum verwendet add den Ausdruck args[1..].join(" ")? Deine Shell zerlegt todo add Milch kaufen in einzelne Wörter: add, Milch, kaufen. Wenn wir alles nach add wieder zusammenfügen, ist der Aufgabentext wieder komplett – du brauchst also keine Anführungszeichen.
Beachte: parse_command stürzt nie ab. Falsche Eingaben werden zu einem Err mit einer hilfreichen Meldung. Was damit passiert, entscheidet der Aufrufer.
Schritt 3: Mit der Aufgabenliste arbeiten
Abschnitt betitelt „Schritt 3: Mit der Aufgabenliste arbeiten“Alle Aufgaben liegen in einem Vec<Task>. Jeder Befehl ist eine kleine Funktion, die den Vektor verändert:
- Add: die größte bisherige Id suchen, 1 dazuzählen und eine neue Aufgabe anhängen.
- Done: die Aufgabe mit dieser Id suchen und
done = truesetzen. - Remove: die Position der Aufgabe suchen und sie entfernen.
- List: alle Aufgaben ausgeben.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
fn add_task(tasks: &mut Vec<Task>, text: String) -> u32 { let id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; tasks.push(Task { id, done: false, text }); id}
fn mark_done(tasks: &mut [Task], id: u32) -> Result<(), String> { match tasks.iter_mut().find(|t| t.id == id) { Some(task) => { task.done = true; Ok(()) } None => Err(format!("es gibt keine Aufgabe {id}")), }}
fn remove_task(tasks: &mut Vec<Task>, id: u32) -> Result<Task, String> { match tasks.iter().position(|t| t.id == id) { Some(index) => Ok(tasks.remove(index)), None => Err(format!("es gibt keine Aufgabe {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Nichts zu tun. Genieß deinen Tag!"); return; } for task in tasks { let mark = if task.done { "x" } else { " " }; println!("[{mark}] {}: {}", task.id, task.text); }}
fn main() { let mut tasks = Vec::new(); add_task(&mut tasks, String::from("Milch kaufen")); add_task(&mut tasks, String::from("Oma anrufen")); add_task(&mut tasks, String::from("Rust lernen"));
mark_done(&mut tasks, 3).unwrap(); let removed = remove_task(&mut tasks, 1).unwrap(); println!("Entfernt: {}", removed.text);
if let Err(message) = mark_done(&mut tasks, 99) { println!("Fehler: {message}"); }
print_tasks(&tasks);}Ausgabe:
Entfernt: Milch kaufenFehler: es gibt keine Aufgabe 99[ ] 2: Oma anrufen[x] 3: Rust lernenEin paar Entscheidungen sind hier ganz bewusst getroffen:
- Die Id kommt von der größten vorhandenen Id, nicht von
tasks.len() + 1. Wenn du Aufgabe 1 entfernst und dann eine neue hinzufügst, bekämst du mit der Länge eine doppelte Id. mark_donenimmt&mut [Task](einen Slice), weil es nur Elemente verändert.add_taskundremove_tasknehmen&mut Vec<Task>, weil sie die Länge ändern.- Fehler sind
Result<_, String>. EinStringals Fehler ist das Einfachste, was funktioniert. Für größere Programme schau dir Eigene Fehlertypen an.
iter_mut().find(...) liefert dir eine veränderbare Referenz auf die passende Aufgabe. Deshalb ändert task.done = true die Aufgabe direkt im Vektor. Mehr zu find und position findest du bei den Iteratoren.
Schritt 4: Die Datei speichern und laden
Abschnitt betitelt „Schritt 4: Die Datei speichern und laden“Jetzt sollen die Aufgaben das Programmende überleben. Wir speichern sie mit std::fs::write und laden sie mit std::fs::read_to_string. Beides kann schiefgehen, also geben beide ein Result zurück. Der ?-Operator reicht jeden Fehler an den Aufrufer weiter:
use std::fs;use std::io;
fn save_tasks(path: &str, tasks: &[Task]) -> Result<(), io::Error> { let mut text = String::new(); for task in tasks { text.push_str(&task.to_line()); text.push('\n'); } fs::write(path, text)?; Ok(())}
fn load_tasks(path: &str) -> Result<Vec<Task>, io::Error> { let text = match fs::read_to_string(path) { Ok(text) => text, // Beim ersten Start gibt es noch keine Datei. // Das ist kein Fehler: Es heißt nur "keine Aufgaben". Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), Err(e) => return Err(e), }; let tasks = text.lines().filter_map(Task::from_line).collect(); Ok(tasks)}Der Fall NotFound ist wichtiger, als er aussieht. Ohne ihn würde schon das allererste todo add mit „No such file or directory“ scheitern, weil du ja noch nichts gespeichert hast. Diese Meldung kommt vom Betriebssystem und erscheint deshalb meist auf Englisch. Alle anderen Fehler (zum Beispiel „permission denied“, also fehlende Rechte) werden weiterhin nach oben gereicht.
filter_map(Task::from_line) liest jede Zeile und behält nur die, die sich einlesen lassen. Sei dir bewusst, dass kaputte Zeilen dabei still übersprungen werden. Das ist freundlich, wenn jemand die Datei von Hand bearbeitet und sich vertippt. Es heißt aber auch, dass ein Fehler unbemerkt bleiben kann. Eine der Erweiterungsideen unten ist, stattdessen eine Warnung auszugeben.
Mehr zum Lesen und Schreiben von Dateien findest du unter Dateien lesen und schreiben.
Schritt 5: Das komplette Programm
Abschnitt betitelt „Schritt 5: Das komplette Programm“Hier ist alles zusammen. Pack es in src/main.rs und starte es mit cargo run -- add Milch kaufen, cargo run -- list und so weiter.
Die eigentliche Logik steckt in run_command. Diese Funktion arbeitet mit einem Vec<Task> und weiß nichts von Dateien. main lädt nur die Datei, ruft run_command auf und speichert. Diese Trennung macht die Logik leicht testbar – und genau dadurch können wir im nächsten Schritt eine Version für den Playground bauen.
use std::env;use std::error::Error;use std::fs;use std::io;use std::process;
const FILE: &str = "tasks.txt";
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
fn from_line(line: &str) -> Option<Task> { let mut parts = line.splitn(3, '|'); let id = parts.next()?.parse().ok()?; let done = parts.next()? == "1"; let text = parts.next()?.to_string(); Some(Task { id, done, text }) }}
#[derive(Debug)]enum Command { Add(String), List, Done(u32), Remove(u32),}
fn parse_id(arg: Option<&String>) -> Result<u32, String> { let arg = arg.ok_or(String::from("bitte gib eine Aufgabennummer an"))?; arg.parse().map_err(|_| format!("'{arg}' ist keine Aufgabennummer"))}
fn parse_command(args: &[String]) -> Result<Command, String> { match args.first().map(|s| s.as_str()) { None | Some("list") => Ok(Command::List), Some("add") => { let text = args[1..].join(" "); if text.is_empty() { Err(String::from("Verwendung: todo add <Aufgabentext>")) } else { Ok(Command::Add(text)) } } Some("done") => parse_id(args.get(1)).map(Command::Done), Some("remove") => parse_id(args.get(1)).map(Command::Remove), Some(other) => Err(format!("unbekannter Befehl '{other}'")), }}
fn add_task(tasks: &mut Vec<Task>, text: String) -> u32 { let id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; tasks.push(Task { id, done: false, text }); id}
fn mark_done(tasks: &mut [Task], id: u32) -> Result<(), String> { match tasks.iter_mut().find(|t| t.id == id) { Some(task) => { task.done = true; Ok(()) } None => Err(format!("es gibt keine Aufgabe {id}")), }}
fn remove_task(tasks: &mut Vec<Task>, id: u32) -> Result<Task, String> { match tasks.iter().position(|t| t.id == id) { Some(index) => Ok(tasks.remove(index)), None => Err(format!("es gibt keine Aufgabe {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Nichts zu tun. Genieß deinen Tag!"); return; } for task in tasks { let mark = if task.done { "x" } else { " " }; println!("[{mark}] {}: {}", task.id, task.text); }}
fn run_command(args: &[String], tasks: &mut Vec<Task>) -> Result<(), String> { match parse_command(args)? { Command::Add(text) => { let id = add_task(tasks, text); println!("Aufgabe {id} hinzugefügt."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Aufgabe {id} als erledigt markiert."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Aufgabe {id} entfernt: {}", task.text); } } Ok(())}
fn save_tasks(path: &str, tasks: &[Task]) -> Result<(), io::Error> { let mut text = String::new(); for task in tasks { text.push_str(&task.to_line()); text.push('\n'); } fs::write(path, text)?; Ok(())}
fn load_tasks(path: &str) -> Result<Vec<Task>, io::Error> { let text = match fs::read_to_string(path) { Ok(text) => text, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), Err(e) => return Err(e), }; let tasks = text.lines().filter_map(Task::from_line).collect(); Ok(tasks)}
fn run() -> Result<(), Box<dyn Error>> { let args: Vec<String> = env::args().skip(1).collect(); let mut tasks = load_tasks(FILE)?; run_command(&args, &mut tasks)?; save_tasks(FILE, &tasks)?; Ok(())}
fn main() { if let Err(e) = run() { eprintln!("Fehler: {e}"); process::exit(1); }}Ein Wort zu run() -> Result<(), Box<dyn Error>>: Unsere Funktionen geben zwei verschiedene Fehlertypen zurück – io::Error bei den Dateifunktionen und String bei den Befehlsfunktionen. Box<dyn Error> ist eine Box, die jeden Fehlertyp aufnehmen kann. So funktioniert ? mit beiden. main gibt den Fehler dann mit eprintln! aus (das schreibt in die Fehlerausgabe) und beendet das Programm mit dem Code 1. Das ist das übliche Signal dafür, dass etwas schiefgegangen ist.
Schritt 6: Eine Version für den Playground
Abschnitt betitelt „Schritt 6: Eine Version für den Playground“Der Playground kann keine Kommandozeilenargumente übergeben, und wir wollen auch nicht von Dateien abhängen. Deshalb nutzt diese Version dasselbe run_command, füttert es aber mit einer festen Liste von Befehlen. Am Ende gibt sie aus, was in tasks.txt stehen würde:
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }}
#[derive(Debug)]enum Command { Add(String), List, Done(u32), Remove(u32),}
fn parse_id(arg: Option<&String>) -> Result<u32, String> { let arg = arg.ok_or(String::from("bitte gib eine Aufgabennummer an"))?; arg.parse().map_err(|_| format!("'{arg}' ist keine Aufgabennummer"))}
fn parse_command(args: &[String]) -> Result<Command, String> { match args.first().map(|s| s.as_str()) { None | Some("list") => Ok(Command::List), Some("add") => { let text = args[1..].join(" "); if text.is_empty() { Err(String::from("Verwendung: todo add <Aufgabentext>")) } else { Ok(Command::Add(text)) } } Some("done") => parse_id(args.get(1)).map(Command::Done), Some("remove") => parse_id(args.get(1)).map(Command::Remove), Some(other) => Err(format!("unbekannter Befehl '{other}'")), }}
fn add_task(tasks: &mut Vec<Task>, text: String) -> u32 { let id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; tasks.push(Task { id, done: false, text }); id}
fn mark_done(tasks: &mut [Task], id: u32) -> Result<(), String> { match tasks.iter_mut().find(|t| t.id == id) { Some(task) => { task.done = true; Ok(()) } None => Err(format!("es gibt keine Aufgabe {id}")), }}
fn remove_task(tasks: &mut Vec<Task>, id: u32) -> Result<Task, String> { match tasks.iter().position(|t| t.id == id) { Some(index) => Ok(tasks.remove(index)), None => Err(format!("es gibt keine Aufgabe {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Nichts zu tun. Genieß deinen Tag!"); return; } for task in tasks { let mark = if task.done { "x" } else { " " }; println!("[{mark}] {}: {}", task.id, task.text); }}
fn run_command(args: &[String], tasks: &mut Vec<Task>) -> Result<(), String> { match parse_command(args)? { Command::Add(text) => { let id = add_task(tasks, text); println!("Aufgabe {id} hinzugefügt."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Aufgabe {id} als erledigt markiert."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Aufgabe {id} entfernt: {}", task.text); } } Ok(())}
fn main() { // Jede Zeile ist das, was du nach `todo` tippen würdest. let script = [ "list", "add Milch kaufen", "add Oma anrufen", "add Rust lernen", "done 3", "remove 1", "done 9", "springen", "list", ];
let mut tasks: Vec<Task> = Vec::new();
for line in script { println!("$ todo {line}"); let args: Vec<String> = line.split_whitespace().map(String::from).collect(); if let Err(message) = run_command(&args, &mut tasks) { println!("Fehler: {message}"); } }
println!(); println!("In tasks.txt würde stehen:"); for task in &tasks { println!("{}", task.to_line()); }}Ausgabe:
$ todo listNichts zu tun. Genieß deinen Tag!$ todo add Milch kaufenAufgabe 1 hinzugefügt.$ todo add Oma anrufenAufgabe 2 hinzugefügt.$ todo add Rust lernenAufgabe 3 hinzugefügt.$ todo done 3Aufgabe 3 als erledigt markiert.$ todo remove 1Aufgabe 1 entfernt: Milch kaufen$ todo done 9Fehler: es gibt keine Aufgabe 9$ todo springenFehler: unbekannter Befehl 'springen'$ todo list[ ] 2: Oma anrufen[x] 3: Rust lernen
In tasks.txt würde stehen:2|0|Oma anrufen3|1|Rust lernenÄndere das Skript und führ es noch einmal aus. Füge zum Beispiel eine Aufgabe hinzu, nachdem du eine entfernt hast, und prüfe, dass die neue Id kein Duplikat ist.
Upgrade: als JSON speichern mit serde
Abschnitt betitelt „Upgrade: als JSON speichern mit serde“Unser Format id|done|text ist gut lesbar, aber to_line und from_line musstest du von Hand schreiben. Wenn deine Aufgaben mehr Felder bekommen (Fälligkeitsdatum, Priorität, Tags), wächst dieser handgeschriebene Code mit.
In echten Projekten überlässt man diese Arbeit meist den serde-Crates (auf Englisch). Füge sie so hinzu:
cargo add serde --features derivecargo add serde_jsonDann bekommst du mit einer einzigen derive-Zeile Speichern und Laden geschenkt:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]struct Task { id: u32, done: bool, text: String,}
fn main() { let tasks = vec![ Task { id: 1, done: false, text: String::from("Milch kaufen") }, Task { id: 2, done: true, text: String::from("Oma anrufen") }, ];
let json = serde_json::to_string(&tasks).unwrap(); println!("{json}");
let loaded: Vec<Task> = serde_json::from_str(&json).unwrap(); println!("{} Aufgaben wieder geladen", loaded.len());}Ausgabe:
[{"id":1,"done":false,"text":"Milch kaufen"},{"id":2,"done":true,"text":"Oma anrufen"}]2 Aufgaben wieder geladenDiesen JSON-String würdest du genau wie vorher mit fs::write in tasks.json schreiben. Die Seite JSON mit Serde erklärt das ausführlich.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“? in einem main verwenden, das nichts zurückgibt. Es ist verlockend, so etwas zu schreiben:
use std::fs;
fn main() { let text = fs::read_to_string("tasks.txt")?; println!("{text}");}error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:47 |3 | fn main() { | --------- this function should return `Result` or `Option` to accept `?`4 | let text = fs::read_to_string("tasks.txt")?; | ^ cannot use the `?` operator in a function that returns `()`Der Compiler sagt dir: Den Operator ? darfst du nur in einer Funktion verwenden, die Result oder Option zurückgibt. ? bedeutet „Wenn das hier ein Fehler ist, gib ihn aus der aktuellen Funktion zurück“. Ein einfaches main gibt () zurück, also gibt es keinen Ort, an den der Fehler gehen könnte. Entweder gibst du main einen Rückgabetyp wie Result<(), Box<dyn Error>>, oder du machst es wie unser Programm: Du steckst die Arbeit in eine Funktion run(), die Result zurückgibt, und behandelst den Fehler einmal in main.
Eine fehlende Datei als Fehlschlag behandeln. Würde load_tasks einfach fs::read_to_string(path)? aufrufen, scheiterte das Programm schon beim allerersten Start, wenn noch gar keine Datei existiert. Prüfe auf io::ErrorKind::NotFound und starte in dem Fall mit einer leeren Liste.
Die Länge der Liste als nächste Id verwenden. Bei den Aufgaben 1, 2 und 3 bleiben nach dem Entfernen von Aufgabe 1 noch zwei übrig. len() + 1 ergäbe wieder 3 – und schon teilen sich zwei Aufgaben eine Id. Nimm stattdessen die größte Id plus eins.
Ideen zum Erweitern
Abschnitt betitelt „Ideen zum Erweitern“- „Erledigt“ rückgängig machen. Füge einen Befehl
undone <id>hinzu, derdonewieder auffalsesetzt. - Eine Aufgabe bearbeiten.
edit 2 Oma heute Abend anrufenersetzt den Text von Aufgabe 2. - Erledigte Aufgaben aufräumen.
clearentfernt alle erledigten Aufgaben. Schau dir dafürVec::retainan (auf Englisch). - Vor kaputten Zeilen warnen. Statt fehlerhafte Zeilen still zu überspringen, gib eine Warnung mit der Zeilennummer aus.
- Ein Befehl
help, der alle Befehle und ihre Verwendung ausgibt. - Tests schreiben.
parse_commandundrun_commandfassen keine Dateien an, deshalb kannst du sie leicht testen. - Einen richtigen Argument-Parser verwenden. Das Crate
clap(auf Englisch), das auf der Seite Kommandozeilenargumente erwähnt wird, liefert dir--helpund Unterbefehle frei Haus.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein Struct
Taskhält die Daten.to_lineundfrom_linemachen daraus Text und wieder zurück. - Ein Enum
Commandbeschreibt, was der Benutzer will.parse_commandmacht aus den Argumenten einCommand– oder einErrmit einer hilfreichen Meldung. - Ein
Vec<Task>hält die Liste. Kleine Funktionen fügen Aufgaben hinzu, finden, ändern und entfernen sie. fs::read_to_stringundfs::writegeben einResultzurück, und?reicht Fehler weiter. Eine fehlende Datei beim ersten Start wird als „keine Aufgaben“ behandelt.- Wenn du die Logik (
run_command) von Dateien und Argumenten trennst, kannst du sie leicht testen und im Playground ausführen.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Warum verwendet from_line den Aufruf splitn(3, '|') statt split('|')?
2. Was macht load_tasks, wenn tasks.txt noch nicht existiert?
3. Warum kannst du ? nicht direkt in einem fn main() ohne Rückgabetyp verwenden?
4. Welches Crate ist das übliche Upgrade, um Structs als JSON zu speichern?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Ein E/A-Projekt: Ein Kommandozeilenprogramm erstellen – Das Rust-Buch: ein größeres angeleitetes CLI-Projekt, mit guten Tipps, wie du
mainin kleinere, testbare Teile aufteilst. std::fs– Doku der Standardbibliothek (auf Englisch):read_to_string,writeund die anderen Dateifunktionen hinter dem Speichern und Laden.std::env::args– Doku der Standardbibliothek (auf Englisch): wie Kommandozeilenargumente ankommen und warum das erste der Name des Programms selbst ist.str::splitn– Doku der Standardbibliothek (auf Englisch): eine Zeile in höchstens N Teile zerlegen. Mit diesem Trick bleibt ein Aufgabentext, der|enthält, in einem Stück.serde_json– docs.rs (auf Englisch): das JSON-Crate aus dem Upgrade-Abschnitt, mit Beispielen fürto_stringundfrom_str. Der serde-Leitfaden (auf Englisch) erklärt die Derive-Attribute.