rustclap

clap command or entity name


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.


Solution

  • 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);
                }
            }
        }
    }