Man pages and shell completions can really put the finishing touches on an already great CLI app! And thankfully, when using the CLI argument parsing crate clap
, they're super easy to generate.
First, let's delve into why you would even want to put time into such a thing. Easily the biggest annoyance in my day-to-day is when I'm working with a tool that doesn't have good completions. I'm spoiled by good shell completions, so I'm left disappointed when I press TAB
and nothing comes up. Even when I'm using a tool that I'm already very used to, it's still annoying not to have that extra bit of help.
Is the flag
--new
,--create
,--init
, or something else entirely!?
Shell completions help to avoid that hassle. Just press TAB
and you're on your way!
Thie segues us on to the next piece of this article, the humble man page. In this case, man stands for manual. The man page is a documentation standard used on nearly every unix-based system. It's super handy to anyone wishing to dig deeper into your program's functionality.
We'll be using clap
, the defacto standard for command-line argument parsing in Rust. If you've written a CLI app in Rust, you've likely already used clap
(it does have close to 57 million downloads...). I'll assume that you've already written a clap
struct and it looks something like this,
#[derive(Parser)]
#[clap(author = "Hisbaan Noorani", version = "1.1.3", about = "Did You Mean: A cli spelling corrector", long_about = None)]
pub struct Cli {
pub search_term: Option<String>,
#[clap(
short = 'n',
long = "number",
default_value_t = 5,
help = "Change the number of matches printed",
long_help = "Change the number of words the..."
)]
pub number: usize,
#[clap(
short = 'v',
long = "verbose",
help = "Print verbose output",
long_help = "Print verbose output including..."
)]
pub verbose: bool,
}
There are other ways to generate your clap
arguments, but I've found this to be the least confusing and most convenient. If you need a guide on getting started with clap
, check out their documentation.
Next, I'd recommend refactoring your struct into its own file and referencing it in your main file. This will make some of the steps we have to take later on a whole lot easier. I've put mine into a file called cli.rs
, and I reference it in the main.rs
file like so:
pub mod cli;
use cli::Cli;
This cli.rs
file must be adjacent to the main.rs
file in the file hierarchy. Next, we'll create a build.rs
file. This script will run during the build process and perform any additional actions you need to do. Note that, unlike the other *.rs
files, we have to put this in the project root, adjacent to your Cargo.toml
file.
// build.rs
// Macro to include the code from `cli.rs` here.
include!("src/cli.rs");
// This function will run during the build process.
fn main() {
// Our generation code will go here.
}
Also, add a [build-dependencies]
section to your Cargo.toml
. This works just like [dependencies]
; however, it is for the build.rs
file instead of your entire project.
And now we're ready to get started with the generation. As with most things, preparation is the hardest part!
A note before we begin, the path that the man page and shell completions are generated to causes some issues with publishing to crates.io
. I'll add a solution as soon as I can figure one out. Currently, it's recommended to only generate build files in the path specified by the OUT_DIR
environment variable; however, those files can be difficult to locate in an installation script, so I've simply generated them in subdirectories of the project root.
To generate the man page, we basically just lean on the power of clap
! We use the clap_mangen
crate, so be sure to add it to the [build-dependencies]
section of your Cargo.toml
. There is very little for us to actually do. Add the following to your build.rs
, modifying project-specific values as necessary.
// build.rs
use clap_mangen::Man;
use clap::CommandFactory;
fn main() {
// Get the directory to generate to.
let man_dir = std::path::PathBuf::from(
env!("CARGO_MANIFEST_DIR")
).join("man");
// Create the directory if it doesn't exist.
std::fs::create_dir_all(&man_dir).unwrap();
// Get information from your struct.
let mut cmd = Cli::command();
cmd.set_bin_name("dym");
// Generate and write the man page.
let man = Man::new(cmd.to_owned());
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer)
.expect("Man page generation failed");
std::fs::write(man_dir.join("dym.1"), buffer)
.expect("Failed to write man page");
}
And just like that, we're done! Go ahead and run cargo build
, and you should see a new man
directory generated with a shiny new man page file inside it. You can test it with man --local-file man/name.1
The path for installing man pages to your system, place them in the /usr/share/man/man*/
folder with the permissions 644
where *
is the category your man page falls into (notice the extension on the generated file):
/dev
)/etc/passwd
There are a few different shells that the clap_complete
crate supports. You can generate shell completions for Bash
, Elvish
, Fish
, Zsh
, and even PowerShell
! We will be using the clap_complete
crate so ensure it is in the [build-dependencies]
section of your Cargo.toml
. Place the following code in your build.rs
, modifying project-specific values as necessary.
// build.rs
use clap::CommandFactory;
use clap_complete::{
generate_to,
Shell::{Bash, Elvish, Fish, PowerShell, Zsh},
};
fn main() {
// Get the directory to generate to.
let comp_dir = std::path::PathBuf::from(
env!("CARGO_MANIFEST_DIR")
).join("completions");
// Create the directory if it doesn't exist.
std::fs::create_dir_all(&comp_dir).unwrap();
// Generate shell completions.
for shell in [Bash, Elvish, Fish, PowerShell, Zsh] {
generate_to(shell, &mut cmd, "dym", &comp_dir)
.unwrap();
}
}
After running a cargo build
, you'll have a completions
directory containing a few files. How you test and install these will differ depending on your shell of choice, but here are a few examples. After installing each of these, restart your shell, and the completions should work as they do with any other program.
To test the file using Bash, simply source it. For example, source completions/dym.bash
. The installation path may differ based on your distribution, but for Arch Linux, place the dym.bash
file in the /usr/share/bash-completion/completions/
directory with the permission 644
. By convention, the .bash
suffix is removed from the file name, but that is not necessary for the completion to work.
To test the file using Fish, simply source it. For example, source completions/dym.fish
. The installation path may differ based on your distribution, but for Arch Linux, place the dym.fish
file in the /usr/share/fish/vendor_completions.d/
directory with the permission 644
.
To test the file using Zsh, run the command compdef completions/_dym dym
. This is supposed to work, but I run into permission issues. The installation path may differ based on your distribution, but for Arch Linux, place the _dym
file in the /usr/share/zsh/site-functions/
directory with the permission 644
.
Now that you have your man pages and shell completions generated make sure that you install them in any packages that you've created. If you need to know how to create a package, check out my article regarding that. There are also tools like cargo-aur and cargo-deb that will make this process easier! Good luck on your journey, and shoot me a message if this article was helpful to you!