mirror of
https://github.com/Noratrieb/elven-forest.git
synced 2026-01-14 18:55:01 +01:00
Start properly parsing options
This commit is contained in:
parent
0b3225c92c
commit
d0da80c3e8
3 changed files with 236 additions and 0 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod opts;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let opts = elven_wald::Opts::parse();
|
let opts = elven_wald::Opts::parse();
|
||||||
|
let (_opts, _input) = elven_wald::opts::parse(std::env::args().skip(1))?;
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
|
|
|
||||||
234
elven-wald/src/opts.rs
Normal file
234
elven-wald/src/opts.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
//! CLI option parsing.
|
||||||
|
//!
|
||||||
|
//! See [man ld](https://man7.org/linux/man-pages/man1/ld.1.html).
|
||||||
|
//!
|
||||||
|
//! ld opts are very whack and weird, so we use no CLI parsing framework
|
||||||
|
//! or library because they'd probably do it wrong!
|
||||||
|
//!
|
||||||
|
//! Bless the linker writers of the past for the mess they have constructed.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InputFile {
|
||||||
|
pub name: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Opt {
|
||||||
|
short: Option<char>,
|
||||||
|
long: &'static str,
|
||||||
|
takes_value: Option<fn(&mut Opts, value: String)>,
|
||||||
|
set: fn(&mut Opts),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_opts {
|
||||||
|
($(
|
||||||
|
$field:ident: $long:literal $(, $short:literal)? $(, $value:ident)? ;
|
||||||
|
)*) => {
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Opts {
|
||||||
|
$(
|
||||||
|
pub $field: Option<String>,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPTS: &[Opt] = &[
|
||||||
|
$(
|
||||||
|
Opt {
|
||||||
|
short: short_opt!($($short)?),
|
||||||
|
long: $long,
|
||||||
|
takes_value: takes_value!($field, $($value)?),
|
||||||
|
set: set!($field, $($value)?)
|
||||||
|
},
|
||||||
|
)*
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! short_opt {
|
||||||
|
() => {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
($opt:tt) => {
|
||||||
|
Some($opt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! takes_value {
|
||||||
|
($field:ident, ) => {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
($field:ident, $opt:tt) => {
|
||||||
|
Some(|opts, value| opts.$field = Some(value))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! set {
|
||||||
|
($field:ident, ) => {
|
||||||
|
|opts| opts.$field = true;
|
||||||
|
};
|
||||||
|
($field:ident, $opt:tt) => {
|
||||||
|
|_| {
|
||||||
|
unreachable!(
|
||||||
|
"set called on option taking a value: {}",
|
||||||
|
stringify!($field)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_opts! {
|
||||||
|
entry: "entry", 'e', String;
|
||||||
|
output: "output", 'o', String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(mut args: impl Iterator<Item = String>) -> anyhow::Result<(Opts, Vec<InputFile>)> {
|
||||||
|
let mut opts = Opts::default();
|
||||||
|
let mut files = Vec::new();
|
||||||
|
let mut require_value: Option<fn(_, _)> = None;
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
if arg.starts_with("@") {
|
||||||
|
bail!("@file parsing syntax is not implemented yet.");
|
||||||
|
} else if let Some(apply_value) = require_value {
|
||||||
|
apply_value(&mut opts, arg);
|
||||||
|
require_value = None;
|
||||||
|
} else if arg.starts_with("-") {
|
||||||
|
let Some(first_c) = arg.chars().nth(1) else {
|
||||||
|
bail!("option starting with - requires a value. stdin/stdout are not supported");
|
||||||
|
};
|
||||||
|
|
||||||
|
// We first need to check for long opts, as -entry should be parsed as --entry and not -e ntry.
|
||||||
|
// Accept both double -- and single -.
|
||||||
|
let long_start = if first_c == '-' { 2 } else { 1 };
|
||||||
|
let long_end = arg.chars().position(|c| c == '=').unwrap_or(arg.len());
|
||||||
|
let long_flag_name = &arg[long_start..long_end];
|
||||||
|
if let Some(long) = OPTS
|
||||||
|
.iter()
|
||||||
|
// Important: any long options starting with -o MUST NOT be parsed as the long options if starting
|
||||||
|
// with a single dash. Just -o. No other flag.
|
||||||
|
.find(|o| {
|
||||||
|
let skip_because_of_o = long_flag_name.starts_with("o") && first_c != '-';
|
||||||
|
!skip_because_of_o && o.long == long_flag_name
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if let Some(takes_value) = long.takes_value {
|
||||||
|
if long_end != arg.len() {
|
||||||
|
let value = &arg[(long_end + 1)..];
|
||||||
|
takes_value(&mut opts, value.to_owned());
|
||||||
|
} else {
|
||||||
|
require_value = Some(takes_value);
|
||||||
|
}
|
||||||
|
} else if long_end != arg.len() {
|
||||||
|
bail!("long option {arg} does not take a value");
|
||||||
|
} else {
|
||||||
|
(long.set)(&mut opts);
|
||||||
|
}
|
||||||
|
// We successfully parsed this as a long option, great. Move on.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No long option. Try short opts instead.
|
||||||
|
if let Some(short) = OPTS.iter().find(|o| o.short == Some(first_c)) {
|
||||||
|
if let Some(takes_value) = short.takes_value {
|
||||||
|
if long_flag_name.len() > 1 {
|
||||||
|
let value = &long_flag_name[1..];
|
||||||
|
takes_value(&mut opts, value.to_owned());
|
||||||
|
} else {
|
||||||
|
require_value = Some(takes_value);
|
||||||
|
}
|
||||||
|
} else if arg.len() > 2 {
|
||||||
|
bail!("short option {arg} does not take a value");
|
||||||
|
} else {
|
||||||
|
(short.set)(&mut opts);
|
||||||
|
}
|
||||||
|
// It's a short option!
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No options exist :(
|
||||||
|
bail!("unrecognized option: {arg}");
|
||||||
|
} else {
|
||||||
|
files.push(InputFile { name: arg.into() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if require_value.is_some() {
|
||||||
|
bail!("last option required a value but none was supplied");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((opts, files))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{InputFile, Opts};
|
||||||
|
|
||||||
|
fn parse(cmd: impl AsRef<[&'static str]>) -> anyhow::Result<(Opts, Vec<InputFile>)> {
|
||||||
|
super::parse(cmd.as_ref().into_iter().map(|&s| s.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_has_dashes() {
|
||||||
|
let cmd = ["--output", "--meow"];
|
||||||
|
let (opts, files) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.output, Some("--meow".to_owned()));
|
||||||
|
assert!(files.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_value_direct() {
|
||||||
|
let cmd = ["-estart"];
|
||||||
|
let (opts, _) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.entry, Some("start".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_value_2() {
|
||||||
|
let cmd = ["-e", "start"];
|
||||||
|
let (opts, _) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.entry, Some("start".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_dash_long_value_eq() {
|
||||||
|
let cmd = ["-entry=start"];
|
||||||
|
let (opts, _) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.entry, Some("start".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_dash_long_value_2() {
|
||||||
|
let cmd = ["-entry", "start"];
|
||||||
|
let (opts, _) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.entry, Some("start".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_value_eq() {
|
||||||
|
let cmd = ["--entry=start"];
|
||||||
|
let (opts, _) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.entry, Some("start".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_value_2() {
|
||||||
|
let cmd = ["--entry", "start"];
|
||||||
|
let (opts, _) = parse(cmd).unwrap();
|
||||||
|
assert_eq!(opts.entry, Some("start".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_option() {
|
||||||
|
let cmd = ["--meow"];
|
||||||
|
parse(cmd).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_value_supplied_end() {
|
||||||
|
let cmd = ["-e"];
|
||||||
|
parse(cmd).unwrap_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue