use std::collections::HashMap; use std::ffi::OsStr; use std::ffi::OsString; use std::path::PathBuf; use clap::{Args, Parser, Subcommand, ValueEnum}; /// 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, #[arg(value_name = "COMMIT")] head: Option, #[arg(last = true)] path: Option, #[arg( long, require_equals = true, value_name = "WHEN", num_args = 0..=1, default_value_t = ColorWhen::Auto, default_missing_value = "always", value_enum )] color: ColorWhen, }, /// 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, }, Stash(StashArgs), #[command(external_subcommand)] External(Vec), } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] enum ColorWhen { Always, Auto, Never, } impl std::fmt::Display for ColorWhen { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_possible_value() .expect("no values are skipped") .get_name() .fmt(f) } } #[derive(Debug, Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] struct StashArgs { #[command(subcommand)] command: Option, #[command(flatten)] push: StashPushArgs, } #[derive(Debug, Subcommand)] enum StashCommands { Push(StashPushArgs), Pop { stash: Option }, Apply { stash: Option }, } #[derive(Debug, Args)] struct StashPushArgs { #[arg(short, long)] message: Option, } fn aliases() -> HashMap<&'static str, Vec<&'static str>> { HashMap::from([ ("last", vec!["diff", "HEAD~", "HEAD", "--"]), ("stage", vec!["add"]), ]) } fn parse_aliases() -> Result { let args = Cli::try_parse()?; expand_aliases(args, Vec::new()) } fn expand_aliases(args: Cli, mut expanded: Vec) -> Result { let Commands::External(external_args) = &args.command else { return Ok(args); }; let Some(name) = external_args.first().and_then(|name| name.to_str()) else { return Ok(args); }; let aliases = aliases(); let Some(alias) = aliases.get(name) else { return Ok(args); }; if expanded.iter().any(|expanded| expanded == name) { return Err(clap::Error::raw( clap::error::ErrorKind::InvalidSubcommand, format!("recursive alias `{}`", expanded[0]), )); } expanded.push(name.to_owned()); let mut alias_args = vec![OsString::from("git")]; alias_args.extend(alias.iter().map(OsString::from)); alias_args.extend(external_args.iter().skip(1).cloned()); let args = Cli::try_parse_from(alias_args)?; expand_aliases(args, expanded) } fn main() { let args = parse_aliases().unwrap_or_else(|error| error.exit()); match args.command { Commands::Clone { remote } => { println!("Cloning {remote}"); } Commands::Diff { mut base, mut head, mut path, color, } => { 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 {}..{} {} (color={})", base, head, path.to_string_lossy(), color ); } 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... }