I'd like to have a clap CLI that can capture these three commands:
cli node create <NAME>
cli node get <NAME>
cli node <NAME> link <TARGET>
The first two are straightforward with clap, but I'm struggling for the best approach for the third.
I think it would also be achievable with a structure like
cli node link --node <NAME> --target <TARGET>
but it feels very verbose.
Original approach is to use an external subcommand:
#[derive(Subcommand)]
enum Commands {
/// Node operations
Node(NodeCommand),
}
#[derive(Args)]
struct NodeCommand {
#[command(subcommand)]
action: Option<NodeAction>,
/// Allow external subcommands (node names)
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
#[arg(hide = true)]
args: Vec<String>,
}
fn handle_node_operations(node_name: &str, args: &[String]) {
// Create a dynamic clap parser for node operations
#[derive(Parser)]
#[command(name = "node-op")]
struct NodeOpCommand {
#[command(subcommand)]
action: NodeOpAction,
}
#[derive(Subcommand)]
enum NodeOpAction {
/// Link this node to a target
Link {
/// Target to link to
target: String,
},
/// Remove a link from this node
Unlink {
/// Target to unlink from
target: String,
},
/// Show status of this node
Status,
}
// Parse the remaining arguments
let args_with_program = std::iter::once(String::from("node-op"))
.chain(args.iter().cloned())
.collect::<Vec<_>>();
match NodeOpCommand::try_parse_from(args_with_program) {
Ok(node_cmd) => {
match node_cmd.action {
NodeOpAction::Link { target } => {
println!("Linking node '{}' to target '{}'", node_name, target);
}
NodeOpAction::Unlink { target } => {
println!("Unlinking node '{}' from target '{}'", node_name, target);
}
NodeOpAction::Status => {
println!("Showing status for node '{}'", node_name);
}
}
}
Err(e) => {
// Print error with proper help text
eprintln!("Error for node '{}': {}", node_name, e);
std::process::exit(1);
}
}
}
But then I lose out on a cohesive help
page. I'm looking for advice on the recommended way of handling this kind of scenario.
I'll start by saying that, at least in this case, I think the requested flow is a bad one. command CATEGORY VERB NOUN [NOUN...]
is probably the best way to set this up, e.g.
$ cli node create <NAME>
$ cli node get <NAME>
$ cli node link <SRC> <TARGET>
However, I was able to come up with some code that works:
#[derive(Subcommand)]
enum Commands {
/// Node operations
Node(NodeCommand),
}
#[derive(Args)]
struct NodeCommand {
/// Optional node name used with certain subcommands, e.g. `node <NAME> link <TARGET>`
#[arg(value_name = "NAME")]
name: Option<String>,
/// Node subcommands (e.g. `create`, `get`, `link`)
///
/// For `link`, the NAME should be provided before the subcommand:
/// `dcli node <NAME> link <TARGET>`
#[command(subcommand)]
action: Option<NodeAction>,
}
#[derive(Subcommand)]
enum NodeAction {
Create { name: String },
Get { name: String },
Link { target: String },
}
then in your main function:
let cli = Cli::parse();
match cli.command {
Commands::Node(node_cmd) => {
match node_cmd.action {
Some(NodeAction::Create { name }) => {
println!("Created node '{}'", name);
todo!()
}
Some(NodeAction::Get { name }) => {
println!(name);
todo!()
}
Some(NodeAction::Link { target }) => {
// NAME must come before the subcommand for `link`
let Some(name) = node_cmd.name.as_deref() else {
// name wasn't given, which is bad.
std::process::exit(2);
};
println!("Linked '{}' -> '{}'", name, target);
todo!()
}
None => {
// No subcommand supplied; provide a helpful message.
todo!()
std::process::exit(2);
}
}
}
}