use std::ffi::{OsStr, OsString};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgType {
Flag,
Option,
Positional,
}
#[derive(Debug, Clone)]
pub struct Arg {
pub arg_type: ArgType,
pub short: Option<&'static str>,
pub long: Option<&'static str>,
pub required: bool,
pub value: Option<OsString>,
}
impl Arg {
pub fn flag(short: &'static str, long: &'static str) -> Self {
Self {
arg_type: ArgType::Flag,
short: Some(short),
long: Some(long),
required: true,
value: None,
}
}
pub fn optional_flag(short: &'static str, long: &'static str) -> Self {
Self {
arg_type: ArgType::Flag,
short: Some(short),
long: Some(long),
required: false,
value: None,
}
}
pub fn option(short: &'static str, long: &'static str) -> Self {
Self {
arg_type: ArgType::Option,
short: Some(short),
long: Some(long),
required: true,
value: None,
}
}
pub fn optional_option(short: &'static str, long: &'static str) -> Self {
Self {
arg_type: ArgType::Option,
short: Some(short),
long: Some(long),
required: false,
value: None,
}
}
pub fn positional() -> Self {
Self {
arg_type: ArgType::Positional,
short: None,
long: None,
required: true,
value: None,
}
}
pub fn optional_positional() -> Self {
Self {
arg_type: ArgType::Positional,
short: None,
long: None,
required: false,
value: None,
}
}
#[must_use]
pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Self {
self.value = Some(value.as_ref().to_os_string());
self
}
pub fn to_strings(&self) -> Vec<OsString> {
let mut result = Vec::new();
match self.arg_type {
ArgType::Flag => {
if let Some(short) = self.short {
result.push(OsString::from(short));
}
}
ArgType::Option => {
if let Some(short) = self.short {
if let Some(ref value) = self.value {
result.push(OsString::from(short));
result.push(value.clone());
}
}
if let Some(long) = self.long {
if let Some(ref value) = self.value {
let mut long_arg = OsString::from(long);
long_arg.push("=");
long_arg.push(value);
result.push(long_arg);
}
}
}
ArgType::Positional => {
if let Some(ref value) = self.value {
result.push(value.clone());
}
}
}
result
}
}
#[derive(Debug, Default)]
pub struct Args {
args: Vec<Arg>,
positional: Vec<Arg>,
}
impl Args {
pub fn new() -> Self {
Self::default()
}
pub fn flag(mut self, short: &'static str, long: &'static str) -> Self {
self.args.push(Arg::flag(short, long));
self
}
pub fn optional_flag(mut self, short: &'static str, long: &'static str) -> Self {
self.args.push(Arg::optional_flag(short, long));
self
}
pub fn option(mut self, short: &'static str, long: &'static str) -> Self {
self.args.push(Arg::option(short, long));
self
}
pub fn optional_option(mut self, short: &'static str, long: &'static str) -> Self {
self.args.push(Arg::optional_option(short, long));
self
}
pub fn positional(mut self) -> Self {
self.positional.push(Arg::positional());
self
}
pub fn optional_positional(mut self) -> Self {
self.positional.push(Arg::optional_positional());
self
}
pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Result<Self, String> {
for arg in self.args.iter_mut().rev() {
if arg.arg_type == ArgType::Option && arg.value.is_none() {
arg.value = Some(value.as_ref().to_os_string());
return Ok(self);
}
}
for arg in self.positional.iter_mut().rev() {
if arg.value.is_none() {
arg.value = Some(value.as_ref().to_os_string());
return Ok(self);
}
}
Err("No argument expects a value".to_string())
}
pub fn build(self) -> Vec<OsString> {
let mut result = Vec::new();
for arg in &self.args {
result.extend(arg.to_strings());
}
for arg in &self.positional {
result.extend(arg.to_strings());
}
result
}
pub fn validate(&self) -> Result<(), String> {
for arg in &self.args {
if arg.required && arg.value.is_none() && arg.arg_type != ArgType::Flag {
let arg_name = arg.long.unwrap_or(arg.short.unwrap_or("unknown"));
return Err(format!("Missing required argument: {arg_name}"));
}
}
for arg in &self.positional {
if arg.required && arg.value.is_none() {
return Err("Missing required positional argument".to_string());
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flag() {
let args = Args::new().flag("-v", "--verbose").build();
assert!(args.contains(&OsString::from("-v")));
}
#[test]
fn test_option() {
let args = Args::new()
.option("-o", "--output")
.value("file.txt")
.unwrap()
.build();
assert!(args.contains(&OsString::from("-o")));
assert!(args.contains(&OsString::from("file.txt")));
}
#[test]
fn test_positional() {
let args = Args::new().positional().value("file.txt").unwrap().build();
assert!(args.contains(&OsString::from("file.txt")));
}
#[test]
fn test_validate_success() {
let args = Args::new()
.flag("-v", "--verbose")
.option("-o", "--output")
.value("file.txt")
.unwrap();
assert!(args.validate().is_ok());
}
#[test]
fn test_validate_failure() {
let args = Args::new()
.option("-o", "--output")
.value("file.txt")
.unwrap()
.positional();
assert!(args.validate().is_err());
}
#[test]
fn test_value_no_target() {
let result = Args::new().value("test");
assert!(result.is_err());
}
}