use clap::{Arg, ArgAction, ArgMatches, Command};
pub fn new(name: impl Into<String>) -> AppBuilder {
AppBuilder {
name: name.into(),
version: None,
author: None,
about: None,
args: Vec::new(),
}
}
pub fn arg(name: impl Into<String>) -> ArgBuilder {
ArgBuilder {
name: name.into(),
short: None,
long: None,
help: None,
required: false,
multiple: false,
default_value: None,
arg_type: ArgType::Positional,
}
}
pub fn flag(name: impl Into<String>) -> ArgBuilder {
ArgBuilder {
name: name.into(),
short: None,
long: None,
help: None,
required: false,
multiple: false,
default_value: None,
arg_type: ArgType::Flag,
}
}
pub fn option(name: impl Into<String>) -> ArgBuilder {
ArgBuilder {
name: name.into(),
short: None,
long: None,
help: None,
required: false,
multiple: false,
default_value: None,
arg_type: ArgType::Option,
}
}
#[derive(Debug, Clone)]
enum ArgType {
Positional,
Flag,
Option,
}
#[derive(Debug, Clone)]
pub struct AppBuilder {
name: String,
version: Option<String>,
author: Option<String>,
about: Option<String>,
args: Vec<ArgBuilder>,
}
#[derive(Debug, Clone)]
pub struct ArgBuilder {
name: String,
short: Option<String>,
long: Option<String>,
help: Option<String>,
required: bool,
multiple: bool,
default_value: Option<String>,
arg_type: ArgType,
}
impl AppBuilder {
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn about(mut self, about: impl Into<String>) -> Self {
self.about = Some(about.into());
self
}
pub fn arg(mut self, arg: ArgBuilder) -> Self {
self.args.push(arg);
self
}
pub fn parse(self) -> CliMatches {
let name: &'static str = Box::leak(self.name.into_boxed_str());
let mut cmd = Command::new(name);
if let Some(version) = self.version {
let version_static: &'static str = Box::leak(version.into_boxed_str());
cmd = cmd.version(version_static);
}
if let Some(author) = self.author {
let author_static: &'static str = Box::leak(author.into_boxed_str());
cmd = cmd.author(author_static);
}
if let Some(about) = self.about {
let about_static: &'static str = Box::leak(about.into_boxed_str());
cmd = cmd.about(about_static);
}
for arg_builder in self.args {
let name: &'static str = Box::leak(arg_builder.name.into_boxed_str());
let mut arg = Arg::new(name);
if let Some(help_str) = arg_builder.help {
let help: &'static str = Box::leak(help_str.into_boxed_str());
arg = arg.help(help);
}
if let Some(short_str) = arg_builder.short {
if let Some(c) = short_str.chars().next() {
arg = arg.short(c);
}
}
if let Some(long_str) = arg_builder.long {
let long: &'static str = Box::leak(long_str.into_boxed_str());
arg = arg.long(long);
}
match arg_builder.arg_type {
ArgType::Positional => {
arg = arg.required(arg_builder.required);
if arg_builder.multiple {
arg = arg.num_args(1..);
}
if let Some(default_str) = arg_builder.default_value {
let default: &'static str = Box::leak(default_str.into_boxed_str());
arg = arg.default_value(default);
}
}
ArgType::Flag => {
arg = arg.action(ArgAction::SetTrue);
}
ArgType::Option => {
arg = arg.required(arg_builder.required);
if arg_builder.multiple {
arg = arg.num_args(0..);
arg = arg.action(ArgAction::Append);
} else {
arg = arg.num_args(0..=1);
}
if let Some(default_str) = arg_builder.default_value {
let default: &'static str = Box::leak(default_str.into_boxed_str());
arg = arg.default_value(default);
}
}
}
cmd = cmd.arg(arg);
}
let matches = cmd.get_matches();
CliMatches { matches }
}
pub fn get_matches(self) -> CliMatches {
self.parse()
}
}
impl ArgBuilder {
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn short(mut self, short: impl Into<String>) -> Self {
self.short = Some(short.into());
self
}
pub fn long(mut self, long: impl Into<String>) -> Self {
self.long = Some(long.into());
self
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn multiple(mut self, multiple: bool) -> Self {
self.multiple = multiple;
self
}
pub fn default_value(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
}
pub struct CliMatches {
matches: ArgMatches,
}
impl CliMatches {
pub fn get(&self, name: &str) -> Option<String> {
self.matches.get_one::<String>(name).cloned()
}
pub fn value_of(&self, name: &str) -> Option<String> {
self.get(name)
}
pub fn is_present(&self, name: &str) -> bool {
self.matches.get_flag(name)
}
pub fn get_many(&self, name: &str) -> Vec<String> {
self.matches
.get_many::<String>(name)
.map(|vals| vals.map(|s| s.to_string()).collect())
.unwrap_or_default()
}
pub fn values_of(&self, name: &str) -> Option<Vec<String>> {
let vals = self
.matches
.get_many::<String>(name)
.map(|vals| vals.map(|s| s.to_string()).collect::<Vec<String>>());
vals
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_builder() {
let app = new("test")
.version("1.0")
.author("Test Author")
.about("Test app")
.arg(arg("pattern").help("Pattern to search").required(true))
.arg(flag("verbose").short("v").help("Verbose output"))
.arg(
option("threads")
.short("j")
.help("Number of threads")
.default_value("4"),
);
assert_eq!(app.name, "test");
assert_eq!(app.version, Some("1.0".to_string()));
assert_eq!(app.args.len(), 3);
}
#[test]
fn test_arg_builder() {
let arg = arg("pattern").help("Pattern").required(true);
assert_eq!(arg.name, "pattern");
assert!(arg.required);
}
#[test]
fn test_flag_builder() {
let flag = flag("verbose").short("v").help("Verbose");
assert_eq!(flag.name, "verbose");
matches!(flag.arg_type, ArgType::Flag);
}
#[test]
fn test_option_builder() {
let opt = option("threads").short("j").default_value("4");
assert_eq!(opt.name, "threads");
matches!(opt.arg_type, ArgType::Option);
}
}