diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 23ce583..9050a3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,11 +22,14 @@ jobs: target: - x86_64-unknown-linux-gnu - x86_64-apple-darwin + - x86_64-pc-windows-msvc include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest - target: x86_64-apple-darwin os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest runs-on: ${{matrix.os}} @@ -54,14 +57,11 @@ jobs: - name: Clippy run: | cargo clippy --release --target ${{ matrix.target }} - cargo clippy --release --all-features --target ${{ matrix.target }} - name: Tests (Debug) run: | cargo test --target ${{ matrix.target }} - cargo test --all-features --target ${{ matrix.target }} - name: Tests (Release) run: | cargo test --release --target ${{ matrix.target }} - cargo test --release --all-features --target ${{ matrix.target }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d44beb..c2cd594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## [0.10.0](https://github.com/Blobfolio/argyle/releases/tag/v0.10.0) - 2024-10-17 + +This release finishes the work of the last one. The streaming version of `Argue` is now stable and all there is; the old methods and structs have been removed. + +Check out the [docs](https://docs.rs/argyle/latest/argyle/) to see how it all works! + + + ## [0.9.0](https://github.com/Blobfolio/argyle/releases/tag/v0.9.0) - 2024-10-14 This release introduces a brand new streaming version of the argument parser `Argue`. It is simpler and cleaner, but works completely differently than the original. diff --git a/CREDITS.md b/CREDITS.md index 32f5fa3..5dee418 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,6 +1,6 @@ # Project Dependencies Package: argyle - Version: 0.9.0 - Generated: 2024-10-14 21:16:53 UTC + Version: 0.10.0 + Generated: 2024-10-17 19:06:19 UTC This package has no dependencies. diff --git a/Cargo.toml b/Cargo.toml index 5a5c764..0946e58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "argyle" -version = "0.9.0" +version = "0.10.0" authors = ["Blobfolio, LLC. "] edition = "2021" rust-version = "1.81" @@ -21,18 +21,10 @@ exclude = [ [package.metadata.docs.rs] rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"] -features = [ "dynamic-help" ] default-target = "x86_64-unknown-linux-gnu" -targets = [ "x86_64-unknown-linux-gnu", "x86_64-apple-darwin" ] [package.metadata.bashman] name = "Argyle" bash-dir = "./" man-dir = "./" credits-dir = "./" - -[features] -default = [] - -# Enables ArgyleError::WantsDynamicHelp variant. -dynamic-help = [] diff --git a/README.md b/README.md index 813f7ad..bf1c60c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This crate provides a simple streaming CLI argument parser/iterator called `Argue`, offering a middle ground between the standard library's barebones `std::env::args_os` helper and full-service crates like [clap](https://crates.io/crates/clap). -`Argue` performs some basic normalization — it handles string conversion in a non-panicking way, recognizes shorthand value assignments like `-kval`, `-k=val`, `--key=val`, and handles end-of-command (`--`) arguments — and will help identify any special subcommands and/or keys/values expected by your app. +`Argue` performs some basic normalization — it handles string conversion in a non-panicking way, recognizes shorthand value assignments like `-kval`, `-k=val`, `--key=val`, and handles end-of-command (`--`) arguments — and will help identify any special keys/values expected by your app. The subsequent validation and handling, however, are left _entirely up to you_. Loop, match, and proceed however you see fit. @@ -33,10 +33,10 @@ argyle = "0.9.*" A general setup might look something like the following. -Refer to the documentation for `Argue` for more information, caveats, etc. +Refer to the documentation for `Argue`, `KeyWord`, and `Argument` for more information, caveats, etc. ```rust -use argyle::stream::Argument; +use argyle::{Argument, KeyWord}; use std::path::PathBuf; #[derive(Debug, Clone, Default)] @@ -47,46 +47,47 @@ struct Settings { paths: Vec, } -fn main() { - let args = argyle::stream::args() - .with_keys([ - ("-h", false), // Boolean flag. - ("--help", false), // Boolean flag. - ("--threads", true), // Expects a value. - ("--verbose", false), // Boolean flag. - ]) - .unwrap(); // An error will only occur if a - // duplicate or invalid key is declared. - - // Loop and handle! - let mut settings = Settings::default(); - for arg in args { - match arg { - Argument::Key("-h" | "--help") => { - println!("Help Screen Goes Here."); - return; - }, - Argument::Key("--verbose") => { - settings.verbose = true; - }, - Argument::KeyWithValue("--threads", threads) => { - settings.threads = threads.parse().expect("Threads must be a number!"); - }, - // Something else… maybe you want to assume it's a path? - Argument::Other(v) => { - settings.paths.push(PathBuf::from(v)); - }, - // Also something else, but not String-able. Paths don't care, - // though, so for this example maybe you just keep it? - Argument::InvalidUtf8(v) => { - settings.paths.push(PathBuf::from(v)); - }, - _ => {}, // Not relevant here. - } +let args = argyle::args() + .with_keywords([ + KeyWord::key("-h").unwrap(), // Boolean flag (short). + KeyWord::key("--help").unwrap(), // Boolean flag (long). + KeyWord::key_with_value("-j").unwrap(), // Expects a value. + KeyWord::key_with_value("--threads").unwrap(), + ]); + +// Loop and handle! +let mut settings = Settings::default(); +for arg in args { + match arg { + // Help flag match. + Argument::Key("-h" | "--help") => { + println!("Help Screen Goes Here."); + return; + }, + + // Thread option match. + Argument::KeyWithValue("-j" | "--threads", value) => { + settings.threads = value.parse() + .expect("Maximum threads must be a number!"); + }, + + // Something else. + Argument::Other(v) => { + settings.paths.push(PathBuf::from(v)); + }, + + // Also something else, but not String-able. PathBuf doesn't care + // about UTF-8, though, so it might be fine! + Argument::InvalidUtf8(v) => { + settings.paths.push(PathBuf::from(v)); + }, + + // Nothing else is relevant here. + _ => {}, } - - // Do something with those settings… } + +// Now that you're set up, do stuff… ``` diff --git a/examples/debug.rs b/examples/debug.rs index 0f60642..e559289 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -5,7 +5,7 @@ This example parses any arbitrary arguments fed to it and displays the results. */ fn main() { - for arg in argyle::stream::args() { + for arg in argyle::args() { println!("\x1b[2m-----\x1b[0m\n{arg:?}"); } println!("\x1b[2m-----\x1b[0m"); diff --git a/justfile b/justfile index 31d0977..682b66e 100644 --- a/justfile +++ b/justfile @@ -38,12 +38,8 @@ doc_dir := justfile_directory() + "/doc" # Clippy. @clippy: clear - cargo clippy \ - --all-features \ - --target-dir "{{ cargo_dir }}" cargo clippy \ --release \ - --all-features \ --target-dir "{{ cargo_dir }}" @@ -58,7 +54,6 @@ doc_dir := justfile_directory() + "/doc" clear cargo run \ -q \ - --all-features \ --release \ --example "debug" \ --target-dir "{{ cargo_dir }}" \ @@ -67,16 +62,9 @@ doc_dir := justfile_directory() + "/doc" # Build Docs. @doc: - # Make sure nightly is installed; this version generates better docs. - env RUSTUP_PERMIT_COPY_RENAME=true rustup install nightly - - # Make the docs. - cargo +nightly rustdoc \ + cargo rustdoc \ --release \ - --all-features \ - --target-dir "{{ cargo_dir }}" \ - -- \ - --cfg docsrs + --target-dir "{{ cargo_dir }}" # Move the docs and clean up ownership. [ ! -d "{{ doc_dir }}" ] || rm -rf "{{ doc_dir }}" diff --git a/src/argue.rs b/src/argue.rs deleted file mode 100644 index b4a76b2..0000000 --- a/src/argue.rs +++ /dev/null @@ -1,1514 +0,0 @@ -/*! -# Argyle: Argue -*/ - -use crate::{ - ArgyleError, - ArgsOsStr, - KeyKind, - Options, - OptionsOsStr, -}; -use std::{ - cell::Cell, - ffi::{ - OsStr, - OsString, - }, - fs::File, - io::{ - BufRead, - BufReader, - IsTerminal, - }, - ops::{ - BitOr, - Deref, - Index, - }, - os::unix::ffi::{ - OsStrExt, - OsStringExt, - }, -}; - - - -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Flag: Argument(s) Required. -/// -/// If a program is called with zero arguments — no flags, options, trailing -/// args —, an error will be printed and the thread will exit with status code -/// `1`. -pub const FLAG_REQUIRED: u8 = 0b0000_0001; - -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Flag: Expect Subcommand. -/// -/// Set this flag to treat the first value as a subcommand rather than a -/// trailing argument. (This fixes the edge case where the command has zero -/// dash-prefixed keys.) -pub const FLAG_SUBCOMMAND: u8 = 0b0000_0010; - -#[cfg(feature = "dynamic-help")] -#[cfg_attr(docsrs, doc(cfg(feature = "dynamic-help")))] -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Flag: Check For Help Flag. -/// -/// When set, [`Argue`] will return [`ArgyleError::WantsDynamicHelp`] if help args -/// are present. The subcommand, if any, is included, allowing the caller to -/// dynamically handle output. -pub const FLAG_DYNAMIC_HELP: u8 = 0b0000_0100; - -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Flag: Check For Help Flag. -/// -/// When set, [`Argue`] will return [`ArgyleError::WantsHelp`] if help args are -/// present. -pub const FLAG_HELP: u8 = 0b0000_1000; - -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Flag: Check For Version Flag. -/// -/// When set, [`Argue`] will return [`ArgyleError::WantsVersion`] if version -/// args are present. -pub const FLAG_VERSION: u8 = 0b0001_0000; - -/// # Flag: Has Help. -/// -/// This flag is set if either `-h` or `--help` switches are present. It has -/// no effect unless [`Argue::FLAG_HELP`] is set. -const FLAG_HAS_HELP: u8 = 0b0010_0000; - -/// # Flag: Has Version. -/// -/// This flag is set if either `-V` or `--version` switches are present. It has -/// no effect unless [`Argue::FLAG_VERSION`] is set. -const FLAG_HAS_VERSION: u8 = 0b0100_0000; - -/// # Flag: Do Version. -/// -/// When both `FLAG_VERSION` and `FLAG_HAS_VERSION` are set. -const FLAG_DO_VERSION: u8 = FLAG_VERSION | FLAG_HAS_VERSION; - -#[cfg(feature = "dynamic-help")] -/// # Flag: Any Help. -/// -/// When either `FLAG_HELP` or `FLAG_DYNAMIC_HELP` are set. -const FLAG_ANY_HELP: u8 = FLAG_HELP | FLAG_DYNAMIC_HELP; - -#[cfg(not(feature = "dynamic-help"))] -/// # Flag: Any Help. -/// -/// When either `FLAG_HELP` or `FLAG_DYNAMIC_HELP` are set. -const FLAG_ANY_HELP: u8 = FLAG_HELP; - - - -#[derive(Debug, Clone, Default)] -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// `Argue` is an agnostic CLI argument parser. Unlike more robust libraries -/// like [clap](https://crates.io/crates/clap), `Argue` does not hold -/// information about expected or required arguments; it merely parses the raw -/// arguments into a consistent state so the implementor can query them as -/// needed. -/// -/// (It is effectively a wrapper around [`std::env::args_os`].) -/// -/// Post-processing is an exercise largely left to the implementing library to -/// do in its own way, in its own time. `Argue` exposes several methods for -/// quickly querying the individual pieces of the set, but it can also be -/// dereferenced to a slice or consumed into an owned vector for fully manual -/// processing if desired. -/// -/// Arguments are processed and held as owned bytes rather than (os)strings, -/// again leaving the choice of later conversion entirely up to the -/// implementor. -/// -/// For simple applications, this agnostic approach can significantly reduce -/// the overhead of processing CLI arguments, but because handling is left to -/// the implementing library, it might be too tedious or limiting for more -/// complex use cases. -/// -/// ## Assumptions -/// -/// `Argue` is built for speed and simplicity, and as such, contains a number -/// of assumptions and limitations that might make it unsuitable for use. -/// -/// ### Keys -/// -/// A "key" is an argument entry beginning with one or two dashes `-` and an -/// ASCII letter (`A..=Z` or `a..=z`). Entries with one dash are "short", and -/// can only consist of two bytes. Entries with two dashes are "long" and can -/// be however long they want to be. -/// -/// If a short key entry is longer than two bytes, everything in range `2..` is -/// assumed to be a value and is split off into its own entry. For example, -/// `-kVal` is equivalent to `-k Val`. -/// -/// If a long key contains an `=`, it is likewise assumed to be a key/value -/// pair, and will be split into two at that index. For example, `--key=Val` is -/// equivalent to `--key Val`. -/// -/// A key without a value is called a "switch". It is `true` if present, -/// `false` if not. -/// -/// A key with one value is called an "option". Multi-value options are *not* -/// supported. -/// -/// ### Trailing Arguments -/// -/// All values beginning after the last known switch or option value are -/// considered to be trailing arguments. Any number (including zero) of -/// trailing arguments can be provided. -/// -/// ### Restrictions -/// -/// 1. Keys are not checked for uniqueness, but only the first occurrence of a given key will ever match. -/// 2. Argument parsing stops if a passthrough separator `--` is found. Anything up to that point is parsed as usual; everything after is discarded. -/// -/// ## Examples -/// -/// `Argue` follows a builder pattern for construction, with a few odds and -/// ends tucked away as flags. -/// -/// ```no_run -/// use argyle::{Argue, FLAG_REQUIRED}; -/// -/// // Parse the env arguments, aborting if the set is empty. -/// let args = Argue::new(FLAG_REQUIRED).unwrap(); -/// -/// // Check to see what's there. -/// let switch: bool = args.switch(b"-s"); -/// let option: Option<&[u8]> = args.option(b"--my-opt"); -/// let extras: &[Vec] = args.args(); -/// ``` -/// -/// If you just want a clean set to iterate over, `Argue` can be dereferenced -/// to a slice: -/// -/// ```ignore -/// let arg_slice: &[Vec] = &args; -/// ``` -/// -/// Or it can be converted into an owned Vector: -/// ```ignore -/// let args: Vec> = args.take(); -/// ``` -pub struct Argue { - /// Parsed arguments. - args: Vec>, - - /// Highest non-arg index. - /// - /// This is used to divide the arguments between named and trailing values. - /// This is inferred during instantiation from the last-found dash-prefixed - /// key, but could be updated `+1` if that key turns out to be an option - /// (its value would then be the last non-trailing argument). - /// - /// The only way `Argue` knows switches from options is by the method - /// invoked by the implementing library. Be sure to request all options - /// before asking for trailing arguments. - last: Cell, - - /// Flags. - flags: u8, -} - -impl Deref for Argue { - type Target = [Vec]; - #[inline] - fn deref(&self) -> &Self::Target { &self.args } -} - -impl<'a> FromIterator<&'a [u8]> for Argue { - fn from_iter>(src: I) -> Self { - src.into_iter().map(<[u8]>::to_vec).collect() - } -} - -impl FromIterator> for Argue { - fn from_iter>>(src: I) -> Self { - src.into_iter().map(OsString::from_vec).collect() - } -} - -impl FromIterator for Argue { - fn from_iter>(src: I) -> Self { - let mut args: Vec> = Vec::with_capacity(16); - let mut last = 0_usize; - let mut flags = 0_u8; - let mut idx = 0_usize; - - for a in src { - let mut a = a.into_vec(); - let key: &[u8] = a.as_slice(); - - // Skip leading empties. - if 0 == idx && (key.is_empty() || key.iter().all(u8::is_ascii_whitespace)) { - continue; - } - - match KeyKind::from(key) { - KeyKind::None => { - if key == b"--" { break; } // Stop on separator. - - args.push(a); - idx += 1; - }, - KeyKind::Short => { - if key == b"-V" { flags |= FLAG_HAS_VERSION; } - else if key == b"-h" { flags |= FLAG_HAS_HELP; } - - args.push(a); - last = idx; - idx += 1; - }, - KeyKind::Long => { - if key == b"--version" { flags |= FLAG_HAS_VERSION; } - else if key == b"--help" { flags |= FLAG_HAS_HELP; } - - args.push(a); - last = idx; - idx += 1; - }, - KeyKind::ShortV => { - let b = a.split_off(2); - args.push(a); - args.push(b); - last = idx + 1; - idx += 2; - }, - KeyKind::LongV(end) => { - let b = - if end + 1 < key.len() { a.split_off(end + 1) } - else { Vec::new() }; - a.truncate(end); // Chop off the equal sign. - args.push(a); - args.push(b); - last = idx + 1; - idx += 2; - }, - } - } - - // Turn it into an object! - Self { - args, - last: Cell::new(last), - flags, - } - } -} - -impl Index for Argue { - type Output = [u8]; - - #[inline] - /// # Argument by Index. - /// - /// This returns the nth CLI argument, which could be a subcommand, key, - /// value, or trailing argument. - /// - /// If you're only interested in trailing arguments, use [`Argue::arg`] - /// instead. - /// - /// If you want everything, you can alternatively dereference [`Argue`] - /// into a slice. - /// - /// ## Panics - /// - /// This will panic if the index is out of range. Use [`Argue::len`] to - /// confirm the length ahead of time, or [`Argue::get`], which wraps the - /// answer in an `Option` instead of panicking. - fn index(&self, idx: usize) -> &Self::Output { &self.args[idx] } -} - -/// ## Instantiation and Builder Patterns. -impl Argue { - #[inline] - /// # New Instance. - /// - /// This simply parses the owned output of [`std::env::args_os`]. - /// - /// ## Examples - /// - /// ``` - /// use argyle::{Argue, ArgyleError, FLAG_VERSION}; - /// - /// // Parse, but abort if -V/--version is present. - /// let args = match Argue::new(FLAG_VERSION) { - /// Ok(a) => a, // No abort. - /// // The version flags were present. - /// Err(ArgyleError::WantsVersion) => { - /// println!("MyApp v{}", env!("CARGO_PKG_VERSION")); - /// return; - /// }, - /// // This probably won't happen with only FLAG_VERSION set, but just - /// // in case… - /// Err(e) => { - /// println!("Error: {}", e); - /// return; - /// }, - /// }; - /// - /// // If we're here, check whatever random args your program needs. - /// let quiet: bool = args.switch(b"-q"); - /// let foo: Option<&[u8]> = args.option2(b"-f", b"--foo"); - /// ``` - /// - /// ## Errors - /// - /// This method's result may represent an actual error, or some form of - /// abort, such as the presence of `-V`/`--version` when `FLAG_VERSION` - /// was passed to the constructor. - /// - /// Generally you'd want to match the specific [`ArgyleError`] variant to - /// make sure you're taking the appropriate action. - pub fn new(chk_flags: u8) -> Result { - let mut out: Self = std::env::args_os().skip(1).collect(); - out.check_flags(chk_flags)?; - Ok(out) - } - - /// # Set/Check Flags. - /// - /// This is run after [`Argue::new`] to see what's what. - fn check_flags(&mut self, flags: u8) -> Result<(), ArgyleError> { - if 0 < flags { - self.flags |= flags; - - // There are no arguments. - if self.args.is_empty() { - // Required? - if FLAG_REQUIRED == self.flags & FLAG_REQUIRED { - return Err(ArgyleError::Empty); - } - } - // Print version. - else if FLAG_DO_VERSION == self.flags & FLAG_DO_VERSION { - return Err(ArgyleError::WantsVersion); - } - // Help. - else if - 0 != self.flags & FLAG_ANY_HELP && - (FLAG_HAS_HELP == self.flags & FLAG_HAS_HELP || self.args[0] == b"help") - { - #[cfg(feature = "dynamic-help")] - if FLAG_DYNAMIC_HELP == self.flags & FLAG_DYNAMIC_HELP { - return Err(ArgyleError::WantsDynamicHelp( - if self.args[0][0] != b'-' && self.args[0] != b"help" { - Some(Box::from(self.args[0].as_slice())) - } - else { None } - )); - } - - return Err(ArgyleError::WantsHelp); - } - } - - Ok(()) - } - - #[must_use] - /// # With "Trailing" Arguments From a Text File or STDIN. - /// - /// Read lines from the text file — or STDIN if "-" — specified by the - /// built-in `-l`/`--list` option (if present), appending them to the set - /// as "trailing" arguments. (One argument per line.) - /// - /// These arguments, if any, along with anything the user included in the - /// actual command, will then be accessible the usual way, via methods like - /// [`Argue::args`], etc. - /// - /// Note that the input must be valid UTF-8. Its lines will be trimmed and - /// checked for length before inclusion, but won't otherwise be altered. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap().with_list(); - /// for arg in args.args() { - /// // Do something… - /// } - /// ``` - pub fn with_list(self) -> Self { - if let Some(p) = self.option2_os(b"-l", b"--list") { - // STDIN. - if p == "-" { - // But only if the stream appears to be redirected… - let stdin = std::io::stdin(); - if ! stdin.is_terminal() { - return self.with_trailing_args(stdin.lines().map_while(Result::ok)); - } - } - // Text file. - else if let Ok(raw) = File::open(p).map(BufReader::new) { - return self.with_trailing_args(raw.lines().map_while(Result::ok)); - } - } - - self - } - - #[must_use] - /// # With "Trailing" Arguments. - /// - /// Append arbitrary strings to the set as "trailing" arguments, making - /// them — along with anything the user included in the actual command — - /// available via methods like [`Argue::args`], etc. - /// - /// Note that arguments are trimmed and checked for length before being - /// added, but are otherwise passed through as-are. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0) - /// .unwrap() - /// .with_trailing_args(&["apples", "bananas", "carrots"]); - /// ``` - pub fn with_trailing_args(mut self, args: I) -> Self - where B: AsRef, I: IntoIterator { - for arg in args { - let bytes = arg.as_ref().trim().as_bytes(); - if ! bytes.is_empty() { - self.args.push(bytes.to_vec()); - } - } - - self - } - - /// # Verify Keys. - /// - /// Return the first key-like entry in the collection that is not a valid - /// switch or option. - /// - /// Key-like, in this context, means anything beginning with one or two - /// dashes, followed by an ASCII letter, that does not directly follow a - /// valid option (as that would make it a value). - /// - /// Note: depending on the context, this may return a false positive. For - /// example, a program expecting file paths as trailing arguments might - /// receive something key-like-but-not-a-key like "--foo.jpg". - pub fn check_keys(&self, switches: &[&[u8]], options: &[&[u8]]) -> Option<&[u8]> { - let len = self.args.len(); - let mut idx = 0; - while idx < len { - let v = self.args[idx].as_slice(); - - // If it's an option, we'll need to skip the next value, and maybe - // move the trailing arg pointer. - if options.contains(&v) { - idx += 1; - if self.last.get() < idx { self.last.set(idx); } - } - // Otherwise if it not a switch and is key-like, we're done! - else if ! switches.contains(&v) && is_key_like(v) { - return Some(v); - } - - // Bump and repeat. - idx += 1; - } - - None - } -} - -/// ## Casting. -/// -/// These methods convert `Argue` into different data structures. -impl Argue { - #[must_use] - #[inline] - /// # Into Owned Vec. - /// - /// Use this method to consume the struct and return the parsed arguments - /// as a `Vec>`. - /// - /// If you merely want something to iterate over, you can alternatively - /// dereference the struct to a string slice. - /// - /// If you're only interested in the _trailing_ arguments, use - /// `Argue::take_trailing` instead. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let args: Vec> = Argue::new(0).unwrap().take(); - /// ``` - pub fn take(self) -> Vec> { self.args } - - #[must_use] - /// # Take Trailing Arguments. - /// - /// Split off and return the instance's (owned) trailing arguments, - /// discarding everything else. - /// - /// As with other trailing argument-related methods, make sure you query - /// expected options before calling this method, otherwise it might - /// mistake a final associated value for a trailing argument. - /// - /// Of course, that matters less here than elsewhere since you'll lose - /// access to the switches and options anyway. Haha. - /// - /// If you want _everything_, use `Argue::take` instead. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let trailing: Vec> = Argue::new(0).unwrap().take_trailing(); - /// ``` - pub fn take_trailing(self) -> Vec> { - let idx = self.arg_idx(); - let Self { mut args, .. } = self; - args.drain(..idx); - args - } -} - -/// ## Queries. -/// -/// These methods allow data to be questioned and extracted. -impl Argue { - #[must_use] - #[inline] - /// # Switch. - /// - /// Returns `true` if the switch is present, `false` if not. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let switch: bool = args.switch(b"--my-switch"); - /// ``` - pub fn switch(&self, key: &[u8]) -> bool { self.args.iter().any(|x| x == key) } - - #[must_use] - #[inline] - /// # Switch x2. - /// - /// This is a convenience method that checks for the existence of two - /// switches at once, returning `true` if either are present. Generally - /// you would use this for a flag that has both a long and short version. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let switch: bool = args.switch2(b"-s", b"--my-switch"); - /// ``` - pub fn switch2(&self, short: &[u8], long: &[u8]) -> bool { - self.args.iter().any(|x| x == short || x == long) - } - - #[must_use] - /// # Switch Count. - /// - /// Return the total number times the switch was specified. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let count: usize = args.switch_count(b"--my-switch"); - /// ``` - pub fn switch_count(&self, key: &[u8]) -> usize { - self.args.iter().filter(|&x| x == key).count() - } - - #[must_use] - /// # Switch x2 Count. - /// - /// Return the total number times the switch was specified. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let switch2_count: usize = args.switch2_count(b"-s", b"--my-switch"); - /// ``` - pub fn switch2_count(&self, short: &[u8], long: &[u8]) -> usize { - self.args.iter().filter(|&x| x == short || x == long).count() - } - - #[must_use] - /// # Switch By Prefix. - /// - /// If you have multiple, mutually exclusive switches that all begin with - /// the same prefix, this method can be used to quickly return the first - /// match (stripped of the common prefix). - /// - /// If no match is found, or an _exact_ match is found — i.e. leaving the - /// key empty — `None` is returned. - /// - /// Do not use this if you have options sharing this prefix; `Argue` - /// doesn't know the difference so will simply return whatever it finds - /// first. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// match args.switch_by_prefix(b"--dump-") { - /// Some(b"addresses") => {}, // --dump-addresses - /// Some(b"names") => {}, // --dump-names - /// _ => {}, // No matches. - /// } - /// ``` - pub fn switch_by_prefix(&self, prefix: &[u8]) -> Option<&[u8]> { - if prefix.is_empty() { None } - else { - self.args.iter().find_map(|x| { - let key = x.strip_prefix(prefix)?; - if key.is_empty() { None } - else { Some(key) } - }) - } - } - - #[must_use] - /// # Switches As Bitflags. - /// - /// If you have a lot of switches that directly correspond to bitflags, you - /// can pass them all to this method and receive the appropriate combined - /// flag value back. - /// - /// This does not conflict with [`Argue::switch`]; if some of your flags - /// require special handling you can mix-and-match calls. - /// - /// Note: the default value of `N` is used as a starting point. For `u8`, - /// `u16`, etc., that's just `0`, but if using a custom type, make sure its - /// default state is the equivalent of "no flags". - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let flags: u8 = args.bitflags([ - /// (&b"-o"[..], 0b0000_0001), - /// (&b"-t"[..], 0b0000_0010), - /// ]); - /// ``` - pub fn bitflags<'a, N, I>(&self, pairs: I) -> N - where - N: BitOr + Default, - I: IntoIterator - { - pairs.into_iter() - .fold(N::default(), |flags, (switch, flag)| - if self.switch(switch) { flags | flag } - else { flags } - ) - } - - /// # Option. - /// - /// Return the value corresponding to `key`, if present. "Value" in this - /// case means the entry immediately following the key. - /// - /// Note: this method is the only way `Argue` knows whether or not a key - /// is an option (with a value) or a switch. Be sure to request all - /// possible options *before* requesting the trailing arguments to ensure - /// the division between named and trailing is properly set. - /// - /// This will only ever match the _last_ occurrence. For options that - /// may be specified more than once, use [`Argue::option_values`] instead. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let opt: Option<&[u8]> = args.option(b"--my-opt"); - /// ``` - pub fn option(&self, key: &[u8]) -> Option<&[u8]> { - let idx = self.args.iter().rposition(|x| x == key)? + 1; - self._option(idx) - } - - /// # Option x2. - /// - /// This is a convenience method that checks for the existence of two - /// options at once, returning the first found value, if any. Generally - /// you would use this for a flag that has both a long and short version. - /// - /// This will only ever match the _last_ occurrence. For options that - /// may be specified more than once, use [`Argue::option2_values`] instead. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let opt: Option<&[u8]> = args.option2(b"-o", b"--my-opt"); - /// ``` - pub fn option2(&self, short: &[u8], long: &[u8]) -> Option<&[u8]> { - let idx = self.args.iter().rposition(|x| x == short || x == long)? + 1; - self._option(idx) - } - - /// # Option Value(s) Iterator. - /// - /// Return any and all values corresponding to `key` (meaning the entries - /// immediately following each instance of `key`). - /// - /// This is useful for programs that accept the same flag multiple times, - /// or those expecting a byte-delimited value, like a comma-separated list. - /// - /// When using `delimiter`, each value will be carved up by the specified - /// byte and returned separately. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// for v in args.option_values(b"--my-option", None) { - /// println!("{:?}", std::str::from_utf8(v)); - /// } - /// ``` - pub fn option_values<'a>(&'a self, key: &'a [u8], delimiter: Option) - -> Options<'a> { - if let Some(idx) = self.args.iter().rposition(|x| x == key) { - let idx = idx + 1; - if idx < self.args.len() { - if self.last.get() < idx { self.last.set(idx); } - return Options::new(&self.args[..=idx], key, None, delimiter); - } - } - - Options::default() - } - - /// # Option 2x Value(s) Iterator. - /// - /// Return any and all values corresponding to either the `short` or `long` - /// version of a key (meaning the entries immediately following each - /// instance of them). - /// - /// This is useful for programs that accept the same flag multiple times, - /// or those expecting a byte-delimited value, like a comma-separated list. - /// - /// When using `delimiter`, each value will be carved up by the specified - /// byte and returned separately. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// for v in args.option2_values(b"-o", b"--my-opt", None) { - /// println!("{:?}", std::str::from_utf8(v)); - /// } - /// ``` - pub fn option2_values<'a>(&'a self, short: &'a [u8], long: &'a [u8], delimiter: Option) - -> Options<'a> { - if let Some(idx) = self.args.iter().rposition(|x| x == short || x == long) { - let idx = idx + 1; - if idx < self.args.len() { - if self.last.get() < idx { self.last.set(idx); } - return Options::new(&self.args[..=idx], short, Some(long), delimiter); - } - } - - Options::default() - } - - #[must_use] - /// # Option By Prefix. - /// - /// If you have multiple, mutually exclusive options that all begin with - /// the same prefix, this method can be used to quickly return the first - /// matching key (stripped of the common prefix) and value. - /// - /// If no match is found, an _exact_ match is found — i.e. leaving the - /// key empty — or no value follows, `None` is returned. - /// - /// Do not use this if you have switches sharing this prefix; `Argue` - /// doesn't know the difference so will simply return whatever it finds - /// first. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// match args.option_by_prefix(b"--color-") { - /// Some((b"solid", val)) => {}, // --color-solid, green - /// Some((b"dashed", val)) => {}, // --color-dashed, blue - /// _ => {}, // No matches. - /// } - /// ``` - pub fn option_by_prefix(&self, prefix: &[u8]) -> Option<(&[u8], &[u8])> { - if prefix.is_empty() { None } - else { - let (idx, key) = self.args.iter() - .enumerate() - .find_map(|(idx, x)| { - let key = x.strip_prefix(prefix)?; - if key.is_empty() { None } - else { Some((idx, key)) } - })?; - - let val = self._option(idx + 1)?; - Some((key, val)) - } - } - - /// # Return Option at Index. - /// - /// This method holds the common code for [`Argue::option`] and - /// [`Argue::option2`]. It returns the argument at the index they find, - /// nudging the options/args boundary upward if needed. - /// - /// This will return `None` if the index is out of range. - fn _option(&self, idx: usize) -> Option<&[u8]> { - let arg = self.args.get(idx)?; - if self.last.get() < idx { self.last.set(idx); } - Some(arg.as_slice()) - } - - #[must_use] - /// # Trailing Arguments. - /// - /// This returns a slice from the end of the result set assumed to - /// represent unnamed arguments. The boundary for the split is determined - /// by the position of the last known key (or key value). - /// - /// It is important to query any expected options prior to calling this - /// method, as the existence of those options might shift the boundary. - /// - /// If there are no arguments, an empty slice is returned. - /// - /// ## Examples - /// - /// ```no_run - /// use argyle::Argue; - /// - /// let mut args = Argue::new(0).unwrap(); - /// let extras: &[Vec] = args.args(); - /// ``` - pub fn args(&self) -> &[Vec] { - let idx = self.arg_idx(); - if idx < self.args.len() { &self.args[idx..] } - else { &[] } - } - - #[must_use] - /// # Arg at Index. - /// - /// Pluck the nth trailing argument by index (starting from zero). - /// - /// Note, this is different than dereferencing the whole `Argue` struct - /// and requesting its zero index; that would refer to the first CLI - /// argument of any kind, which could be a subcommand or key. - pub fn arg(&self, idx: usize) -> Option<&[u8]> { - let start_idx = self.arg_idx(); - self.args.get(start_idx + idx).map(Vec::as_slice) - } -} - -/// ## Misc Indexing. -impl Argue { - #[must_use] - /// # Get Argument. - /// - /// This is the non-panicking way to index into a specific subcommand, key, - /// value, etc. It will be returned if it exists, otherwise you'll get `None` - /// if the index is out of range. - /// - /// If you _know_ the index is valid, you can leverage the `std::ops::Index` - /// trait to fetch the value directly. - pub fn get(&self, idx: usize) -> Option<&[u8]> { - self.args.get(idx).map(Vec::as_slice) - } - - #[inline] - #[must_use] - /// # Is Empty? - pub fn is_empty(&self) -> bool { self.args.is_empty() } - - #[inline] - #[must_use] - /// # Length. - /// - /// Return the length of all the arguments (keys, values, etc.) held by - /// the instance. - pub fn len(&self) -> usize { self.args.len() } -} - -/// # `OsStr` Methods. -impl Argue { - #[must_use] - /// # Switch Starting With… as `OsStr`. - /// - /// This works just like [`Argue::switch_by_prefix`], except it returns the - /// value as an [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn switch_by_prefix_os(&self, prefix: &[u8]) -> Option<&OsStr> { - self.switch_by_prefix(prefix).map(OsStr::from_bytes) - } - - #[must_use] - /// # Option as `OsStr`. - /// - /// This works just like [`Argue::option`], except it returns the value as - /// an [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn option_os(&self, key: &[u8]) -> Option<&OsStr> { - self.option(key).map(OsStr::from_bytes) - } - - #[must_use] - /// # Option x2 as `OsStr`. - /// - /// This works just like [`Argue::option2`], except it returns the value as - /// an [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn option2_os(&self, short: &[u8], long: &[u8]) -> Option<&OsStr> { - self.option2(short, long).map(OsStr::from_bytes) - } - - /// # Option Value(s) Iterator. - /// - /// This works just like [`Argue::option_values`], except it returns the value - /// as an [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn option_values_os<'a>(&'a self, key: &'a [u8], delimiter: Option) - -> OptionsOsStr<'a> { - OptionsOsStr(self.option_values(key, delimiter)) - } - - /// # Option 2x Value(s) Iterator. - /// - /// This works just like [`Argue::option2_values`], except it returns the value - /// as an [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn option2_values_os<'a>(&'a self, short: &'a [u8], long: &'a [u8], delimiter: Option) - -> OptionsOsStr<'a> { - OptionsOsStr(self.option2_values(short, long, delimiter)) - } - - #[must_use] - /// # Option Starting With… as `OsStr`. - /// - /// This works just like [`Argue::option_by_prefix`], except it returns the - /// key/value as [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn option_by_prefix_os(&self, prefix: &[u8]) -> Option<(&OsStr, &OsStr)> { - self.option_by_prefix(prefix) - .map(|(k, v)| (OsStr::from_bytes(k), OsStr::from_bytes(v))) - } - - #[must_use] - /// # Trailing Arguments as `OsStr`. - /// - /// This works just like [`Argue::args`], except it returns an iterator - /// that yields [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn args_os(&self) -> ArgsOsStr { ArgsOsStr::new(self.args()) } - - #[must_use] - /// # Arg at Index as `OsStr`. - /// - /// This works just like [`Argue::arg`], except it returns the value as an - /// [`OsStr`](std::ffi::OsStr) instead of bytes. - pub fn arg_os(&self, idx: usize) -> Option<&OsStr> { - self.arg(idx).map(OsStr::from_bytes) - } -} - -/// ## Internal Helpers. -impl Argue { - /// # Arg Index. - /// - /// This is an internal method that returns the index at which the first - /// unnamed argument may be found. - /// - /// Note: the index may be out of range, but won't be used in that case. - fn arg_idx(&self) -> usize { - let last = self.last.get(); - if 0 == last && 0 == self.flags & FLAG_SUBCOMMAND { 0 } - else { last + 1 } - } -} - - - -/// # Is Key Like? -/// -/// Returns true if the value begins with one or two dashes followed by an -/// ASCII letter. -const fn is_key_like(v: &[u8]) -> bool { - let len = v.len(); - if len >= 2 && v[0] == b'-' { - if v[1] == b'-' { len > 2 && v[2].is_ascii_alphabetic() } - else { v[1].is_ascii_alphabetic() } - } - else { false } -} - - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[allow(clippy::cognitive_complexity)] // It is what it is. - fn t_parse_args() { - let mut base: Vec<&[u8]> = vec![ - b"hey", - b"-kVal", - b"--empty=", - b"--key=Val", - ]; - - let mut args: Argue = base.iter().copied().collect(); - - // Check the overall structure. - assert_eq!( - *args, - [ - b"hey".to_vec(), - b"-k".to_vec(), - b"Val".to_vec(), - b"--empty".to_vec(), - vec![], - b"--key".to_vec(), - b"Val".to_vec(), - ] - ); - - // Test the finders. - assert_eq!(args.get(0), Some(&b"hey"[..])); - - assert_eq!(&args[1], b"-k"); - assert!(args.switch(b"-k")); - assert!(args.switch(b"--key")); - assert!(args.switch2(b"-k", b"--key")); - - assert_eq!(args.option(b"--key"), Some(&b"Val"[..])); - assert_eq!(args.option2(b"-k", b"--key"), Some(&b"Val"[..])); - assert!(args.args().is_empty()); - - // These shouldn't exist. - assert!(! args.switch(b"-c")); - assert!(! args.switch2(b"-c", b"--copy")); - assert!(args.option(b"-c").is_none()); - assert!(args.option2(b"-c", b"--copy").is_none()); - assert!(args.get(100).is_none()); - - // Let's test a first-position key. - base.insert(0, b"--prefix"); - args = base.iter().copied().collect(); - - // The whole thing again. - assert_eq!( - *args, - [ - b"--prefix".to_vec(), - b"hey".to_vec(), - b"-k".to_vec(), - b"Val".to_vec(), - b"--empty".to_vec(), - vec![], - b"--key".to_vec(), - b"Val".to_vec(), - ] - ); - - assert_eq!(args.get(0), Some(&b"--prefix"[..])); - assert!(args.switch(b"--prefix")); - assert_eq!(args.option(b"--prefix"), Some(&b"hey"[..])); - - // This is as good a place as any to double-check the _os version links - // up correctly. - let hey = OsStr::new("hey"); - assert_eq!(args.option_os(b"--prefix"), Some(hey)); - - // Let's see what trailing args look like when there are none. - assert_eq!(args.arg(0), None); - - // If there are no keys, the first entry should also be the first - // argument. - args = std::iter::once(b"hello".to_vec()).collect(); - assert_eq!(args.arg(0), Some(&b"hello"[..])); - - // Unless we're expecting a subcommand... - args.flags |= FLAG_SUBCOMMAND; - assert!(args.arg(0).is_none()); - - // Let's also make sure the trailing arguments work too. - let trailing: &[&[u8]] = &[b"Hello", b"World"]; - base.extend_from_slice(trailing); - args = base.iter().copied().collect(); - assert_eq!(args.arg(0), Some(&b"Hello"[..])); - assert_eq!(args.arg(1), Some(&b"World"[..])); - assert_eq!(args.arg(2), None); - assert_eq!(args.args(), trailing); - assert_eq!(args.take_trailing(), trailing); // This should match too. - } - - #[test] - fn t_bitflags() { - const FLAG_EMPTY: u8 = 0b0000_0001; - const FLAG_HELLO: u8 = 0b0000_0010; - const FLAG_K: u8 = 0b0000_0100; - const FLAG_ONE_MORE: u8 = 0b0000_1000; - const FLAG_OTHER: u8 = 0b0001_0000; - - let base: Vec<&[u8]> = vec![ - b"hey", - b"-k", - b"--empty", - b"--key=Val", - b"--hello", - b"--one-more", - ]; - - let args: Argue = base.iter().copied().collect(); - let flags: u8 = args.bitflags([ - (&b"-k"[..], FLAG_K), - (&b"--empty"[..], FLAG_EMPTY), - (&b"--hello"[..], FLAG_HELLO), - (&b"--one-more"[..], FLAG_ONE_MORE), - (&b"--other"[..], FLAG_OTHER), - ]); - - assert_eq!(flags & FLAG_K, FLAG_K); - assert_eq!(flags & FLAG_EMPTY, FLAG_EMPTY); - assert_eq!(flags & FLAG_HELLO, FLAG_HELLO); - assert_eq!(flags & FLAG_ONE_MORE, FLAG_ONE_MORE); - assert_eq!(flags & FLAG_OTHER, 0); - } - - #[test] - fn t_by_prefix() { - let base: Vec<&[u8]> = vec![ - b"hey", - b"-k", - b"--dump-three", - b"--key-1=Val", - b"--dump-four", - b"--one-more", - ]; - - let args = base.iter().copied().collect::(); - assert_eq!(args.switch_by_prefix(b"--dump"), Some(&b"-three"[..])); - assert_eq!(args.switch_by_prefix(b"--dump-"), Some(&b"three"[..])); - assert_eq!(args.switch_by_prefix(b"--with"), None); - assert_eq!(args.switch_by_prefix(b"-k"), None); // Full matches suppressed. - - assert_eq!( - args.option_by_prefix(b"--key-"), - Some((&b"1"[..], &b"Val"[..])) - ); - assert_eq!(args.option_by_prefix(b"--foo"), None); - assert_eq!(args.option_by_prefix(b"--key-1"), None); // Full matches suppressed. - } - - #[test] - fn t_check_keys() { - let base: Vec<&[u8]> = vec![ - b"hey", - b"-kVal", - b"--empty=", - b"-f", - b"--key", - b"Val", - b"trailing", - b"args", - b"here", - ]; - - let args: Argue = base.iter().copied().collect(); - - // Before we do anything, the trailing arg marker will be in the wrong - // place. - assert_eq!(args.arg(0), Some(b"Val".as_slice())); - - // All keys accounted for. - assert_eq!( - args.check_keys(&[b"-f"], &[b"-k", b"--empty", b"--key"]), - None, - ); - - // Now that we've learned --key is an option, the marker should have - // moved. - assert_eq!(args.arg(0), Some(b"trailing".as_slice())); - - // Missing --key. - assert_eq!( - args.check_keys(&[b"-f"], &[b"-k", b"--empty"]), - Some(b"--key".as_slice()), - ); - - // Missing everything. - assert_eq!(args.check_keys(&[], &[]), Some(b"-k".as_slice())); - } - - #[test] - fn t_doubledash() { - let base: Vec<&[u8]> = vec![ - b"hey", - b"-kVal", - b"--", - b"more", - b"things", - b"here", - ]; - - let args: Argue = base.iter().copied().collect(); - - // It should stop at the double-dash. - assert_eq!( - *args, - [ - b"hey".to_vec(), - b"-k".to_vec(), - b"Val".to_vec(), - ] - ); - } - - #[test] - fn t_help() { - let mut base: Vec<&[u8]> = vec![ - b"hey", - b"-h", - ]; - - // We should be wanting a static help. - let mut args: Argue = base.iter().copied().collect(); - assert!(matches!( - args.check_flags(FLAG_HELP), - Err(ArgyleError::WantsHelp) - )); - - #[cfg(feature = "dynamic-help")] - { - // Dynamic help this time. - args = base.iter().copied().collect(); - match args.check_flags(FLAG_DYNAMIC_HELP) { - Err(ArgyleError::WantsDynamicHelp(e)) => { - let expected: Option> = Some(Box::from(&b"hey"[..])); - assert_eq!(e, expected); - }, - _ => panic!("Test should have produced an error with Some(Box(hey))."), - } - } - - // Same thing without wanting help. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_VERSION).is_ok()); - - // Again with help flag first. - base[0] = b"--help"; - - // We should be wanting a static help. - args = base.iter().copied().collect(); - assert!(matches!( - args.check_flags(FLAG_HELP), - Err(ArgyleError::WantsHelp) - )); - - #[cfg(feature = "dynamic-help")] - { - args = base.iter().copied().collect(); - // Dynamic help this time. - assert!(matches!( - args.check_flags(FLAG_DYNAMIC_HELP), - Err(ArgyleError::WantsDynamicHelp(None)) - )); - } - - // Same thing without wanting help. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_VERSION).is_ok()); - - // Same thing without wanting help. - base[0] = b"help"; - base[1] = b"--foo"; - - // We should be wanting a static help. - args = base.iter().copied().collect(); - assert!(matches!( - args.check_flags(FLAG_HELP), - Err(ArgyleError::WantsHelp) - )); - - #[cfg(feature = "dynamic-help")] - { - args = base.iter().copied().collect(); - // Dynamic help this time. - assert!(matches!( - args.check_flags(FLAG_DYNAMIC_HELP), - Err(ArgyleError::WantsDynamicHelp(None)) - )); - } - - // Same thing without wanting help. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_VERSION).is_ok()); - - // One last time with no helpish things. - base[0] = b"hey"; - - // We should be wanting a static help. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_HELP).is_ok()); - - #[cfg(feature = "dynamic-help")] - { - // Dynamic help this time. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_DYNAMIC_HELP).is_ok()); - } - - // Same thing without wanting help. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_VERSION).is_ok()); - } - - #[test] - fn t_key_like() { - assert!(is_key_like(b"-hi")); - assert!(is_key_like(b"-Hi")); - assert!(is_key_like(b"--hi")); - assert!(is_key_like(b"--Hi")); - assert!(! is_key_like(b"hi")); - assert!(! is_key_like(b"- hi")); - assert!(! is_key_like(b"--9")); - assert!(! is_key_like(b"--")); - assert!(! is_key_like(b"Z")); - } - - #[test] - fn t_version() { - let mut base: Vec<&[u8]> = vec![ - b"hey", - b"-V", - ]; - - // We should be wanting a version. - let mut args: Argue = base.iter().copied().collect(); - - assert!(matches!( - args.check_flags(FLAG_VERSION), - Err(ArgyleError::WantsVersion) - )); - - // Same thing without the version flag. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_HELP).is_ok()); - - // Repeat with the long flag. - base[1] = b"--version"; - - // We should be wanting a version. - args = base.iter().copied().collect(); - assert!(matches!( - args.check_flags(FLAG_VERSION), - Err(ArgyleError::WantsVersion) - )); - - // Same thing without the version flag. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_HELP).is_ok()); - - // One last time without a version arg present. - base[1] = b"--ok"; - - // We should be wanting a version. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_VERSION).is_ok()); - - // Same thing without the version flag. - args = base.iter().copied().collect(); - assert!(args.check_flags(FLAG_HELP).is_ok()); - } - - #[test] - fn t_with_list() { - let list = std::path::Path::new("skel/list.txt"); - assert!(list.exists(), "Missing list.txt"); - - let mut base: Vec> = vec![ - b"print".to_vec(), - b"-l".to_vec(), - b"skel/list.txt".to_vec(), - ]; - - let mut args = base.iter().cloned().collect::().with_list(); - assert_eq!( - *args, - [ - b"print".to_vec(), - b"-l".to_vec(), - b"skel/list.txt".to_vec(), - b"/foo/bar/one".to_vec(), - b"/foo/bar/two".to_vec(), - b"/foo/bar/three".to_vec(), - ] - ); - - // These should be trailing args. - assert_eq!(args.arg(0), Some(&b"/foo/bar/one"[..])); - assert_eq!(args.arg(1), Some(&b"/foo/bar/two"[..])); - assert_eq!(args.arg(2), Some(&b"/foo/bar/three"[..])); - - // Now try it with a bad file. - base[2] = b"skel/not-list.txt".to_vec(); - args = base.iter().cloned().collect::().with_list(); - assert_eq!( - *args, - [ - b"print".to_vec(), - b"-l".to_vec(), - b"skel/not-list.txt".to_vec(), - ] - ); - } - - #[test] - fn t_with_trailing_args() { - let base: Vec<&[u8]> = vec![ b"foo" ]; - - // As is. - let args = base.iter().copied().collect::(); - assert_eq!(args.args(), base); - - // With extra stuff. - let args = base.iter().copied().collect::() - .with_trailing_args([ - "bar", - " baz ", // Should be trimmed. - " ", // Should be ignored. - ]); - assert_eq!(args.args(), &[b"foo", b"bar", b"baz"]); - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index efe314e..0000000 --- a/src/error.rs +++ /dev/null @@ -1,107 +0,0 @@ -/*! -# Argyle: Errors - -This is the obligatory error enum. It implements `Copy` unless the crate -feature `dynamic-help` is enabled, in which case it can only be `Clone`. -*/ - -use std::{ - error::Error, - fmt, -}; - - - -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(not(feature = "dynamic-help"), derive(Copy))] -#[deprecated(since = "0.9.0", note = "use stream::ArgyleError instead")] -/// # Error Struct. -pub enum ArgyleError { - /// A custom error. - Custom(&'static str), - - /// Missing anything/everything. - Empty, - - /// Expected subcommand. - NoSubCmd, - - /// Miscellaneous Silent Failure. - /// - /// This has no corresponding error text, but does have its own exit code. - Passthru(i32), - - #[cfg(feature = "dynamic-help")] - #[cfg_attr(docsrs, doc(cfg(feature = "dynamic-help")))] - /// Wants subcommand help. - WantsDynamicHelp(Option>), - - /// Wants help. - WantsHelp, - - /// Wants version. - WantsVersion, -} - -impl AsRef for ArgyleError { - #[inline] - fn as_ref(&self) -> &str { self.as_str() } -} - -impl Error for ArgyleError {} - -impl fmt::Display for ArgyleError { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl ArgyleError { - #[must_use] - /// # Exit code. - /// - /// This returns the exit code for the error. Non-error errors like help - /// and version have a non-error exit code of `0`. [`ArgyleError::Passthru`] - /// returns whatever code was defined, while everything else just returns - /// `1`. - pub const fn exit_code(&self) -> i32 { - match self { - Self::Passthru(c) => *c, - - #[cfg(feature = "dynamic-help")] - Self::WantsDynamicHelp(_) - | Self::WantsHelp - | Self::WantsVersion => 0, - - #[cfg(not(feature = "dynamic-help"))] - Self::WantsHelp | Self::WantsVersion => 0, - - _ => 1, - } - } - - #[must_use] - /// # As Str. - /// - /// Return as a string slice. - pub const fn as_str(&self) -> &'static str { - match self { - Self::Custom(s) => s, - Self::Empty => "Missing options, flags, arguments, and/or ketchup.", - Self::NoSubCmd => "Missing/invalid subcommand.", - - #[cfg(feature = "dynamic-help")] - Self::Passthru(_) - | Self::WantsDynamicHelp(_) - | Self::WantsHelp - | Self::WantsVersion => "", - - #[cfg(not(feature = "dynamic-help"))] - Self::Passthru(_) - | Self::WantsHelp - | Self::WantsVersion => "", - - } - } -} diff --git a/src/iter.rs b/src/iter.rs deleted file mode 100644 index dc0e7cd..0000000 --- a/src/iter.rs +++ /dev/null @@ -1,250 +0,0 @@ -/*! -# Argyle: Argument Iterator -*/ - -use std::{ - ffi::OsStr, - os::unix::ffi::OsStrExt, -}; - - - -#[derive(Debug, Clone)] -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Argument `OsStr` Iterator. -/// -/// This iterates through the arguments of an [`Argue`](crate::Argue) as [`OsStr`](std::ffi::OsStr) values. -pub struct ArgsOsStr<'a> { - /// # Slice. - /// - /// A borrowed copy of the arguments, as a slice. - inner: &'a [Vec], - - /// # Next Index. - /// - /// The position of the next argument to pull, i.e. `inner[pos]`. The value - /// is incremented after each successful fetch. - pos: usize, -} - -impl<'a> Iterator for ArgsOsStr<'a> { - type Item = &'a OsStr; - - /// # Next. - fn next(&mut self) -> Option { - if self.pos < self.inner.len() { - let out = OsStr::from_bytes(&self.inner[self.pos]); - self.pos += 1; - Some(out) - } - else { None } - } - - /// # Size Hint. - fn size_hint(&self) -> (usize, Option) { - let len = self.inner.len() - self.pos; - (len, Some(len)) - } -} - -impl ExactSizeIterator for ArgsOsStr<'_> { - /// # Length. - fn len(&self) -> usize { self.inner.len() - self.pos } -} - -impl<'a> ArgsOsStr<'a> { - #[inline] - /// # New. - pub(crate) const fn new(inner: &'a [Vec]) -> Self { - Self { - inner, - pos: 0, - } - } -} - - - -#[derive(Debug, Clone, Default)] -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Option Values Iterator. -/// -/// This iterator yields the value(s) corresponding to a given option, useful -/// for commands that accept the same argument multiple times. -/// -/// It is the return value for [`Argue::option_values`](crate::Argue::option_values) and [`Argue::option2_values`](crate::Argue::option2_values). -pub struct Options<'a> { - /// # Found-but-Unyielded Values. - /// - /// If iteration encounters more values than it can return, the extras are - /// added to this buffer so they can be yielded on subsequent passes. - buf: Vec<&'a [u8]>, - - /// # Slice. - /// - /// A borrowed copy of the arguments. Note iteration potentially shrinks - /// this slice. If both it and `buf` are empty, iteration is done. - inner: &'a [Vec], - - /// # Needle. - /// - /// Only values corresponding to this key are yielded. - k1: &'a [u8], - - /// # Optional Second Needle. - k2: Option<&'a [u8]>, - - /// # Value Delimiter. - /// - /// If specified, a matching value will be split on this character, - /// potentially yielded multiple values instead of just one. For example, - /// a comma would turn `one,two,three` into `one`, `two`, and `three`. - delimiter: Option, -} - -impl<'a> Iterator for Options<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - // Steal from the buffer first. - if let Some(o) = self.buf.pop() { return Some(o); } - - // Cut away parts until we reach the end. - let mut found = false; - while let [first, rest @ ..] = self.inner { - self.inner = rest; - - if found { - // If we're splitting values, use the buffer. - if let Some(d) = self.delimiter { - self.buf.extend(first.split(|&b| b == d)); - self.buf.reverse(); - return self.buf.pop(); - } - - // Otherwise return it whole. - return Some(first.as_slice()); - } - else if self.k1 == first || self.k2.map_or(false, |k2| k2 == first) { - found = true; - } - } - - None - } - - fn size_hint(&self) -> (usize, Option) { - (0, Some(self.inner.len() + self.buf.len())) - } -} - -impl<'a> Options<'a> { - /// # New. - pub(crate) const fn new( - inner: &'a [Vec], - k1: &'a [u8], - k2: Option<&'a [u8]>, - delimiter: Option - ) -> Self { - Self { - buf: Vec::new(), - inner, k1, k2, delimiter, - } - } -} - - - -#[derive(Debug, Clone, Default)] -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -/// # Option Values (`OsStr`) Iterator. -/// -/// This iterator yields the value(s) corresponding to a given option, useful -/// for commands that accept the same argument multiple times. -/// -/// It is the return value for [`Argue::option_values_os`](crate::Argue::option_values_os) and [`Argue::option2_values_os`](crate::Argue::option2_values_os). -pub struct OptionsOsStr<'a>(pub(crate) Options<'a>); - -impl<'a> Iterator for OptionsOsStr<'a> { - type Item = &'a OsStr; - - fn next(&mut self) -> Option { - self.0.next().map(OsStr::from_bytes) - } - - fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } -} - - - -#[cfg(test)] -mod tests { - use super::*; - use crate::Argue; - - #[test] - fn t_option_values() { - let base: Vec<&[u8]> = vec![ - b"hey", - b"-kVal", - b"-k", - b"hello,world", - b"--key=nice", - ]; - - let args: Argue = base.iter().copied().collect(); - - assert_eq!( - args.option_values(b"-k", None).collect::>(), - [&b"Val"[..], b"hello,world"], - ); - - assert_eq!( - args.option_values(b"-k", Some(b',')).collect::>(), - [&b"Val"[..], b"hello", b"world"], - ); - - assert_eq!( - args.option2_values(b"-k", b"--key", None).collect::>(), - [&b"Val"[..], b"hello,world", b"nice"], - ); - - assert_eq!( - args.option2_values(b"-k", b"--key", Some(b',')).collect::>(), - [&b"Val"[..], b"hello", b"world", b"nice"], - ); - } - - #[test] - fn t_option_values_os() { - let base: Vec<&[u8]> = vec![ - b"hey", - b"-kVal", - b"-k", - b"hello,world", - b"--key=nice", - ]; - - let args: Argue = base.iter().copied().collect(); - - assert_eq!( - args.option_values_os(b"-k", None).collect::>(), - [OsStr::new("Val"), OsStr::new("hello,world")], - ); - - assert_eq!( - args.option_values_os(b"-k", Some(b',')).collect::>(), - [OsStr::new("Val"), OsStr::new("hello"), OsStr::new("world")], - ); - - assert_eq!( - args.option2_values_os(b"-k", b"--key", None).collect::>(), - [OsStr::new("Val"), OsStr::new("hello,world"), OsStr::new("nice")], - ); - - assert_eq!( - args.option2_values_os(b"-k", b"--key", Some(b',')).collect::>(), - [OsStr::new("Val"), OsStr::new("hello"), OsStr::new("world"), OsStr::new("nice")], - ); - } -} diff --git a/src/keykind.rs b/src/keykind.rs deleted file mode 100644 index 01f4f85..0000000 --- a/src/keykind.rs +++ /dev/null @@ -1,108 +0,0 @@ -/*! -# Argyle: Key Kind - -**Note:** This is not intended for external use and is subject to change. -*/ - -#[doc(hidden)] -#[deprecated(since = "0.9.0", note = "use stream::Argue instead")] -#[derive(Debug, Clone, Copy, Default, Eq, Hash, PartialEq)] -/// The `KeyKind` enum is used to differentiate between the types of CLI argument -/// keys [`Argue`](crate::Argue) might encounter during parsing (and `None` in the case of a -/// non-key-looking entry). -/// -/// In keeping with the general ethos of this crate, speed is the name of the game, -/// which is achieved primarily through simplicity: -/// * If an entry begins with a single `-` and an ASCII letter, it is assumed to be a short key. -/// * If a short key consists of more than two characters, `2..` is assumed to be a value. -/// * If an entry begins with two `--` and an ASCII letter, it is assumed to be a long key. -/// * If a long key contains an `=`, everything after that is assumed to be a value. -pub enum KeyKind { - #[default] - /// Not a key. - None, - /// A short key. - Short, - /// A short key with a value. - ShortV, - /// A long key. - Long, - /// A long key with a value. The number indicates the position of the `=` - /// character. Everything before is the key; everything after the value. - LongV(usize), -} - -impl From<&[u8]> for KeyKind { - fn from(txt: &[u8]) -> Self { - let len: usize = txt.len(); - if len >= 2 && txt[0] == b'-' { - // Could be long. - if txt[1] == b'-' { - // Is a long. - if len > 2 && txt[2].is_ascii_alphabetic() { - return txt.iter() - .position(|&x| x == b'=') - .map_or(Self::Long, Self::LongV); - } - } - // Is short. - else if txt[1].is_ascii_alphabetic() { - if len == 2 { return Self::Short; } - return Self::ShortV; - } - } - - Self::None - } -} - - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[allow(clippy::cognitive_complexity)] // It is what it is. - fn t_from() { - assert_eq!(KeyKind::from(&b"Your Mom"[..]), KeyKind::None); - assert_eq!(KeyKind::from(&b"--"[..]), KeyKind::None); - assert_eq!(KeyKind::from(&b"-"[..]), KeyKind::None); - assert_eq!(KeyKind::from(&b"-0"[..]), KeyKind::None); - assert_eq!(KeyKind::from(&b"-y"[..]), KeyKind::Short); - assert_eq!(KeyKind::from(&b"-yp"[..]), KeyKind::ShortV); - assert_eq!(KeyKind::from(&b"--0"[..]), KeyKind::None); - assert_eq!(KeyKind::from(&b"--yes"[..]), KeyKind::Long); - assert_eq!(KeyKind::from(&b"--y-p"[..]), KeyKind::Long); - assert_eq!(KeyKind::from(&b"--yes=no"[..]), KeyKind::LongV(5)); - assert_eq!(KeyKind::from(&b"--yes="[..]), KeyKind::LongV(5)); - - // Test in and around the 16-char boundary. - assert_eq!(KeyKind::from(&b"--yes_="[..]), KeyKind::LongV(6)); - assert_eq!(KeyKind::from(&b"--yes__="[..]), KeyKind::LongV(7)); - assert_eq!(KeyKind::from(&b"--yes___="[..]), KeyKind::LongV(8)); - assert_eq!(KeyKind::from(&b"--yes____="[..]), KeyKind::LongV(9)); - assert_eq!(KeyKind::from(&b"--yes_____="[..]), KeyKind::LongV(10)); - assert_eq!(KeyKind::from(&b"--yes______="[..]), KeyKind::LongV(11)); - assert_eq!(KeyKind::from(&b"--yes_______="[..]), KeyKind::LongV(12)); - assert_eq!(KeyKind::from(&b"--yes________="[..]), KeyKind::LongV(13)); - assert_eq!(KeyKind::from(&b"--yes_________="[..]), KeyKind::LongV(14)); - assert_eq!(KeyKind::from(&b"--yes__________="[..]), KeyKind::LongV(15)); - assert_eq!(KeyKind::from(&b"--yes___________="[..]), KeyKind::LongV(16)); - assert_eq!(KeyKind::from(&b"--yes____________="[..]), KeyKind::LongV(17)); - assert_eq!(KeyKind::from(&b"--yes____________-="[..]), KeyKind::LongV(18)); - assert_eq!(KeyKind::from(&b"--yes_____________-="[..]), KeyKind::LongV(19)); - assert_eq!(KeyKind::from(&b"--yes______________-="[..]), KeyKind::LongV(20)); - assert_eq!(KeyKind::from(&b"--yes_____________"[..]), KeyKind::Long); - - // Does this work? - assert_eq!( - KeyKind::from("--BjörkGuðmundsdóttir".as_bytes()), - KeyKind::Long - ); - assert_eq!( - KeyKind::from("--BjörkGuðmunds=dóttir".as_bytes()), - KeyKind::LongV(17) - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index b96cc33..b28e60a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,9 +9,9 @@ [![license](https://img.shields.io/badge/license-wtfpl-ff1493?style=flat-square)](https://en.wikipedia.org/wiki/WTFPL) [![contributions welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&label=contributions)](https://github.com/Blobfolio/argyle/issues) -This crate provides a simple streaming CLI argument parser/iterator called [`Argue`](crate::stream::Argue), offering a middle ground between the standard library's barebones [`std::env::args_os`] helper and full-service crates like [clap](https://crates.io/crates/clap). +This crate provides a simple streaming CLI argument parser/iterator called [`Argue`], offering a middle ground between the standard library's barebones [`std::env::args_os`] helper and full-service crates like [clap](https://crates.io/crates/clap). -[`Argue`](crate::stream::Argue) performs some basic normalization — it handles string conversion in a non-panicking way, recognizes shorthand value assignments like `-kval`, `-k=val`, `--key=val`, and handles end-of-command (`--`) arguments — and will help identify any special subcommands and/or keys/values expected by your app. +[`Argue`] performs some basic normalization — it handles string conversion in a non-panicking way, recognizes shorthand value assignments like `-kval`, `-k=val`, `--key=val`, and handles end-of-command (`--`) arguments — and will help identify any special keys/values expected by your app. The subsequent validation and handling, however, are left _entirely up to you_. Loop, match, and proceed however you see fit. @@ -23,10 +23,10 @@ If that sounds terrible, just use [clap](https://crates.io/crates/clap) instead. A general setup might look something like the following. -Refer to the documentation for [`Argue`](crate::stream::Argue) and [`Argumuent`](crate::stream::Argument) for more information, caveats, etc. +Refer to the documentation for [`Argue`], [`KeyWord`], and [`Argument`] for more information, caveats, etc. ``` -use argyle::stream::Argument; +use argyle::{Argument, KeyWord}; use std::path::PathBuf; #[derive(Debug, Clone, Default)] @@ -37,45 +37,47 @@ struct Settings { paths: Vec, } -let args = argyle::stream::args() - .with_keys([ - ("-h", false), // Boolean flag. - ("--help", false), // Boolean flag. - ("--threads", true), // Expects a value. - ("--verbose", false), // Boolean flag. - ]) - .unwrap(); // An error will only occur if a - // duplicate or invalid key is declared. +let args = argyle::args() + .with_keywords([ + KeyWord::key("-h").unwrap(), // Boolean flag (short). + KeyWord::key("--help").unwrap(), // Boolean flag (long). + KeyWord::key_with_value("-j").unwrap(), // Expects a value. + KeyWord::key_with_value("--threads").unwrap(), + ]); // Loop and handle! let mut settings = Settings::default(); for arg in args { match arg { + // Help flag match. Argument::Key("-h" | "--help") => { println!("Help Screen Goes Here."); return; }, - Argument::Key("--verbose") => { - settings.verbose = true; - }, - Argument::KeyWithValue("--threads", threads) => { - settings.threads = threads.parse().expect("Threads must be a number!"); + + // Thread option match. + Argument::KeyWithValue("-j" | "--threads", value) => { + settings.threads = value.parse() + .expect("Maximum threads must be a number!"); }, - // Something else… maybe you want to assume it's a path? + + // Something else. Argument::Other(v) => { settings.paths.push(PathBuf::from(v)); }, - // Also something else, but not String-able. Paths don't care, - // though, so for this example maybe you just keep it? + + // Also something else, but not String-able. PathBuf doesn't care + // about UTF-8, though, so it might be fine! Argument::InvalidUtf8(v) => { settings.paths.push(PathBuf::from(v)); }, - _ => {}, // Not relevant here. + + // Nothing else is relevant here. + _ => {}, } } -// Do something with those settings… - +// Now that you're set up, do stuff… ``` */ @@ -129,34 +131,15 @@ for arg in args { )] #![expect(clippy::module_name_repetitions, reason = "Repetition is preferred.")] -#![expect(deprecated, reason = "The deprecated parts aren't gone yet.")] -#![cfg_attr(docsrs, feature(doc_cfg))] - -mod argue; -mod error; -mod iter; -mod keykind; -pub mod stream; - -pub use argue::{ +mod stream; +pub use stream::{ + args, Argue, - FLAG_HELP, - FLAG_REQUIRED, - FLAG_SUBCOMMAND, - FLAG_VERSION, -}; - -#[cfg(feature = "dynamic-help")] -#[cfg_attr(docsrs, doc(cfg(feature = "dynamic-help")))] -pub use argue::FLAG_DYNAMIC_HELP; - -pub use error::ArgyleError; -pub use iter::{ - ArgsOsStr, - Options, - OptionsOsStr, + ArgueEnv, + Argument, + KeyWord, + KeyWordsBuilder, }; -pub use keykind::KeyKind; diff --git a/src/stream/error.rs b/src/stream/error.rs deleted file mode 100644 index 4038cf5..0000000 --- a/src/stream/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -/*! -# Argyle: Errors. -*/ - -use std::fmt; - - - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -/// # Error! -pub enum ArgyleError { - /// # Duplicate Key. - DuplicateKey(&'static str), - - /// # Invalid Key. - InvalidKey(&'static str), -} - -impl std::error::Error for ArgyleError {} - -impl fmt::Display for ArgyleError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DuplicateKey(s) => write!(f, "Duplicate key: {s}"), - Self::InvalidKey(s) => write!(f, "Invalid key: {s}"), - } - } -} - -impl ArgyleError { - #[must_use] - /// # As String Slice. - pub const fn as_str(&self) -> &'static str { - match self { - Self::DuplicateKey(_) => "Duplicate key.", - Self::InvalidKey(_) => "Invalid key.", - } - } -} diff --git a/src/stream/key.rs b/src/stream/key.rs new file mode 100644 index 0000000..a19e7b5 --- /dev/null +++ b/src/stream/key.rs @@ -0,0 +1,549 @@ +/*! +# Argyle: Keywords. +*/ + +use std::{ + borrow::Borrow, + cmp::Ordering, + collections::BTreeMap, + fmt, + path::Path, +}; + + + +#[derive(Debug, Clone, Copy)] +/// # Keyword. +/// +/// This enum is used by [`Argue::with_keywords`](crate::Argue::with_keywords) +/// to declare the special CLI (sub)commands and/or keys used by the app. +/// +/// Each variant has its own formatting requirements, so it is recommended you +/// create new instances using the [`KeyWord::command`], [`KeyWord::key`], and +/// [`KeyWord::key_with_value`] methods rather than populating variants +/// directly. +/// +/// For a compile-time alternative, see [`KeyWordsBuilder`]. +/// +/// Note that for the purposes of equality and ordering, the variants are +/// irrelevant; only the words are used. +/// +/// For example, the following are "equal" despite one requiring a value and +/// one not. +/// +/// ``` +/// assert_eq!( +/// argyle::KeyWord::key("--help"), +/// argyle::KeyWord::key_with_value("--help"), +/// ); +/// ``` +pub enum KeyWord { + /// # (Sub)command. + Command(&'static str), + + /// # Boolean key. + Key(&'static str), + + /// # Key with Value. + KeyWithValue(&'static str), +} + +impl Borrow for KeyWord { + #[inline] + fn borrow(&self) -> &str { self.as_str() } +} + +impl Eq for KeyWord {} + +impl Ord for KeyWord { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { self.as_str().cmp(other.as_str()) } +} + +impl PartialEq for KeyWord { + #[inline] + fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } +} + +impl PartialOrd for KeyWord { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } +} + +impl KeyWord { + #[must_use] + /// # New (Sub)Command. + /// + /// Validate and return a new (sub)command keyword, or `None` if invalid. + /// + /// (Sub)commands may only contain ASCII alphanumeric characters, `-`, + /// and `_`, and must begin with an alphanumeric. + /// + /// ## Examples + /// + /// ``` + /// use argyle::KeyWord; + /// + /// // Totally fine. + /// assert!(KeyWord::command("make").is_some()); + /// + /// // This, however, does not work. + /// assert!(KeyWord::command("--help").is_none()); + /// ``` + /// + /// For a compile-time alternative, see [`KeyWordsBuilder`]. + pub const fn command(word: &'static str) -> Option { + if valid_command(word.as_bytes()) { Some(Self::Command(word)) } + else { None } + } + + #[must_use] + /// # New Boolean Key. + /// + /// Validate and return a new boolean/switch keyword — a flag that stands + /// on its own — or `None` if invalid. + /// + /// Both long and short style keys are supported: + /// * Short keys must be two bytes: a dash and an ASCII alphanumeric character. + /// * Long keys can be any length, but must start with two dashes and an ASCII alphanumeric character; all other characters must be alphanumerics, `-`, and `_`. + /// + /// ## Examples + /// + /// ``` + /// use argyle::KeyWord; + /// + /// // Totally fine. + /// assert!(KeyWord::key("--help").is_some()); + /// + /// // These, however, do not work. + /// assert!(KeyWord::key("--björk").is_none()); + /// assert!(KeyWord::key("-too_long_to_be_short").is_none()); + /// ``` + /// + /// For a compile-time alternative, see [`KeyWordsBuilder`]. + pub const fn key(keyword: &'static str) -> Option { + if valid_key(keyword.as_bytes()) { Some(Self::Key(keyword)) } + else { None } + } + + #[must_use] + /// # New Option Key. + /// + /// Validate and return a new option keyword — a key that expects a value — + /// or `None` if invalid. + /// + /// Both long and short style keys are supported: + /// * Short keys must be two bytes: a dash and an ASCII alphanumeric character. + /// * Long keys can be any length, but must start with two dashes and an ASCII alphanumeric character; all other characters must be alphanumerics, `-`, and `_`. + /// + /// ## Examples + /// + /// ``` + /// use argyle::KeyWord; + /// + /// // Totally fine. + /// assert!(KeyWord::key_with_value("--help").is_some()); + /// + /// // These, however, do not work. + /// assert!(KeyWord::key_with_value("--björk").is_none()); + /// assert!(KeyWord::key_with_value("-too_long_to_be_short").is_none()); + /// ``` + /// + /// For a compile-time alternative, see [`KeyWordsBuilder`]. + pub const fn key_with_value(keyword: &'static str) -> Option { + if valid_key(keyword.as_bytes()) { Some(Self::KeyWithValue(keyword)) } + else { None } + } +} + +impl KeyWord { + #[must_use] + /// # As String Slice. + /// + /// Return the keyword's inner value. + pub const fn as_str(&self) -> &'static str { + match self { Self::Command(s) | Self::Key(s) | Self::KeyWithValue(s) => s } + } +} + + + +#[derive(Debug, Default, Clone)] +/// # Compile-Time [`KeyWord`]s Codegen. +/// +/// This struct can be used by build scripts to generate a [`KeyWord`] array +/// suitable for use with [`Argue::with_keywords`](crate::Argue::with_keywords). +/// +/// It provides the same semantic safety guarantees as [`KeyWord::key`] +/// and family, but at compile-time, eliminating the (mild) runtime overhead. +/// +/// The builder also frees you from [`KeyWord`]'s usual `&'static` lifetime +/// constraints, allowing for more programmatic population. +/// +/// ## Examples +/// +/// ``` +/// use argyle::KeyWordsBuilder; +/// +/// // Start by adding your keywords. +/// let mut words = KeyWordsBuilder::default(); +/// for i in 0..10_u8 { +/// words.push_key(format!("-{i}")); // Automation for the win! +/// } +/// ``` +/// +/// You can grab a copy of the code by leveraging `Display::to_string`, but in +/// most cases you'll probably just want to use [`KeyWordsBuilder::save`] to write +/// it straight to a file: +/// +/// ```ignore +/// let out_dir: &Path = std::env::var("OUT_DIR").unwrap().as_ref(); +/// words.save(out_dir.join("keyz.rs")); +/// ``` +/// +/// Having done that, just `include!` it where needed: +/// +/// ```ignore +/// let ags = argyle::args() +/// .with_keywords(include!(concat!(env!("OUT_DIR"), "/keyz.rs"))); +/// ``` +pub struct KeyWordsBuilder(BTreeMap); + +impl fmt::Display for KeyWordsBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("[")?; + + let mut iter = self.0.values(); + if let Some(v) = iter.next() { + // Write the first value. + ::fmt(v, f)?; + + // Write the rest with leading comma/space separators. + for v in iter { + f.write_str(", ")?; + ::fmt(v, f)?; + } + } + + f.write_str("]") + } +} + +impl KeyWordsBuilder { + #[inline] + #[must_use] + /// # Is Empty? + /// + /// Returns `true` if there are no keywords. + pub fn is_empty(&self) -> bool { self.0.is_empty() } + + #[inline] + #[must_use] + /// # Length. + /// + /// Returns the number of keywords currently in the set. + pub fn len(&self) -> usize { self.0.len() } +} + +impl KeyWordsBuilder { + /// # Add Keyword. + /// + /// Add a keyword, ensuring the string portion is unique. + /// + /// ## Panics + /// + /// This will panic if the string part is not unique. + fn push(&mut self, k: &str, v: String) { + assert!(! self.0.contains_key(k), "Duplicate key: {k}"); + self.0.insert(k.to_owned(), v); + } + + /// # Add a Command. + /// + /// Use this to add a [`KeyWord::Command`] to the list. + /// + /// ## Panics + /// + /// This will panic if the command is invalid or repeated; + pub fn push_command>(&mut self, key: S) { + let k: &str = key.as_ref().trim(); + assert!(valid_command(k.as_bytes()), "Invalid command: {k}"); + let v = format!("argyle::KeyWord::Command({k:?})"); + self.push(k, v); + } + + /// # Add Commands. + /// + /// Use this to add one or more [`KeyWord::Command`] to the list. + /// + /// ## Panics + /// + /// This will panic if any commands are invalid or repeated; + pub fn push_commands, S: AsRef>(&mut self, keys: I) { + for k in keys { self.push_command(k); } + } + + /// # Add a Boolean Key. + /// + /// Use this to add a [`KeyWord::Key`] to the list. + /// + /// ## Panics + /// + /// This will panic if the key is invalid or repeated. + pub fn push_key>(&mut self, key: S) { + let k: &str = key.as_ref().trim(); + assert!(valid_key(k.as_bytes()), "Invalid key: {k}"); + let v = format!("argyle::KeyWord::Key({k:?})"); + self.push(k, v); + } + + /// # Add Boolean Keys. + /// + /// Use this to add one or more [`KeyWord::Key`] to the list. + /// + /// ## Panics + /// + /// This will panic if any keys are invalid or repeated. + pub fn push_keys, S: AsRef>(&mut self, keys: I) { + for k in keys { self.push_key(k); } + } + + /// # Add a Key that Expects a Value. + /// + /// Use this to add a [`KeyWord::KeyWithValue`] to the list. + /// + /// ## Panics + /// + /// This will panic if the key is invalid or repeated. + pub fn push_key_with_value>(&mut self, key: S) { + let k: &str = key.as_ref().trim(); + assert!(valid_key(k.as_bytes()), "Invalid key: {k}"); + let v = format!("argyle::KeyWord::KeyWithValue({k:?})"); + self.push(k, v); + } + + /// # Add Keys that Expect Values. + /// + /// Use this to add one or more [`KeyWord::KeyWithValue`] to the list. + /// + /// ## Panics + /// + /// This will panic if any keys are invalid or repeated. + pub fn push_keys_with_values, S: AsRef>(&mut self, keys: I) { + for k in keys { self.push_key_with_value(k); } + } +} + +impl KeyWordsBuilder { + /// # Save it to a File! + /// + /// Generate and save the [`KeyWord`] array code to the specified file. + /// + /// Note that many environments prohibit writes to arbitrary locations; for + /// best results, your path should be somewhere under `OUT_DIR`. + /// + /// ## Examples + /// + /// ```ignore + /// let out_dir: &Path = std::env::var("OUT_DIR").unwrap().as_ref(); + /// words.save(out_dir.join("keyz.rs")); + /// ``` + /// + /// ## Panics + /// + /// This method will panic if the write fails for any reason. + pub fn save>(&self, file: P) { + use std::io::Write; + + let file = file.as_ref(); + let code = self.to_string(); + + // Save it! + assert!( + std::fs::File::create(file).and_then(|mut out| + out.write_all(code.as_bytes()).and_then(|()| out.flush()) + ).is_ok(), + "Unable to write to {file:?}.", + ); + } +} + + + +/// # Valid Command? +const fn valid_command(bytes: &[u8]) -> bool { + if let [b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9', rest @ ..] = bytes { + valid_suffix(rest) + } + else { false } +} + +/// # Valid Key? +const fn valid_key(bytes: &[u8]) -> bool { + match bytes { + // Short keys are easy. + [b'-', b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9'] => true, + // Long keys have a variable suffix. + [b'-', b'-', b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9', rest @ ..] => valid_suffix(rest), + // There is no such thing as a medium key. + _ => false, + } +} + +/// # Valid Keyword Suffix? +/// +/// Check that all bytes are ASCII alphanumeric, `-`, or `_`. This is required +/// for both long keys and commands. +const fn valid_suffix(mut bytes: &[u8]) -> bool { + while let [b'-' | b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9', rest @ ..] = bytes { + bytes = rest; + } + + // By process of elimination, everything validated! + bytes.is_empty() +} + + + +#[cfg(test)] +mod test { + use super::*; + use std::collections::BTreeSet; + + #[test] + fn t_valid_key() { + let first: BTreeSet = ('0'..='9').into_iter() + .chain('a'..='z') + .chain('A'..='Z') + .collect(); + + let suffix: BTreeSet = first.iter() + .copied() + .chain(['-', '_']) + .collect(); + + let bad: BTreeSet = ['!', '?', '.', 'ö', '\n'].into_iter().collect(); + + // The suffix allows two things the initial character doesn't. + assert_eq!(first.len(), 26 * 2 + 10); + assert_eq!(first.len() + 2, suffix.len()); + + // None of the bad characters should be present in either. + assert!(bad.iter().all(|c| ! first.contains(c) && ! suffix.contains(c))); + + // Let's build up some keys to make sure we aren't missing anything + // in the match-based validation. + for a in first.iter().copied() { + // This should work for both long and short. + assert!(valid_key(format!("-{a}").as_bytes())); + assert!(valid_key(format!("--{a}").as_bytes())); + + // But not with the wrong number of dashes. + assert!(! valid_key(format!("{a}").as_bytes())); + assert!(! valid_key(format!("---{a}").as_bytes())); + + // Longer variations. + for b in suffix.iter().copied() { + // This should work for long keys. + assert!(valid_key(format!("--{a}{b}").as_bytes())); + + // But not any other dash count. + assert!(! valid_key(format!("{a}{b}").as_bytes())); + assert!(! valid_key(format!("-{a}{b}").as_bytes())); + assert!(! valid_key(format!("---{a}{b}").as_bytes())); + + // Not with bad stuff though. + for c in bad.iter().copied() { + assert!(! valid_key(format!("--{a}{c}{b}").as_bytes())); + assert!(! valid_key(format!("--{a}{b}{c}").as_bytes())); + } + } + } + + // Bad letters on their own should never work. + for c in bad { + assert!(! valid_key(format!("{c}").as_bytes())); + assert!(! valid_key(format!("-{c}").as_bytes())); + assert!(! valid_key(format!("--{c}").as_bytes())); + assert!(! valid_key(format!("---{c}").as_bytes())); + } + + // Bad starts should never work either. + assert!(! valid_key(b"")); + assert!(! valid_key(b"-")); + assert!(! valid_key(b"--")); + assert!(! valid_key(b"---")); + } + + #[test] + fn t_builder() { + let mut builder = KeyWordsBuilder::default(); + assert_eq!(builder.to_string(), "[]"); // Empty. + + builder.push_key("-h"); + assert_eq!(builder.to_string(), "[argyle::KeyWord::Key(\"-h\")]"); + + builder.push_key("--help"); + assert_eq!( + builder.to_string(), + "[argyle::KeyWord::Key(\"--help\"), argyle::KeyWord::Key(\"-h\")]" + ); + + builder.push_key_with_value("--output"); + assert_eq!( + builder.to_string(), + "[argyle::KeyWord::Key(\"--help\"), argyle::KeyWord::KeyWithValue(\"--output\"), argyle::KeyWord::Key(\"-h\")]" + ); + + builder.push_command("make"); + assert_eq!( + builder.to_string(), + "[argyle::KeyWord::Key(\"--help\"), argyle::KeyWord::KeyWithValue(\"--output\"), argyle::KeyWord::Key(\"-h\"), argyle::KeyWord::Command(\"make\")]" + ); + } + + #[test] + #[should_panic] + fn t_builder_invalid() { + let mut builder = KeyWordsBuilder::default(); + builder.push_key("--Björk"); // Invalid characters. + } + + #[test] + #[should_panic] + fn t_builder_duplicate() { + let mut builder = KeyWordsBuilder::default(); + builder.push_key("--help"); + builder.push_key_with_value("--help"); // Repeated string. + } + + #[test] + fn t_builder_plural() { + let mut builder1 = KeyWordsBuilder::default(); + builder1.push_key("-h"); + builder1.push_key("--help"); + + let mut builder2 = KeyWordsBuilder::default(); + builder2.push_keys(["-h", "--help"]); + + assert_eq!(builder1.to_string(), builder2.to_string()); + + builder1 = KeyWordsBuilder::default(); + builder1.push_key_with_value("-o"); + builder1.push_key_with_value("--output"); + + builder2 = KeyWordsBuilder::default(); + builder2.push_keys_with_values(["-o", "--output"]); + + assert_eq!(builder1.to_string(), builder2.to_string()); + + builder1 = KeyWordsBuilder::default(); + builder1.push_command("build"); + builder1.push_command("check"); + + builder2 = KeyWordsBuilder::default(); + builder2.push_commands(["build", "check"]); + + assert_eq!(builder1.to_string(), builder2.to_string()); + } +} diff --git a/src/stream/mod.rs b/src/stream/mod.rs index beb52ed..be02a17 100644 --- a/src/stream/mod.rs +++ b/src/stream/mod.rs @@ -1,27 +1,17 @@ /*! # Argyle: Streaming Argument Iterator. - -This module contains a streaming alternative to the crate's original (and -deprecated) [`Argue`](crate::Argue) structure that avoids the overhead associated -with argument collection and searching. - -This [`Argue`] is simpler and cleaner than the original, but less agnostic as -it requires declaring reserved keywords — subcommands, switches, and options — -upfront (via builder-style methods) to remove the guesswork during iteration. */ -mod error; +mod key; -pub use error::ArgyleError; +pub use key::{ + KeyWord, + KeyWordsBuilder, +}; use std::{ - borrow::Borrow, - cmp::Ordering, collections::BTreeSet, env::ArgsOs, - ffi::{ - OsStr, - OsString, - }, + ffi::OsString, iter::Skip, }; @@ -45,11 +35,8 @@ pub type ArgueEnv = Argue>; /// and keys expected by your app, but leaves any subsequent validation- and /// handling-related particulars _to you_. /// -/// That said, it does have a few _opinions_ that are worth noting: -/// * Keywords may only contain ASCII alphanumeric characters, `-`, and `_`; -/// * Subcommands must start with an ASCII alphanumeric character; -/// * Short keys can only be two bytes: a dash followed by an ASCII alphanumeric character; -/// * Long keys can be any length provided they start with two dashes and an ASCII alphanumeric character; +/// That said, it does have a few _opinions_. Refer to [`KeyWord`] for +/// formatting constraints. /// /// `Argue` supports both combined and consecutive key/value association. For /// example, the following are equivalent: @@ -63,15 +50,21 @@ pub type ArgueEnv = Argue>; /// ## Examples /// /// ``` -/// use argyle::stream::Argument; +/// use argyle::{Argument, KeyWord}; /// /// // Most of the time you'll probably want to parse env args, which the -/// // helper method `args` sets up. Add switches, options, etc, then loop to +/// // helper method [`args`] sets up. Add switches, options, etc, then loop to /// // see what all comes in! -/// for arg in argyle::stream::args().with_key("--help", false).unwrap() { +/// let args = argyle::args() +/// .with_keywords([ +/// KeyWord::key("--version").unwrap(), +/// KeyWord::key("--help").unwrap(), +/// ]); +/// +/// for arg in args { /// match arg { -/// // The user wants help. /// Argument::Key("--help") => println!("Help! Help!"), +/// Argument::Key("--version") => println!("v1.2.3"), /// /// // The user passed something else. /// Argument::Other(s) => println!("Found: {s}"), @@ -90,196 +83,59 @@ pub struct Argue { /// # Raw Iterator. iter: I, - /// # Keys to Look For. - special: BTreeSet, + /// # Keywords to Look For. + keys: BTreeSet, } impl> From for Argue { + #[inline] fn from(src: I) -> Self { Self { iter: src.into_iter(), - special: BTreeSet::new(), + keys: BTreeSet::new(), } } } impl Argue { - /// # With (Sub)Command. + #[must_use] + /// # With Keywords. /// - /// Add a (sub)command to the watchlist. + /// Specify the various keywords you'd like [`Argue`] to keep an eye out + /// for during parsing. It'll call them out specially if/when they appear. /// /// ## Examples /// /// ``` - /// let args = argyle::stream::args() - /// .with_command("make").unwrap(); + /// use argyle::{Argument, KeyWord}; /// - /// for arg in args { - /// // Do stuff! - /// } - /// ``` + /// let args = argyle::args() + /// .with_keywords([ + /// // Boolean keys: + /// KeyWord::key("--help").unwrap(), + /// KeyWord::key("-h").unwrap(), /// - /// ## Errors - /// - /// This will return an error if the command was previously specified (as - /// any type of key) or contains invalid characters. - pub fn with_command(mut self, key: &'static str) -> Result { - let key = key.trim(); - - // Ignore empties. - if key.is_empty() { Ok(self) } - // Call out invalid characters specially. - else if ! valid_command(key.as_bytes()) { Err(ArgyleError::InvalidKey(key)) } - // Add it if unique! - else if self.special.insert(KeyKind::Command(key)) { Ok(self) } - // It wasn't unique… - else { Err(ArgyleError::DuplicateKey(key)) } - } - - /// # With Key. - /// - /// Add a key to the watchlist, optionally requiring a value. - /// - /// ## Examples - /// - /// ``` - /// let args = argyle::stream::args() - /// .with_key("--verbose", false).unwrap() // Boolean flag. - /// .with_key("--output", true).unwrap(); // Expects a value. + /// // Keys that expect a value: + /// KeyWord::key_with_value("-o").unwrap(), + /// KeyWord::key_with_value("--output").unwrap(), + /// ]); /// /// for arg in args { - /// // Do stuff! + /// match arg { + /// Argument::Key("-h" | "--help") => {}, + /// Argument::KeyWithValue("-o" | "--output", value) => {}, + /// _ => {}, // Other stuff. + /// } /// } /// ``` - /// - /// ## Errors - /// - /// This will return an error if the key was previously specified - /// or contains invalid characters. - pub fn with_key(mut self, key: &'static str, value: bool) - -> Result { - let key = key.trim(); - - // Ignore empties. - if key.is_empty() { Ok(self) } - // Call out invalid characters specially. - else if ! valid_key(key.as_bytes()) { Err(ArgyleError::InvalidKey(key)) } - // Add it if unique! - else { - let k = if value { KeyKind::KeyWithValue(key) } else { KeyKind::Key(key) }; - if self.special.insert(k) { Ok(self) } - else { Err(ArgyleError::DuplicateKey(key)) } + pub fn with_keywords>(mut self, keys: I2) -> Self { + for key in keys { + // Note: we're using `replace` instead of `insert` to keep the + // variants synced. + let _res = self.keys.replace(key); } - } -} -impl Argue { - /// # With (Sub)Commands. - /// - /// Add one or more (sub)commands to the watchlist. - /// - /// ## Examples - /// - /// ``` - /// let args = argyle::stream::args() - /// .with_commands(["help", "verify"]).unwrap(); - /// - /// for arg in args { - /// // Do stuff! - /// } - /// ``` - /// - /// ## Errors - /// - /// This will return an error if any of the commands were previously - /// specified (as any type of key) or contain invalid characters. - pub fn with_commands>(self, keys: I2) - -> Result { - keys.into_iter().try_fold(self, Self::with_command) - } - - /// # With Keys. - /// - /// Add one or more keys to the watchlist. - /// - /// If you find the tuples messy, you can use [`Argue::with_switches`] and - /// [`Argue::with_options`] to declare your keys instead. - /// - /// ## Examples - /// - /// ``` - /// let args = argyle::stream::args() - /// .with_keys([ - /// ("--verbose", false), // Boolean flag. - /// ("--output", true), // Expects a value. - /// ]).unwrap(); - /// - /// for arg in args { - /// // Do stuff! - /// } - /// ``` - /// - /// ## Errors - /// - /// This will return an error if any of the keys were previously specified - /// or contain invalid characters. - pub fn with_keys>(self, keys: I2) - -> Result { - keys.into_iter().try_fold(self, |acc, (k, v)| acc.with_key(k, v)) - } - - /// # With Options. - /// - /// Add one or more keys to the watchlist that require values. - /// - /// ## Examples - /// - /// ``` - /// let args = argyle::stream::args() - /// .with_options(["--input", "--output"]) - /// .unwrap(); - /// - /// for arg in args { - /// // Do stuff! - /// } - /// ``` - /// - /// ## Errors - /// - /// This will return an error if any of the keys were previously specified - /// or contain invalid characters. - pub fn with_options>(self, keys: I2) - -> Result { - keys.into_iter().try_fold(self, |acc, k| acc.with_key(k, true)) - } - - /// # With Switches. - /// - /// Add one or more boolean keys to the watchlist. - /// - /// This can be used instead of [`Argue::with_keys`] if you have a bunch - /// of keys that do not require values. (It saves you the trouble of - /// tuple-izing everything.) - /// - /// ## Examples - /// - /// ``` - /// let args = argyle::stream::args() - /// .with_switches(["--verbose", "--strict"]) - /// .unwrap(); - /// - /// for arg in args { - /// // Do stuff! - /// } - /// ``` - /// - /// ## Errors - /// - /// This will return an error if any of the keys were previously specified - /// or contain invalid characters. - pub fn with_switches>(self, keys: I2) - -> Result { - keys.into_iter().try_fold(self, |acc, k| acc.with_key(k, false)) + self } } @@ -287,7 +143,7 @@ impl Argue { /// # Find Key. /// /// Find and return the key associated with `raw`, if any. - fn find_keyword(&self, raw: &str) -> Option { + fn find_keyword(&self, raw: &str) -> Option { // Short circuit; keywords must start with a dash or alphanumeric. let bytes = raw.as_bytes(); if bytes.is_empty() || ! (bytes[0] == b'-' || bytes[0].is_ascii_alphanumeric()) { @@ -295,7 +151,7 @@ impl Argue { } // Direct hit! - if let Some(key) = self.special.get(raw) { return Some(*key); } + if let Some(key) = self.keys.get(raw) { return Some(*key); } // Keylike strings could have a value gumming up the works; separate // and try again if that is the case. @@ -310,7 +166,7 @@ impl Argue { } // No dice. else { None }?; - self.special.get(needle).copied() + self.keys.get(needle).copied() } else { None } } @@ -346,9 +202,9 @@ impl> Iterator for Argue { // Return whatever we're meant to based on the match type. return Some(match key { - KeyKind::Command(_) => Argument::Command(k), - KeyKind::Key(_) => Argument::Key(k), - KeyKind::KeyWithValue(_) => { + KeyWord::Command(_) => Argument::Command(k), + KeyWord::Key(_) => Argument::Key(k), + KeyWord::KeyWithValue(_) => { // We need a value for this one! let v: String = // Pull it from the next argument. @@ -359,7 +215,7 @@ impl> Iterator for Argue { // value into a single OsString that can be // returned instead. Err(e) => { - let mut boo = OsStr::new(k).to_owned(); + let mut boo = OsString::from(k); boo.push("="); boo.push(e); return Some(Argument::InvalidUtf8(boo)); @@ -386,7 +242,6 @@ impl> Iterator for Argue { - #[derive(Debug, Clone, Eq, PartialEq)] /// # Parsed Argument. /// @@ -396,24 +251,22 @@ impl> Iterator for Argue { pub enum Argument { /// # (Sub)command. /// - /// This is for arguments matching keywords declared via [`Argue::with_command`] - /// or [`Argue::with_commands`]. + /// This is for arguments matching a [`KeyWord::Command`]. Command(&'static str), /// # Boolean Key. /// - /// This is for arguments matching keywords declared via [`Argue::with_key`] - /// or [`Argue::with_keys`] that do not require a value. (The key is a - /// boolean flag.) + /// This is for arguments matching a [`KeyWord::Key`]. Key(&'static str), /// # Key and Value. /// - /// This is for arguments matching keywords declared via [`Argue::with_key`] - /// or [`Argue::with_keys`] that require a value. + /// This is for arguments matching [`KeyWord::KeyWithValue`], along with + /// the associated value. /// - /// Note that values are simply "whatever follows" so might represent the - /// wrong thing if the user mistypes the command. + /// Note: values are simply "the next entry" — unless split off from combo + /// args like `--key=val` — so may or may not be _logically_ correct, but + /// that's CLI arguments in a nutshell. Haha. KeyWithValue(&'static str, String), /// # Everything Else. @@ -425,25 +278,25 @@ pub enum Argument { /// # Invalid UTF-8. /// /// This is for arguments that could not be converted to a String because - /// of invalid UTF-8. The original [`OsString`] representation is maintained - /// in case you want to dig deeper. + /// of invalid UTF-8. The original [`OsString`] representation is passed + /// through for your consideration. InvalidUtf8(OsString), /// # Everything after "--". /// - /// This holds all remaining arguments after the end-of-command terminator. - /// (The terminator itself is stripped out.) + /// This holds all remaining arguments after an end-of-command terminator + /// is encountered. (The terminator itself is stripped out.) /// - /// Note that these arguments are collected as-are with no normalization or - /// scrutiny of any kind. If you want them parsed, they can be fed directly - /// into a new [`Argue`] instance. + /// The arguments are collected as-are without any normalization or + /// parsing. If you _want_ them parsed, you can create a new [`Argue`] + /// instance from the collection by passing it to `Argue::from`. /// /// ## Example /// /// ``` - /// use argyle::stream::{Argue, Argument}; + /// use argyle::{Argue, Argument}; /// - /// let mut args = argyle::stream::args(); + /// let mut args = argyle::args(); /// if let Some(Argument::End(extra)) = args.next() { /// for arg in Argue::from(extra.into_iter()) { /// // Do more stuff! @@ -459,150 +312,63 @@ pub enum Argument { /// # CLI Argument Iterator. /// /// Return an [`Argue`] iterator seeded with [`ArgsOs`], skipping the first -/// (command path) entry. +/// entry — the script path — since that isn't super useful. /// -/// If you'd rather not skip that first entry, create your instance with -/// `Argue::from(std::env::args_os())` instead. +/// (If you disagree on that last point, create your instance using +/// `Argue::from(std::env::args_os())` instead.) pub fn args() -> Argue> { Argue { iter: std::env::args_os().skip(1), - special: BTreeSet::new(), - } -} - - - -#[derive(Debug, Clone, Copy)] -/// # Key Kinds. -/// -/// This enum is used for the internal keyword collection, enabling -/// type-independent matching with the option of subsequently giving a shit -/// about said types. Haha. -enum KeyKind { - /// # (Sub)command. - Command(&'static str), - - /// # Boolean key. - Key(&'static str), - - /// # Key with Value. - KeyWithValue(&'static str), -} - -impl Borrow for KeyKind { - fn borrow(&self) -> &str { self.as_str() } -} - -impl Eq for KeyKind {} - -impl Ord for KeyKind { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { self.as_str().cmp(other.as_str()) } -} - -impl PartialEq for KeyKind { - #[inline] - fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } -} - -impl PartialOrd for KeyKind { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } -} - -impl KeyKind { - /// # As String Slice. - /// - /// Return the inner value of the key. - const fn as_str(&self) -> &'static str { - match self { Self::Command(s) | Self::Key(s) | Self::KeyWithValue(s) => s } + keys: BTreeSet::new(), } } -/// # Valid Command? -const fn valid_command(mut key: &[u8]) -> bool { - // The first character must be alphanumeric. - let [b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9', rest @ ..] = key else { return false; }; - key = rest; - - // The remaining characters must be alphanumeric, dashes, or underscores. - while let [a, rest @ ..] = key { - if ! valid_key_byte(*a) { return false; } - key = rest; - } - - true -} - -/// # Valid Key? -const fn valid_key(mut key: &[u8]) -> bool { - // Strip leading dashes. - let len = key.len(); - while let [b'-', rest @ ..] = key { key = rest; } - let dashes = len - key.len(); - - // A short key must be exactly two bytes with an alphanumeric second. - if dashes == 1 { - return key.len() == 1 && key[0].is_ascii_alphanumeric(); - } - - // Long keys must be at least three bytes with the third alphanumeric. If - // longer, everything else must be alphanumeric or a dash or underscore. - if dashes == 2 { - let [b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9', rest @ ..] = key else { return false; }; - key = rest; - while let [a, rest @ ..] = key { - if ! valid_key_byte(*a) { return false; } - key = rest; - } - return true; - } - - false -} - -/// # Valid Key Char? -const fn valid_key_byte(b: u8) -> bool { - matches!(b, b'-' | b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') -} - - - #[cfg(test)] mod test { use super::*; + use std::ffi::OsString; #[test] fn t_argue() { - use std::ffi::OsStr; let mut cli = vec![ - OsStr::new("subcommand").to_owned(), - OsStr::new("").to_owned(), - OsStr::new("-s").to_owned(), - OsStr::new("--long").to_owned(), - OsStr::new("-t2").to_owned(), - OsStr::new("--m=yar").to_owned(), - OsStr::new("--n").to_owned(), - OsStr::new("yar").to_owned(), - OsStr::new("-u").to_owned(), - OsStr::new("2").to_owned(), - OsStr::new("/foo/bar").to_owned(), - OsStr::new("--").to_owned(), + OsString::from(""), + OsString::from("-s"), + OsString::from("--long"), + OsString::from("-t2"), + OsString::from("--m=yar"), + OsString::from("--n"), + OsString::from("yar"), + OsString::from("-u"), + OsString::from("2"), + OsString::from("/foo/bar"), + OsString::from("--"), ]; - let mut args = Argue::from(cli.iter().cloned()) - .with_command("subcommand").unwrap() - .with_keys([ - ("--long", false), - ("--m", true), - ("--n", true), - ("-s", false), - ("-t", true), - ("-u", true), - ]).unwrap(); - - assert_eq!(args.next(), Some(Argument::Command("subcommand"))); + + // Without keywords, everything should turn up other. + let mut args = Argue::from(cli.iter().cloned()); + assert_eq!(args.next(), Some(Argument::Other("-s".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("--long".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("-t2".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("--m=yar".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("--n".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("yar".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("-u".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("2".to_owned()))); + assert_eq!(args.next(), Some(Argument::Other("/foo/bar".to_owned()))); + assert_eq!(args.next(), None); // Without trailing arguments, the end is noned. + + // Try again with some keywords. + args = Argue::from(cli.iter().cloned()) + .with_keywords([ + KeyWord::Key("-s"), + KeyWord::Key("--long"), + KeyWord::KeyWithValue("-t"), + KeyWord::KeyWithValue("--m"), + KeyWord::KeyWithValue("--n"), + KeyWord::KeyWithValue("-u"), + ]); assert_eq!(args.next(), Some(Argument::Key("-s"))); assert_eq!(args.next(), Some(Argument::Key("--long"))); assert_eq!(args.next(), Some(Argument::KeyWithValue("-t", "2".to_owned()))); @@ -610,24 +376,22 @@ mod test { assert_eq!(args.next(), Some(Argument::KeyWithValue("--n", "yar".to_owned()))); assert_eq!(args.next(), Some(Argument::KeyWithValue("-u", "2".to_owned()))); assert_eq!(args.next(), Some(Argument::Other("/foo/bar".to_owned()))); - assert!(args.next().is_none()); - - // One more time around with a couple end-of-command args. - cli.push(OsStr::new("--end").to_owned()); - cli.push(OsStr::new("--m=yar").to_owned()); - - let mut args = Argue::from(cli.into_iter()) - .with_command("subcommand").unwrap() - .with_keys([ - ("--long", false), - ("--m", true), - ("--n", true), - ("-s", false), - ("-t", true), - ("-u", true), - ]).unwrap(); - - assert_eq!(args.next(), Some(Argument::Command("subcommand"))); + assert_eq!(args.next(), None); // Without trailing arguments, the end is noned. + + // Add some trailing arguments for good measure. + cli.push(OsString::from("Björk")); + cli.push(OsString::from("is")); + cli.push(OsString::from("best")); + + args = Argue::from(cli.iter().cloned()) + .with_keywords([ + KeyWord::Key("-s"), + KeyWord::Key("--long"), + KeyWord::KeyWithValue("-t"), + KeyWord::KeyWithValue("--m"), + KeyWord::KeyWithValue("--n"), + KeyWord::KeyWithValue("-u"), + ]); assert_eq!(args.next(), Some(Argument::Key("-s"))); assert_eq!(args.next(), Some(Argument::Key("--long"))); assert_eq!(args.next(), Some(Argument::KeyWithValue("-t", "2".to_owned()))); @@ -635,105 +399,55 @@ mod test { assert_eq!(args.next(), Some(Argument::KeyWithValue("--n", "yar".to_owned()))); assert_eq!(args.next(), Some(Argument::KeyWithValue("-u", "2".to_owned()))); assert_eq!(args.next(), Some(Argument::Other("/foo/bar".to_owned()))); - assert_eq!( - args.next(), - Some(Argument::End(vec![ - OsStr::new("--end").to_owned(), - OsStr::new("--m=yar").to_owned(), - ])) - ); - assert!(args.next().is_none()); - } - - #[test] - fn t_argue_with_keys() { - // Define keys all together. - let arg1 = Argue::from(std::iter::once(OsString::new())) - .with_keys([ - ("--switch1", false), - ("--switch2", false), - ("--opt1", true), - ("--opt2", true), - ]) - .expect("Argue::with_keys failed."); - - // Define them separately. - let arg2 = Argue::from(std::iter::once(OsString::new())) - .with_switches(["--switch1", "--switch2"]) - .expect("Argue::with_switches failed.") - .with_options(["--opt1", "--opt2"]) - .expect("Argue::with_options failed."); - - // The special list should be the same either way. - assert_eq!(arg1.special, arg2.special); - - // While we're here, let's make sure we can't repeat a key. - assert!(arg2.with_key("--switch1", false).is_err()); - } - - #[test] - fn t_valid_key() { - // Happy first letters. - let first: BTreeSet = ('0'..='9').into_iter() - .chain('a'..='z') - .chain('A'..='Z') - .collect(); - - // Make sure we got everything! - assert_eq!(first.len(), 26 * 2 + 10); - - // Any short/long key beginning with one or two dashes and one of these - // characters should be good. - let mut key = String::new(); - for i in first.iter().copied() { - "-".clone_into(&mut key); - key.push(i); - assert!(valid_key(key.as_bytes()), "Bug: -{i} should be a valid key."); - - key.insert(0, '-'); - assert!(valid_key(key.as_bytes()), "Bug: --{i} should be a valid key."); - - // Chuck a few extras on there for good measure. - key.push_str("a-Z_0123"); - assert!(valid_key(key.as_bytes()), "Bug: {key:?} should be a valid key."); - } - - // These should all be bad. - for k in [ - "", // Empty. - "-", // No alphanumeric. - "--", - "---", - "--_", - "--Björk", // The ö is invalid. - "-abc", // Too long. - "-Björk", // Too long and ö. - "0", // No leading dash(es). - "0bc", - "_abc", - "a", - "A", - "abc", - ] { - assert!(! valid_key(k.as_bytes()), "Bug: Key {k:?} shouldn't be valid."); - } + assert_eq!(args.next(), Some(Argument::End(vec![ + OsString::from("Björk"), + OsString::from("is"), + OsString::from("best"), + ]))); + assert_eq!(args.next(), None); + + // Shorten the test so we can focus on key types. + cli.truncate(0); + cli.push(OsString::from("-t2")); + cli.push(OsString::from("--m=yar")); + + // As before. + args = Argue::from(cli.iter().cloned()) + .with_keywords([ + KeyWord::Key("--long"), // Unused. + KeyWord::KeyWithValue("-t"), + KeyWord::KeyWithValue("--m"), + ]); + assert_eq!(args.next(), Some(Argument::KeyWithValue("-t", "2".to_owned()))); + assert_eq!(args.next(), Some(Argument::KeyWithValue("--m", "yar".to_owned()))); + assert_eq!(args.next(), None); + + // The values should get dropped for booleans. + args = Argue::from(cli.iter().cloned()) + .with_keywords([ + KeyWord::Key("-t"), + KeyWord::Key("--m"), + ]); + assert_eq!(args.next(), Some(Argument::Key("-t"))); + assert_eq!(args.next(), Some(Argument::Key("--m"))); + assert_eq!(args.next(), None); } #[test] - fn t_valid_key_byte() { - for i in 0..u8::MAX { - // Our validation method uses matches!() instead, so let's make - // sure there's agreement with this approach. - let expected: bool = - i == b'-' || - i == b'_' || - i.is_ascii_alphanumeric(); - - assert_eq!( - expected, - valid_key_byte(i), - "Key byte validation mismatch {i}", - ); - } + fn t_argue_duplicate() { + let cli: Vec = Vec::new(); + let mut args = Argue::from(cli.iter().cloned()) + .with_keywords([KeyWord::Key("-h")]); + + // It should be a boolean. + let key = args.keys.get("-h").copied().unwrap(); + assert!(matches!(key, KeyWord::Key("-h"))); + assert!(! matches!(key, KeyWord::KeyWithValue("-h"))); + + // Now it should require a value. + args = args.with_keywords([KeyWord::KeyWithValue("-h")]); + let key = args.keys.get("-h").copied().unwrap(); + assert!(! matches!(key, KeyWord::Key("-h"))); + assert!(matches!(key, KeyWord::KeyWithValue("-h"))); } }