logo
Available on crate feature unstable-doc only.
Expand description

Example: git-like CLI (Derive API)

use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::PathBuf;

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

/// A fictional versioning CLI
#[derive(Debug, Parser)] // requires `derive` feature
#[command(name = "git")]
#[command(about = "A fictional versioning CLI", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Clones repos
    #[command(arg_required_else_help = true)]
    Clone {
        /// The remote to clone
        remote: String,
    },
    /// Compare two commits
    Diff {
        #[arg(value_name = "COMMIT")]
        base: Option<OsString>,
        #[arg(value_name = "COMMIT")]
        head: Option<OsString>,
        #[arg(last = true)]
        path: Option<OsString>,
    },
    /// pushes things
    #[command(arg_required_else_help = true)]
    Push {
        /// The remote to target
        remote: String,
    },
    /// adds things
    #[command(arg_required_else_help = true)]
    Add {
        /// Stuff to add
        #[arg(required = true)]
        path: Vec<PathBuf>,
    },
    Stash(Stash),
    #[command(external_subcommand)]
    External(Vec<OsString>),
}

#[derive(Debug, Args)]
#[command(args_conflicts_with_subcommands = true)]
struct Stash {
    #[command(subcommand)]
    command: Option<StashCommands>,

    #[command(flatten)]
    push: StashPush,
}

#[derive(Debug, Subcommand)]
enum StashCommands {
    Push(StashPush),
    Pop { stash: Option<String> },
    Apply { stash: Option<String> },
}

#[derive(Debug, Args)]
struct StashPush {
    #[arg(short, long)]
    message: Option<String>,
}

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

    match args.command {
        Commands::Clone { remote } => {
            println!("Cloning {}", remote);
        }
        Commands::Diff {
            mut base,
            mut head,
            mut path,
        } => {
            if path.is_none() {
                path = head;
                head = None;
                if path.is_none() {
                    path = base;
                    base = None;
                }
            }
            let base = base
                .as_deref()
                .map(|s| s.to_str().unwrap())
                .unwrap_or("stage");
            let head = head
                .as_deref()
                .map(|s| s.to_str().unwrap())
                .unwrap_or("worktree");
            let path = path.as_deref().unwrap_or_else(|| OsStr::new(""));
            println!("Diffing {}..{} {}", base, head, path.to_string_lossy());
        }
        Commands::Push { remote } => {
            println!("Pushing to {}", remote);
        }
        Commands::Add { path } => {
            println!("Adding {:?}", path);
        }
        Commands::Stash(stash) => {
            let stash_cmd = stash.command.unwrap_or(StashCommands::Push(stash.push));
            match stash_cmd {
                StashCommands::Push(push) => {
                    println!("Pushing {:?}", push);
                }
                StashCommands::Pop { stash } => {
                    println!("Popping {:?}", stash);
                }
                StashCommands::Apply { stash } => {
                    println!("Applying {:?}", stash);
                }
            }
        }
        Commands::External(args) => {
            println!("Calling out to {:?} with {:?}", &args[0], &args[1..]);
        }
    }

    // Continued program logic goes here...
}

This requires enabling the derive feature flag.

Git is an example of several common subcommand patterns.

Help:

$ git-derive
? failed
A fictional versioning CLI

Usage: git-derive[EXE] <COMMAND>

Commands:
  clone  Clones repos
  diff   Compare two commits
  push   pushes things
  add    adds things
  stash  
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help information

$ git-derive help
A fictional versioning CLI

Usage: git-derive[EXE] <COMMAND>

Commands:
  clone  Clones repos
  diff   Compare two commits
  push   pushes things
  add    adds things
  stash  
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help information

$ git-derive help add
adds things

Usage: git-derive[EXE] add <PATH>...

Arguments:
  <PATH>...  Stuff to add

Options:
  -h, --help  Print help information

A basic argument:

$ git-derive add
? failed
adds things

Usage: git-derive[EXE] add <PATH>...

Arguments:
  <PATH>...  Stuff to add

Options:
  -h, --help  Print help information

$ git-derive add Cargo.toml Cargo.lock
Adding ["Cargo.toml", "Cargo.lock"]

Default subcommand:

$ git-derive stash -h
Usage: git-derive[EXE] stash [OPTIONS]
       git-derive[EXE] stash <COMMAND>

Commands:
  push   
  pop    
  apply  
  help   Print this message or the help of the given subcommand(s)

Options:
  -m, --message <MESSAGE>  
  -h, --help               Print help information

$ git-derive stash push -h
Usage: git-derive[EXE] stash push [OPTIONS]

Options:
  -m, --message <MESSAGE>  
  -h, --help               Print help information

$ git-derive stash pop -h
Usage: git-derive[EXE] stash pop [STASH]

Arguments:
  [STASH]  

Options:
  -h, --help  Print help information

$ git-derive stash -m "Prototype"
Pushing StashPush { message: Some("Prototype") }

$ git-derive stash pop
Popping None

$ git-derive stash push -m "Prototype"
Pushing StashPush { message: Some("Prototype") }

$ git-derive stash pop
Popping None

External subcommands:

$ git-derive custom-tool arg1 --foo bar
Calling out to "custom-tool" with ["arg1", "--foo", "bar"]

Last argument:

$ git-derive diff --help
Compare two commits

Usage: git-derive[EXE] diff [COMMIT] [COMMIT] [-- <PATH>]

Arguments:
  [COMMIT]  
  [COMMIT]  
  [PATH]    

Options:
  -h, --help  Print help information

$ git-derive diff
Diffing stage..worktree 

$ git-derive diff ./src
Diffing stage..worktree ./src

$ git-derive diff HEAD ./src
Diffing HEAD..worktree ./src

$ git-derive diff HEAD~~ -- HEAD
Diffing HEAD~~..worktree HEAD