πŸ¦€ CLI with πŸ‘

An ecosystem quick peek

Pablo COVES

Grenoble Rust Meetup :: 2024-01-11

Let’s create a new Rust πŸ¦€ project for tonight

❯ cargo new meetup
     Created binary (application) `meetup` package
❯ cd meetup

Clap

❯ cargo add clap --features derive
    Updating crates.io index
      Adding clap v4.4.11 to dependencies.
             Features:
             + color
             + derive
             + error-context
             + help
             + std
             + suggestions
             + usage
             [...]
    Updating crates.io index

Usage

options.rs

pub use clap::Parser;

[derive(Parser)]
pub struct Options;

main.rs

mod options;

use options::{Options, Parser};

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

Output πŸŽ‰

❯ cargo run -- -h
Usage: meetup

Options:
  -h, --help  Print help

Positional arguments

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    pub name: String,
}

main.rs

mod options;

use options::{Options, Parser};

fn main() {
    let options = Options::parse();

    println!("Hello {}", options.name);
}

Output

❯ cargo run -- -h
Usage: meetup <NAME>

Arguments:
  <NAME>

Options:
  -h, --help  Print help

❯ cargo run -- meetup
Hello meetup

Docstring

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    /// Name to say hello to πŸ‘‹
    pub name: String,
}

Output

❯ cargo run -- -h
Usage: meetup <NAME>

Arguments:
  <NAME>  Name to say hello to πŸ‘‹

Options:
  -h, --help  Print help

Optional arguments

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    /// Name to say hello to πŸ‘‹
    pub name: Option<String>,
}

main.rs

mod options;

use options::{Options, Parser};

fn main() {
    let options = Options::parse();

    println!("Hello {}", options.name.unwrap_or("world".to_string()));
}

Output

❯ cargo run -- -h
Arguments:
  [NAME]  Name to say hello to πŸ‘‹

Options:
  -h, --help  Print help

❯ cargo run -- meetup
Hello meetup

❯ cargo run
Hello world

Named options

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    /// Name to say hello to πŸ‘‹
    #[clap(short, long)]
    pub name: Option<String>,
}

Output

❯ cargo run -- -h
Usage: meetup [OPTIONS]

Options:
  -n, --name <NAME>  Name to say hello to πŸ‘‹
  -h, --help         Print help

❯ cargo run -- -n meetup
Hello meetup

❯ cargo run
Hello world

Default value

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    /// Name to say hello to πŸ‘‹
    #[clap(short, long, default_value_t = String::from("world"))]
    pub name: String,
}

main.rs

mod options;

use options::{Options, Parser};

fn main() {
    let options = Options::parse();

    println!("Hello {}", options.name);
}

Output

❯ cargo run -- -h
Usage: meetup [OPTIONS]

Options:
  -n, --name <NAME>  Name to say hello to πŸ‘‹ [default: world]
  -h, --help         Print help

❯ cargo run -- -n meetup
Hello meetup

❯ cargo run
Hello world

Toggle/Flag

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    /// Did you play a pokemon game ?
    #[clap(short, long)]
    pub pokemon: bool,
}

main.rs

mod options;

use options::{Options, Parser};

fn main() {
    let options = Options::parse();

    if options.pokemon {
        println!("We have so much in common!");
    }
}

Output

❯ cargo run -- -h
Usage: meetup [OPTIONS]

Options:
  -p, --pokemon      Did you play a pokemon game ?
  -h, --help         Print help

❯ cargo run -- -p
We have so much in common!

Enumerations

options.rs

pub use clap::Parser;

[derive(Debug, Parser)]
pub struct Options {
    /// Life defining choice
    pub starter: Starter,
}

[derive(Debug, Clone, clap::ValueEnum)]
pub enum Starter {
    Charmander,
    Bulbasaur,
    Squirtle,
}

main.rs

mod options;

use options::{Options, Parser, Starter::*};

fn main() {
    println!(
        "{}",
        match Options::parse().starter {
            Charmander => "Way to go pal πŸ”₯",
            Bulbasaur => "I like πŸ₯— too",
            Squirtle => "Yeah, bubbles 🫧",
        }
    )
}

Output

❯ cargo run -- -h
Usage: meetup [OPTIONS] <STARTER>

Arguments:
  <STARTER>  Life defining choice [possible values: charmander, bulbasaur, squirtle]

Options:
  -h, --help         Print help

❯ cargo run -- charmander
Way to go pal πŸ”₯

Subcommands

options.rs

pub use clap::Parser;
use clap::{Args, Subcommand, ValueEnum};

[derive(Debug, Parser)]
pub struct Options {
    /// Pokemon's generation
    #[clap(subcommand)]
    pub generation: Generation,
}
[derive(Debug, Subcommand)]
pub enum Generation {
    /// Red and Blue cartriges
    First(first::RedBlue),

    /// Gold and silver cartriges
    Second(second::GoldSilver),
}
pub mod first {
    #[derive(Debug, clap::Args)]
    pub struct RedBlue {
        /// First generations's starters
        pub starter: Starter,
    }

    #[derive(Debug, Clone, clap::ValueEnum)]
    pub enum Starter {
        Charmander,
        Bulbasaur,
        Squirtle,
    }
}
pub mod second {
    #[derive(Debug, clap::Args)]
    pub struct GoldSilver {
        /// Second generations's starters
        pub starter: Starter,
    }

    #[derive(Debug, Clone, clap::ValueEnum)]
    pub enum Starter {
        Cyndaquil,
        Totodile,
        Chikorita,
    }
}

Output

❯ cargo run -- -h
Usage: meetup <COMMAND>

Commands:
  first   Red and Blue cartriges
  second  Gold and silver cartriges
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
❯ cargo run -- first -h
Red and Blue cartriges

Usage: meetup first <STARTER>

Arguments:
  <STARTER>  First generations's starters [possible values: charmander, bulbasaur, squirtle]

Options:
  -h, --help  Print help
❯ cargo run -- second -h
Gold and silver cartriges

Usage: meetup second <STARTER>

Arguments:
  <STARTER>  Second generations's starters [possible values: cyndaquil, totodile, chikorita]

Options:
  -h, --help  Print help

Clap Complete

❯ cargo add clap_complete
    Updating crates.io index
      Adding clap_complete v4.4.5 to dependencies.
             Features:
             - debug
             - unstable-doc
             - unstable-dynamic
    Updating crates.io index

Run time

options.rs

pub use clap::Parser;
use clap::{Args, Subcommand, ValueEnum};
use clap_complete::Shell;

#[derive(Debug, Parser)]
pub struct Options {
    /// Pokemon's generation
    #[clap(subcommand)]
    pub generation: Option<Generation>,

    /// Shell to generate completion for
    pub shell: Option<Shell>,
}

main.rs

mod options;

use clap::CommandFactory;
use options::{Options, Parser};

fn main() {
    if let Some(shell) = Options::parse().shell {
        clap_complete::generate(
            shell,
            &mut Options::command(),
            "meetup",
            &mut std::io::stdout(),
        )
    }
}

Output

❯ cargo run -- bash > meetup.bash && source meetup.bash

❯ ./target/debug/meetup <TAB>
-h          bash        fish        zsh         second
--help      elvish      powershell  first       help

❯ ./target/debug/meetup first <TAB>
-h          --help      charmander  bulbasaur   squirtle

❯ ./target/debug/meetup second <TAB>
-h         --help     cyndaquil  totodile   chikorita

Build time

options.rs

pub use clap::Parser;
use clap::{Args, Subcommand, ValueEnum};

[derive(Debug, Parser)]
pub struct Options {
    /// Pokemon's generation
    #[clap(subcommand)]
    pub generation: Generation,
}

build.rs

use clap::CommandFactory;
use clap_complete::{
    generate_to,
    shells::{Bash, Zsh},
};
use std::env;
use std::io::Error;

include!("./src/options.rs");
fn main() -> Result<(), Error> {
    println!("cargo:rerun-if-changed=src/options.rs");

    if let Ok(directory) = env::var("CARGO_MANIFEST_DIR").as_ref() {
        let command = &mut Options::command();
        let name = &command.get_name().to_string();

        generate_to(Bash, command, name, directory)?;
        generate_to(Zsh, command, name, directory)?;
    }
    Ok(())
}

Output

❯ ls -lh
-rw-r--r--@ 1 pcoves  staff   6.2K Dec 27 22:38 Cargo.lock
-rw-r--r--@ 1 pcoves  staff   327B Dec 27 22:39 Cargo.toml
-rw-r--r--@ 1 pcoves  staff   542B Dec 27 22:50 build.rs
drwxr-xr-x@ 4 pcoves  staff   128B Dec 27 22:40 src
drwxr-xr-x@ 5 pcoves  staff   160B Dec 27 17:46 target
❯ cargo build
❯ ls -lh
-rw-r--r--@ 1 pcoves  staff   6.2K Dec 27 22:38 Cargo.lock
-rw-r--r--@ 1 pcoves  staff   327B Dec 27 22:39 Cargo.toml
-rw-r--r--@ 1 pcoves  staff   3.1K Dec 27 22:50 _meetup
-rw-r--r--@ 1 pcoves  staff   542B Dec 27 22:50 build.rs
-rw-r--r--@ 1 pcoves  staff   4.0K Dec 27 22:50 meetup.bash
drwxr-xr-x@ 4 pcoves  staff   128B Dec 27 22:40 src
drwxr-xr-x@ 5 pcoves  staff   160B Dec 27 17:46 target
❯ cargo run -- -h
Usage: meetup [COMMAND]

Commands:
  first   Red and Blue cartriges
  second  Gold and silver cartriges
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
❯ source meetup.bash
❯ ./target/debug/meetup
-h      --help  first   second  help

Clap mangen

❯ cargo add --build clap_mangen
    Updating crates.io index
      Adding clap_mangen v0.2.16 to build-dependencies.
             Features:
             - debug
    Updating crates.io index

build.rs

use clap::CommandFactory;
use clap_mangen::Man;
use std::{env::var, fs::write, io::Error, path::PathBuf};
include!("./src/options.rs");

fn main() -> Result<(), Error> {
    println!("cargo:rerun-if-changed=src/options.rs");
    if let Ok(directory) = var("CARGO_MANIFEST_DIR").as_ref() {
        let mut buffer: Vec<u8> = Default::default();
        Man::new(Options::command()).render(&mut buffer)?;

        write(PathBuf::from(directory).join("meetup.1"), buffer)?;
    }
    Ok(())
}

Output

meetup(1)                   General Commands Manual

NAME
       meetup

SYNOPSIS
       meetup [-h|--help] [subcommands]

DESCRIPTION
OPTIONS
       -h, --help
              Print help

SUBCOMMANDS
       meetup-first(1)
              Red and Blue cartriges

       meetup-second(1)
              Gold and silver cartriges

       meetup-help(1)
              Print this message or the help of the given subcommand(s)

                                    meetup

Questions ?

Resources