From 5108b61a151a7f749ecf0d1716a887224ad38589 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Tue, 21 Oct 2025 19:12:16 -0700 Subject: [PATCH] implement new argument parser with --help support --- src/main.rs | 4 +- src/options.rs | 60 ++++++++++++ src/options/parser.rs | 210 ++++++++++++++++++++++++++---------------- 3 files changed, 195 insertions(+), 79 deletions(-) diff --git a/src/main.rs b/src/main.rs index 77918b2..bfb5a3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ #![feature(uefi_std)] use crate::context::{RootContext, SproutContext}; +use crate::options::SproutOptions; +use crate::options::parser::OptionsRepresentable; use crate::phases::phase; use anyhow::{Context, Result}; use log::info; @@ -51,7 +53,7 @@ fn main() -> Result<()> { setup::init()?; // Parse the options to the sprout executable. - let options = options::parser::parse().context("unable to parse options")?; + let options = SproutOptions::parse().context("unable to parse options")?; // Load the configuration of sprout. // At this point, the configuration has been validated and the specified diff --git a/src/options.rs b/src/options.rs index f204466..db209e5 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,3 +1,7 @@ +use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable}; +use anyhow::{Context, Result, bail}; +use std::collections::BTreeMap; + /// The Sprout options parser. pub mod parser; @@ -22,3 +26,59 @@ impl Default for SproutOptions { } } } + +/// The options parser mechanism for Sprout. +impl OptionsRepresentable for SproutOptions { + /// Produce the [SproutOptions] structure. + type Output = Self; + + /// All the Sprout options that are defined. + fn options() -> &'static [(&'static str, OptionDescription<'static>)] { + &[ + ( + "config", + OptionDescription { + description: "Path to Sprout configuration file", + form: OptionForm::Value, + }, + ), + ( + "boot", + OptionDescription { + description: "Entry to boot, bypassing the menu", + form: OptionForm::Value, + }, + ), + ( + "help", + OptionDescription { + description: "Display Sprout Help", + form: OptionForm::Help, + }, + ), + ] + } + + /// Produces [SproutOptions] from the parsed raw `options` map. + fn produce(options: BTreeMap>) -> Result { + // Use the default value of sprout options and have the raw options be parsed into it. + let mut result = Self::default(); + + for (key, value) in options { + match key.as_str() { + "config" => { + // The configuration file to load. + result.config = value.context("--config option requires a value")?; + } + + "boot" => { + // The entry to boot. + result.boot = Some(value.context("--boot option requires a value")?); + } + + _ => bail!("unknown option: --{key}"), + } + } + Ok(result) + } +} diff --git a/src/options/parser.rs b/src/options/parser.rs index d1e6c6a..23b50ad 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -1,96 +1,150 @@ -use crate::options::SproutOptions; use anyhow::{Context, Result, bail}; use std::collections::BTreeMap; -/// For minimalism, we don't want a full argument parser. Instead, we use -/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format. -/// We also support --abc=123 = abc: Some("123") format. -fn parse_raw() -> Result>> { - // Collect all the arguments to Sprout. - // Skip the first argument which is the path to our executable. - let args = std::env::args().skip(1).collect::>(); +/// The type of option. This disambiguates different behavior +/// of how options are handled. +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum OptionForm { + /// A flag, like --verbose. + Flag, + /// A value, in the form --abc 123 or --abc=123. + Value, + /// Help flag, like --help. + Help, +} - // Represent options as key-value pairs. - let mut options = BTreeMap::new(); +/// The description of an option, used in the options parser +/// to make decisions about how to progress. +#[derive(Debug, Clone)] +pub struct OptionDescription<'a> { + /// The description of the option. + pub description: &'a str, + /// The type of option to parse as. + pub form: OptionForm, +} - // Iterators makes this way easier. - let mut iterator = args.into_iter().peekable(); +/// Represents a type that can be parsed from command line arguments. +/// This is a super minimal options parser mechanism just for Sprout. +pub trait OptionsRepresentable { + /// The output type that parsing will produce. + type Output; - loop { - // Consume the next option, if any. - let Some(option) = iterator.next() else { - break; - }; + /// The configured options for this type. This should describe all the options + /// that are valid to produce the type. The left hand side is the name of the option, + /// and the right hand side is the description. + fn options() -> &'static [(&'static str, OptionDescription<'static>)]; - // If the doesn't start with --, that is invalid. - if !option.starts_with("--") { - bail!("invalid option: {option}"); - } + /// Produces the type by taking the `options` and processing it into the output. + fn produce(options: BTreeMap>) -> Result; - // Strip the -- prefix off. - let mut option = option["--".len()..].trim().to_string(); + /// For minimalism, we don't want a full argument parser. Instead, we use + /// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format. + /// We also support --abc=123 = abc: Some("123") format. + fn parse_raw() -> Result>> { + // Access the configured options for this type. + let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec()); - // An optional value. - let mut value = None; + // Collect all the arguments to Sprout. + // Skip the first argument which is the path to our executable. + let args = std::env::args().skip(1).collect::>(); - // Check if the option is of the form --abc=123 - if option.contains("=") { - let Some((part_key, part_value)) = option.split_once("=") else { + // Represent options as key-value pairs. + let mut options = BTreeMap::new(); + + // Iterators makes this way easier. + let mut iterator = args.into_iter().peekable(); + + loop { + // Consume the next option, if any. + let Some(option) = iterator.next() else { + break; + }; + + // If the doesn't start with --, that is invalid. + if !option.starts_with("--") { bail!("invalid option: {option}"); - }; - - let part_key = part_key.to_string(); - let part_value = part_value.to_string(); - option = part_key; - value = Some(part_value); - } - - if value.is_none() { - // Check for the next value. - let maybe_next = iterator.peek(); - - // If the next value isn't another option, set the value to the next value. - // Otherwise, it is an empty string. - value = if let Some(next) = maybe_next - && !next.starts_with("--") - { - iterator.next() - } else { - None - }; - } - - // Error on empty option names. - if option.is_empty() { - bail!("invalid empty option: {option}"); - } - - // Insert the option and the value into the map. - options.insert(option, value); - } - Ok(options) -} - -/// Parse the arguments to Sprout as a [SproutOptions] structure. -pub fn parse() -> anyhow::Result { - // Use the default value of sprout options and have the raw options be parsed into it. - let mut result = SproutOptions::default(); - let options = parse_raw().context("unable to parse options")?; - - for (key, value) in options { - match key.as_str() { - "config" => { - // The configuration file to load. - result.config = value.context("--config option requires a value")?; } - "boot" => { - // The entry to boot. - result.boot = Some(value.context("--boot option requires a value")?); + // Strip the -- prefix off. + let mut option = option["--".len()..].trim().to_string(); + + // An optional value. + let mut value = None; + + // Check if the option is of the form --abc=123 + if option.contains("=") { + let Some((part_key, part_value)) = option.split_once("=") else { + bail!("invalid option: {option}"); + }; + + let part_key = part_key.to_string(); + let part_value = part_value.to_string(); + option = part_key; + value = Some(part_value); } - _ => bail!("unknown option: --{key}"), + // Error on empty option names. + if option.is_empty() { + bail!("invalid empty option"); + } + + // Find the description of the configured option, if any. + let Some(description) = configured.get(option.as_str()) else { + bail!("invalid option: --{option}"); + }; + + // Check if the option requires a value and error if none was provided. + if description.form == OptionForm::Value && value.is_none() { + // Check for the next value. + let maybe_next = iterator.peek(); + + // If the next value isn't another option, set the value to the next value. + // Otherwise, it is an empty string. + value = if let Some(next) = maybe_next + && !next.starts_with("--") + { + iterator.next() + } else { + None + }; + } + + // If the option form does not support a value and there is a value, error. + if description.form != OptionForm::Value && value.is_some() { + bail!("option --{} does not take a value", option); + } + + // Handle the --help flag case. + if description.form == OptionForm::Help { + // Generic configured options output. + println!("Configured Options:"); + for (name, description) in &configured { + println!( + " --{}{}: {}", + name, + if description.form == OptionForm::Value { + " " + } else { + "" + }, + description.description + ); + } + // Exit because the help has been displayed. + std::process::exit(1); + } + + // Insert the option and the value into the map. + options.insert(option, value); } + Ok(options) + } + + /// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce]. + fn parse() -> Result { + // Parse the program arguments into a raw map. + let options = Self::parse_raw().context("unable to parse options")?; + // Produce the options from the map. + Self::produce(options) } - Ok(result) }