use std::{fmt::Display, str::FromStr};
use crate::exec_err;
macro_rules! parse_err {
($($arg:tt)*) => {{
#[cfg(not(test))]
{
eprintln!($($arg)*);
std::process::exit(1);
}
#[cfg(test)]
panic!($($arg)*);
}};
}
#[derive(Debug, Default)]
pub(crate) struct ArgOption {
pub(crate) key: String,
pub(crate) value: Option<String>,
pub(crate) values: Option<Vec<String>>,
}
#[derive(Debug, Default)]
pub(crate) struct ArgOptionGroup {
pub(crate) command: String,
pub(crate) opts: Vec<ArgOption>,
pub(crate) args: Vec<String>,
}
#[derive(Debug, Default)]
pub(crate) struct Arg {
pub(crate) opts: Vec<ArgOption>,
pub(crate) group: Vec<ArgOptionGroup>,
}
impl Arg {
pub(crate) fn find_opt(&self, opt: &str) -> Option<&ArgOption> {
self.opts.iter().find(|s| s.key == opt)
}
}
#[derive(Debug)]
pub(crate) enum OptionType {
Array,
NoParamter,
String,
Number,
}
#[derive(Debug)]
pub(crate) struct OptionDef {
pub(crate) key: String,
pub(crate) _type: OptionType,
pub(crate) desc: String,
pub(crate) required: bool,
}
impl Display for OptionDef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "-{:10} {}", self.key, self.desc)
}
}
impl OptionDef {
pub(crate) fn create<T: Into<String>>(key: T, desc: T, t: OptionType, required: bool) -> Self {
OptionDef {
key: key.into(),
desc: desc.into(),
_type: t,
required,
}
}
pub(crate) fn over() -> Self {
OptionDef::create("y", "覆盖已存在文件", OptionType::NoParamter, false)
}
}
#[derive(Debug, Default)]
pub(crate) struct CommandOptionDef {
pub(crate) command: String,
pub(crate) opts: Vec<OptionDef>,
pub(crate) support_args: i32,
pub(crate) desc: String,
}
impl std::fmt::Display for CommandOptionDef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{:10} {}\n", self.command, self.desc)?;
for ele in &self.opts {
writeln!(f, " {}", ele)?;
}
Ok(())
}
}
fn trim_arg(value: &str) -> String {
if value.starts_with('\"') && value.ends_with('\"') {
return String::from(&value[1..value.len() - 1]);
}
if value.starts_with('\'') && value.ends_with('\'') {
return String::from(&value[1..value.len() - 1]);
}
value.to_string()
}
pub(crate) fn parse_global_arg(
args: Vec<String>,
option_def: Vec<OptionDef>,
) -> Result<(Arg, usize), String> {
let mut arg = Arg::default();
let mut current: Option<&OptionDef> = None;
for (index, ele) in args.iter().enumerate() {
if let Some(key) = ele.strip_prefix('-') {
if key == "h" {
arg.opts.push(ArgOption {
key: key.to_string(),
value: None,
..Default::default()
});
return Ok((arg, index));
}
current = option_def.iter().find(|s| s.key == key);
if current.is_some() {
if let OptionType::NoParamter = current.unwrap()._type {
current = None;
}
arg.opts.push(ArgOption {
key: key.to_string(),
value: None,
..Default::default()
})
} else if ele == "-h" {
arg.opts.push(ArgOption {
key: "h".to_string(),
value: None,
..Default::default()
});
break;
} else {
parse_err!("unsupport args {}", ele);
}
} else if let Some(cu) = &mut current {
match cu._type {
OptionType::String => {
arg.opts.last_mut().unwrap().value = Some(trim_arg(ele));
}
OptionType::Array => {
let v = arg.opts.last_mut().unwrap();
v.values.get_or_insert_with(Vec::new).push(trim_arg(ele));
}
OptionType::Number => {
if ele.parse::<isize>().is_ok() {
let v = arg.opts.last_mut().unwrap();
v.value = Some(trim_arg(ele));
} else {
parse_err!("opt -{} need a number, get {}", cu.key, ele);
}
}
_ => {}
}
current = None;
} else {
check_global_opts(&arg, &option_def[..]);
return Ok((arg, index));
}
}
check_global_opts(&arg, &option_def[..]);
Ok((arg, args.len() - 1))
}
fn check_global_opts(arg: &Arg, option_def: &[OptionDef]) {
let m = option_def
.iter()
.filter(|f| f.required)
.find(|f| !arg.opts.iter().any(|m| m.key == f.key));
if let Some(m) = m {
parse_err!("global opts -{} not set", m.key);
}
for ele in &arg.opts {
if let Some(m) = option_def.iter().find(|f| f.key == ele.key) {
match m._type {
OptionType::String => {
if ele.value.is_none() {
parse_err!("ops -{} must set value", m.key);
}
}
OptionType::Array => {
if ele.values.is_none() {
parse_err!("ops -{} must set value", m.key);
}
}
_ => {}
}
}
}
}
pub(crate) fn parse_command_arg(
arg: &mut Arg,
args: Vec<String>,
command_option_def: Vec<CommandOptionDef>,
) {
let mut current_command: Option<&CommandOptionDef> = None;
let mut current: Option<&OptionDef> = None;
for ele in args {
if let Some(key) = ele.strip_prefix('-') {
match current_command {
None => {
parse_err!("error param location {}", ele);
}
Some(com) => {
current = com.opts.iter().find(|s| s.key == key);
match current {
Some(cu) => {
match cu._type {
OptionType::NoParamter => {
if let Some(m) = arg
.group
.iter_mut()
.find(|s| s.command == com.command)
.map(|f| &mut f.opts)
{
m.push(ArgOption {
key: key.to_string(),
value: None,
values: None,
})
}
current = None;
}
_ => {
if let Some(m) = arg
.group
.iter_mut()
.find(|s| s.command == com.command)
.map(|f| &mut f.opts)
{
if m.iter().all(|v| v.key != key) {
m.push(ArgOption {
key: key.to_string(),
value: None,
values: None,
})
}
}
}
}
}
None => {
parse_err!("command {} has no param {}", com.command, ele);
}
}
}
}
} else if let Some(cu) = &mut current {
match cu._type {
OptionType::String => {
arg.group
.last_mut()
.and_then(|f| f.opts.last_mut())
.unwrap()
.value = Some(trim_arg(&ele));
current = None;
}
OptionType::Array => {
arg.group
.last_mut()
.and_then(|f| f.opts.last_mut())
.unwrap()
.values
.get_or_insert_with(Vec::new)
.push(trim_arg(&ele));
}
OptionType::Number => {
if ele.parse::<isize>().is_ok() {
arg.group
.last_mut()
.and_then(|f| f.opts.last_mut())
.unwrap()
.value = Some(trim_arg(&ele));
} else {
parse_err!("opt -{} need a number, get {}", cu.key, ele)
}
}
_ => {}
}
} else if current_command.map_or(0, |f| f.support_args) != 0 {
let mut v: Option<&mut Vec<String>> = arg.group.last_mut().map(|f| f.args.as_mut());
let support = current_command.unwrap().support_args;
if support == -1 || v.as_ref().unwrap().len() < support as usize {
v.as_mut().unwrap().push(ele);
} else {
current_command = None;
}
} else {
current_command = command_option_def.iter().find(|f| f.command == ele);
if current_command.is_none() {
parse_err!("unsupport sub command {}", ele);
}
if let Some(cmd) = current_command {
arg.group.push(ArgOptionGroup {
command: cmd.command.clone(),
opts: Vec::new(),
args: Vec::new(),
})
}
}
}
check_command_opt(arg, &command_option_def);
}
fn check_command_opt(arg: &Arg, command_option_def: &[CommandOptionDef]) {
for ele in command_option_def {
let c: Vec<&ArgOptionGroup> = arg
.group
.iter()
.filter(|s| s.command == ele.command)
.collect();
if !c.is_empty() {
for def in &ele.opts {
if def.required && !c[0].opts.iter().any(|f| f.key == def.key) {
parse_err!("sub command {} need arg -{}", ele.command, def.key)
}
}
}
}
}
pub(crate) trait OptUtil {
fn has_opt<T: AsRef<str>>(&self, opt: T) -> bool;
fn get_values<T: AsRef<str>, V: FromStr>(&self, opt: T) -> Option<Vec<V>>;
fn get_value<T: AsRef<str>, V: FromStr>(&self, opt: T) -> Option<V>;
fn get_value_or_default<T: AsRef<str>, V: FromStr>(&self, opt: T, default: V) -> V;
}
impl OptUtil for &[ArgOption] {
fn has_opt<T: AsRef<str>>(&self, opt: T) -> bool {
self.iter()
.find(|s| s.key == opt.as_ref())
.is_some_and(|_| true)
}
fn get_values<T: AsRef<str>, V: FromStr>(&self, opt: T) -> Option<Vec<V>> {
self.iter()
.find(|f| f.key == opt.as_ref())
.and_then(|f| f.values.clone())
.map(|f| {
f.iter()
.map(|v| {
V::from_str(v.as_str())
.unwrap_or_else(|_e| exec_err!("{v} type is not support"))
})
.collect::<Vec<V>>()
})
}
fn get_value<T: AsRef<str>, V: FromStr>(&self, opt: T) -> Option<V> {
self.iter()
.find(|f| f.key == opt.as_ref())
.and_then(|f| f.value.clone())
.and_then(|f| f.parse::<V>().ok())
}
fn get_value_or_default<T: AsRef<str>, V: FromStr>(&self, opt: T, default: V) -> V {
self.get_value(opt).unwrap_or(default)
}
}
#[cfg(test)]
mod tests {
use crate::cli::arg::{OptUtil, OptionType};
use super::{parse_command_arg, parse_global_arg, Arg, CommandOptionDef, OptionDef};
fn create_option_def() -> Vec<OptionDef> {
vec![
OptionDef::create("i", "输入文件,epub", OptionType::String, true),
OptionDef::over(),
OptionDef::create("l", "打开终端日志输出", OptionType::NoParamter, false),
]
}
#[test]
fn test_parse_global_args() {
let args = ["-l", "-i", "弹丸论破雾切 - 北山猛邦.epub", "nav"]
.iter()
.map(|f| f.to_string())
.collect();
let arg = parse_global_arg(args, create_option_def()).unwrap();
println!("arg={:?}", arg);
assert_eq!(2, arg.0.opts.len());
assert_eq!("i", arg.0.opts.last().unwrap().key);
assert_eq!(
"弹丸论破雾切 - 北山猛邦.epub",
arg.0.opts.last().as_ref().unwrap().value.as_ref().unwrap()
);
}
#[test]
#[should_panic(expected = "global opts -i not set")]
fn test_pase_global_check() {
let args = ["-l", "nav"].iter().map(|f| f.to_string()).collect();
let _ = parse_global_arg(args, create_option_def()).unwrap();
}
#[test]
#[should_panic(expected = "ops -i must set value")]
fn test_pase_global_check2() {
let args = ["-l", "-i"].iter().map(|f| f.to_string()).collect();
let (arg, _) = parse_global_arg(args, create_option_def()).unwrap();
println!("{:?}", arg);
}
#[test]
fn test_parse_command() {
let mut arg = Arg::default();
let mut args = ["get-info", "-all"].iter().map(|f| f.to_string()).collect();
let mut command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::NoParamter,
desc: "St".to_string(),
required: false,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
println!("{:?}", arg.group);
assert_eq!(1, arg.group.len());
assert_eq!(1, arg.group.first().unwrap().opts.len());
command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![
OptionDef {
key: "all".to_string(),
_type: OptionType::NoParamter,
desc: "St".to_string(),
required: false,
},
OptionDef {
key: "demo".to_string(),
_type: OptionType::String,
desc: "St".to_string(),
required: false,
},
],
support_args: 0,
..Default::default()
}];
args = ["get-info", "-all", "-demo", "h"]
.iter()
.map(|f| f.to_string())
.collect();
arg = Arg::default();
parse_command_arg(&mut arg, args, command_option_def);
println!("{:?}", arg.group);
assert_eq!(1, arg.group.len());
assert_eq!(2, arg.group.first().unwrap().opts.len());
assert_eq!(
"h",
arg.group
.first()
.as_ref()
.unwrap()
.opts
.last()
.as_ref()
.unwrap()
.value
.as_ref()
.unwrap()
);
}
#[test]
#[should_panic(expected = "sub command get-info need arg -all")]
fn test_parse_command_requird() {
let mut arg = Arg::default();
let args = ["get-info", "-all"].iter().map(|f| f.to_string()).collect();
let command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::NoParamter,
desc: "St".to_string(),
required: true,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
let mut arg = Arg::default();
let args = ["get-info"].iter().map(|f| f.to_string()).collect();
let command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::NoParamter,
desc: "St".to_string(),
required: true,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
}
#[test]
#[should_panic(expected = "opt -all need a number, get 302k")]
fn test_parse_command_number() {
let mut arg = Arg::default();
let args = ["get-info", "-all", "43"]
.iter()
.map(|f| f.to_string())
.collect();
let command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::Number,
desc: "St".to_string(),
required: true,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
let mut arg = Arg::default();
let args = ["get-info", "-all", "302k"]
.iter()
.map(|f| f.to_string())
.collect();
let command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::Number,
desc: "St".to_string(),
required: true,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
}
#[test]
fn test_array() {
let mut arg = Arg::default();
let args = ["get-info", "-all", "43", "48"]
.iter()
.map(|f| f.to_string())
.collect();
let command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::Array,
desc: "St".to_string(),
required: true,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
let v = arg
.group
.iter()
.find(|f| f.command == "get-info")
.map(|f| f.opts.as_slice())
.unwrap();
let s: Vec<String> = v.get_values("all").unwrap();
assert_eq!(2, s.len());
assert_eq!("43", s[0]);
assert_eq!("48", s[1]);
let mut arg = Arg::default();
let args = ["get-info", "-all", "43", "-all", "48"]
.iter()
.map(|f| f.to_string())
.collect();
let command_option_def = vec![CommandOptionDef {
command: "get-info".to_string(),
opts: vec![OptionDef {
key: "all".to_string(),
_type: OptionType::Array,
desc: "St".to_string(),
required: true,
}],
support_args: 0,
..Default::default()
}];
parse_command_arg(&mut arg, args, command_option_def);
let v = arg
.group
.iter()
.find(|f| f.command == "get-info")
.map(|f| f.opts.as_slice())
.unwrap();
let s: Vec<String> = v.get_values("all").unwrap();
assert_eq!(2, s.len());
assert_eq!("43", s[0]);
assert_eq!("48", s[1]);
}
}