5.1 Голос у двери
Табличка над входом мастерской - не украшение. Она объясняет посетителю, куда он пришёл, ещё до того как постучал. Нигде, впрочем, не записано, что за дверью должен кто-нибудь находиться: это слишком очевидно, чтобы тратить на него чернила. Мастерская без хозяина - просто помещение с табличкой.
Что нужно
main из M4.5 запускает одну и ту же последовательность при каждом вызове: загрузить конфиг,
добавить три задачи, вывести список, завершить одну. Командная строка не влияет ни на что.
Написать tq list или tq add "Buy coffee" - всё равно что говорить в стену.
Нужен разборщик аргументов командной строки. clap - стандартный выбор в Rust-экосистеме:
derive-интерфейс позволяет описать CLI как структуру, а --help генерируется из неё
автоматически.
Сборка
В Cargo.toml два изменения: [[bin]] добавить вручную, clap - через
cargo add clap --features derive:
# Cargo.toml - CHANGED
[[bin]]
name = "tq"
path = "src/main.rs"
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1.1"
[[bin]] в Cargo.toml называет исполняемый файл tq - именно это имя появится в Usage: в
--help и в target/debug/. Без него исполняемый файл берёт имя из поля name в [package] -
это подходит, если пакет уже называется tq. В примерах кода из папки tq/ пакет называется
по теме главы, поэтому там [[bin]] необходим.
Типы и логика CLI живут в отдельном src/cli/mod.rs. src/main.rs остаётся точкой входа:
распарсить аргументы, загрузить конфиг, передать управление.
В src/cli/mod.rs:
// src/cli/mod.rs - NEW
use crate::config::Config;
use crate::error::TqResult;
use crate::persistence;
use crate::store::{Store, TaskStore};
use crate::task::Task;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(about = "Task queue manager", version)]
pub struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Add a new task
Add {
/// Task title
title: String,
},
/// List all tasks
List,
/// Show a task
Get {
/// Task id
id: u64,
},
/// Mark a task done
Done {
/// Task id
id: u64,
},
}
pub fn run(cli: Cli, config: Config) -> TqResult<()> {
let path = config.data_dir.join("tasks.json");
let tasks = persistence::load(&path)?;
let mut store = TaskStore::new(tasks);
match cli.command {
Commands::Add { title } => {
let task = Task::new(0, &title)?;
store.add(task)?;
persistence::save(store.all(), &path)?;
println!("added");
}
Commands::List => {
for task in store.all() {
let marker = if task.is_done() { "✓" } else { "·" };
println!("{marker} {task}");
}
}
Commands::Get { id } => {
println!("{}", store.get(id)?);
}
Commands::Done { id } => {
store.get_mut(id)?.complete();
persistence::save(store.all(), &path)?;
println!("done");
}
}
Ok(())
}
#[derive(Parser)] просит clap сгенерировать парсер из Cli. #[command(...)] описывает
инструмент: about - строка описания; version без аргумента - clap сам возьмёт версию из
Cargo.toml. #[command(subcommand)] говорит, что поле command - диспетчер: clap ищет
первый позиционный аргумент в вариантах Commands.
run принимает уже разобранный Cli и загруженный Config - само чтение окружения остаётся
на стороне main. Add создаёт задачу через Task::new - пустой заголовок вернёт ошибку
ещё до записи; 0 как ID - заглушка, store перепишет его своим следующим значением. List
помечает выполненные задачи ✓, невыполненные ·.
src/main.rs добавляет mod cli и сводит fn main к трём строкам:
// src/main.rs - CHANGED
mod cli;
mod config;
mod error;
mod persistence;
mod store;
mod task;
use clap::Parser;
use cli::Cli;
use config::Config;
use error::TqResult;
use std::path::Path;
fn main() -> TqResult<()> {
let cli = Cli::parse();
let config = Config::load(Path::new("config.toml"))?;
cli::run(cli, config)
}
parse_from проверяет разбор аргументов без чтения env::args(). В src/cli/tests.rs:
// src/cli/tests.rs - NEW
use super::*;
#[test]
fn cli_parse_add() {
let cli = Cli::parse_from(["tq", "add", "Buy coffee"]);
let Commands::Add { title } = cli.command else { panic!("expected Add") };
assert_eq!(title, "Buy coffee");
}
#[test]
fn cli_parse_list() {
let cli = Cli::parse_from(["tq", "list"]);
assert!(matches!(cli.command, Commands::List));
}
#[test]
fn cli_parse_done() {
let cli = Cli::parse_from(["tq", "done", "42"]);
assert!(matches!(cli.command, Commands::Done { id: 42 }));
}
Результат
$ cargo run -- --help
Task queue manager
Usage: tq <COMMAND>
Commands:
add Add a new task
list List all tasks
get Show a task
done Mark a task done
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
-- отделяет аргументы Cargo от аргументов самой программы - без него Cargo попытался бы
разобрать add как свою собственную команду.
$ cargo run -- add --help
Add a new task
Usage: tq add <TITLE>
Arguments:
<TITLE> Task title
Options:
-h, --help Print help
cargo run -- add "Buy coffee" добавляет задачу. cargo run -- list выводит список.
cargo run -- done 1 завершает.
make ci проходит.
Примечание.
make run -- listне работает:--make забирает себе, программа его не получает. Аргументы передаются через переменнуюARGS:
# Makefile - CHANGED
run:
@cargo run -- $(ARGS)
После этого:
make run ARGS="list",make run ARGS="add 'Buy coffee'".
Полный код
tqдля этой главы -5-interface/01-the-voice-at-the-door/.
Lore: mod объявляет, use находит
В main.rs есть строки, которые fn main не использует напрямую:
mod persistence;
mod store;
mod task;
mod foo; - не импорт. Это объявление: “файл src/foo.rs существует и компилируется вместе
с программой”. Все такие объявления живут в main.rs - точке входа, с которой компилятор
начинает сборку. src/cli/mod.rs обращается к ним через crate::persistence, crate::store,
crate::task - но только потому что main.rs сначала объявил их существование.
Убрать mod persistence; - и crate::persistence в src/cli/mod.rs перестанет существовать, хотя
файл на диске никуда не денется.
Lore: как clap читает документацию
Строки /// Add a new task - doc-комментарии Rust. Компилятор хранит их как атрибут
#[doc = "..."] на типе или поле. clap читает именно этот атрибут - не строки напрямую.
Следствие практическое: убрать комментарий - и вариант останется, но в --help появится
пустое место. Не ошибка. Просто помощь без объяснений - состояние, которое встречается
чаще, чем хотелось бы.