mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-19 07:20:18 +00:00
Move special treatment of help flag into lib to improve usage display and BTreeMap use case ergonomics
This commit is contained in:
@@ -203,7 +203,7 @@ pub fn main() -> ExitCode {
|
||||
// Read & parse arguments from the command line, store results into the above structure
|
||||
enum Arg { Out, Bin, Txt, Whitespace, Help }
|
||||
const OPTIONS: Opts<Arg> = Opts::new(&[
|
||||
Opt::flag(Arg::Help, &["--help", "-h"], "Show this help message and exit"),
|
||||
Opt::help_flag(Arg::Help, &["--help", "-h"], "Show this help message and exit"),
|
||||
Opt::positional_required(Arg::Out, "out", "Path to generated header file"),
|
||||
Opt::value(Arg::Bin, &["--bin", "-b"], "data.bin", "Add a binary file"),
|
||||
Opt::value(Arg::Txt, &["--txt", "-t"], "text.txt", "Add a text file"),
|
||||
|
||||
@@ -8,18 +8,13 @@ use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
const OPTIONS: Opts<&'static str> = Opts::new(&[
|
||||
Opt::flag("help", &["--help"], "Show this help"),
|
||||
Opt::help_flag("help", &["--help"], "Show this help"),
|
||||
Opt::positional("positional", "positional", "Positional argument"),
|
||||
Opt::value("value", &["-v", "--value"], "path", "Value option"),
|
||||
Opt::value("value", &["-v", "--value"], "string", "Value option"),
|
||||
Opt::flag("flag", &["-f", "--flag"], "Flag option"),
|
||||
]);
|
||||
|
||||
let map = match OPTIONS.parse_map_easy() {
|
||||
// TODO: There should probably be a more efficient way to make jaarg handle help for us
|
||||
ParseMapResult::Map(map) if map.contains_key("help") => {
|
||||
OPTIONS.print_full_help("btreemap");
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
ParseMapResult::Map(map) => map,
|
||||
ParseMapResult::Exit(code) => { return code; }
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ impl<ID: 'static> Opts<ID> {
|
||||
|
||||
// Ensure that all required positional arguments have been provided
|
||||
for option in self.options[state.positional_index..].iter() {
|
||||
if matches!(option.r#type, OptType::Positional) && option.required {
|
||||
if matches!(option.r#type, OptType::Positional) && option.is_required() {
|
||||
error(program_name, ParseError::RequiredPositional(option.first_name()));
|
||||
return ParseResult::ExitError;
|
||||
}
|
||||
|
||||
12
src/help.rs
12
src/help.rs
@@ -25,7 +25,7 @@ impl<ID: 'static> core::fmt::Display for StandardShortUsageWriter<'_, ID> {
|
||||
// Write option parameter arguments
|
||||
for option in self.0.options.options.iter()
|
||||
.filter(|o| matches!(o.r#type, OptType::Value | OptType::Flag)) {
|
||||
write!(f, " {}", if option.required { '<' } else { '[' })?;
|
||||
write!(f, " {}", if option.is_required() { '<' } else { '[' })?;
|
||||
match (option.first_short_name(), option.first_long_name()) {
|
||||
(Some(short_name), Some(long_name)) => write!(f, "{short_name}|{long_name}")?,
|
||||
(Some(short_name), None) => f.write_str(short_name)?,
|
||||
@@ -35,14 +35,14 @@ impl<ID: 'static> core::fmt::Display for StandardShortUsageWriter<'_, ID> {
|
||||
if let Some(value_name) = option.value_name {
|
||||
write!(f, " {value_name}")?;
|
||||
}
|
||||
write!(f, "{}", if option.required { '>' } else { ']' })?;
|
||||
write!(f, "{}", if option.is_required() { '>' } else { ']' })?;
|
||||
}
|
||||
|
||||
// Write positional arguments
|
||||
for option in self.0.options.options.iter()
|
||||
.filter(|o| matches!(o.r#type, OptType::Positional)) {
|
||||
let name = option.first_name();
|
||||
match option.required {
|
||||
match option.is_required() {
|
||||
true => write!(f, " <{name}>")?,
|
||||
false => write!(f, " [{name}]")?,
|
||||
}
|
||||
@@ -68,7 +68,7 @@ impl<ID> core::fmt::Display for StandardFullHelpWriter<'_, ID> {
|
||||
// Write optional short options
|
||||
let mut first = true;
|
||||
for option in self.0.options.options {
|
||||
if let (OptType::Flag | OptType::Value, false) = (option.r#type, option.required) {
|
||||
if let (OptType::Flag | OptType::Value, false) = (option.r#type, option.is_required()) {
|
||||
if let Some(c) = option.first_short_name_char() {
|
||||
if first {
|
||||
write!(f, " [{short_flag}")?;
|
||||
@@ -85,7 +85,7 @@ impl<ID> core::fmt::Display for StandardFullHelpWriter<'_, ID> {
|
||||
// Write required short options
|
||||
first = true;
|
||||
for option in self.0.options.options {
|
||||
if let (OptType::Flag | OptType::Value, true) = (option.r#type, option.required) {
|
||||
if let (OptType::Flag | OptType::Value, true) = (option.r#type, option.is_required()) {
|
||||
if let Some(c) = option.first_short_name_char() {
|
||||
if first {
|
||||
write!(f, " <{short_flag}")?;
|
||||
@@ -103,7 +103,7 @@ impl<ID> core::fmt::Display for StandardFullHelpWriter<'_, ID> {
|
||||
for option in self.0.options.options.iter()
|
||||
.filter(|o| matches!(o.r#type, OptType::Positional)) {
|
||||
let name = option.first_name();
|
||||
match option.required {
|
||||
match option.is_required() {
|
||||
true => write!(f, " <{name}>")?,
|
||||
false => write!(f, " [{name}]")?,
|
||||
}
|
||||
|
||||
@@ -24,44 +24,75 @@ pub struct Opt<ID> {
|
||||
value_name: Option<&'static str>,
|
||||
help_string: &'static str,
|
||||
r#type: OptType,
|
||||
required: bool,
|
||||
flags: OptFlag,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OptFlag(u8);
|
||||
|
||||
impl OptFlag {
|
||||
pub const REQUIRED: Self = OptFlag(1 << 0);
|
||||
pub const HELP: Self = OptFlag(1 << 1);
|
||||
|
||||
pub const NONE: Self = OptFlag(0);
|
||||
}
|
||||
|
||||
// TODO: Improve this interface by making the name field take AsOptIdentifier when const traits are stabilised
|
||||
impl<ID> Opt<ID> {
|
||||
#[inline]
|
||||
const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, help_string: &'static str, r#type: OptType, required: bool) -> Self {
|
||||
const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, help_string: &'static str, r#type: OptType) -> Self {
|
||||
assert!(match names {
|
||||
OptIdentifier::Single(_) => true,
|
||||
OptIdentifier::Multi(names) => !names.is_empty(),
|
||||
}, "Option names cannot be an empty slice");
|
||||
Self { id, names, value_name, help_string, r#type, required }
|
||||
Self { id, names, value_name, help_string, r#type, flags: OptFlag::NONE }
|
||||
}
|
||||
|
||||
/// A positional argument that is parsed sequentially without being invoked by an option flag
|
||||
pub const fn positional(id: ID, name: &'static str, help_string: &'static str) -> Self {
|
||||
Self { id, names: OptIdentifier::Single(name), value_name: None, help_string, r#type: OptType::Positional, required: false }
|
||||
Self::new(id, OptIdentifier::Single(name), None, help_string, OptType::Positional)
|
||||
}
|
||||
/// A required positional argument that is parsed sequentially without being invoked by an option flag
|
||||
pub const fn positional_required(id: ID, name: &'static str, help_string: &'static str) -> Self {
|
||||
Self::new(id, OptIdentifier::Single(name), None, help_string, OptType::Positional, true)
|
||||
Self::new(id, OptIdentifier::Single(name), None, help_string, OptType::Positional).with_required()
|
||||
}
|
||||
/// A flag-type option that serves as the interface's help flag
|
||||
pub const fn help_flag(id: ID, names: &'static[&'static str], help_string: &'static str) -> Self {
|
||||
Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag).with_help_flag()
|
||||
}
|
||||
/// A flag-type option that takes no value
|
||||
pub const fn flag(id: ID, names: &'static[&'static str], help_string: &'static str) -> Self {
|
||||
Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag, false)
|
||||
Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag)
|
||||
}
|
||||
/// A required flag-type option that takes no value
|
||||
pub const fn flag_required(id: ID, names: &'static[&'static str], help_string: &'static str) -> Self {
|
||||
Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag, true)
|
||||
Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag).with_required()
|
||||
}
|
||||
/// An option argument that takes a value
|
||||
pub const fn value(id: ID, names: &'static[&'static str], value_name: &'static str, help_string: &'static str) -> Self {
|
||||
Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value, false)
|
||||
Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value)
|
||||
}
|
||||
/// A required option argument that takes a value
|
||||
pub const fn value_required(id: ID, names: &'static[&'static str], value_name: &'static str, help_string: &'static str) -> Self {
|
||||
Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value, true)
|
||||
Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value).with_required()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn with_required(mut self) -> Self {
|
||||
assert!(!self.is_help(), "Help flag cannot be made required");
|
||||
self.flags.0 |= OptFlag::REQUIRED.0;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn with_help_flag(mut self) -> Self {
|
||||
assert!(matches!(self.r#type, OptType::Flag), "Only flags are allowed to be help options");
|
||||
self.flags.0 |= OptFlag::HELP.0;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)] const fn is_required(&self) -> bool { (self.flags.0 & OptFlag::REQUIRED.0) != 0 }
|
||||
#[inline(always)] const fn is_help(&self) -> bool { (self.flags.0 & OptFlag::HELP.0) != 0 }
|
||||
}
|
||||
|
||||
impl<ID: 'static> Opt<ID> {
|
||||
|
||||
25
src/std.rs
25
src/std.rs
@@ -56,8 +56,10 @@ impl<ID: 'static> Opts<ID> {
|
||||
fn easy_error(&self, program_name: &str, err: ParseError) {
|
||||
eprintln!("{program_name}: {err}");
|
||||
self.eprint_help::<StandardShortUsageWriter<'_, ID>>(program_name);
|
||||
// TODO: only show when an option is marked help
|
||||
eprintln!("Run '{program_name} --help' to view all available options.");
|
||||
if let Some(help_option) = self.options.iter().find(|o| o.is_help()) {
|
||||
eprintln!("Run '{program_name} {help}' to view all available options.",
|
||||
help = help_option.first_long_name().unwrap_or(help_option.first_name()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +74,17 @@ impl Opts<&'static str> {
|
||||
///
|
||||
/// Requires features = [std]
|
||||
pub fn parse_map<'a, S: AsRef<str> + 'a, I: Iterator<Item = S>>(&self, program_name: &str, args: I,
|
||||
error: impl FnOnce(&str, ParseError)) -> ParseMapResult {
|
||||
help: impl Fn(&str), error: impl FnOnce(&str, ParseError)
|
||||
) -> ParseMapResult {
|
||||
let mut out: BTreeMap<&'static str, String> = BTreeMap::new();
|
||||
match self.parse(&program_name, args, |_program_name, id, _opt, _name, arg| {
|
||||
out.insert(id, arg.into());
|
||||
Ok(ParseControl::Continue)
|
||||
match self.parse(&program_name, args, |_program_name, id, opt, _name, arg| {
|
||||
if opt.is_help() {
|
||||
help(program_name);
|
||||
Ok(ParseControl::Quit)
|
||||
} else {
|
||||
out.insert(id, arg.into());
|
||||
Ok(ParseControl::Continue)
|
||||
}
|
||||
}, error) {
|
||||
ParseResult::ContinueSuccess => ParseMapResult::Map(out),
|
||||
ParseResult::ExitSuccess => ParseMapResult::Exit(std::process::ExitCode::SUCCESS),
|
||||
@@ -85,10 +93,13 @@ impl Opts<&'static str> {
|
||||
}
|
||||
|
||||
/// Parse arguments from the command line and return the results in a BTreeMap.
|
||||
/// Help and errors are formatted in a standard user-friendly format.
|
||||
///
|
||||
/// Requires features = [std]
|
||||
pub fn parse_map_easy(&self) -> ParseMapResult {
|
||||
let (program_name, argv) = Self::easy_args();
|
||||
self.parse_map(&program_name, argv, |name, e| self.easy_error(name, e))
|
||||
self.parse_map(&program_name, argv,
|
||||
|name| self.print_full_help(name),
|
||||
|name, e| self.easy_error(name, e))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user