implement new argument parser with --help support

This commit is contained in:
2025-10-21 19:12:16 -07:00
parent 2aeb0474e6
commit 5108b61a15
3 changed files with 195 additions and 79 deletions

View File

@@ -2,6 +2,8 @@
#![feature(uefi_std)] #![feature(uefi_std)]
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::info; use log::info;
@@ -51,7 +53,7 @@ fn main() -> Result<()> {
setup::init()?; setup::init()?;
// Parse the options to the sprout executable. // 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. // Load the configuration of sprout.
// At this point, the configuration has been validated and the specified // At this point, the configuration has been validated and the specified

View File

@@ -1,3 +1,7 @@
use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable};
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// The Sprout options parser. /// The Sprout options parser.
pub mod 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<String, Option<String>>) -> Result<Self> {
// 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)
}
}

View File

@@ -1,11 +1,49 @@
use crate::options::SproutOptions;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// 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,
}
/// 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,
}
/// 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;
/// 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>)];
/// Produces the type by taking the `options` and processing it into the output.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self::Output>;
/// For minimalism, we don't want a full argument parser. Instead, we use /// 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. /// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format. /// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> { fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Access the configured options for this type.
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout. // Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable. // Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>(); let args = std::env::args().skip(1).collect::<Vec<_>>();
@@ -45,7 +83,18 @@ fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
value = Some(part_value); value = Some(part_value);
} }
if value.is_none() { // 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. // Check for the next value.
let maybe_next = iterator.peek(); let maybe_next = iterator.peek();
@@ -60,9 +109,29 @@ fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
}; };
} }
// Error on empty option names. // If the option form does not support a value and there is a value, error.
if option.is_empty() { if description.form != OptionForm::Value && value.is_some() {
bail!("invalid empty option: {option}"); 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 {
" <value>"
} else {
""
},
description.description
);
}
// Exit because the help has been displayed.
std::process::exit(1);
} }
// Insert the option and the value into the map. // Insert the option and the value into the map.
@@ -71,26 +140,11 @@ fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
Ok(options) Ok(options)
} }
/// Parse the arguments to Sprout as a [SproutOptions] structure. /// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce].
pub fn parse() -> anyhow::Result<SproutOptions> { fn parse() -> Result<Self::Output> {
// Use the default value of sprout options and have the raw options be parsed into it. // Parse the program arguments into a raw map.
let mut result = SproutOptions::default(); let options = Self::parse_raw().context("unable to parse options")?;
let options = parse_raw().context("unable to parse options")?; // Produce the options from the map.
Self::produce(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")?);
}
_ => bail!("unknown option: --{key}"),
} }
} }
Ok(result)
}