socktop/docs/CLAP_MIGRATION.md
jasonwitty f82a5903b8
Some checks failed
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
WIP: Man pages generation with clap_mangen
2025-11-20 23:35:29 -08:00

9.9 KiB

Migration to Clap for Auto-Generated Man Pages

Summary

Socktop has been migrated from manual argument parsing to clap (Command Line Argument Parser) with automatic man page generation via clap_mangen. This provides several benefits:

Auto-generated man pages - Always in sync with CLI
Better help output - Rich, formatted --help text
Type safety - Compile-time checking of arguments
Environment variable support - Built-in env var integration
Shell completions - Easy to add bash/zsh/fish completions
Single source of truth - CLI definitions generate everything

What Changed

Before (Manual Parsing)

// Old approach: Manual argument parsing
fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
    let mut it = args.into_iter();
    let prog = it.next().unwrap_or_else(|| "socktop".into());
    let mut url: Option<String> = None;
    let mut tls_ca: Option<String> = None;
    // ... lots of manual parsing code ...
    while let Some(arg) = it.next() {
        match arg.as_str() {
            "-h" | "--help" => {
                return Err(format!("Usage: {prog} ..."));
            }
            "--tls-ca" | "-t" => {
                tls_ca = it.next();
            }
            // ... more matches ...
        }
    }
    Ok(ParsedArgs { url, tls_ca, ... })
}

Problems:

  • Manual parsing is error-prone
  • Help text gets out of sync
  • No automatic man page generation
  • Duplicated logic for --flag and --flag=value
  • No environment variable support
  • Hard to test

After (Clap Derive)

// New approach: Clap derive macros
use clap::Parser;

#[derive(Parser, Debug)]
#[command(
    name = "socktop",
    version,
    author,
    about = "Remote system monitor with a rich TUI over WebSocket",
    long_about = "socktop is a remote system monitor..."
)]
pub struct Cli {
    /// WebSocket URL to connect to
    #[arg(value_name = "URL")]
    pub url: Option<String>,

    /// Path to TLS certificate PEM file for WSS connections
    #[arg(short = 't', long = "tls-ca", value_name = "CERT_PEM")]
    pub tls_ca: Option<String>,
    
    // ... more fields ...
}

// Usage:
let cli = Cli::parse();

Benefits:

  • Declarative and concise
  • Auto-generated help and man pages
  • Type-safe argument parsing
  • Automatic support for --flag and --flag=value
  • Built-in env var support with env attribute
  • Easy to test

Files Modified

Core Changes

  1. socktop/Cargo.toml - Added clap dependencies

  2. socktop/src/cli.rs - NEW: CLI definition using clap derive

  3. socktop/src/main.rs - Updated to use Cli::parse_args()

  4. socktop/build.rs - NEW: Auto-generates man pages at build time

  5. socktop_agent/Cargo.toml - Added clap dependencies

  6. socktop_agent/src/cli.rs - NEW: CLI definition using clap derive

  7. socktop_agent/src/main.rs - Updated to use Cli::parse_args()

  8. socktop_agent/build.rs - Updated to auto-generate man pages

Documentation

  1. docs/AUTO_MAN_PAGES.md - Comprehensive guide to auto-generated man pages
  2. docs/CLAP_MIGRATION.md - This file
  3. README.md - Updated Man Pages section
  4. scripts/install-with-man.sh - NEW: Installation script that includes man pages

Man Page Generation

How It Works

  1. Build Time - When you run cargo build, the build.rs script:

    • Includes the CLI definition from src/cli.rs
    • Creates a clap Command instance
    • Uses clap_mangen to generate a man page
    • Saves it to $OUT_DIR/man/*.1
  2. Location - Generated man pages are at:

    target/release/build/socktop-*/out/man/socktop.1
    target/release/build/socktop_agent-*/out/man/socktop_agent.1
    
  3. Installation - Use the installation script:

    ./scripts/install-with-man.sh         # User install
    sudo ./scripts/install-with-man.sh --system   # System install
    
  4. Viewing - After installation:

    man socktop
    man socktop_agent
    

Man Page Content

The man pages include:

  • NAME - From about attribute
  • SYNOPSIS - Auto-generated from arguments
  • DESCRIPTION - From long_about attribute
  • OPTIONS - From field doc comments and #[arg(...)] attributes
  • VERSION - From Cargo.toml
  • AUTHORS - From Cargo.toml

CLI Definition Format

Structure

use clap::Parser;

#[derive(Parser, Debug)]
#[command(
    name = "myapp",
    version,                    // Uses Cargo.toml version
    author,                     // Uses Cargo.toml authors
    about = "Short description",
    long_about = "Longer description for man page and --help"
)]
pub struct Cli {
    /// Short description
    ///
    /// Longer description that appears in the man page.
    /// Multiple paragraphs supported.
    #[arg(short = 't', long = "thing", value_name = "VALUE")]
    pub thing: Option<String>,
}

Common Attributes

Attribute Purpose Example
short = 'x' Short flag -x
long = "example" Long flag --example
value_name = "FOO" Display name --thing <FOO>
env = "VAR" Environment variable env = "MY_VAR"
default_value = "x" Default value Default: "x"
hide = true Hide from help/man For internal options
value_parser = func Custom parser Validation

Environment Variables

/// Port to listen on
///
/// Can be set via SOCKTOP_PORT environment variable.
#[arg(short = 'p', long = "port", env = "SOCKTOP_PORT")]
pub port: Option<u16>,

This automatically:

  • Checks the environment variable
  • Shows [env: SOCKTOP_PORT=] in help
  • Documents it in the man page

Testing

Unit Tests

Both CLI modules include comprehensive unit tests:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_parsing() {
        let cli = Cli::try_parse_from(&["socktop", "ws://localhost:8080/ws"]).unwrap();
        assert_eq!(cli.url, Some("ws://localhost:8080/ws".to_string()));
    }

    #[test]
    fn test_tls_options() {
        let cli = Cli::try_parse_from(&[
            "socktop",
            "-t", "/path/to/cert.pem",
            "--verify-hostname",
            "wss://example.com:8443/ws",
        ]).unwrap();
        assert_eq!(cli.tls_ca, Some("/path/to/cert.pem".to_string()));
        assert!(cli.verify_hostname);
    }
}

Run tests with:

cargo test --package socktop cli::
cargo test --package socktop_agent cli::

Manual Testing

Test the help output:

cargo run --package socktop -- --help
cargo run --package socktop_agent -- --help

Test argument parsing:

cargo run --package socktop -- ws://localhost:8080/ws
cargo run --package socktop -- -t cert.pem wss://localhost:8443/ws
cargo run --package socktop_agent -- --port 8080
cargo run --package socktop_agent -- --enableSSL

Test environment variables:

SOCKTOP_PORT=9000 cargo run --package socktop_agent
SOCKTOP_ENABLE_SSL=1 cargo run --package socktop_agent

Backwards Compatibility

Command-Line Interface

Fully compatible - All existing command-line arguments work exactly the same:

# Still works
socktop -t cert.pem wss://host:8443/ws
socktop --profile myprofile
socktop --demo

socktop_agent --port 8080
socktop_agent --enableSSL

Environment Variables

Fully compatible - All environment variables still work:

SOCKTOP_PORT=8080 socktop_agent
SOCKTOP_ENABLE_SSL=1 socktop_agent

Breaking Changes

None - This is a drop-in replacement for the old parser.

Comparison: Manual vs Clap

Feature Manual Parsing Clap
Code lines ~120 lines ~50 lines
Man pages Separate files Auto-generated
Help text Hardcoded strings Auto-generated
Type safety Runtime errors Compile-time
Env vars Manual std::env::var Built-in env attribute
Testing Hard to test Easy with try_parse_from
Maintenance High Low
Consistency Can drift Always in sync
Completions Manual Auto-generate

Future Enhancements

Now that we're using clap, we can easily add:

Shell Completions

// In build.rs
use clap_complete::{generate_to, shells::*};

let cmd = Cli::command();
generate_to(Bash, &mut cmd, "socktop", &out_dir)?;
generate_to(Zsh, &mut cmd, "socktop", &out_dir)?;
generate_to(Fish, &mut cmd, "socktop", &out_dir)?;

Subcommands

#[derive(Parser)]
enum Commands {
    /// Connect to an agent
    Connect {
        #[arg(value_name = "URL")]
        url: String,
    },
    /// List saved profiles
    Profiles,
    /// Run demo mode
    Demo,
}

Better Validation

#[arg(value_parser = clap::value_parser!(u16).range(1..=65535))]
pub port: Option<u16>,

Custom Help Sections

#[command(
    after_help = "EXAMPLES:\n  socktop ws://localhost:8080/ws\n  socktop --demo"
)]

Migration Checklist

If migrating other Rust projects to clap:

  • Add clap and clap_mangen dependencies
  • Create src/cli.rs with derive macros
  • Update main.rs to use Cli::parse()
  • Create/update build.rs for man page generation
  • Write unit tests for CLI parsing
  • Test all existing command-line arguments
  • Test environment variables
  • Update documentation
  • Create installation scripts for man pages
  • Consider adding shell completions

Resources

Conclusion

The migration to clap provides:

  • Better developer experience
  • Auto-generated, always-in-sync documentation
  • Reduced maintenance burden
  • Professional-quality help output and man pages
  • Foundation for future enhancements (completions, subcommands)

All with zero breaking changes to the existing CLI.