How to refer to the program name in help strings?

990 Views Asked by At

In my CLI program the usage examples are provided as part of the help message. Using the clap derive interface I can do the following

#[derive(Parser, Debug, Default)]
#[clap( after_help = "EXAMPLES:\n  $ foo abc.txt")]
pub struct CmdLine {...}

The program name foo is hard coded in the literal string above.

How can I avoid hard-coding the program name and get it dynamically; for example, from std::env::args[0] or clap::App:get_bin_name() ?

2

There are 2 best solutions below

6
On

clap provides a macro called crate_name! that will take the name from your cargo.toml.

For example, suppose you have this in your cargo.toml.

[package]
name = "myapp"
description = "myapp description"
version = "0.1.0"
edition = "2021"
authors = [ "John Doe" ]

Then, in your application, you can fetch these values using the macros, like this:

    let matches = Command::new(clap::crate_name!())
        .version(clap::crate_version!())
        .author(clap::crate_authors!())
        .about(clap::crate_description!())
        //
        // abbreviated
        //

The section below is appended to respond to the original poster's specific question. See the comments below for context. Also, including some learnings as well.


Appended per the discussion in comments.

Based on the comments/discussion below, initial thought is just to stuff the binary name from the arguments into a string and pass into the after_help() function. For example, something like this:

let bin_name = std::env:args().into_iter().next().unwrap();
let matches = Command::new(bin_name)
    .after_help(format!("Text that includes {}", bin_name)) // This won't compile
    .get_matches();

Taking this approach, you quickly run into a lifetime requirement in the function signature for after_help(). From clap's repo:

pub fn after_help<S: Into<&'help str>>(mut self, help: S)

In fact, if you look, there are many fields in the Command struct that have the lifetime annotation (&'help) on them. The Command::new() method doesn't have this lifetime annotation so it worked fine to just pass it bin_name as shown above.

Below is an abbreviated solution that dynamically generates after-help text in a manner that adheres to the lifetime requirements. Assuming a clean binary (application), called "foo", add the following code:

cargo.toml

[package]
name = "foo"
version = "0.1.0"
description = "A foo cli application"
authors = [ "John Doe" ]
edition = "2021"

[dependencies]
clap = { version = "3.1.6", features = ["cargo"] }

main.rs

fn main() {
    // Get the binary name from the command line
    let bin_name = std::env::args().into_iter().next().unwrap();

    // Construct text that will be used in after_help.
    let after_help_text = format!(
        "Some after-help text that includes the binary name: {}",
        bin_name
    );

    // clap, by default, will reference the name of your package. So, if you're
    // doing the above, you might as well override the usage text too so you're
    // being consistent.
    let usage_text = format!("{}", bin_name);

    if let Err(e) = foo::get_args(bin_name, after_help_text, usage_text).and_then(foo::run) {
        eprintln!("{e}");
        std::process::exit(1);
    }
}

lib.rs

use clap::{ArgMatches, Command};

pub fn get_args(
    bin_name: String,
    after_help_text: String,
    usage_text: String,
) -> std::io::Result<ArgMatches> {
    let matches = Command::new(bin_name)
        .override_usage(usage_text.as_str())
        .version(clap::crate_version!())
        .after_help(after_help_text.as_str())
        .author(clap::crate_authors!())
        .about(clap::crate_description!())
        // add and configure args...
        .get_matches();

    Result::Ok(matches)
}

pub fn run(matches: ArgMatches) -> std::io::Result<()> {
    // Do your CLI logic here based on matches.
    Ok(())
}

Running the solution ( cargo run -- --help ) will produce the following output:

./foo 0.1.0
John Doe
A foo cli application

USAGE:
    ./foo

OPTIONS:
    -h, --help       Print help information
    -V, --version    Print version information

Some after-help text that includes the binary name: ./foo
0
On

I ran into the same problem and eventually stumbled on Cargo environment variables. Here is the description of the variable of interest:

CARGO_CRATE_NAME — The name of the crate that is currently being compiled. It is the name of the Cargo target with - converted to _, such as the name of the library, binary, example, integration test, or benchmark.

Here is a trivial sample project which demonstrates how to use it to solve the problem you presented:

# tree sample/
sample/
├── Cargo.lock
├── Cargo.toml
└── src
    └── bin
        └── different-name.rs

3 directories, 3 files

Cargo.toml

[package]
name = "sample"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.4.0", features = ["derive"] }

different-name.rs

use clap::Parser;

#[derive(Parser)]
#[command(name = env!("CARGO_CRATE_NAME"))]
struct Args {}

fn main() {
    Args::parse();
}

Running the solution (cargo run -- --help) produces the following output:

Usage: different-name

Options:
  -h, --help  Print help

Note that the env!() macro expands to a static string literal slice at compile time (see edit below). There are similar options for evaluating at runtime, though you probably want compile time for your use case.

My use case is a cargo project containing multiple binary crates automating application packaging and deployment, so I can confirm this works correctly on a per-crate basis.


Edit: I just wanted to clarify a point about the env!() macro. It does execute at compile time, but it returns a &'static str, which is a static string slice, not a static string literal. For most purposes, there isn't much difference, but some things like the concat!() macro only accept literals. If you're like me and want to create const-like variables using values retrieved with env!(), you can use the lazy_static crate.