diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2024-04-24 20:10:19 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2024-04-24 20:10:19 +0200 |
| commit | 1a0fde95fc2403041f9f477f4220618ee916853b (patch) | |
| tree | 753a7bb6acb52210564e9a6d37d11dd0301a70b9 | |
| parent | bb73ba4c05e6a509bc8097c285f382578bc9070c (diff) | |
args: Argument parser
Still getting used to rust
| -rw-r--r-- | Cargo.lock | 122 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/args.rs | 305 | ||||
| -rw-r--r-- | src/main.rs | 68 |
4 files changed, 494 insertions, 2 deletions
@@ -3,5 +3,127 @@ version = 3 [[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] name = "rrjvm" version = "0.0.1" +dependencies = [ + "derive_builder", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" @@ -8,3 +8,4 @@ description = "Red Rocket JVM" license = "MIT" [dependencies] +derive_builder = "0.20.0" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..3cd9d8a --- /dev/null +++ b/src/args.rs @@ -0,0 +1,305 @@ +use derive_builder::Builder; +use std::collections::HashMap; + +#[allow(dead_code)] +pub enum ValueRequirement { + None, + Required(&'static str), + Optional(&'static str), +} + +#[derive(Builder)] +#[builder(pattern = "owned")] +pub struct Option { + #[builder(setter(into), default = "'\\0'")] + short: char, + long: &'static str, + #[builder(setter(into), default = "\"\"")] + description: &'static str, + #[builder(setter(name = "value"), default = "ValueRequirement::None")] + value_req: ValueRequirement, + #[builder(setter(skip))] + is_set: bool, + #[builder(setter(skip))] + value: std::option::Option<String>, +} + +pub struct Options { + options: Vec<Option>, + short: HashMap<char, usize>, + long: HashMap<&'static str, usize>, +} + +impl Option { + pub fn short(&self) -> char { + self.short + } + + pub fn long(&self) -> &'static str { + self.long + } + + pub fn description(&self) -> &'static str { + self.description + } + + pub fn is_set(&self) -> bool { + self.is_set + } + + pub fn value(&self) -> &std::option::Option<String> { + &self.value + } + + fn set(&mut self, value: std::option::Option<String>) { + self.is_set = true; + self.value = value; + } +} + +impl Options { + pub fn new() -> Self { + Options { + options: Vec::new(), + short: HashMap::new(), + long: HashMap::new(), + } + } + + pub fn push(&mut self, option: Option) -> usize { + let index = self.options.len(); + if option.short != '\0' { + self.short.insert(option.short, index); + } + if option.long != "" { + self.long.insert(option.long, index); + } + self.options.push(option); + index + } +} + +impl std::ops::Index<usize> for Options { + type Output = Option; + + fn index(&self, index: usize) -> &Self::Output { + self.options.index(index) + } +} + +pub struct Arguments { + pub program: std::option::Option<String>, + pub args: Vec<String>, +} + +pub trait Parser { + fn run(&self, options: &mut Options, args: impl IntoIterator<Item=String>) -> Result<Arguments, String>; + fn print_help(&self, options: &Options); +} + +#[allow(dead_code)] +pub struct LongOnlyParser { +} + +#[allow(dead_code)] +pub struct ShortAndLongParser { +} + +fn print_list(list: Vec<(String, &str)>) { + let mut left_len: usize = 0; + for (left, _) in &list { + left_len = std::cmp::max(left_len, left.len()); + } + for (left, right) in &list { + println!("{left:<left_len$} {right}"); + } +} + +#[allow(dead_code)] +impl LongOnlyParser { + pub fn new() -> LongOnlyParser { + LongOnlyParser { } + } +} + +impl Parser for LongOnlyParser { + fn run(&self, options: &mut Options, args: impl IntoIterator<Item=String>) -> Result<Arguments, String> { + let mut ret = Vec::new(); + let mut args_iter = args.into_iter(); + let program = args_iter.next(); + while let Some(arg) = args_iter.next() { + if arg.starts_with("-") { + if arg == "--" { + // All following arguments are just that. + while let Some(arg) = args_iter.next() { + ret.push(arg) + } + break + } + let start = 1; + let name; + let mut value; + if let Some(end) = arg.find('=') { + name = arg.get(start..end).unwrap(); + value = Some(arg.get(end + 1..).unwrap().to_string()); + } else { + name = arg.get(start..).unwrap(); + value = None; + } + if let Some(index) = options.long.get(name) { + let ref mut option = options.options[*index]; + match option.value_req { + ValueRequirement::None => { + if value.is_some() { + return Err(format!("option '{}' doesn't allow an argument", arg)) + } + }, + ValueRequirement::Required(_) => { + if value.is_none() { + value = args_iter.next(); + if value.is_none() { + return Err(format!("option '{}' requires an argument", arg)) + } + } + }, + ValueRequirement::Optional(_) => {}, + } + option.set(value); + } else { + return Err(format!("unrecognized option '{}'", arg)) + } + } else { + ret.push(arg) + } + } + Ok(Arguments { program, args: ret }) + } + + fn print_help(&self, options: &Options) { + let mut lines = Vec::new(); + for (_, index) in &options.long { + let ref option = options[*index]; + let left = match option.value_req { + ValueRequirement::None => format!("-{}", option.long()), + ValueRequirement::Required(name) => format!("-{}={}", option.long(), name), + ValueRequirement::Optional(name) => format!("-{}[={}]", option.long(), name), + }; + lines.push((left, option.description())); + } + print_list(lines); + } +} + +#[allow(dead_code)] +impl ShortAndLongParser { + pub fn new() -> ShortAndLongParser { + ShortAndLongParser { } + } +} + +impl Parser for ShortAndLongParser { + fn run(&self, options: &mut Options, args: impl IntoIterator<Item=String>) -> Result<Arguments, String> { + let mut ret = Vec::new(); + let mut args_iter = args.into_iter(); + let program = args_iter.next(); + while let Some(arg) = args_iter.next() { + if arg.starts_with("--") { + if arg.len() == 2 { + // All following arguments are just that. + while let Some(arg) = args_iter.next() { + ret.push(arg) + } + break + } + let start = 2; + let name; + let mut value; + if let Some(end) = arg.find('=') { + name = arg.get(start..end).unwrap(); + value = Some(arg.get(end + 1..).unwrap().to_string()); + } else { + name = arg.get(start..).unwrap(); + value = None; + } + if let Some(index) = options.long.get(name) { + let ref mut option = options.options[*index]; + match option.value_req { + ValueRequirement::None => { + if value.is_some() { + return Err(format!("option '{}' doesn't allow an argument", arg)) + } + }, + ValueRequirement::Required(_) => { + if value.is_none() { + value = args_iter.next(); + if value.is_none() { + return Err(format!("option '{}' requires an argument", arg)) + } + } + }, + ValueRequirement::Optional(_) => {}, + } + option.set(value); + } else { + return Err(format!("unrecognized option '{}'", arg)) + } + } else if arg.starts_with("-") && arg.len() > 1 { + for c in arg.get(1..).unwrap().chars() { + if let Some(index) = options.short.get(&c) { + let ref mut option = options.options[*index]; + let mut value = None; + match option.value_req { + ValueRequirement::None => {}, + ValueRequirement::Required(_) => { + value = args_iter.next(); + if value.is_none() { + return Err(format!("option requires an argument -- '{}", c)) + } + }, + ValueRequirement::Optional(_) => {}, + } + option.set(value); + } else { + return Err(format!("invalid option -- '{}'", c)) + } + } + } else { + ret.push(arg) + } + } + Ok(Arguments { program, args: ret }) + } + + fn print_help(&self, options: &Options) { + let mut lines = Vec::new(); + for option in &options.options { + let left: String; + if option.short() == '\0' { + left = match option.value_req { + ValueRequirement::None => format!(" --{}", option.long()), + ValueRequirement::Required(name) => + format!(" --{}={}", option.long(), name), + ValueRequirement::Optional(name) => + format!(" --{}[={}]", option.long(), name), + }; + } else if option.long() == "" { + left = match option.value_req { + ValueRequirement::None => format!("-{}", option.short()), + ValueRequirement::Required(name) => format!("-{}={}", option.short(), name), + ValueRequirement::Optional(name) => + format!("-{}[={}]", option.short(), name), + }; + } else { + left = match option.value_req { + ValueRequirement::None => format!("-{}, --{}", option.short(), option.long()), + ValueRequirement::Required(name) => + format!("-{}, --{}={}", option.short(), option.long(), name), + ValueRequirement::Optional(name) => + format!("-{}, --{}[={}]", option.short(), option.long(), name), + }; + } + lines.push((left, option.description())); + } + print_list(lines); + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..6b7c181 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,67 @@ -fn main() { - println!("Hello, world!"); +mod args; + +use crate::args::Parser; +use std::process::ExitCode; + +fn parse_args() -> Result<bool, String> { + let mut options = args::Options::new(); + let help_idx = options.push( + args::OptionBuilder::default() + .short('h') + .long("help") + .description("display this and exit") + .build().unwrap()); + let version_idx = options.push( + args::OptionBuilder::default() + .short('V') + .long("version") + .description("display version and exit") + .build().unwrap()); + let verbose_idx = options.push( + args::OptionBuilder::default() + .long("verbose") + .description("be verbose") + .build().unwrap()); + let test_idx = options.push( + args::OptionBuilder::default() + .short('t') + .long("test") + .description("testing") + .value(args::ValueRequirement::Required("FILE")) + .build().unwrap()); + + let parser = args::ShortAndLongParser::new(); + let args = parser.run(&mut options, std::env::args())?; + let ref help = options[help_idx]; + if help.is_set() { + parser.print_help(&options); + return Ok(true); + } + let ref version = options[version_idx]; + if version.is_set() { + println!("Version is 0.0.1"); + return Ok(true); + } + let ref verbose = options[verbose_idx]; + let ref test = options[test_idx]; + println!("verbose: {}", verbose.is_set()); + println!("test: {:?}", test.value()); + println!("args: {:?}", args.args); + Ok(false) +} + +fn main() -> ExitCode { + match parse_args() { + Ok(exit) => { + if exit { + return ExitCode::SUCCESS; + } + } + Err(msg) => { + eprintln!("{}", msg); + return ExitCode::FAILURE; + } + } + + ExitCode::SUCCESS } |
