Einführung
Eine nützliche Kommandozeilenanwendung verbindet mehrere Grenzen: typisierte Argumente, Domänendaten, dauerhafte Speicherung, eine klare Ausgabe, behandelbare Fehler und automatisierte Prüfungen. Wenn Sie all das in einer leeren Datei erstellen würden, würde die eigentliche Struktur in einer großen Menge Tipparbeit untergehen.
In diesem Lab stellt die Einrichtung ein vollständiges Grundgerüst für einen Task-Tracker bereit. Sie lesen zunächst die Modulstruktur und vervollständigen anschließend jeweils eine kleine Schnittstelle: Speicherdaten dekodieren und speichern, Tasks hinzufügen, Tasks auflisten, Tasks als erledigt markieren und einen getesteten Release-Build erstellen. Sie müssen niemals eine vollständige Quelldatei einfügen.
Projektstruktur und Befehlsschnittstelle lesen
In diesem Schritt verschaffen Sie sich einen Überblick über das vorbereitete Projekt und ordnen die Dateien dem Datenfluss zu, bevor Sie Code bearbeiten.
Wechseln Sie in das Projekt und listen Sie die Quelldateien auf:
cd /home/labex/project/tasker
ls src
lib.rs main.rs model.rs store.rs
Jede Datei hat eine zentrale Aufgabe:
main.rsbildet die Prozessgrenze: Argumente parsen, Erfolge ausgeben und Fehler melden;lib.rsenthält die Task-Operationen, die von der CLI und den Tests aufgerufen werden;model.rsdefiniert einenTaskund wandelt ihn in eine Speicherzeile um beziehungsweise liest ihn daraus ein;store.rsliest und schreibt die Task-Sammlung.
Durch diese Trennung bleiben das Parsen der Argumente, die Domänenoperationen und die Dateidetails voneinander getrennt, statt in einer großen Funktion zusammenzulaufen. Die Einrichtung hat die Moduldeklarationen und den längeren Parser für das Modell bereits angelegt. Sie vervollständigen nur die markierten Operationsschnittstellen.
Sehen Sie sich den CLI-Einstiegspunkt an:
nano src/main.rs
#[command(subcommand)] teilt clap mit, dass das nächste Befehlswort die Variante der Aufzählung Commands auswählt. Die Option --file ist mit global = true gekennzeichnet. Deshalb können Benutzer sie vor oder nach einem Unterbefehl platzieren. Der Wert vom Typ PathBuf verwendet standardmäßig tasks.db. Drücken Sie Ctrl+X, ohne die Datei zu ändern.
Zeigen Sie die automatisch erzeugte Hilfe auf oberster Ebene an:
cargo run --quiet -- --help
Die Hilfe führt add, list und done auf. Fordern Sie gezielt Hilfe für add an:
cargo run --quiet -- add --help
Das erforderliche <TITLE> stammt aus dem Feld title: String in der Variante Add. Die Schnittstelle ist bereits vorhanden. In den folgenden Schritten sorgen Sie dafür, dass die Bibliotheksoperation jedes Befehls funktioniert.
Die Speichergrenze vervollständigen
In diesem Schritt vervollständigen Sie die beiden kleinen Umwandlungen, die Task-Werte mit einer lokalen Textdatei verbinden.
Öffnen Sie das vorbereitete Speichermodul:
nano src/store.rs
Das Dateiformat verwendet eine Task pro Zeile mit drei durch Tabulatoren getrennten Feldern:
id<TAB>status<TAB>title
model.rs stellt bereits Task::encode und Task::decode bereit. Das Speichermodul muss diese Hilfsfunktionen lediglich auf die vollständige Sammlung anwenden.
Ersetzen Sie das TODO zum Laden und die beiden folgenden Zeilen durch:
let tasks = contents
.lines()
.filter(|line| !line.is_empty())
.map(Task::decode)
.collect::<Result<Vec<_>, _>>()?;
Ok(tasks)
Der Iterator wandelt jede nicht leere Zeile in ein Result<Task, String> um. Das Sammeln in Result<Vec<_>, _> bricht bei der ersten ungültigen Zeile ab oder liefert alle dekodierten Tasks zurück. Das Fragezeichen gibt diesen Fehler aus load weiter.
Ersetzen Sie in save das TODO und die letzten drei Zeilen durch:
fs::write(path, contents)
.map_err(|error| format!("could not write {}: {error}", path.display()))
fs::write erstellt die Speicherdatei oder ersetzt sie. map_err ergänzt den fehlerhaften Pfad und erhält dabei ein behandelbares Result.
Speichern Sie die Datei und beenden Sie nano. Führen Sie nur den gezielten Speichertest aus:
cargo test store::tests::saves_and_loads_tasks
Ein bestandener Test bestätigt, dass eine Task-Sammlung die Dateigrenze überqueren und als identische Rust-Werte zurückkehren kann.
Neue Tasks hinzufügen und speichern
In diesem Schritt implementieren Sie die Bibliotheksoperation hinter dem Unterbefehl add.
Öffnen Sie den Einstiegspunkt der Bibliothek:
nano src/lib.rs
Ersetzen Sie das TODO und den Platzhalterkörper von add_task durch:
let mut tasks = store::load(path)?;
let next_id = tasks.iter().map(|task| task.id).max().unwrap_or(0) + 1;
let task = Task::new(next_id, title);
tasks.push(task.clone());
store::save(path, &tasks)?;
Ok(task)
Die Operation lädt zunächst den aktuellen Zustand. max().unwrap_or(0) + 1 erzeugt für eine leere Datei die ID 1 und ansonsten eine ID, die um eins größer ist als die größte vorhandene ID. Der Task wird einmal geklont, weil eine eigene Kopie in den Vektor gelangt, während die zurückgegebene Kopie es der CLI ermöglicht, den hinzugefügten Task zu beschreiben.
Speichern Sie die Datei und beenden Sie nano. Fügen Sie zwei Tasks zu einer eigenen Demonstrationsdatei hinzu:
cargo run --quiet -- --file add-demo.db add "Write release notes"
Added 1: Write release notes
cargo run --quiet -- --file add-demo.db add "Tag version"
Added 2: Tag version
Die zweite ID zeigt, dass der Befehl den ersten Datensatz geladen hat, bevor er die nächste ID ausgewählt und gespeichert hat.
Die Task-Liste formatieren
In diesem Schritt wandeln Sie gespeicherte Tasks in eine stabile, für Menschen lesbare Befehlsausgabe um.
Öffnen Sie src/lib.rs erneut:
nano src/lib.rs
Ersetzen Sie das TODO und den Platzhalterkörper von list_tasks durch:
let tasks = store::load(path)?;
Ok(tasks
.iter()
.map(|task| {
let marker = if task.done { "x" } else { " " };
format!("[{marker}] {}: {}", task.id, task.title)
})
.collect())
Der Marker zeigt den Status kompakt an: [ ] bedeutet offen und [x] bedeutet erledigt. Diese Funktion gibt Anzeigezeilen zurück, statt sie auszugeben. Dadurch können Tests und andere Aufrufer das Ergebnis untersuchen, ohne die Terminalausgabe abfangen zu müssen.
Speichern Sie die Datei und beenden Sie nano. Verwenden Sie die im vorherigen Schritt erstellte Datei erneut:
cargo run --quiet -- --file add-demo.db list
[ ] 1: Write release notes
[ ] 2: Tag version
Die Bibliothek ist für die Formatierung der Task-Zeilen zuständig. main.rs muss weiterhin nur die zurückgegebenen Zeilen ausgeben.
Einen Task als erledigt markieren
In diesem Schritt aktualisieren Sie einen Task und bewahren dabei die übrige gespeicherte Sammlung.
Öffnen Sie den Quellcode der Bibliothek:
nano src/lib.rs
Ersetzen Sie das TODO und den Platzhalterkörper von complete_task durch:
let mut tasks = store::load(path)?;
let task = tasks
.iter_mut()
.find(|task| task.id == id)
.ok_or_else(|| format!("task {id} was not found"))?;
task.done = true;
let completed = task.clone();
store::save(path, &tasks)?;
Ok(completed)
iter_mut() stellt veränderbare Referenzen bereit, damit der passende Datensatz direkt geändert werden kann. Wenn die ID nicht vorhanden ist, liefert find den Wert None. ok_or_else wandelt dieses Fehlen in den erklärenden Fehler der Funktion um. Der erledigte Task wird vor dem Speichern geklont, weil die veränderbare Ausleihe zum zu speichernden Vektor gehört.
Speichern Sie die Datei und beenden Sie nano. Markieren Sie Task 1 in der Demonstrationsdatei als erledigt:
cargo run --quiet -- --file add-demo.db done 1
Completed 1: Write release notes
Listen Sie den gespeicherten Zustand erneut auf:
cargo run --quiet -- --file add-demo.db list
[x] 1: Write release notes
[ ] 2: Tag version
Probieren Sie eine nicht vorhandene ID aus:
cargo run --quiet -- --file add-demo.db done 99
Dieser Befehl soll fehlschlagen. Die Meldung wird nach stderr geschrieben und der Prozess wird mit einem Status ungleich null beendet, weil main.rs den Bibliotheksfehler bereits an der Prozessgrenze umwandelt.
Testen und den Release-Build erstellen
In diesem Schritt führen Sie die Qualitätsprüfung für die Übergabe durch und erstellen aus dem fertigen Projekt eine ausführbare Release-Datei.
Formatieren Sie zunächst die Änderungen, die Sie in den Speicher- und Bibliotheksmodulen vorgenommen haben:
cargo fmt
Stellen Sie sicher, dass der Formatter keine weiteren Änderungen meldet:
cargo fmt -- --check
Führen Sie Clippy mit strengen Prüfungen aus:
cargo clippy -- -D warnings
Führen Sie die vollständige Testsuite aus:
cargo test
Es sollten zwei Tests bestehen: der gezielte Speicherrundlauf und der vollständige Bibliotheksworkflow für Hinzufügen, Auflisten und Erledigen. Diese Tests verwenden temporäre Dateien. Sie prüfen daher die tatsächliche Persistenz, ohne von Ihrer Demonstrationsdatenbank abzuhängen.
Erstellen Sie das optimierte Release-Ziel:
cargo build --release --locked
--release wählt Cargos optimiertes Release-Profil anstelle des schneller zu kompilierenden Entwicklungsprofils. --locked verlangt exakt den in Cargo.lock aufgezeichneten Abhängigkeitsgraphen. Führen Sie die entstandene ausführbare Datei direkt aus:
./target/release/tasker --file release-demo.db add "Publish tasker"
Added 1: Publish tasker
./target/release/tasker --file release-demo.db list
[ ] 1: Publish tasker
Der direkte Pfad bestätigt, dass Sie das erstellte Artefakt ausführen, statt Cargo zum Kompilieren und Starten aufzufordern.
Zusammenfassung
Sie haben eine Rust-CLI mit mehreren Befehlen vervollständigt, ohne ihre Architektur erneut eingeben zu müssen. Das fertige Projekt parst typisierte Unterbefehle mit clap, speichert Task-Modelle über ein fokussiertes Speichermodul, gibt Fehler mit Kontext weiter, belässt die Prozessausgabe in main, besteht gezielte sowie durchgängige Bibliothekstests, erfüllt die Qualitätsschritte und erzeugt eine gesperrte ausführbare Release-Datei.


