#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![allow(clippy::let_underscore_untyped)]
use crate::parser::{ArgFlag, ArgOption, ArgProperty, ArgType, ArgView, ArgumentStruct};
use myn::utils::spanned_error;
use proc_macro::{Ident, Span, TokenStream};
use std::{collections::HashMap, fmt::Write as _, str::FromStr as _};
mod parser;
#[allow(clippy::too_many_lines)]
#[proc_macro_derive(
OnlyArgs,
attributes(footer, default, long, positional, required, short)
)]
pub fn derive_parser(input: TokenStream) -> TokenStream {
let ast = match ArgumentStruct::parse(input) {
Ok(ast) => ast,
Err(err) => return err,
};
let mut flags = vec![
ArgFlag::new_priv(
Ident::new("help", Span::call_site()),
Some('h'),
vec!["Show this help message.".to_string()],
),
ArgFlag::new_priv(
Ident::new("version", Span::call_site()),
Some('V'),
vec!["Show the application version.".to_string()],
),
];
flags.extend(ast.flags);
let mut dupes = HashMap::new();
for flag in &flags {
if let Err(err) = dedupe(&mut dupes, flag.as_view()) {
return err;
}
}
for opt in &ast.options {
if let Err(err) = dedupe(&mut dupes, opt.as_view()) {
return err;
}
}
let max_width = get_max_width(flags.iter().map(ArgFlag::as_view));
let flags_help = flags
.iter()
.map(|arg| to_help(arg.as_view(), max_width))
.collect::<String>();
let max_width = get_max_width(ast.options.iter().map(ArgOption::as_view));
let options_help = ast
.options
.iter()
.map(|arg| to_help(arg.as_view(), max_width))
.collect::<String>();
let positional_header = ast
.positional
.as_ref()
.map(|opt| format!(" [{}...]", opt.name))
.unwrap_or_default();
let positional_help = ast
.positional
.as_ref()
.map(|opt| format!("\n{}:\n {}\n", opt.name, opt.doc.join("\n ")))
.unwrap_or_default();
let flags_vars =
flags
.iter()
.filter(|&flag| flag.output)
.fold(String::new(), |mut flags, flag| {
write!(
flags,
"let mut {name} = {default:?};",
name = flag.name,
default = flag.default,
)
.unwrap();
flags
});
let options_vars = ast
.options
.iter()
.map(|opt| {
let name = &opt.name;
if let Some(default) = opt.default.as_ref() {
format!("let mut {name} = {default}{};", opt.ty_help.converter())
} else {
match opt.property {
ArgProperty::Optional | ArgProperty::Required => {
format!("let mut {name} = None;")
}
ArgProperty::MultiValue { .. } => {
format!("let mut {name} = vec![];")
}
ArgProperty::Positional { .. } => unreachable!(),
}
}
})
.collect::<String>();
let positional_var = ast
.positional
.as_ref()
.map(|opt| {
let name = &opt.name;
format!("let mut {name} = vec![];")
})
.unwrap_or_default();
let flags_matchers =
flags
.iter()
.filter(|&flag| flag.output)
.fold(String::new(), |mut matchers, flag| {
let name = &flag.name;
let short = flag
.short
.map(|ch| format!(r#"| Some("-{ch}")"#))
.unwrap_or_default();
write!(
matchers,
r#"Some("--{arg}") {short} => {name} = true,"#,
arg = to_arg_name(name)
)
.unwrap();
matchers
});
let options_matchers = ast.options.iter().fold(String::new(), |mut matchers, opt| {
let name = &opt.name;
let short = opt
.short
.map(|ch| format!(r#"| Some(arg_name_ @ "-{ch}")"#))
.unwrap_or_default();
let assignment = if opt.default.is_some() {
match opt.ty_help {
ArgType::Float => format!("{name} = args.next().parse_float(arg_name_)?"),
ArgType::Integer => format!("{name} = args.next().parse_int(arg_name_)?"),
ArgType::OsString => format!("{name} = args.next().parse_osstr(arg_name_)?"),
ArgType::Path => format!("{name} = args.next().parse_path(arg_name_)?"),
ArgType::String => format!("{name} = args.next().parse_str(arg_name_)?"),
}
} else {
match opt.property {
ArgProperty::Optional | ArgProperty::Required => match opt.ty_help {
ArgType::Float => format!("{name} = Some(args.next().parse_float(arg_name_)?)"),
ArgType::Integer => format!("{name} = Some(args.next().parse_int(arg_name_)?)"),
ArgType::OsString => {
format!("{name} = Some(args.next().parse_osstr(arg_name_)?)")
}
ArgType::Path => format!("{name} = Some(args.next().parse_path(arg_name_)?)"),
ArgType::String => format!("{name} = Some(args.next().parse_str(arg_name_)?)"),
},
ArgProperty::MultiValue { .. } => match opt.ty_help {
ArgType::Float => format!("{name}.push(args.next().parse_float(arg_name_)?)"),
ArgType::Integer => format!("{name}.push(args.next().parse_int(arg_name_)?)"),
ArgType::OsString => {
format!("{name}.push(args.next().parse_osstr(arg_name_)?)")
}
ArgType::Path => format!("{name}.push(args.next().parse_path(arg_name_)?)"),
ArgType::String => format!("{name}.push(args.next().parse_str(arg_name_)?)"),
},
ArgProperty::Positional { .. } => unreachable!(),
}
};
write!(
matchers,
r#"Some(arg_name_ @ "--{arg}") {short} => {assignment},"#,
arg = to_arg_name(name)
)
.unwrap();
matchers
});
let positional_matcher = match ast.positional.as_ref() {
Some(opt) => {
let name = &opt.name;
let value = match opt.ty_help {
ArgType::Float => r#"arg.parse_float("<POSITIONAL>")?"#,
ArgType::Integer => r#"arg.parse_int("<POSITIONAL>")?"#,
ArgType::OsString => r#"arg.parse_osstr("<POSITIONAL>")?"#,
ArgType::Path => r#"arg.parse_path("<POSITIONAL>")?"#,
ArgType::String => r#"arg.parse_str("<POSITIONAL>")?"#,
};
format!(
r#"
Some("--") => {{
for arg in args {{
{name}.push({value});
}}
break;
}}
_ => {name}.push({value}),
"#
)
}
None => r#"
Some("--") => break,
_ => return Err(::onlyargs::CliError::Unknown(arg)),
"#
.to_string(),
};
let flags_idents = flags
.iter()
.filter_map(|flag| flag.output.then_some(format!("{},", flag.name)))
.collect::<String>();
let options_idents = ast
.options
.iter()
.map(|opt| {
let name = &opt.name;
let optional = matches!(
opt.property,
ArgProperty::Optional
| ArgProperty::Positional { required: false }
| ArgProperty::MultiValue { required: false }
);
if opt.default.is_some() || optional {
format!("{name},")
} else {
format!(
r#"{name}: {name}.required("--{arg}")?,"#,
arg = to_arg_name(name)
)
}
})
.collect::<String>();
let positional_ident = ast
.positional
.map(|opt| {
if matches!(opt.property, ArgProperty::Positional { required: true }) {
format!(
r#"{}: {}.required("{arg}")?,"#,
opt.name,
opt.name,
arg = to_arg_name(&opt.name),
)
} else {
format!("{},", opt.name)
}
})
.unwrap_or_default();
let name = ast.name;
let doc_comment = if ast.doc.is_empty() {
String::new()
} else {
format!("\n{}\n", ast.doc.join("\n"))
};
let footer = if ast.footer.is_empty() {
String::new()
} else {
format!("\n{}\n", ast.footer.join("\n"))
};
let bin_name = std::env::var_os("CARGO_BIN_NAME").and_then(|name| name.into_string().ok());
let help_impl = if bin_name.is_none() {
r#"fn help() -> ! {
let bin_name = ::std::env::args_os()
.next()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
::std::eprintln!("{}", Self::HELP.replace("{bin_name}", &bin_name));
::std::process::exit(0);
}"#
} else {
""
};
let bin_name = bin_name.unwrap_or_else(|| "{bin_name}".to_string());
let code = TokenStream::from_str(&format!(
r#"
impl ::onlyargs::OnlyArgs for {name} {{
const HELP: &'static str = ::std::concat!(
env!("CARGO_PKG_NAME"),
" v",
env!("CARGO_PKG_VERSION"),
"\n",
env!("CARGO_PKG_DESCRIPTION"),
"\n",
{doc_comment:?},
"\nUsage:\n ",
{bin_name:?},
" [flags] [options]",
{positional_header:?},
"\n\nFlags:\n",
{flags_help:?},
"\nOptions:\n",
{options_help:?},
{positional_help:?},
{footer:?},
);
const VERSION: &'static str = concat!(
env!("CARGO_PKG_NAME"),
" v",
env!("CARGO_PKG_VERSION"),
"\n",
);
{help_impl}
fn parse(args: Vec<::std::ffi::OsString>) ->
::std::result::Result<Self, ::onlyargs::CliError>
{{
use ::onlyargs::traits::*;
use ::std::option::Option::{{None, Some}};
use ::std::result::Result::{{Err, Ok}};
{flags_vars}
{options_vars}
{positional_var}
let mut args = args.into_iter();
while let Some(arg) = args.next() {{
match arg.to_str() {{
// TODO: Add an attribute to disable help/version.
Some("--help") | Some("-h") => Self::help(),
Some("--version") | Some("-V") => Self::version(),
{flags_matchers}
{options_matchers}
{positional_matcher}
}}
}}
Ok(Self {{
{flags_idents}
{options_idents}
{positional_ident}
}})
}}
}}
"#
));
match code {
Ok(stream) => stream,
Err(err) => spanned_error(err.to_string(), Span::call_site()),
}
}
const SHORT_PAD: usize = 3;
const LONG_PAD: usize = 6;
fn to_arg_name(ident: &Ident) -> String {
let mut name = ident.to_string().replace('_', "-");
name.make_ascii_lowercase();
name
}
fn to_help(view: ArgView, max_width: usize) -> String {
let name = to_arg_name(view.name);
let ty = match view.ty_help.as_ref() {
Some(ty_help) => ty_help.as_str(),
None => "",
};
let pad = " ".repeat(max_width + LONG_PAD);
let help = view.doc.join(&format!("\n{pad}"));
let width = max_width - name.len();
if let Some(ch) = view.short {
let width = width - SHORT_PAD;
format!(" -{ch} --{name}{ty:<width$} {help}\n")
} else {
format!(" --{name}{ty:<width$} {help}\n")
}
}
fn get_max_width<'a, I>(iter: I) -> usize
where
I: Iterator<Item = ArgView<'a>>,
{
iter.fold(0, |acc, view| {
let short = view.short.map(|_| SHORT_PAD).unwrap_or_default();
let ty = match view.ty_help.as_ref() {
Some(ty_help) => ty_help.as_str(),
None => "",
};
acc.max(view.name.to_string().len() + ty.len() + short)
})
}
fn dedupe<'a>(dupes: &mut HashMap<char, &'a Ident>, arg: ArgView<'a>) -> Result<(), TokenStream> {
if let Some(ch) = arg.short {
if let Some(other) = dupes.get(&ch) {
let msg =
format!("Only one short arg is allowed. `-{ch}` also used on field `{other}`");
return Err(spanned_error(msg, arg.name.span()));
}
dupes.insert(ch, arg.name);
}
Ok(())
}