Proyek: Daftar Tugas CLI dengan Rust
Di proyek ini kamu akan membuat daftar tugas (to-do list) kecil yang tinggal di terminal. Kamu ketik todo add Beli susu, dan program mengingatnya. Tutup terminal, kembali besok, ketik todo list, dan tugas-tugasmu masih ada.
Ini proyek kedua yang pas karena hasilnya terasa seperti alat sungguhan. Proyek ini memakai struct, enum, vektor, argumen command-line, baca-tulis file, dan Result dengan ?. Kita bangun setiap bagian satu per satu, mengujinya di Playground, lalu menggabungkan semuanya.
Yang akan kamu buat
Section titled “Yang akan kamu buat”Begini tampilan alat yang sudah jadi saat dipakai:
$ cargo run -- add Beli susuTugas 1 ditambahkan.$ cargo run -- add Telepon nenekTugas 2 ditambahkan.$ cargo run -- done 1Tugas 1 ditandai selesai.$ cargo run -- list[x] 1: Beli susu[ ] 2: Telepon nenek$ cargo run -- remove 1Tugas 1 dihapus: Beli susuSemua yang ada setelah -- diteruskan ke programmu, bukan ke Cargo. Nama perintahnya (add, list, done, dan remove) sengaja dibiarkan dalam bahasa Inggris, seperti kebanyakan alat terminal. Kalau kamu lebih suka tambah atau lihat, cukup ganti di parse_command (Langkah 2).
Tugas-tugas disimpan di file bernama tasks.txt, satu tugas per baris:
2|0|Telepon nenekSetiap baris berbentuk id|done|text, dengan done bernilai 1 kalau tugas sudah selesai dan 0 kalau belum.
Mulai proyek baru dengan:
cargo new todocd todoLangkah 1: Struct Task
Section titled “Langkah 1: Struct Task”Setiap tugas punya nomor (supaya bisa kamu sebut), tanda selesai, dan teks. Struct adalah pilihan yang paling pas. Kita juga memberinya dua method: satu untuk mengubah tugas menjadi satu baris teks untuk file, dan satu lagi untuk membaca tugas kembali dari sebuah baris.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { // Ubah tugas menjadi satu baris, misalnya "1|0|Beli susu". fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
// Baca tugas kembali dari sebuah baris. Mengembalikan None kalau barisnya rusak. 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("Beli susu") }; let line = task.to_line(); println!("{line}");
println!("{:?}", Task::from_line("2|1|Telepon nenek")); println!("{:?}", Task::from_line("3|0|Baca tentang A|B testing")); println!("{:?}", Task::from_line("baris ini rusak"));}Output:
1|0|Beli susuSome(Task { id: 2, done: true, text: "Telepon nenek" })Some(Task { id: 3, done: false, text: "Baca tentang A|B testing" })NoneAda dua detail yang perlu kita bahas pelan-pelan:
splitn(3, '|')memecah teks menjadi paling banyak 3 bagian. Dua karakter|pertama memisahkan kolom-kolomnya, dan semua sisanya adalah teks, walaupun teks itu sendiri berisi|. Dengansplit('|')biasa, tugas “Baca tentang A|B testing” akan kehilangan separuh teksnya.from_linemengembalikanOption, dan memakai?di setiap langkah. Kalau ada bagian yang hilang atau id-nya bukan angka, seluruh fungsi mengembalikanNone..ok()mengubahResultdariparse()menjadiOptionsupaya?bisa dipakai di situ.
Langkah 2: Enum Command
Section titled “Langkah 2: Enum Command”Alat ini mengerti empat perintah. Setiap perintah adalah satu varian dari sebuah enum, dan beberapa varian membawa data: Add butuh teks, sedangkan Done dan Remove butuh nomor tugas.
#[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("tolong beri nomor tugas"))?; arg.parse().map_err(|_| format!("'{arg}' bukan nomor tugas"))}
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("cara pakai: todo add <teks tugas>")) } 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!("perintah tidak dikenal '{other}'")), }}
fn main() { let examples = [ vec!["add", "Beli", "susu"], vec!["list"], vec![], vec!["done", "2"], vec!["done", "dua"], vec!["remove"], vec!["menari"], ];
for words in examples { let args: Vec<String> = words.iter().map(|w| w.to_string()).collect(); println!("{:?} -> {:?}", words, parse_command(&args)); }}Output:
["add", "Beli", "susu"] -> Ok(Add("Beli susu"))["list"] -> Ok(List)[] -> Ok(List)["done", "2"] -> Ok(Done(2))["done", "dua"] -> Err("'dua' bukan nomor tugas")["remove"] -> Err("tolong beri nomor tugas")["menari"] -> Err("perintah tidak dikenal 'menari'")Di program aslinya, args datang dari command line. Seperti yang kamu lihat di halaman Argumen Command-Line, item pertama dari std::env::args() adalah nama program itu sendiri, jadi kita lewati:
let args: Vec<String> = std::env::args().skip(1).collect();Kenapa add memakai args[1..].join(" ")? Shell kamu memecah todo add Beli susu menjadi kata-kata terpisah: add, Beli, susu. Menggabungkan semua yang ada setelah add menyatukan kembali teks tugasnya, jadi kamu tidak perlu tanda kutip.
Perhatikan bahwa parse_command tidak pernah crash. Input yang salah menjadi Err dengan pesan yang membantu. Pemanggilnya yang memutuskan mau diapakan.
Langkah 3: Mengolah daftar tugas
Section titled “Langkah 3: Mengolah daftar tugas”Semua tugas tinggal di sebuah Vec<Task>. Setiap perintah adalah fungsi kecil yang mengubah vektor itu:
- Add: cari id terbesar sejauh ini, tambah 1, lalu push tugas baru.
- Done: cari tugas dengan id itu dan set
done = true. - Remove: cari posisi tugas itu lalu hapus.
- List: cetak semua tugas.
#[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!("tidak ada tugas {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!("tidak ada tugas {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Tidak ada tugas. Selamat menikmati harimu!"); 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("Beli susu")); add_task(&mut tasks, String::from("Telepon nenek")); add_task(&mut tasks, String::from("Belajar Rust"));
mark_done(&mut tasks, 3).unwrap(); let removed = remove_task(&mut tasks, 1).unwrap(); println!("Dihapus: {}", removed.text);
if let Err(message) = mark_done(&mut tasks, 99) { println!("Error: {message}"); }
print_tasks(&tasks);}Output:
Dihapus: Beli susuError: tidak ada tugas 99[ ] 2: Telepon nenek[x] 3: Belajar RustBeberapa pilihan di sini memang disengaja:
- Id diambil dari id terbesar yang sudah ada, bukan dari
tasks.len() + 1. Kalau kamu menghapus tugas 1 lalu menambah tugas baru, memakai panjang vektor akan menghasilkan id kembar. mark_donemenerima&mut [Task](sebuah slice, potongan dari vektor; lihat Slice) karena fungsi ini hanya mengubah isi item.add_taskdanremove_taskmenerima&mut Vec<Task>karena keduanya mengubah panjang vektor.- Error-nya berupa
Result<_, String>. Error berbentukStringadalah cara paling sederhana yang sudah cukup. Untuk program yang lebih besar, lihat Error Kustom.
iter_mut().find(...) memberimu referensi mutable ke tugas yang cocok, itulah sebabnya task.done = true mengubah tugas yang ada di dalam vektor. Lihat Iterator untuk penjelasan lebih lanjut tentang find dan position.
Langkah 4: Menyimpan dan memuat file
Section titled “Langkah 4: Menyimpan dan memuat file”Sekarang tugas-tugas harus tetap ada setelah program selesai. Kita menyimpannya dengan std::fs::write dan memuatnya dengan std::fs::read_to_string. Keduanya bisa gagal, jadi keduanya mengembalikan Result, dan operator ? meneruskan error apa pun ke pemanggil:
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, // Saat program pertama kali dijalankan, file-nya belum ada. // Itu bukan error: artinya cuma "belum ada tugas". 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)}Kasus NotFound lebih penting daripada kelihatannya. Tanpa itu, todo add yang paling pertama akan gagal dengan “No such file or directory” (pesan dari sistem operasi yang artinya “file atau direktori tidak ditemukan”), karena kamu belum pernah menyimpan apa pun. Semua error lainnya (seperti “permission denied”, akses ditolak) tetap diteruskan ke atas.
filter_map(Task::from_line) membaca setiap baris dan hanya menyimpan baris yang berhasil di-parse. Perlu diingat, cara ini diam-diam melewati baris yang rusak. Ramah kalau seseorang mengedit file secara manual dan salah ketik, tapi artinya kesalahan bisa tidak ketahuan. Salah satu ide pengembangan di bawah adalah memberi peringatan untuk baris seperti itu.
Lihat Baca-Tulis File untuk penjelasan lebih lanjut tentang membaca dan menulis file.
Langkah 5: Program lengkap
Section titled “Langkah 5: Program lengkap”Ini semuanya digabung jadi satu. Taruh di src/main.rs dan jalankan dengan cargo run -- add Beli susu, cargo run -- list, dan seterusnya.
Logika intinya ada di run_command, yang bekerja dengan Vec<Task> dan tidak tahu apa-apa soal file. main hanya memuat file, memanggil run_command, lalu menyimpan. Memisahkan keduanya membuat logikanya mudah diuji, dan itulah yang memungkinkan kita membuat versi Playground di langkah berikutnya.
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("tolong beri nomor tugas"))?; arg.parse().map_err(|_| format!("'{arg}' bukan nomor tugas"))}
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("cara pakai: todo add <teks tugas>")) } 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!("perintah tidak dikenal '{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!("tidak ada tugas {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!("tidak ada tugas {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Tidak ada tugas. Selamat menikmati harimu!"); 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!("Tugas {id} ditambahkan."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Tugas {id} ditandai selesai."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Tugas {id} dihapus: {}", 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!("Error: {e}"); process::exit(1); }}Sedikit catatan tentang run() -> Result<(), Box<dyn Error>>. Fungsi-fungsi kita mengembalikan dua tipe error yang berbeda: io::Error dari fungsi file dan String dari fungsi perintah. Box<dyn Error> adalah “kotak” yang bisa menampung tipe error apa saja, jadi ? bisa dipakai untuk keduanya. Lalu main mencetak error dengan eprintln! (yang menulis ke output error) dan keluar dengan kode 1, tanda yang umum dipakai bahwa ada yang salah.
Langkah 6: Versi yang bisa dijalankan di Playground
Section titled “Langkah 6: Versi yang bisa dijalankan di Playground”Playground tidak bisa mengirim argumen command-line, dan kita tidak mau bergantung pada file. Jadi versi ini memakai run_command yang sama, tapi memberinya daftar perintah yang sudah disiapkan, lalu mencetak isi tasks.txt di akhir:
#[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("tolong beri nomor tugas"))?; arg.parse().map_err(|_| format!("'{arg}' bukan nomor tugas"))}
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("cara pakai: todo add <teks tugas>")) } 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!("perintah tidak dikenal '{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!("tidak ada tugas {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!("tidak ada tugas {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Tidak ada tugas. Selamat menikmati harimu!"); 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!("Tugas {id} ditambahkan."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Tugas {id} ditandai selesai."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Tugas {id} dihapus: {}", task.text); } } Ok(())}
fn main() { // Setiap baris adalah apa yang kamu ketik setelah `todo`. let script = [ "list", "add Beli susu", "add Telepon nenek", "add Belajar Rust", "done 3", "remove 1", "done 9", "lompat", "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!("Error: {message}"); } }
println!(); println!("Isi tasks.txt nantinya:"); for task in &tasks { println!("{}", task.to_line()); }}Output:
$ todo listTidak ada tugas. Selamat menikmati harimu!$ todo add Beli susuTugas 1 ditambahkan.$ todo add Telepon nenekTugas 2 ditambahkan.$ todo add Belajar RustTugas 3 ditambahkan.$ todo done 3Tugas 3 ditandai selesai.$ todo remove 1Tugas 1 dihapus: Beli susu$ todo done 9Error: tidak ada tugas 9$ todo lompatError: perintah tidak dikenal 'lompat'$ todo list[ ] 2: Telepon nenek[x] 3: Belajar Rust
Isi tasks.txt nantinya:2|0|Telepon nenek3|1|Belajar RustUbah daftar perintahnya lalu jalankan lagi. Coba tambahkan tugas setelah menghapus satu tugas, lalu pastikan id barunya tidak kembar.
Upgrade: simpan sebagai JSON dengan serde
Section titled “Upgrade: simpan sebagai JSON dengan serde”Format id|done|text kita mudah dibaca, tapi kamu harus menulis to_line dan from_line sendiri. Saat tugasmu makin lengkap (tenggat waktu, prioritas, tag), kode buatan tangan itu ikut membesar.
Di proyek sungguhan, biasanya pekerjaan ini diserahkan ke crate serde (bahasa Inggris). Tambahkan dengan:
cargo add serde --features derivecargo add serde_jsonLalu satu baris derive sudah memberimu fitur simpan dan muat secara gratis:
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("Beli susu") }, Task { id: 2, done: true, text: String::from("Telepon nenek") }, ];
let json = serde_json::to_string(&tasks).unwrap(); println!("{json}");
let loaded: Vec<Task> = serde_json::from_str(&json).unwrap(); println!("Berhasil memuat kembali {} tugas", loaded.len());}Output:
[{"id":1,"done":false,"text":"Beli susu"},{"id":2,"done":true,"text":"Telepon nenek"}]Berhasil memuat kembali 2 tugasString JSON itu tinggal kamu tulis ke tasks.json dengan fs::write, persis seperti sebelumnya. Halaman JSON dengan serde membahasnya secara detail.
Kesalahan umum
Section titled “Kesalahan umum”Memakai ? di main yang tidak mengembalikan apa-apa. Godaannya adalah menulis seperti ini:
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 `()`Compiler bilang operator ? hanya bisa dipakai di fungsi yang mengembalikan Result atau Option. ? artinya “kalau ini error, kembalikan error itu dari fungsi yang sedang berjalan”. main biasa mengembalikan (), jadi tidak ada tempat untuk mengirim error-nya. Pilihannya: beri main tipe kembalian seperti Result<(), Box<dyn Error>>, atau lakukan seperti program kita: taruh pekerjaannya di fungsi run() yang mengembalikan Result, lalu tangani error-nya sekali saja di main.
Menganggap file yang belum ada sebagai kegagalan. Kalau load_tasks hanya melakukan fs::read_to_string(path)?, program akan gagal saat pertama kali dijalankan, sebelum file-nya ada. Periksa io::ErrorKind::NotFound dan mulai dengan daftar kosong.
Memakai panjang daftar sebagai id berikutnya. Dengan tugas 1, 2, 3, menghapus tugas 1 menyisakan dua tugas. len() + 1 akan menghasilkan 3 lagi, dan sekarang ada dua tugas dengan id yang sama. Pakai id terbesar ditambah satu.
Ide pengembangan
Section titled “Ide pengembangan”- Batalkan “done”. Tambahkan perintah
undone <id>yang mengembalikandonemenjadifalse. - Edit tugas.
edit 2 Telepon nenek nanti malammengganti teks tugas 2. - Bersihkan tugas yang sudah selesai.
clearmenghapus semua tugas yang sudah selesai. LihatVec::retain(bahasa Inggris). - Beri peringatan untuk baris yang rusak. Daripada diam-diam melewati baris yang rusak, cetak peringatan beserta nomor barisnya.
- Perintah
helpyang mencetak semua perintah dan cara memakainya. - Tulis test.
parse_commanddanrun_commandtidak menyentuh file, jadi mudah diuji. - Pakai parser argumen sungguhan. Crate
clap(bahasa Inggris), yang disebut di halaman Argumen Command-Line, memberimu--helpdan subcommand secara gratis.
Ringkasan
Section titled “Ringkasan”- Struct
Taskmenyimpan datanya.to_linedanfrom_linemengubahnya menjadi teks dan sebaliknya. - Enum
Commandmenggambarkan apa yang diminta pengguna.parse_commandmengubah argumen menjadiCommand, atauErrdengan pesan yang membantu. - Sebuah
Vec<Task>menyimpan daftarnya. Fungsi-fungsi kecil menambah, mencari, mengubah, dan menghapus tugas. fs::read_to_stringdanfs::writemengembalikanResult, dan?meneruskan error ke atas. File yang belum ada saat pertama kali dijalankan dianggap “belum ada tugas”.- Memisahkan logika (
run_command) dari urusan file dan argumen membuatnya mudah diuji dan bisa dijalankan di Playground.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa from_line memakai splitn(3, '|') dan bukan split('|')?
2. Apa yang dilakukan load_tasks kalau tasks.txt belum ada?
3. Kenapa kamu tidak bisa memakai ? langsung di dalam fn main() tanpa tipe kembalian?
4. Crate apa yang biasanya dipakai untuk upgrade menyimpan struct sebagai JSON?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- An I/O Project: Building a Command Line Program — The Rust Book (bahasa Inggris): proyek CLI terpandu yang lebih besar, dengan saran bagus tentang memecah
mainmenjadi bagian-bagian kecil yang mudah diuji. std::fs— dokumentasi std (bahasa Inggris):read_to_string,write, dan fungsi file lain di balik kode simpan dan muat.std::env::args— dokumentasi std (bahasa Inggris): bagaimana argumen command-line diterima, dan kenapa argumen pertama adalah nama program itu sendiri.str::splitn— dokumentasi std (bahasa Inggris): memecah sebuah baris menjadi paling banyak N bagian. Inilah trik yang menjaga teks tugas berisi|tetap utuh.serde_json— docs.rs (bahasa Inggris): crate JSON dari bagian upgrade, dengan contohto_stringdanfrom_str. Panduan serde (bahasa Inggris) menjelaskan atribut derive-nya.