From 007084482e05becd7288914c23e5a2a3e2bc603e Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 1 Sep 2021 21:40:14 +0200 Subject: [PATCH] optimize cli parser --- .github/workflows/rust.yml | 2 + Cargo.lock | 123 ++------- Cargo.toml | 4 +- README.md | 39 +-- src/cli.rs | 495 ++++++++++++++++++++++++++++++++----- src/main.rs | 10 +- 6 files changed, 499 insertions(+), 174 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a8c74e6..ccf062f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,6 +15,8 @@ jobs: - uses: actions/checkout@v2 - name: Lint run: cargo clippy + - name: Test + run: cargo test - name: Build run: cargo build --release --locked - name: Release diff --git a/Cargo.lock b/Cargo.lock index 6899f8e..220d2fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "cfg-if" @@ -185,42 +185,42 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-io" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-sink" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" [[package]] name = "futures-task" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", "futures-core", @@ -511,7 +511,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "milcheck" -version = "0.2.5" +version = "0.2.6" dependencies = [ "html2text", "reqwest", @@ -588,82 +588,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e97c412795abf6c24ba30055a8f20642ea57ca12875220b854cfa501bf1e48" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - [[package]] name = "num_cpus" version = "1.13.0" @@ -826,9 +750,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] @@ -1037,23 +961,22 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1016a0b396a0e68d6c541a54370e0db49524aead4c9e6aa263d6855d978d78" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", - "num", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6f179cd85a30f8652b3f8830f73861c76e87e70b939773e72daf38be3afc02" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ "core-foundation-sys", "libc", @@ -1290,9 +1213,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" +checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce" dependencies = [ "autocfg", "bytes", diff --git a/Cargo.toml b/Cargo.toml index e8611c5..8c1b964 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "milcheck" -version = "0.2.5" +version = "0.2.6" authors = ["pierre "] license = "MPL-2.0" edition = "2018" -description = "A small binary that displays the status of your pacman mirrorlist in your terminal" +description = "A simple program that displays the status of your pacman mirrorlist and the Arch Linux lastest news right in your terminal" repository = "https://github.com/doums/milcheck" readme = "README.md" keywords = ["archlinux", "pacman", "mirror", "mirrorlist"] diff --git a/README.md b/README.md index 0ee4d89..81bef7f 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,28 @@ ![milcheck](https://raw.githubusercontent.com/doums/milcheck/master/public/milcheck.png) -A small binary that displays the status of your pacman mirrorlist in your terminal and optionally the lastest news +A simple program that displays the status of your pacman +mirrorlist and the Arch Linux lastest news right in your terminal ### How ? -milcheck just reads your `/etc/pacman.d/mirrorlist` and retrieves the corresponding data from the official [mirror status page](https://www.archlinux.org/mirrors/status/) +Milcheck just reads your `/etc/pacman.d/mirrorlist` and retrieves +the corresponding data from the official +[mirror status page](https://www.archlinux.org/mirrors/status/). + +The latest news are directly scraped from https://archlinux.org/. ### Why ? -As explained in the [mirror doc](https://wiki.archlinux.org/index.php/Mirrors),\ -before a system upgrade i.e. `sudo pacman -Syu`, you must check that the mirrors in your mirrorlist are up to date e.g. not out of sync. +As explained in the +[mirror doc](https://wiki.archlinux.org/index.php/Mirrors), before +a system upgrade i.e. `sudo pacman -Syu`, you should check that +the mirrors in your mirrorlist are up to date e.g. not out of +sync. ### It's not -..an additional mirrorlist ranking utility - -### Install - -Rust is a language that compiles to native code and by default statically links all dependencies.\ -Simply download the latest [release](https://github.com/doums/milcheck/releases) of the precompiled binary and use it! -(do not forget to make it executable `chmod 755 milcheck`) +..an additional mirrorlist ranking utility. ### Install from [crates.io](https://crates.io/crates/milcheck) @@ -37,7 +39,9 @@ cargo install milcheck ### Arch Linux AUR package -milcheck is present as a [package](https://aur.archlinux.org/packages/milcheck) in the Arch User Repository. +Milcheck is present as a +[package](https://aur.archlinux.org/packages/milcheck) in the Arch +User Repository. ### Build from sources @@ -51,7 +55,13 @@ to build for release ``` cargo build --release ``` -the binary is located under `target/debug` or `target/release` +the binary is located under `target/debug` or `target/release`. + +### Pre-built binary + +Rust is a language that compiles to native code and by default +statically links. Simply download the pre-built binary from latest +[release](https://github.com/doums/milcheck/releases/latest). ### Usage @@ -59,7 +69,8 @@ the binary is located under `target/debug` or `target/release` milcheck ``` -you can print the lastest news, handy to stay informed +In addition to the mirrorlist output you can print the Arch Linux +[lastest news](https://archlinux.org/), handy to stay informed: ``` milcheck -n ``` diff --git a/src/cli.rs b/src/cli.rs index d1f36ac..34d5ba1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use std::env::Args; #[derive(Debug)] pub struct Parser<'a> { - args: std::env::Args, + args: Vec, flags: Vec>, binary: String, } @@ -14,24 +14,22 @@ pub struct Parser<'a> { #[derive(Debug)] pub struct Flag<'a>(pub &'a str, char, &'a str, bool); -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Token<'a> { - Argument(String), - Option(&'a Flag<'a>, Option), - UnknownOpt(String), + Argument(&'a str), + Option(&'a Flag<'a>, Option<&'a str>), + UnknownFlag(&'a str), + UnknownShortFlag(char), } -fn parse_token<'a>(options: &'a [Flag], arg: &str) -> Vec> { +fn parse_token<'a>(options: &'a [Flag], arg: &'a str) -> Vec> { let current_arg = &arg[1..]; let mut tokens = vec![]; for (i, c) in current_arg.char_indices() { if let Some(option) = options.iter().find(|option| c == option.1) { if option.3 { if i + 1 < current_arg.len() { - tokens.push(Token::Option( - option, - Some(String::from(¤t_arg[i + 1..])), - )); + tokens.push(Token::Option(option, Some(¤t_arg[i + 1..]))); break; } else { tokens.push(Token::Option(option, None)); @@ -40,20 +38,20 @@ fn parse_token<'a>(options: &'a [Flag], arg: &str) -> Vec> { tokens.push(Token::Option(option, None)); } } else { - tokens.push(Token::UnknownOpt(c.to_string())); + tokens.push(Token::UnknownShortFlag(c)); } } tokens } -fn parse_long_token<'a>(options: &'a [Flag], arg: &str) -> Token<'a> { +fn parse_long_token<'a>(options: &'a [Flag], arg: &'a str) -> Token<'a> { let current_arg = &arg[2..]; match current_arg.find('=') { None => { return if let Some(option) = options.iter().find(|option| current_arg == option.2) { Token::Option(option, None) } else { - Token::UnknownOpt(current_arg.to_string()) + Token::UnknownFlag(current_arg) } } Some(i) => { @@ -61,37 +59,29 @@ fn parse_long_token<'a>(options: &'a [Flag], arg: &str) -> Token<'a> { let last = ¤t_arg[i + 1..]; if let Some(option) = options.iter().find(|option| first == option.2) { return if option.3 && !last.is_empty() { - Token::Option(option, Some(String::from(last))) + Token::Option(option, Some(last)) } else { Token::Option(option, None) }; } else { - Token::UnknownOpt(current_arg.to_string()) + Token::UnknownFlag(current_arg) } } } } -fn tokenize<'a>(args: &mut Args, flags: &'a [Flag]) -> Vec> { +fn tokenize<'a>(args: &'a [String], flags: &'a [Flag]) -> Vec> { let mut tokens = vec![]; let mut accept_opt = true; for arg in args { - if !tokens.is_empty() { - let tokens_len = tokens.len(); - let prev_token = &tokens[tokens_len - 1]; - if let Token::Option(opt, None) = prev_token { - tokens[tokens_len - 1] = Token::Option(opt, Some(String::from(&arg))); - continue; - } - } if arg == "-" { - tokens.push(Token::Argument(String::from("-"))); + tokens.push(Token::Argument(arg)); } else if arg == "--" { accept_opt = false; } else if arg.len() > 2 && arg.starts_with("--") && accept_opt { - tokens.push(parse_long_token(flags, &arg)); + tokens.push(parse_long_token(flags, arg)); } else if arg.len() > 1 && arg.starts_with('-') && accept_opt { - tokens.append(&mut parse_token(flags, &arg)); + tokens.append(&mut parse_token(flags, arg)); } else { tokens.push(Token::Argument(arg)); } @@ -99,29 +89,26 @@ fn tokenize<'a>(args: &mut Args, flags: &'a [Flag]) -> Vec> { tokens } -pub fn normalize(tokens: &mut Vec) { - let mut to_merge = vec![]; - let mut inc = 0; - let mut token_iter = tokens.iter().enumerate().peekable(); - while let Some((i, token)) = token_iter.next() { - if let Token::Option(flag, arg) = token { - if flag.3 && *arg == None { - if let Some((_j, Token::Argument(value))) = token_iter.peek() { - to_merge.push((i - inc, *flag, value.to_string())); - inc += 1; +pub fn normalize<'a>(tokens: &[Token<'a>]) -> Vec> { + let mut result = vec![]; + let mut token_iter = tokens.iter().peekable(); + while let Some(token) = token_iter.next() { + if let Token::Option(flag, None) = token { + if flag.3 { + if let Some(Token::Argument(value)) = token_iter.peek() { + result.push(Token::Option(flag, Some(value))); + token_iter.next(); + continue; } } } + result.push(*token); } - for (i, flag, arg) in to_merge { - tokens.remove(i); - tokens.remove(i); - tokens.insert(i, Token::Option(flag, Some(arg))); - } + result } -impl<'a> Parser<'a> { - pub fn new(mut args: Args) -> Parser<'a> { +impl<'a> From for Parser<'a> { + fn from(mut args: Args) -> Self { let mut binary = env!("CARGO_PKG_NAME").to_string(); if let Some(name) = args.next() { let path: Vec<&str> = name.split('/').collect(); @@ -130,12 +117,31 @@ impl<'a> Parser<'a> { } } Parser { - args, + args: args.collect(), flags: vec![], binary, } } +} +impl<'a, const N: usize> From<&[&str; N]> for Parser<'a> { + fn from(args: &[&str; N]) -> Self { + let mut binary = env!("CARGO_PKG_NAME").to_string(); + if let Some(name) = args.first() { + let path: Vec<&str> = name.split('/').collect(); + if let Some(value) = path.last() { + binary = value.to_string(); + } + } + Parser { + args: args.iter().skip(1).map(|str| str.to_string()).collect(), + flags: vec![], + binary, + } + } +} + +impl<'a> Parser<'a> { pub fn binary_name(&self) -> String { self.binary.to_string() } @@ -156,10 +162,10 @@ impl<'a> Parser<'a> { self } - // pub fn verbose(&mut self) -> &mut Parser { - // self.flags.push(Flag("verbose", 'V', "verbose", false)); - // self - // } + pub fn verbose(&mut self) -> &mut Parser<'a> { + self.flags.push(Flag("verbose", 'V', "verbose", false)); + self + } pub fn version(&mut self) -> &mut Parser<'a> { self.flags.push(Flag("version", 'v', "version", false)); @@ -170,14 +176,391 @@ impl<'a> Parser<'a> { self.flags.push(Flag("license", 'L', "license", false)); self } - // pub fn debug(&mut self) -> &mut Parser { - // self.flags.push(Flag("debug", 'd', "debug", false)); - // self - // } + + pub fn debug(&mut self) -> &mut Parser<'a> { + self.flags.push(Flag("debug", 'd', "debug", false)); + self + } pub fn parse(&'a mut self) -> Vec> { - let mut tokens = tokenize(&mut self.args, &self.flags); - normalize(&mut tokens); - tokens + let tokens = tokenize(&self.args, &self.flags); + normalize(&tokens) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_empty_args() { + let mut parser = Parser::from(&[]); + let parsed = parser.parse(); + assert!(parsed.is_empty()); + } + + #[test] + fn binary_name_empty_args() { + let parser = Parser::from(&[]); + let binary_name = parser.binary_name(); + assert_eq!(binary_name, env!("CARGO_PKG_NAME")); + } + + #[test] + fn binary_name_one_arg() { + let parser = Parser::from(&["bin"]); + let binary_name = parser.binary_name(); + assert_eq!(binary_name, "bin"); + } + + #[test] + fn binary_name_args() { + let parser = Parser::from(&["bin", "un"]); + let binary_name = parser.binary_name(); + assert_eq!(binary_name, "bin"); + } + + #[test] + fn parse_with_one_arg() { + let mut parser = Parser::from(&["un"]); + let parsed = parser.parse(); + assert!(parsed.is_empty()); + } + + #[test] + fn parse_with_args_no_config() { + let mut parser = Parser::from(&["un", "deux", "trois"]); + let parsed = parser.parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Argument("deux")) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Argument("trois")) => {} + _ => panic!(), + } + } + + #[test] + fn flag_help() { + let mut parser = Parser::from(&["bin", "-h"]); + let parsed = parser.help().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("help", 'h', "help", false), None)) => {} + _ => panic!(), + } + let mut parser = Parser::from(&["bin", "--help"]); + let parsed = parser.help().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("help", 'h', "help", false), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_version() { + let mut parser = Parser::from(&["bin", "-v"]); + let parsed = parser.version().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("version", 'v', "version", false), None)) => {} + _ => panic!(), + } + let mut parser = Parser::from(&["bin", "--version"]); + let parsed = parser.version().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("version", 'v', "version", false), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_license() { + let mut parser = Parser::from(&["bin", "-L"]); + let parsed = parser.license().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("license", 'L', "license", false), None)) => {} + _ => panic!(), + } + let mut parser = Parser::from(&["bin", "--license"]); + let parsed = parser.license().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("license", 'L', "license", false), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_verbose() { + let mut parser = Parser::from(&["bin", "-V"]); + let parsed = parser.verbose().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("verbose", 'V', "verbose", false), None)) => {} + _ => panic!(), + } + let mut parser = Parser::from(&["bin", "--verbose"]); + let parsed = parser.verbose().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("verbose", 'V', "verbose", false), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_debug() { + let mut parser = Parser::from(&["bin", "-d"]); + let parsed = parser.debug().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("debug", 'd', "debug", false), None)) => {} + _ => panic!(), + } + let mut parser = Parser::from(&["bin", "--debug"]); + let parsed = parser.debug().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("debug", 'd', "debug", false), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_custom() { + let mut parser = Parser::from(&["bin", "-f"]); + let parsed = parser.flag("flag", 'f', "flag", false).parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("flag", 'f', "flag", false), None)) => {} + _ => panic!(), + } + let mut parser = Parser::from(&["bin", "--flag"]); + let parsed = parser.flag("flag", 'f', "flag", false).parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("flag", 'f', "flag", false), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_no_arg_long() { + let mut parser = Parser::from(&["bin", "--flag=ignored", "arg"]); + let parsed = parser.flag("flag", 'f', "flag", false).parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Argument("arg")) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_no_arg_short() { + let mut parser = Parser::from(&["bin", "-f", "arg"]); + let parsed = parser.flag("flag", 'f', "flag", false).parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Argument("arg")) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_long() { + let mut parser = Parser::from(&["bin", "--flag=arg", "arg"]); + let parsed = parser.flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), Some("arg"))) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_long_split_1() { + let mut parser = Parser::from(&["bin", "--flag", "arg"]); + let parsed = parser.flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(flag, Some(arg))) => { + assert_eq!(flag.0, "flag"); + assert_eq!(*arg, "arg"); + } + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_long_split_2() { + let mut parser = Parser::from(&["bin", "--flag", "-h"]); + let parsed = parser.help().flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Option(Flag("help", ..), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_short_1() { + let mut parser = Parser::from(&["bin", "-f", "arg"]); + let parsed = parser.flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), Some("arg"))) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_short_2() { + let mut parser = Parser::from(&["bin", "-f", "-h"]); + let parsed = parser.help().flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Option(Flag("help", ..), None)) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_short_glued_1() { + let mut parser = Parser::from(&["bin", "-farg", "un"]); + let parsed = parser.flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), Some("arg"))) => {} + _ => panic!(), + } + } + + #[test] + fn flag_takes_arg_short_glued_2() { + let mut parser = Parser::from(&["bin", "-f-h"]); + let parsed = parser.help().flag("flag", 'f', "flag", true).parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::Option(Flag("flag", ..), Some("-h"))) => {} + _ => panic!(), + } + } + + #[test] + fn unknown_short_flag() { + let mut parser = Parser::from(&["bin", "-n"]); + let parsed = parser.help().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::UnknownShortFlag('n')) => {} + _ => panic!(), + } + } + + #[test] + fn unknown_long_flag() { + let mut parser = Parser::from(&["bin", "--nobody"]); + let parsed = parser.help().parse(); + assert_eq!(parsed.len(), 1); + match parsed.get(0) { + Some(Token::UnknownFlag("nobody")) => {} + _ => panic!(), + } + } + + #[test] + fn parse_short_series_1() { + let mut parser = Parser::from(&["bin", "-hnvd"]); + let parsed = parser.version().verbose().debug().help().parse(); + assert_eq!(parsed.len(), 4); + match parsed.get(0) { + Some(Token::Option(Flag("help", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::UnknownShortFlag('n')) => {} + _ => panic!(), + } + match parsed.get(2) { + Some(Token::Option(Flag("version", ..), None)) => {} + _ => panic!(), + } + match parsed.get(3) { + Some(Token::Option(Flag("debug", ..), None)) => {} + _ => panic!(), + } + } + + #[test] + fn parse_short_series_2() { + let mut parser = Parser::from(&["bin", "-hfvd"]); + let parsed = parser + .version() + .verbose() + .debug() + .help() + .flag("flag", 'f', "flag", true) + .parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("help", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Option(Flag("flag", ..), Some("vd"))) => {} + _ => panic!(), + } + } + + #[test] + fn just_a_dash() { + let mut parser = Parser::from(&["bin", "-", "-h"]); + let parsed = parser.help().parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Argument("-")) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Option(Flag("help", ..), None)) => {} + _ => panic!(), + } + } + + #[test] + fn double_dash() { + let mut parser = Parser::from(&["bin", "-v", "--", "-h"]); + let parsed = parser.help().version().parse(); + assert_eq!(parsed.len(), 2); + match parsed.get(0) { + Some(Token::Option(Flag("version", ..), None)) => {} + _ => panic!(), + } + match parsed.get(1) { + Some(Token::Argument("-h")) => {} + _ => panic!(), + } } } diff --git a/src/main.rs b/src/main.rs index 8231157..1f64a5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,12 +40,18 @@ FLAGS: process::exit(0); } } - Token::UnknownOpt(option) => { + Token::UnknownFlag(option) => { return Err(format!( "unknown option \"{}\", run {} --help", option, binary_name )); } + Token::UnknownShortFlag(c) => { + return Err(format!( + "unknown option \"{}\", run {} --help", + c, binary_name + )); + } Token::Argument(arg) => { return Err(format!( "unexpected argument \"{}\", run {} --help", @@ -58,7 +64,7 @@ FLAGS: } fn main() { - let mut parser = Parser::new(env::args()); + let mut parser = Parser::from(env::args()); let binary_name = parser.binary_name(); let parsed = parser .help()