How to Create Man Pages for Rust CLI Applications

Cli
Generate man pages for Rust CLI apps using the mdbook-man crate or clap's built-in support to output standard Unix help files.

When --help isn't enough

You built a CLI tool. It works. You run it, it does the thing. Then you try to share it with a colleague who insists on reading documentation in man. They type man your-tool and get No manual entry for your-tool. You point them to --help, but the output scrolls off the screen before they can read the flags. You need a man page. Not just a text file, but a proper section 1 manual that integrates with the system, supports paging, and feels native to the Unix environment.

Rust compiles to binaries. It does not generate documentation files by default. The gap between your Rust code and a man page is a gap you have to bridge yourself. The ecosystem provides tools to automate this bridge, keeping your documentation in sync with your code so you never have to maintain two sources of truth.

What a man page actually is

A man page is a structured text document formatted for the man pager. It uses a markup language called roff (or troff). The markup is plain text with special codes for bold, italics, and indentation. The man command reads the roff file, formats it, and pipes it to a pager like less.

Man pages are organized into sections. Section 1 is for user commands. Section 2 is for system calls. Section 3 is for library functions. Section 5 is for file formats. The section number is encoded in the filename. A man page for a command named my-tool lives in a file called my-tool.1. The .1 is not an extension. It is part of the name. The man command uses the section number to decide which file to load when you type man my-tool.

Convention dictates that man pages are compressed. The file on disk is usually my-tool.1.gz. The man command decompresses the file on the fly. If you ship an uncompressed file, it works, but it wastes space and breaks convention. Compress your man pages.

Generating from help text with mdbook-man

The mdbook-man crate provides a binary tool that generates man pages from standard help text. It reads the output of --help and parses it into roff format. This approach works with any CLI library that produces standard help text, including clap, argh, and pico-args.

Install the tool globally. You do not add mdbook-man to your project dependencies. It is a build-time utility, not a runtime dependency.

cargo install mdbook-man

Generate the man page by piping your CLI's help output into the tool. The command below runs your binary, captures the help text, and writes the roff output to my-cli.1.

cargo run --bin my-cli -- --help | mdbook-man > my-cli.1

The tool looks for standard sections in the help text. It identifies OPTIONS and assumes flags follow. It identifies ARGUMENTS and assumes positional arguments follow. It converts the plain text into roff commands. The result is a valid man page file.

You can view the generated file immediately. The man command accepts a path to a file.

man ./my-cli.1

If the output looks correct, compress the file for distribution.

gzip my-cli.1

The file is now my-cli.1.gz. This is the artifact you ship.

Convention aside: Help text stability

mdbook-man relies on the structure of your help text. If you change the help format, the parser might break. Stick to standard help output. Do not use custom formatters that deviate from the expected layout. If your CLI library supports a "man page compatible" help mode, use it. clap produces standard output by default, so it works well with mdbook-man.

Generating from structure with clap

If you use clap, you can generate man pages directly from the command definition. This approach is structural. It reads the Command object and emits roff without parsing text. This is more robust than text parsing because it does not depend on help formatting.

Enable the cargo feature in clap if you are using the derive macro, or use the builder API. The clap crate includes a render_man method on Command.

use clap::{Command, Arg};

fn main() {
    let cmd = Command::new("my-cli")
        .version("0.1.0")
        .about("A demonstration CLI")
        .arg(
            Arg::new("file")
                .help("The input file to process")
                .required(true)
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("Enable verbose output")
                .action(clap::ArgAction::SetTrue)
        );

    // Render the man page to stdout
    clap_mangen::Man::new(cmd.clone())
        .render(&mut std::io::stdout())
        .expect("Failed to render man page");
}

The clap_mangen crate is the official generator for clap. Add it to your dependencies.

[dependencies]
clap = { version = "4", features = ["derive"] }
clap_mangen = "0.2"

Run the binary and redirect the output.

cargo run --bin my-cli -- --generate-man > my-cli.1

This requires a custom subcommand or flag in your code to trigger generation. The example above renders to stdout in main, which is not ideal for a real app. In practice, you add a --generate-man flag that calls the renderer and exits.

use clap::{Parser, Command};
use clap_mangen::Man;
use std::io::stdout;

#[derive(Parser)]
#[command(name = "my-cli", version, about = "A demonstration CLI")]
struct Cli {
    #[arg(help = "The input file to process")]
    file: String,

    #[arg(short, long, help = "Enable verbose output")]
    verbose: bool,

    #[arg(long, hide = true, action = clap::ArgAction::SetTrue)]
    generate_man: bool,
}

fn main() {
    let cli = Cli::parse();

    if cli.generate_man {
        let cmd = Cli::command();
        Man::new(cmd)
            .render(&mut stdout())
            .expect("Failed to render man page");
        return;
    }

    // Normal execution
    println!("Processing {}", cli.file);
}

Generate the man page by running the flag.

cargo run --bin my-cli -- --generate-man > my-cli.1

The structural approach captures all metadata from your Command definition. It includes version numbers, authors, and usage strings automatically. It is the preferred method for clap users.

The installation gap

Generating the man page is only half the work. Users need to install the file in the correct location. The man command searches directories listed in the MANPATH environment variable. The default path usually includes /usr/share/man/man1/ and /usr/local/share/man/man1/.

cargo install does not support man pages. It only installs the binary. You must provide a custom installation step. This is a common pain point for Rust CLI developers. You can solve it with a shell script, a Makefile, or a package manager.

A simple install script copies the binary and the man page to the target directories.

#!/bin/bash
set -e

PREFIX="${PREFIX:-/usr/local}"
BINDIR="${PREFIX}/bin"
MANDIR="${PREFIX}/share/man/man1"

# Install binary
install -Dm755 target/release/my-cli "$BINDIR/my-cli"

# Install man page
install -Dm644 my-cli.1.gz "$MANDIR/my-cli.1.gz"

echo "Installed my-cli and man page."

Users run this script with sudo or with a custom PREFIX. The install command creates directories as needed and sets permissions. The man page ends up in the right place. Users can then run man my-cli and see your documentation.

If you distribute via a package manager like Homebrew, Nix, or Debian packages, the package definition handles the man page installation. You just provide the .1.gz file as an artifact.

Pitfall: Section numbers matter

If you name your file my-cli.5, it goes to section 5. Users running man my-cli might not find it if the system prioritizes section 1. Always use section 1 for user commands. If you have a configuration file format, use section 5 for that. Name it my-cli.conf.5. The section number is part of the identity. Get it right.

Pitfall: build.rs limitations

You might want to generate the man page in build.rs. This is tricky. The binary does not exist yet when build.rs runs. You cannot run the binary to generate help text. You can use clap_mangen in build.rs if you re-export the Command definition, but this requires duplicating the definition or using a shared crate. Most projects generate man pages in a post-build step or in CI, not in build.rs. Keep generation separate from compilation.

Decision: when to use which tool

Use clap_mangen when you are using clap and want structural generation that derives directly from your command definition without parsing text. Use mdbook-man when you use argh, pico-args, or any CLI library that produces standard help text but lacks a dedicated man page generator. Use manual roff when you need precise control over formatting that generators cannot handle, though this is rare and error-prone. Use a custom install script when you distribute binaries directly and need to place the man page in the system directories. Use a package manager when you target Linux distributions or macOS and want the system to handle installation paths.

Generating the file is half the battle. Shipping it is the other half. Make sure your users can find the man page.

Where to go next