Projet : liste de tâches en Rust
Dans ce projet, vous allez construire une petite liste de tâches qui vit dans votre terminal. Vous tapez todo add Acheter du lait, et elle s’en souvient. Vous fermez le terminal, vous revenez le lendemain, vous tapez todo list, et vos tâches sont toujours là.
C’est un excellent deuxième projet, parce qu’il ressemble à un vrai outil. Il utilise des structs (structures), des enums (énumérations), des vecteurs, les arguments de ligne de commande, la lecture et l’écriture de fichiers et Result avec ?. Nous allons construire chaque morceau séparément, le tester dans le Playground, puis tout assembler.
Ce que vous allez construire
Section intitulée « Ce que vous allez construire »Voici à quoi ressemble l’outil terminé quand on l’utilise :
$ cargo run -- add Acheter du laitTâche 1 ajoutée.$ cargo run -- add Appeler mamieTâche 2 ajoutée.$ cargo run -- done 1Tâche 1 marquée comme terminée.$ cargo run -- list[x] 1: Acheter du lait[ ] 2: Appeler mamie$ cargo run -- remove 1Tâche 1 supprimée : Acheter du laitTout ce qui suit -- est transmis à votre programme, pas à Cargo. Les noms des commandes (add, list, done et remove) restent en anglais, comme dans la plupart des outils en ligne de commande. Si vous préférez ajouter ou lister, il suffit de les changer dans parse_command (étape 2).
Les tâches sont enregistrées dans un fichier appelé tasks.txt, une tâche par ligne :
2|0|Appeler mamieChaque ligne a la forme id|done|text, où done vaut 1 si la tâche est terminée et 0 sinon.
Créez un nouveau projet avec :
cargo new todocd todoÉtape 1 : la struct Task
Section intitulée « Étape 1 : la struct Task »Chaque tâche a un numéro (pour pouvoir y faire référence), un indicateur « terminée » et un texte. Une struct convient parfaitement. Nous lui ajoutons aussi deux méthodes : l’une transforme une tâche en ligne de texte pour le fichier, l’autre relit une tâche à partir d’une ligne.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { // Transforme une tâche en une ligne, par exemple "1|0|Acheter du lait". fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
// Relit une tâche à partir d'une ligne. Renvoie None si la ligne est abîmée. 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("Acheter du lait") }; let line = task.to_line(); println!("{line}");
println!("{:?}", Task::from_line("2|1|Appeler mamie")); println!("{:?}", Task::from_line("3|0|Se renseigner sur les tests A|B")); println!("{:?}", Task::from_line("cette ligne est abîmée"));}Sortie :
1|0|Acheter du laitSome(Task { id: 2, done: true, text: "Appeler mamie" })Some(Task { id: 3, done: false, text: "Se renseigner sur les tests A|B" })NoneDeux détails méritent qu’on s’y arrête :
splitn(3, '|')découpe en au plus 3 morceaux. Les deux premiers|séparent les champs, et tout ce qui suit est le texte, même si ce texte contient lui-même un|. Avec un simplesplit('|'), la tâche « Se renseigner sur les tests A|B » perdrait une partie de son texte.from_linerenvoie uneOptionet utilise?à chaque étape. S’il manque un morceau ou si l’id n’est pas un nombre, toute la fonction renvoieNone. Le.ok()transforme leResultdeparse()enOptionpour que?puisse s’appliquer.
Étape 2 : l’enum Command
Section intitulée « Étape 2 : l’enum Command »L’outil comprend quatre commandes. Chacune est une variante d’un enum, et certaines variantes transportent des données : Add a besoin du texte, Done et Remove ont besoin d’un numéro de tâche.
#[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("veuillez indiquer un numéro de tâche"))?; arg.parse().map_err(|_| format!("'{arg}' n'est pas un numéro de tâche"))}
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("utilisation : todo add <texte de la tâche>")) } 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!("commande inconnue '{other}'")), }}
fn main() { let examples = [ vec!["add", "Acheter", "du", "lait"], vec!["list"], vec![], vec!["done", "2"], vec!["done", "deux"], vec!["remove"], vec!["danser"], ];
for words in examples { let args: Vec<String> = words.iter().map(|w| w.to_string()).collect(); println!("{:?} -> {:?}", words, parse_command(&args)); }}Sortie :
["add", "Acheter", "du", "lait"] -> Ok(Add("Acheter du lait"))["list"] -> Ok(List)[] -> Ok(List)["done", "2"] -> Ok(Done(2))["done", "deux"] -> Err("'deux' n'est pas un numéro de tâche")["remove"] -> Err("veuillez indiquer un numéro de tâche")["danser"] -> Err("commande inconnue 'danser'")Dans le vrai programme, args vient de la ligne de commande. Comme vous l’avez vu sur la page Arguments de ligne de commande, le premier élément de std::env::args() est le nom du programme lui-même, donc on le saute :
let args: Vec<String> = std::env::args().skip(1).collect();Pourquoi add utilise-t-il args[1..].join(" ") ? Votre shell découpe todo add Acheter du lait en mots séparés : add, Acheter, du, lait. En rassemblant tout ce qui suit add, on reconstitue le texte de la tâche, et vous n’avez pas besoin de guillemets.
Remarquez que parse_command ne plante jamais. Une saisie incorrecte devient un Err avec un message utile. C’est l’appelant qui décide quoi en faire.
Étape 3 : manipuler la liste des tâches
Section intitulée « Étape 3 : manipuler la liste des tâches »Toutes les tâches vivent dans un Vec<Task>. Chaque commande est une petite fonction qui modifie le vecteur :
- Add : trouver le plus grand id existant, ajouter 1, et pousser une nouvelle tâche.
- Done : trouver la tâche qui a cet id et mettre
done = true. - Remove : trouver la position de la tâche et la retirer.
- List : afficher toutes les tâches.
#[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!("la tâche {id} n'existe pas")), }}
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!("la tâche {id} n'existe pas")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Rien à faire. Profitez de votre journée !"); 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("Acheter du lait")); add_task(&mut tasks, String::from("Appeler mamie")); add_task(&mut tasks, String::from("Apprendre Rust"));
mark_done(&mut tasks, 3).unwrap(); let removed = remove_task(&mut tasks, 1).unwrap(); println!("Supprimée : {}", removed.text);
if let Err(message) = mark_done(&mut tasks, 99) { println!("Erreur : {message}"); }
print_tasks(&tasks);}Sortie :
Supprimée : Acheter du laitErreur : la tâche 99 n'existe pas[ ] 2: Appeler mamie[x] 3: Apprendre RustCertains choix sont faits exprès :
- Les ids viennent du plus grand id existant, et non de
tasks.len() + 1. Si vous supprimez la tâche 1 puis en ajoutez une nouvelle, utiliser la longueur vous donnerait un id en double. mark_doneprend un&mut [Task](une slice, c’est-à-dire une tranche) parce qu’elle ne fait que modifier des éléments.add_tasketremove_taskprennent un&mut Vec<Task>parce qu’elles changent la longueur.- Les erreurs sont des
Result<_, String>. Une erreur de typeString, c’est la solution la plus simple qui fonctionne. Pour des programmes plus gros, voyez Erreurs personnalisées.
iter_mut().find(...) vous donne une référence mutable vers la tâche trouvée : c’est pour cela que task.done = true modifie la tâche à l’intérieur du vecteur. Voyez Itérateurs pour en savoir plus sur find et position.
Étape 4 : enregistrer et charger le fichier
Section intitulée « Étape 4 : enregistrer et charger le fichier »Il faut maintenant que les tâches survivent à la fin du programme. On les enregistre avec std::fs::write et on les charge avec std::fs::read_to_string. Les deux peuvent échouer, donc les deux renvoient un Result, et l’opérateur ? fait remonter toute erreur à l’appelant :
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, // La première fois que vous lancez le programme, le fichier n'existe pas encore. // Ce n'est pas une erreur : cela veut simplement dire « aucune tâche ». 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)}Le cas NotFound compte plus qu’il n’y paraît. Sans lui, le tout premier todo add échouerait avec « No such file or directory » (le message du système, en anglais : « fichier ou dossier introuvable »), parce que vous n’avez encore rien enregistré. Toutes les autres erreurs (comme « permission denied », accès refusé) remontent quand même.
filter_map(Task::from_line) lit chaque ligne et ne garde que celles qui se lisent correctement. Attention : cela ignore en silence les lignes abîmées. C’est pratique si quelqu’un modifie le fichier à la main et fait une faute de frappe, mais cela veut aussi dire qu’une erreur peut passer inaperçue. L’une des idées d’extension plus bas consiste à afficher un avertissement à la place.
Voyez Lire et écrire des fichiers pour en savoir plus.
Étape 5 : le programme complet
Section intitulée « Étape 5 : le programme complet »Voici tout le code réuni. Mettez-le dans src/main.rs et lancez-le avec cargo run -- add Acheter du lait, cargo run -- list, et ainsi de suite.
La logique principale se trouve dans run_command, qui travaille sur un Vec<Task> et ne sait rien des fichiers. main se contente de charger le fichier, d’appeler run_command et d’enregistrer. Séparer les deux rend la logique facile à tester, et c’est ce qui nous permet de faire une version pour le Playground à l’étape suivante.
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("veuillez indiquer un numéro de tâche"))?; arg.parse().map_err(|_| format!("'{arg}' n'est pas un numéro de tâche"))}
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("utilisation : todo add <texte de la tâche>")) } 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!("commande inconnue '{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!("la tâche {id} n'existe pas")), }}
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!("la tâche {id} n'existe pas")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Rien à faire. Profitez de votre journée !"); 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!("Tâche {id} ajoutée."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Tâche {id} marquée comme terminée."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Tâche {id} supprimée : {}", 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!("Erreur : {e}"); process::exit(1); }}Un mot sur run() -> Result<(), Box<dyn Error>>. Nos fonctions renvoient deux types d’erreur différents : io::Error pour les fonctions de fichiers et String pour les fonctions de commandes. Box<dyn Error> est une boîte qui peut contenir n’importe quel type d’erreur, donc ? fonctionne avec les deux. main affiche ensuite l’erreur avec eprintln! (qui écrit sur la sortie d’erreur) et quitte avec le code 1, le signal habituel pour dire que quelque chose s’est mal passé.
Étape 6 : une version à lancer dans le Playground
Section intitulée « Étape 6 : une version à lancer dans le Playground »Le Playground ne peut pas passer d’arguments de ligne de commande, et nous ne voulons pas dépendre de fichiers. Cette version garde donc le même run_command, mais lui fournit une liste de commandes écrite à l’avance, et affiche à la fin ce que contiendrait 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) }}
#[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("veuillez indiquer un numéro de tâche"))?; arg.parse().map_err(|_| format!("'{arg}' n'est pas un numéro de tâche"))}
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("utilisation : todo add <texte de la tâche>")) } 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!("commande inconnue '{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!("la tâche {id} n'existe pas")), }}
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!("la tâche {id} n'existe pas")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Rien à faire. Profitez de votre journée !"); 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!("Tâche {id} ajoutée."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Tâche {id} marquée comme terminée."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Tâche {id} supprimée : {}", task.text); } } Ok(())}
fn main() { // Chaque ligne correspond à ce que vous taperiez après `todo`. let script = [ "list", "add Acheter du lait", "add Appeler mamie", "add Apprendre Rust", "done 3", "remove 1", "done 9", "sauter", "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!("Erreur : {message}"); } }
println!(); println!("tasks.txt contiendrait :"); for task in &tasks { println!("{}", task.to_line()); }}Sortie :
$ todo listRien à faire. Profitez de votre journée !$ todo add Acheter du laitTâche 1 ajoutée.$ todo add Appeler mamieTâche 2 ajoutée.$ todo add Apprendre RustTâche 3 ajoutée.$ todo done 3Tâche 3 marquée comme terminée.$ todo remove 1Tâche 1 supprimée : Acheter du lait$ todo done 9Erreur : la tâche 9 n'existe pas$ todo sauterErreur : commande inconnue 'sauter'$ todo list[ ] 2: Appeler mamie[x] 3: Apprendre Rust
tasks.txt contiendrait :2|0|Appeler mamie3|1|Apprendre RustModifiez le script et relancez-le. Essayez d’ajouter une tâche après en avoir supprimé une, et vérifiez que le nouvel id n’est pas un doublon.
Amélioration : enregistrer en JSON avec serde
Section intitulée « Amélioration : enregistrer en JSON avec serde »Notre format id|done|text est facile à lire, mais vous avez dû écrire to_line et from_line à la main. À mesure que vos tâches s’enrichissent (dates d’échéance, priorités, étiquettes), ce code écrit à la main grossit aussi.
Dans les vrais projets, on laisse généralement les crates serde (en anglais) faire ce travail. Ajoutez-les avec :
cargo add serde --features derivecargo add serde_jsonEnsuite, une seule ligne derive vous offre l’enregistrement et le chargement :
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("Acheter du lait") }, Task { id: 2, done: true, text: String::from("Appeler mamie") }, ];
let json = serde_json::to_string(&tasks).unwrap(); println!("{json}");
let loaded: Vec<Task> = serde_json::from_str(&json).unwrap(); println!("{} tâches rechargées", loaded.len());}Sortie :
[{"id":1,"done":false,"text":"Acheter du lait"},{"id":2,"done":true,"text":"Appeler mamie"}]2 tâches rechargéesIl vous suffirait d’écrire cette chaîne JSON dans tasks.json avec fs::write, exactement comme avant. La page JSON avec serde couvre le sujet en détail.
Erreurs courantes
Section intitulée « Erreurs courantes »Utiliser ? dans un main qui ne renvoie rien. On est tenté d’écrire ceci :
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 `()`Le compilateur dit que l’opérateur ? ne peut être utilisé que dans une fonction qui renvoie Result ou Option. ? signifie « si c’est une erreur, renvoie-la depuis la fonction actuelle ». Un main simple renvoie (), il n’y a donc nulle part où envoyer l’erreur. Soit vous donnez à main un type de retour comme Result<(), Box<dyn Error>>, soit vous faites comme notre programme : vous mettez le travail dans une fonction run() qui renvoie un Result, et vous gérez l’erreur une seule fois dans main.
Considérer un fichier absent comme un échec. Si load_tasks faisait simplement fs::read_to_string(path)?, le programme échouerait dès le tout premier lancement, avant qu’un fichier existe. Testez io::ErrorKind::NotFound et commencez plutôt avec une liste vide.
Utiliser la longueur de la liste comme prochain id. Avec les tâches 1, 2 et 3, supprimer la tâche 1 laisse deux tâches. len() + 1 redonnerait 3, et deux tâches partageraient le même id. Utilisez le plus grand id plus un.
Idées pour aller plus loin
Section intitulée « Idées pour aller plus loin »- Annuler « terminée ». Ajoutez une commande
undone <id>qui remetdoneàfalse. - Modifier une tâche.
edit 2 Appeler mamie ce soirremplace le texte de la tâche 2. - Effacer les tâches terminées.
clearsupprime toutes les tâches terminées. Jetez un œil àVec::retain(en anglais). - Signaler les lignes abîmées. Au lieu d’ignorer les mauvaises lignes en silence, affichez un avertissement avec le numéro de la ligne.
- Une commande
helpqui affiche toutes les commandes et la façon de les utiliser. - Écrire des tests.
parse_commandetrun_commandne touchent pas aux fichiers, elles sont donc faciles à tester. - Utiliser un vrai analyseur d’arguments. La crate
clap(en anglais), mentionnée sur la page Arguments de ligne de commande, vous offre--helpet des sous-commandes sans effort.
Récapitulatif
Section intitulée « Récapitulatif »- Une struct
Taskcontient les données.to_lineetfrom_linela transforment en texte et inversement. - Un enum
Commanddécrit ce que l’utilisateur a demandé.parse_commandtransforme les arguments enCommand, ou enErravec un message utile. - Un
Vec<Task>contient la liste. De petites fonctions ajoutent, cherchent, modifient et suppriment des tâches. fs::read_to_stringetfs::writerenvoient unResult, et?fait remonter les erreurs. Un fichier absent au premier lancement est traité comme « aucune tâche ».- Séparer la logique (
run_command) des fichiers et des arguments la rend facile à tester et à lancer dans le Playground.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Pourquoi from_line utilise-t-elle splitn(3, '|') plutôt que split('|') ?
2. Que fait load_tasks quand tasks.txt n'existe pas encore ?
3. Pourquoi ne peut-on pas utiliser ? directement dans un fn main() sans type de retour ?
4. Quelle crate est l'amélioration habituelle pour enregistrer des structs en JSON ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- An I/O Project: Building a Command Line Program — The Rust Book (en anglais) : un projet CLI guidé plus complet, avec de bons conseils pour découper
mainen morceaux plus petits et testables. std::fs— documentation de std (en anglais) :read_to_string,writeet les autres fonctions de fichiers derrière le code d’enregistrement et de chargement.std::env::args— documentation de std (en anglais) : comment arrivent les arguments de ligne de commande, et pourquoi le premier est le nom du programme lui-même.str::splitn— documentation de std (en anglais) : découper une ligne en au plus N morceaux. C’est l’astuce qui garde d’un seul tenant le texte d’une tâche contenant un|.serde_json— docs.rs (en anglais) : la crate JSON de la section sur l’amélioration, avec des exemples deto_stringetfrom_str. Le guide de serde (en anglais) explique les attributs de derive.