Projeto: Lista de tarefas em Rust
Neste projeto você vai criar uma pequena lista de tarefas que mora no seu terminal. Você digita todo add Comprar leite e ela guarda. Fecha o terminal, volta amanhã, digita todo list, e suas tarefas continuam lá.
É um ótimo segundo projeto, porque parece uma ferramenta de verdade. Ele usa structs (estruturas), enums (enumerações), vetores, argumentos de linha de comando, leitura e escrita de arquivos e Result com ?. Vamos construir cada parte separadamente, testar no Playground e, no fim, juntar tudo.
O que você vai construir
Seção intitulada “O que você vai construir”Veja como fica o uso da ferramenta pronta:
$ cargo run -- add Comprar leiteTarefa 1 adicionada.$ cargo run -- add Ligar para a vovóTarefa 2 adicionada.$ cargo run -- done 1Tarefa 1 marcada como concluída.$ cargo run -- list[x] 1: Comprar leite[ ] 2: Ligar para a vovó$ cargo run -- remove 1Tarefa 1 removida: Comprar leiteTudo o que vem depois de -- é passado para o seu programa, e não para o Cargo. Os nomes dos comandos (add, list, done e remove) ficam em inglês, como na maioria das ferramentas de terminal. Se você preferir adicionar ou listar, é só trocá-los em parse_command (Passo 2).
As tarefas ficam salvas em um arquivo chamado tasks.txt, uma tarefa por linha:
2|0|Ligar para a vovóCada linha tem o formato id|done|text, em que done vale 1 para tarefa concluída e 0 para tarefa pendente.
Crie um projeto novo com:
cargo new todocd todoPasso 1: O struct Task
Seção intitulada “Passo 1: O struct Task”Toda tarefa tem um número (para você poder se referir a ela), uma marcação de “concluída” e um texto. Um struct encaixa perfeitamente. Também damos a ele dois métodos: um que transforma a tarefa em uma linha de texto para o arquivo, e outro que lê a tarefa de volta a partir de uma linha.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { // Transforma a tarefa em uma linha, como "1|0|Comprar leite". fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
// Lê a tarefa de volta a partir de uma linha. Retorna None se a linha estiver quebrada. 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("Comprar leite") }; let line = task.to_line(); println!("{line}");
println!("{:?}", Task::from_line("2|1|Ligar para a vovó")); println!("{:?}", Task::from_line("3|0|Ler sobre testes A|B")); println!("{:?}", Task::from_line("esta linha está quebrada"));}Saída:
1|0|Comprar leiteSome(Task { id: 2, done: true, text: "Ligar para a vovó" })Some(Task { id: 3, done: false, text: "Ler sobre testes A|B" })NoneVale a pena olhar com calma para dois detalhes:
splitn(3, '|')divide o texto em no máximo 3 pedaços. Os dois primeiros|separam os campos, e tudo o que vem depois é o texto, mesmo que ele tenha um|. Com umsplit('|')simples, a tarefa “Ler sobre testes A|B” perderia metade do texto.from_lineretorna umOptione usa?em cada passo. Se faltar algum pedaço ou se o id não for um número, a função inteira retornaNone. O.ok()transforma oResultdeparse()em umOption, para que o?funcione com ele.
Passo 2: O enum Command
Seção intitulada “Passo 2: O enum Command”A ferramenta entende quatro comandos. Cada um é uma variante de um enum, e algumas variantes carregam dados: Add precisa do texto, e Done e Remove precisam do número da tarefa.
#[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("informe o número da tarefa"))?; arg.parse().map_err(|_| format!("'{arg}' não é um número de tarefa"))}
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("uso: todo add <texto da tarefa>")) } 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!("comando desconhecido '{other}'")), }}
fn main() { let examples = [ vec!["add", "Comprar", "leite"], vec!["list"], vec![], vec!["done", "2"], vec!["done", "dois"], vec!["remove"], vec!["dançar"], ];
for words in examples { let args: Vec<String> = words.iter().map(|w| w.to_string()).collect(); println!("{:?} -> {:?}", words, parse_command(&args)); }}Saída:
["add", "Comprar", "leite"] -> Ok(Add("Comprar leite"))["list"] -> Ok(List)[] -> Ok(List)["done", "2"] -> Ok(Done(2))["done", "dois"] -> Err("'dois' não é um número de tarefa")["remove"] -> Err("informe o número da tarefa")["dançar"] -> Err("comando desconhecido 'dançar'")No programa de verdade, args vem da linha de comando. Como você viu na página Argumentos de linha de comando, o primeiro item de std::env::args() é o nome do próprio programa, então pulamos esse item:
let args: Vec<String> = std::env::args().skip(1).collect();Por que o add usa args[1..].join(" ")? O seu shell divide todo add Comprar leite em palavras separadas: add, Comprar, leite. Juntar tudo o que vem depois de add remonta o texto da tarefa, então você não precisa de aspas.
Repare que parse_command nunca quebra o programa. Uma entrada inválida vira um Err com uma mensagem útil, e quem chamou a função decide o que fazer com ela.
Passo 3: Trabalhando com a lista de tarefas
Seção intitulada “Passo 3: Trabalhando com a lista de tarefas”Todas as tarefas ficam em um Vec<Task>. Cada comando é uma pequena função que altera o vetor:
- Add: encontra o maior id até agora, soma 1 e adiciona (
push) uma tarefa nova. - Done: encontra a tarefa com aquele id e define
done = true. - Remove: encontra a posição da tarefa e a remove.
- List: mostra todas as tarefas.
#[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!("a tarefa {id} não existe")), }}
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!("a tarefa {id} não existe")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Nada para fazer. Aproveite o seu dia!"); 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("Comprar leite")); add_task(&mut tasks, String::from("Ligar para a vovó")); add_task(&mut tasks, String::from("Aprender Rust"));
mark_done(&mut tasks, 3).unwrap(); let removed = remove_task(&mut tasks, 1).unwrap(); println!("Removida: {}", removed.text);
if let Err(message) = mark_done(&mut tasks, 99) { println!("Erro: {message}"); }
print_tasks(&tasks);}Saída:
Removida: Comprar leiteErro: a tarefa 99 não existe[ ] 2: Ligar para a vovó[x] 3: Aprender RustAlgumas escolhas aqui são de propósito:
- Os ids vêm do maior id existente, e não de
tasks.len() + 1. Se você remover a tarefa 1 e depois adicionar outra, usar o tamanho da lista daria um id repetido. mark_donerecebe&mut [Task](um slice, ou fatia), porque só altera os itens. Jáadd_taskeremove_taskrecebem&mut Vec<Task>, porque mudam o tamanho do vetor.- Os erros são
Result<_, String>. Um erro do tipoStringé a solução mais simples que funciona. Para programas maiores, veja Erros personalizados.
iter_mut().find(...) dá a você uma referência mutável para a tarefa encontrada. É por isso que task.done = true altera a tarefa dentro do vetor. Veja Iteradores para saber mais sobre find e position.
Passo 4: Salvando e carregando o arquivo
Seção intitulada “Passo 4: Salvando e carregando o arquivo”Agora as tarefas precisam sobreviver depois que o programa termina. Nós as salvamos com std::fs::write e carregamos com std::fs::read_to_string. As duas funções podem falhar, então as duas retornam Result, e o operador ? repassa qualquer erro para quem chamou:
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, // Na primeira vez que você roda o programa, o arquivo ainda não existe. // Isso não é um erro: só quer dizer "nenhuma tarefa". 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)}O caso NotFound é mais importante do que parece. Sem ele, o primeiríssimo todo add falharia com “No such file or directory” (“arquivo ou diretório não encontrado”, mensagem do sistema em inglês), porque você ainda não salvou nada. Todos os outros erros (como “permission denied”, permissão negada) continuam sendo repassados.
filter_map(Task::from_line) lê cada linha e fica só com as que conseguem ser interpretadas. Atenção: isso pula as linhas quebradas sem avisar. É gentil se alguém editar o arquivo à mão e errar a digitação, mas também significa que um erro pode passar despercebido. Uma das ideias de extensão lá embaixo é mostrar um aviso em vez disso.
Veja a página Arquivos em Rust para saber mais sobre como ler e escrever arquivos.
Passo 5: O programa completo
Seção intitulada “Passo 5: O programa completo”Aqui está tudo junto. Coloque em src/main.rs e rode com cargo run -- add Comprar leite, cargo run -- list e assim por diante.
A lógica principal fica em run_command, que trabalha com um Vec<Task> e não sabe nada sobre arquivos. A main só carrega o arquivo, chama run_command e salva. Manter essas partes separadas deixa a lógica fácil de testar, e é isso que nos permite montar uma versão para o Playground no próximo passo.
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("informe o número da tarefa"))?; arg.parse().map_err(|_| format!("'{arg}' não é um número de tarefa"))}
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("uso: todo add <texto da tarefa>")) } 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!("comando desconhecido '{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!("a tarefa {id} não existe")), }}
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!("a tarefa {id} não existe")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Nada para fazer. Aproveite o seu dia!"); 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!("Tarefa {id} adicionada."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Tarefa {id} marcada como concluída."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Tarefa {id} removida: {}", 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!("Erro: {e}"); process::exit(1); }}Uma observação sobre run() -> Result<(), Box<dyn Error>>. Nossas funções retornam dois tipos de erro diferentes: io::Error nas funções de arquivo e String nas funções de comando. Box<dyn Error> é uma caixa que pode guardar qualquer tipo de erro, então o ? funciona com os dois. Depois, a main mostra o erro com eprintln! (que escreve na saída de erros) e encerra com o código 1, o sinal habitual de que algo deu errado.
Passo 6: Uma versão para rodar no Playground
Seção intitulada “Passo 6: Uma versão para rodar no Playground”O Playground não consegue passar argumentos de linha de comando, e não queremos depender de arquivos. Por isso, esta versão mantém o mesmo run_command, mas o alimenta com uma lista de comandos pré-definida e, no fim, mostra o que o tasks.txt conteria:
#[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("informe o número da tarefa"))?; arg.parse().map_err(|_| format!("'{arg}' não é um número de tarefa"))}
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("uso: todo add <texto da tarefa>")) } 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!("comando desconhecido '{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!("a tarefa {id} não existe")), }}
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!("a tarefa {id} não existe")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Nada para fazer. Aproveite o seu dia!"); 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!("Tarefa {id} adicionada."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Tarefa {id} marcada como concluída."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Tarefa {id} removida: {}", task.text); } } Ok(())}
fn main() { // Cada linha é o que você digitaria depois de `todo`. let script = [ "list", "add Comprar leite", "add Ligar para a vovó", "add Aprender Rust", "done 3", "remove 1", "done 9", "pular", "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!("Erro: {message}"); } }
println!(); println!("O tasks.txt ficaria assim:"); for task in &tasks { println!("{}", task.to_line()); }}Saída:
$ todo listNada para fazer. Aproveite o seu dia!$ todo add Comprar leiteTarefa 1 adicionada.$ todo add Ligar para a vovóTarefa 2 adicionada.$ todo add Aprender RustTarefa 3 adicionada.$ todo done 3Tarefa 3 marcada como concluída.$ todo remove 1Tarefa 1 removida: Comprar leite$ todo done 9Erro: a tarefa 9 não existe$ todo pularErro: comando desconhecido 'pular'$ todo list[ ] 2: Ligar para a vovó[x] 3: Aprender Rust
O tasks.txt ficaria assim:2|0|Ligar para a vovó3|1|Aprender RustMude o script e rode de novo. Experimente adicionar uma tarefa depois de remover outra e confira que o novo id não se repete.
Melhoria: salvar em JSON com serde
Seção intitulada “Melhoria: salvar em JSON com serde”Nosso formato id|done|text é fácil de ler, mas você teve que escrever to_line e from_line à mão. Conforme sua tarefa ganha mais informações (prazos, prioridades, etiquetas), esse código escrito à mão cresce junto.
Em projetos reais, o mais comum é deixar esse trabalho para o serde (em inglês), um conjunto de crates (bibliotecas do Rust). Adicione os crates com:
cargo add serde --features derivecargo add serde_jsonDepois, uma única linha de derive já te dá o salvamento e o carregamento de graça:
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("Comprar leite") }, Task { id: 2, done: true, text: String::from("Ligar para a vovó") }, ];
let json = serde_json::to_string(&tasks).unwrap(); println!("{json}");
let loaded: Vec<Task> = serde_json::from_str(&json).unwrap(); println!("{} tarefas carregadas de volta", loaded.len());}Saída:
[{"id":1,"done":false,"text":"Comprar leite"},{"id":2,"done":true,"text":"Ligar para a vovó"}]2 tarefas carregadas de voltaVocê gravaria esse texto JSON em tasks.json com fs::write, exatamente como antes. A página JSON com Serde explica isso em detalhes.
Erros comuns
Seção intitulada “Erros comuns”Usar ? em uma main que não retorna nada. É tentador escrever assim:
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 `()`O compilador está dizendo que o operador ? só pode ser usado em uma função que retorna Result ou Option, e que esta main retorna (). O ? quer dizer “se isto for um erro, retorne-o da função atual”. Uma main simples retorna (), então não há para onde mandar o erro. Ou você dá à main um tipo de retorno como Result<(), Box<dyn Error>>, ou faz como o nosso programa: coloca o trabalho em uma função run() que retorna Result e trata o erro uma única vez na main.
Tratar a falta do arquivo como uma falha. Se load_tasks fizesse só fs::read_to_string(path)?, o programa falharia logo na primeira execução, antes de qualquer arquivo existir. Verifique io::ErrorKind::NotFound e comece com uma lista vazia.
Usar o tamanho da lista como próximo id. Com as tarefas 1, 2 e 3, remover a tarefa 1 deixa duas tarefas. len() + 1 daria 3 de novo, e agora duas tarefas teriam o mesmo id. Use o maior id mais um.
Ideias para ampliar o projeto
Seção intitulada “Ideias para ampliar o projeto”- Desfazer o “done”. Adicione um comando
undone <id>que voltadoneparafalse. - Editar uma tarefa.
edit 2 Ligar para a vovó hoje à noitetroca o texto da tarefa 2. - Limpar as tarefas concluídas.
clearremove todas as tarefas que já estão concluídas. Dê uma olhada emVec::retain(em inglês). - Avisar sobre linhas quebradas. Em vez de pular as linhas inválidas em silêncio, mostre um aviso com o número da linha.
- Um comando
helpque mostra todos os comandos e como usá-los. - Escrever testes.
parse_commanderun_commandnão mexem em arquivos, então são fáceis de testar. - Usar um parser de argumentos de verdade. O crate
clap(em inglês), citado na página Argumentos de linha de comando, te dá--helpe subcomandos de graça.
- Um struct
Taskguarda os dados.to_lineefrom_lineo transformam em texto e de volta. - Um enum
Commanddescreve o que o usuário pediu.parse_commandtransforma os argumentos em umCommand, ou em umErrcom uma mensagem útil. - Um
Vec<Task>guarda a lista. Pequenas funções adicionam, encontram, alteram e removem tarefas. fs::read_to_stringefs::writeretornamResult, e o?repassa os erros. A falta do arquivo na primeira execução é tratada como “nenhuma tarefa”.- Manter a lógica (
run_command) separada dos arquivos e dos argumentos facilita os testes e permite rodar o programa no Playground.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Por que from_line usa splitn(3, '|') em vez de split('|')?
2. O que load_tasks faz quando o tasks.txt ainda não existe?
3. Por que você não pode usar ? diretamente dentro de uma fn main() sem tipo de retorno?
4. Qual crate é a melhoria mais comum para salvar structs como JSON?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- An I/O Project: Building a Command Line Program — The Rust Book (em inglês): um projeto de CLI guiado e maior, com bons conselhos sobre como dividir o
mainem partes menores e testáveis. std::fs— documentação da std (em inglês):read_to_string,writee as outras funções de arquivo por trás do código de salvar e carregar.std::env::args— documentação da std (em inglês): como os argumentos de linha de comando chegam e por que o primeiro é o nome do próprio programa.str::splitn— documentação da std (em inglês): divide uma linha em no máximo N partes. É o truque que mantém inteiro o texto de uma tarefa que contém|.serde_json— docs.rs (em inglês): o crate de JSON da seção de melhoria, com exemplos deto_stringefrom_str. O guia do serde (em inglês) explica os atributos do derive.