mod args;
#[cfg(test)]
mod tests;
use core::fmt;
use std::{env, error::Error, str::FromStr};
use args::Args;
#[derive(Debug, Clone)]
pub enum HelpReason {
UserAsked,
MissingAction,
MissingOption(CLIOption),
MissingArgument(usize, usize),
}
#[derive(Debug, Clone)]
pub enum CommandError {
MissingOption(CLIOption),
MissingArgument(usize, usize),
}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingOption(option) => write!(f, "Missing option: {:#?}", option),
Self::MissingArgument(start, end) => {
write!(f, "Missing arguments from {start} to {end}")
}
}
}
}
impl Error for CommandError {}
#[derive(Debug, Clone)]
pub struct CLIOption {
pub names: Vec<String>,
pub desc: String,
pub required: bool,
}
#[derive(Debug, Clone)]
pub struct CLIArgument {
pub desc: String,
pub required: bool,
pub array: bool,
}
pub struct Command {
names: Vec<String>,
action: Option<Box<dyn Fn(Args) -> Result<(), Box<dyn Error + Send + Sync>>>>,
help: Option<Box<dyn Fn(HelpReason, &Command, Args)>>,
desc: Option<String>,
children: Vec<Command>,
options: Vec<CLIOption>,
arguments: Vec<CLIArgument>,
}
impl Command {
pub fn new(name: &str) -> Command {
Command {
names: vec![name.to_string()],
desc: None,
children: Vec::new(),
options: Vec::new(),
arguments: Vec::new(),
action: None,
help: None,
}
}
pub fn action<T: Fn(Args) -> Result<(), Box<dyn Error + Send + Sync>> + 'static>(
&mut self,
action: T,
) -> &mut Self {
self.action = Some(Box::new(action));
self
}
pub fn help<T: Fn(HelpReason, &Command, Args) -> () + 'static>(
&mut self,
action: T,
) -> &mut Self {
self.help = Some(Box::new(action));
self
}
pub fn desc(&mut self, desc: &str) -> &mut Self {
self.desc = Some(desc.to_string());
self
}
pub fn alias(&mut self, alias: &str) -> &mut Self {
self.names.push(alias.to_string());
self
}
pub fn option(&mut self, names: &str, desc: &str) -> &mut Self {
let split = names.split(",");
self.options.push(CLIOption {
names: split.map(|a| a.trim().to_string()).collect(),
desc: desc.to_string(),
required: true,
});
self
}
pub fn argument(&mut self, desc: &str) -> &mut Self {
self.arguments.push(CLIArgument {
desc: desc.to_string(),
required: true,
array: false,
});
self
}
pub fn array_argument(&mut self, desc: &str) -> &mut Self {
self.arguments.push(CLIArgument {
desc: desc.to_string(),
required: false,
array: true,
});
self
}
pub fn opt_option(&mut self, names: &str, desc: &str) -> &mut Self {
let split = names.split(",");
self.options.push(CLIOption {
names: split.map(|a| a.trim().to_string()).collect(),
desc: desc.to_string(),
required: false,
});
self
}
pub fn opt_argument(&mut self, desc: &str) -> &mut Self {
self.arguments.push(CLIArgument {
desc: desc.to_string(),
required: false,
array: false,
});
self
}
pub fn add(&mut self, other: Command) -> &mut Self {
self.children.push(other);
self
}
pub fn command(&mut self, name: &str) -> &mut Command {
let command = Command::new(name);
self.children.push(command);
self.children.last_mut().unwrap()
}
pub fn run(&self, args: Vec<String>) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
let (command, args, help_option) = Args::parse(self, args);
if args.has("--help") {
let reason = HelpReason::MissingAction;
match help_option {
Some(help) => help(reason, command, args),
None => command.default_help(reason),
}
return Ok(());
}
for option in command.options.iter() {
if !option.required {
continue;
}
let mut found = false;
for name in option.names.iter() {
if args.has(name) {
found = true;
break;
}
}
if !found {
let reason = HelpReason::MissingOption(option.clone());
match help_option {
Some(help) => help(reason, command, args),
None => command.default_help(reason),
}
return Err(Box::new(CommandError::MissingOption(option.clone())));
}
}
for (pos, arg) in command.arguments.iter().enumerate() {
if arg.required && !args.has_at(pos) {
let end = if arg.array {
command.arguments.len()
} else {
pos
};
let reason = HelpReason::MissingArgument(pos, end);
match help_option {
Some(help) => help(reason, command, args),
None => command.default_help(reason),
}
return Err(Box::new(CommandError::MissingArgument(pos, end)));
}
}
match &command.action {
Some(action) => action(args),
None => {
let reason = HelpReason::MissingAction;
match help_option {
Some(help) => help(reason, command, args),
None => command.default_help(reason),
}
Ok(())
}
}
}
pub fn run_str(&self, args: Vec<&str>) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
self.run(args.iter().map(|arg| arg.to_string()).collect())
}
pub fn run_env(&self) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
self.run(env::args().skip(1).collect())
}
fn default_help(&self, reason: HelpReason) {
match &reason {
HelpReason::MissingAction | HelpReason::UserAsked => {
println!("{}", self.generate_help());
}
HelpReason::MissingArgument(start, end) => {
eprintln!(
"missing argument from positions {} to {}!",
start + 1,
end + 1
);
eprintln!("{}", self.generate_help());
}
HelpReason::MissingOption(option) => {
eprintln!("missing option {}!", option.names.join(" or "));
eprintln!("{}", self.generate_help())
}
}
}
pub fn generate_help(&self) -> String {
let mut builder = String::new();
builder.push_str(&format!("usage:{}\n", self.generate_usage(" ")));
builder.push_str(&format!("arguments:\n{}", self.generate_args("\t", "\n")));
builder.push_str(&format!("options:\n{}", self.generate_opts("\t", "\n")));
builder.push_str(&format!(
"commands:\n{}",
self.generate_sub_commands("\t", "\n")
));
builder
}
pub fn generate_usage(&self, prefix: &str) -> String {
let mut builder = String::from_str(prefix).unwrap();
builder.push_str(&self.names.get(0).unwrap());
if self.options.len() > 0 {
builder.push_str(" [--options]");
}
if self.arguments.len() > 0 {
builder.push_str(" [<arguments>]");
}
if self.children.len() > 0 {
builder.push_str(" <command>");
}
builder
}
pub fn generate_args(&self, prefix: &str, separator: &str) -> String {
let mut builder = String::new();
for (i, arg) in self.arguments.iter().enumerate() {
builder.push_str(&format!(
"{}{}: {}{}{}",
prefix,
if arg.array {
if i != 0 {
"<everything else>".to_string()
} else {
"all arguments".to_string()
}
} else {
format!("#{i}")
},
arg.desc,
if arg.required { " (required)" } else { "" },
separator
));
}
builder
}
pub fn generate_opts(&self, prefix: &str, separator: &str) -> String {
let mut builder = String::new();
for opt in &self.options {
builder.push_str(&format!(
"{}{}: {} ({}){}",
prefix,
opt.names.join(", "),
opt.desc,
if opt.required {
"required"
} else {
"not required"
},
separator
));
}
builder
}
pub fn generate_sub_commands(&self, prefix: &str, separator: &str) -> String {
let mut builder = String::new();
for command in &self.children {
builder.push_str(&format!(
"{}{}: {}{}",
prefix,
command.names.join(", "),
command
.desc
.clone()
.unwrap_or("(no description)".to_string()),
separator,
));
}
builder
}
}