Skip to content

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:

[dependencies]
linkme = "0.3"

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:

fn subcommand_passthrough(&self) -> bool { true }

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:?}");
}