Register a command¶
Write the command and register it¶
Three pieces: a type, a Command implementation, and a factory function carrying the
#[distributed_slice] attribute.
use linkme::distributed_slice;
use rtb_app::app::App;
use rtb_app::command::{Command, CommandSpec, BUILTIN_COMMANDS};
pub struct Deploy;
#[async_trait::async_trait]
impl Command for Deploy {
fn spec(&self) -> &CommandSpec {
static SPEC: CommandSpec = CommandSpec {
name: "deploy",
about: "Deploy the thing",
..CommandSpec::DEFAULT
};
&SPEC
}
async fn run(&self, app: App) -> miette::Result<()> {
println!("deploying with {}", app.metadata.name);
Ok(())
}
}
#[distributed_slice(BUILTIN_COMMANDS)]
fn __register_deploy() -> Box<dyn Command> {
Box::new(Deploy)
}
Use ..CommandSpec::DEFAULT rather than naming every field. A literal that names all
six compiles now and breaks the next time an optional field is added.
You need three direct dependencies for this to compile: linkme, async-trait and
miette.
Fix "cannot find linkme in the crate root"¶
This is the error you get when linkme is missing from your own Cargo.toml:
error[E0433]: cannot find `linkme` in the crate root
--> src/lib.rs:17:1
|
17 | #[distributed_slice(BUILTIN_COMMANDS)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ could not find `linkme` in the list of imported crates
|
= note: this error originates in the attribute macro `distributed_slice`
It appears even if you imported the attribute from rtb_app::linkme. The macro
expands to absolute ::linkme::… paths, and ::linkme resolves against your crate's
extern prelude — which a re-export through another crate does not populate.
There are two fixes.
Add the dependency. This is the normal answer:
Keep it in the 0.3 range. rtb-app depends on linkme 0.3, and Cargo unifies
semver-compatible requirements into one copy — which is what you need, because
BUILTIN_COMMANDS is declared by rtb-app using its linkme and your attribute
has to be talking about the same one.
Or point the macro at the re-export. If you would rather not carry the
dependency, tell the macro where linkme lives:
use rtb_app::linkme::distributed_slice;
#[distributed_slice(BUILTIN_COMMANDS)]
#[linkme(crate = rtb_app::linkme)]
fn __register_deploy() -> Box<dyn Command> {
Box::new(Deploy)
}
This compiles with rtb-app as your only relevant dependency, and has the useful
side effect of guaranteeing you are registering into the same linkme version
rtb-app uses. The cost is one extra attribute on every registration site.
Hide a command behind a runtime feature¶
Set feature on the spec. The command is then offered only when that feature is
enabled for the invocation:
static SPEC: CommandSpec = CommandSpec {
name: "ask",
about: "Ask the docs a question",
feature: Some(Feature::Ai),
..CommandSpec::DEFAULT
};
Feature::Ai is off by default, so this command is hidden unless the tool enables
it. Leave feature as None for a command that should always be visible. The full
list of features and their defaults is in Features.
Add aliases and a short flag¶
static SPEC: CommandSpec = CommandSpec {
name: "deploy",
about: "Deploy the thing",
aliases: &["ship", "push"],
short: Some('d'),
long_about: Some("Deploy the thing to the configured environment.\n\n…"),
..CommandSpec::DEFAULT
};
All four of those fields are 'static. Nothing here can be computed at runtime.
Take arguments the outer parser should not touch¶
If the command owns its own clap subtree, opt into passthrough:
Then read the arguments off the App, not the process:
async fn run(&self, app: App) -> miette::Result<()> {
let args = app.trailing_args(); // &[OsString]
// re-parse with your own clap::Parser
Ok(())
}
Reaching for std::env::args_os() here works when a user runs the binary and breaks
under Application::run_with_args, which is how these commands get driven in tests.
Replacing a built-in command¶
Registering a Command whose spec().name matches a built-in is the documented way
to override it, and rtb-cli deduplicates by name. Be aware of what that does not
guarantee: rtb-app states plainly that link-time registration order across
crates is not deterministic, so which of two same-named entries survives is not
something this crate promises. Treat name collision as a mechanism to use
deliberately, with a test asserting the command you expect is the one that runs, and
not as something to rely on implicitly.
Check it registered¶
Nothing fails loudly when a registration is missed, so assert it:
#[test]
fn deploy_is_registered() {
let names: Vec<&str> =
BUILTIN_COMMANDS.iter().map(|f| f().spec().name).collect();
assert!(names.contains(&"deploy"), "got: {names:?}");
}