diff --git a/GNUmakefile b/GNUmakefile index 7c03eea..ee77926 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,7 +40,7 @@ endif build: build_dir false intcmp scrut str strcmp true build_dir: - mkdir -p build/o build/lib build/bin + mkdir -p build/o build/lib build/bin build/test clean: rm -rf build/ @@ -53,9 +53,10 @@ install: build cp -f docs/*.1 $(PREFIX)/share/man/man1/ # cp -f docs/*.3 $(PREFIX)/share/man/man3/ -test: build +test: build_dir tests/cc-compat.sh tests/posix-compat.sh src/getopt-rs/tests.rs tests/cc-compat.sh tests/posix-compat.sh + $(RUSTC) --test src/getopt-rs/lib.rs -o build/test/getopt false: src/false.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/false src/false.rs @@ -75,3 +76,6 @@ strcmp: src/strcmp.c build_dir true: src/true.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/true src/true.rs +libgetopt: src/getopt-rs/lib.rs + $(RUSTC) $(RUSTCFLAGS) --crate-type=lib --crate-name=getopt \ + -o build/o/libgetopt.rlib src/lib.rs diff --git a/src/getopt-rs/error.rs b/src/getopt-rs/error.rs new file mode 100644 index 0000000..322af02 --- /dev/null +++ b/src/getopt-rs/error.rs @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::{ error, fmt }; + +use crate::ErrorKind::{ self, * }; + +/// A basic error type for [`Parser`](struct.Parser.html) +#[derive(Debug, Eq, PartialEq)] +pub struct Error { + culprit: char, + kind: ErrorKind, +} + +impl Error { + /// Creates a new error using a known kind and the character that caused the + /// issue. + pub fn new(kind: ErrorKind, culprit: char) -> Self { + Self { culprit, kind } + } + + /// Returns the [`ErrorKind`](enum.ErrorKind.html) for this error. + pub fn kind(self) -> ErrorKind { + self.kind + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.kind { + MissingArgument => write!( + f, + "option requires an argument -- {:?}", + self.culprit, + ), + UnknownOption => write!(f, "unknown option -- {:?}", self.culprit), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} diff --git a/src/getopt-rs/errorkind.rs b/src/getopt-rs/errorkind.rs new file mode 100644 index 0000000..5475d8e --- /dev/null +++ b/src/getopt-rs/errorkind.rs @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/// What kinds of errors [`Parser`](struct.Parser.html) can return. +#[derive(Debug, Eq, PartialEq)] +pub enum ErrorKind { + /// An argument was not found for an option that was expecting one. + MissingArgument, + /// An unknown option character was encountered. + UnknownOption, +} diff --git a/src/getopt-rs/lib.rs b/src/getopt-rs/lib.rs new file mode 100644 index 0000000..62f0e0d --- /dev/null +++ b/src/getopt-rs/lib.rs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +//! # getopt +//! +//! `getopt` provides a minimal, (essentially) POSIX-compliant option parser. + +pub use crate::{ + error::Error, + errorkind::ErrorKind, + opt::Opt, + parser::Parser, + result::Result +}; + +mod error; +mod errorkind; +mod opt; +mod parser; +mod result; +#[cfg(test)] +mod tests; diff --git a/src/getopt-rs/opt.rs b/src/getopt-rs/opt.rs new file mode 100644 index 0000000..05b51e6 --- /dev/null +++ b/src/getopt-rs/opt.rs @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::fmt; + +/// A single option. +/// +/// For `Opt(x, y)`: +/// - `x` is the character representing the option. +/// - `y` is `Some` string, or `None` if no argument was expected. +/// +/// # Example +/// +/// ``` +/// # fn main() -> Result<(), Box> { +/// use getopt::Opt; +/// +/// // args = ["program", "-abc", "foo"]; +/// # let args: Vec = vec!["program", "-abc", "foo"] +/// # .into_iter() +/// # .map(String::from) +/// # .collect(); +/// let optstring = "ab:c"; +/// let mut opts = getopt::Parser::new(&args, optstring); +/// +/// assert_eq!(Opt('a', None), opts.next().transpose()?.unwrap()); +/// assert_eq!(Opt('b', Some("c".to_string())), opts.next().transpose()?.unwrap()); +/// assert_eq!(None, opts.next().transpose()?); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Opt(pub char, pub Option); + +impl fmt::Display for Opt { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Opt({:?}, {:?})", self.0, self.1) + } +} diff --git a/src/getopt-rs/parser.rs b/src/getopt-rs/parser.rs new file mode 100644 index 0000000..6f06cc3 --- /dev/null +++ b/src/getopt-rs/parser.rs @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::collections::HashMap; + +use crate::{ error::Error, errorkind::ErrorKind, opt::Opt, result::Result }; + +/// The core of the `getopt` crate. +/// +/// `Parser` is implemented as an iterator over the options present in the given +/// argument vector. +/// +/// The method [`next`](#method.next) does the heavy lifting. +/// +/// # Examples +/// +/// ## Simplified usage: +/// ``` +/// # fn main() -> Result<(), Box> { +/// use getopt::Opt; +/// +/// // args = ["program", "-abc", "foo"]; +/// # let args: Vec = vec!["program", "-abc", "foo"] +/// # .into_iter() +/// # .map(String::from) +/// # .collect(); +/// let mut opts = getopt::Parser::new(&args, "ab:c"); +/// +/// assert_eq!(Some(Opt('a', None)), opts.next().transpose()?); +/// assert_eq!(1, opts.index()); +/// assert_eq!(Some(Opt('b', Some("c".to_string()))), opts.next().transpose()?); +/// assert_eq!(2, opts.index()); +/// assert_eq!(None, opts.next()); +/// assert_eq!(2, opts.index()); +/// assert_eq!("foo", args[opts.index()]); +/// # Ok(()) +/// # } +/// ``` +/// +/// ## A more idiomatic example: +/// ``` +/// # fn main() -> Result<(), Box> { +/// use getopt::Opt; +/// +/// // args = ["program", "-abc", "-d", "foo", "-e", "bar"]; +/// # let mut args: Vec = vec!["program", "-abc", "-d", "foo", "-e", "bar"] +/// # .into_iter() +/// # .map(String::from) +/// # .collect(); +/// let mut opts = getopt::Parser::new(&args, "ab:cd:e"); +/// +/// let mut a_flag = false; +/// let mut b_flag = String::new(); +/// let mut c_flag = false; +/// let mut d_flag = String::new(); +/// let mut e_flag = false; +/// +/// loop { +/// match opts.next().transpose()? { +/// None => break, +/// Some(opt) => match opt { +/// Opt('a', None) => a_flag = true, +/// Opt('b', Some(arg)) => b_flag = arg.clone(), +/// Opt('c', None) => c_flag = true, +/// Opt('d', Some(arg)) => d_flag = arg.clone(), +/// Opt('e', None) => e_flag = true, +/// _ => unreachable!(), +/// }, +/// } +/// } +/// +/// let new_args = args.split_off(opts.index()); +/// +/// assert_eq!(true, a_flag); +/// assert_eq!("c", b_flag); +/// assert_eq!(false, c_flag); +/// assert_eq!("foo", d_flag); +/// assert_eq!(true, e_flag); +/// +/// assert_eq!(1, new_args.len()); +/// assert_eq!("bar", new_args.first().unwrap()); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Eq, PartialEq)] +pub struct Parser { + opts: HashMap, + args: Vec>, + index: usize, + point: usize, +} + +impl Parser { + /// Create a new `Parser`, which will process the arguments in `args` + /// according to the options specified in `optstring`. + /// + /// For compatibility with + /// [`std::env::args`](https://doc.rust-lang.org/std/env/fn.args.html), + /// valid options are expected to begin at the second element of `args`, and + /// `index` is + /// initialised to `1`. + /// If `args` is structured differently, be sure to call + /// [`set_index`](#method.set_index) before the first invocation of + /// [`next`](#method.next). + /// + /// `optstring` is a string of recognised option characters; if a character + /// is followed by a colon (`:`), that option takes an argument. + /// + /// # Note: + /// Transforming the OS-specific argument strings into a vector of `String`s + /// is the sole responsibility of the calling program, as it involves some + /// level of potential information loss (which this crate does not presume + /// to handle unilaterally) and error handling (which would complicate the + /// interface). + pub fn new(args: &[String], optstring: &str) -> Self { + let optstring: Vec = optstring.chars().collect(); + let mut opts = HashMap::new(); + let mut i = 0; + let len = optstring.len(); + + while i < len { + let j = i + 1; + + if j < len && optstring[j] == ':' { + opts.insert(optstring[i], true); + i += 1; + } else { + opts.insert(optstring[i], false); + } + i += 1; + } + + Self { + opts, + // "explode" the args into a vector of character vectors, to allow + // indexing + args: args.iter().map(|e| e.chars().collect()).collect(), + index: 1, + point: 0, + } + } + + /// Return the current `index` of the parser. + /// + /// `args[index]` will always point to the the next element of `args`; when + /// the parser is + /// finished with an element, it will increment `index`. + /// + /// After the last option has been parsed (and [`next`](#method.next) is + /// returning `None`), + /// `index` will point to the first non-option argument. + pub fn index(&self) -> usize { + self.index + } + + // `point` must be reset to 0 whenever `index` is changed + + /// Modify the current `index` of the parser. + pub fn set_index(&mut self, value: usize) { + self.index = value; + self.point = 0; + } + + /// Increment the current `index` of the parser. + /// + /// This use case is common enough to warrant its own optimised method. + pub fn incr_index(&mut self) { + self.index += 1; + self.point = 0; + } +} + +impl Iterator for Parser { + type Item = Result; + + /// Returns the next option, if any. + /// + /// Returns an [`Error`](struct.Error.html) if an unexpected option is + /// encountered or if an + /// expected argument is not found. + /// + /// Parsing stops at the first non-hyphenated argument; or at the first + /// argument matching "-"; + /// or after the first argument matching "--". + /// + /// When no more options are available, `next` returns `None`. + /// + /// # Examples + /// + /// ## "-" + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "-", "-a"]; + /// # let args: Vec = vec!["program", "-", "-a"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a"); + /// + /// assert_eq!(None, opts.next()); + /// assert_eq!("-", args[opts.index()]); + /// ``` + /// + /// ## "--" + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "--", "-a"]; + /// # let args: Vec = vec!["program", "--", "-a"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a"); + /// + /// assert_eq!(None, opts.next()); + /// assert_eq!("-a", args[opts.index()]); + /// ``` + /// + /// ## Unexpected option: + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "-b"]; + /// # let args: Vec = vec!["program", "-b"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a"); + /// + /// assert_eq!( + /// "unknown option -- 'b'".to_string(), + /// opts.next().unwrap().unwrap_err().to_string() + /// ); + /// ``` + /// + /// ## Missing argument: + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "-a"]; + /// # let args: Vec = vec!["program", "-a"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a:"); + /// + /// assert_eq!( + /// "option requires an argument -- 'a'".to_string(), + /// opts.next().unwrap().unwrap_err().to_string() + /// ); + /// ``` + fn next(&mut self) -> Option> { + if self.point == 0 { + /* + * Rationale excerpts below taken verbatim from "The Open Group Base + * Specifications Issue 7, 2018 edition", IEEE Std 1003.1-2017 + * (Revision of IEEE Std 1003.1-2008). + * Copyright © 2001-2018 IEEE and The Open Group. + */ + + /* + * If, when getopt() is called: + * argv[optind] is a null pointer + * *argv[optind] is not the character '-' + * argv[optind] points to the string "-" + * getopt() shall return -1 without changing optind. + */ + if self.index >= self.args.len() + || self.args[self.index].is_empty() + || self.args[self.index][0] != '-' + || self.args[self.index].len() == 1 + { + return None; + } + + /* + * If: + * argv[optind] points to the string "--" + * getopt() shall return -1 after incrementing index. + */ + if self.args[self.index][1] == '-' && self.args[self.index].len() == 2 { + self.incr_index(); + return None; + } + + // move past the starting '-' + self.point += 1; + } + + let opt = self.args[self.index][self.point]; + self.point += 1; + + match self.opts.get(&opt) { + None => { + if self.point >= self.args[self.index].len() { + self.incr_index(); + } + Some(Err(Error::new(ErrorKind::UnknownOption, opt))) + } + Some(false) => { + if self.point >= self.args[self.index].len() { + self.incr_index(); + } + + Some(Ok(Opt(opt, None))) + } + Some(true) => { + let arg: String = if self.point >= self.args[self.index].len() { + self.incr_index(); + if self.index >= self.args.len() { + return Some(Err(Error::new( + ErrorKind::MissingArgument, + opt, + ))); + } + self.args[self.index].iter().collect() + } else { + self.args[self.index] + .clone() + .split_off(self.point) + .iter() + .collect() + }; + + self.incr_index(); + + Some(Ok(Opt(opt, Some(arg)))) + } + } + } +} diff --git a/src/getopt-rs/result.rs b/src/getopt-rs/result.rs new file mode 100644 index 0000000..015a402 --- /dev/null +++ b/src/getopt-rs/result.rs @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::result; + +use crate::error::Error; + +/// A specialized `Result` type for use with [`Parser`](struct.Parser.html) +pub type Result = result::Result; diff --git a/src/getopt-rs/tests.rs b/src/getopt-rs/tests.rs new file mode 100644 index 0000000..c53d517 --- /dev/null +++ b/src/getopt-rs/tests.rs @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use crate::{Opt, Parser}; + +macro_rules! basic_test { + ($name:ident, $expect:expr, $next:expr, [$($arg:expr),+], $optstr:expr) => ( + #[test] + fn $name() -> Result<(), String> { + let expect: Option = $expect; + let args: Vec = vec![$($arg),+] + .into_iter() + .map(String::from) + .collect(); + let next: Option = $next; + let mut opts = Parser::new(&args, $optstr); + + match opts.next().transpose() { + Err(error) => { + return Err(format!("next() returned {:?}", error)) + }, + Ok(actual) => if actual != expect { + return Err( + format!("expected {:?}; got {:?}", expect, actual) + ) + }, + }; + + match next { + None => if opts.index() < args.len() { + return Err(format!( + "expected end of args; got {:?}", args[opts.index()] + )) + }, + Some(n) => if args[opts.index()] != n { + return Err(format!( + "next arg: expected {:?}; got {:?}", + n, + args[opts.index()] + )) + }, + }; + + Ok(()) + } + ) +} + +#[rustfmt::skip] basic_test!( + blank_arg, None, Some(String::new()), ["x", ""], "a" +); +#[rustfmt::skip] basic_test!( + double_dash, None, Some("-a".to_string()), ["x", "--", "-a", "foo"], "a" +); +#[rustfmt::skip] basic_test!(no_opts_1, None, None, ["x"], "a"); +#[rustfmt::skip] basic_test!( + no_opts_2, None, Some("foo".to_string()), ["x", "foo"], "a" +); +#[rustfmt::skip] basic_test!( + no_opts_3, None, Some("foo".to_string()), ["x", "foo", "-a"], "a" +); +#[rustfmt::skip] basic_test!( + single_dash, None, Some("-".to_string()), ["x", "-", "-a", "foo"], "a" +); +#[rustfmt::skip] basic_test!( + single_opt, + Some(Opt('a', None)), + Some("foo".to_string()), + ["x", "-a", "foo"], + "a" +); +#[rustfmt::skip] basic_test!( + single_optarg, + Some(Opt('a', Some("foo".to_string()))), + None, + ["x", "-a", "foo"], + "a:" +); + +macro_rules! error_test { + ($name:ident, $expect:expr, [$($arg:expr),+], $optstr:expr) => ( + #[test] + fn $name() -> Result<(), String> { + let expect: String = $expect.to_string(); + let args: Vec = vec![$($arg),+] + .into_iter() + .map(String::from) + .collect(); + let mut opts = Parser::new(&args, $optstr); + + match opts.next() { + None => { + return Err(format!( + "unexpected successful response: end of options" + )) + }, + Some(Err(actual)) => { + let actual = actual.to_string(); + + if actual != expect { + return Err( + format!("expected {:?}; got {:?}", expect, actual) + ); + } + }, + Some(Ok(opt)) => { + return Err( + format!("unexpected successful response: {:?}", opt) + ) + }, + }; + + Ok(()) + } + ) +} + +#[rustfmt::skip] error_test!( + bad_opt, + "unknown option -- 'b'", + ["x", "-b"], + "a" +); + +#[rustfmt::skip] error_test!( + missing_optarg, + "option requires an argument -- 'a'", + ["x", "-a"], + "a:" +); + +#[test] +fn multiple() -> Result<(), String> { + let args: Vec = vec!["x", "-abc", "-d", "foo", "-e", "bar"] + .into_iter() + .map(String::from) + .collect(); + let optstring = "ab:d:e".to_string(); + let mut opts = Parser::new(&args, &optstring); + + macro_rules! check_result { + ($expect:expr) => { + let expect: Option = $expect; + match opts.next().transpose() { + Err(error) => { + return Err(format!("next() returned {:?}", error)); + }, + Ok(actual) => { + if actual != expect { + return Err( + format!("expected {:?}; got {:?}", expect, actual) + ); + } + } + }; + }; + } + + check_result!(Some(Opt('a', None))); + check_result!(Some(Opt('b', Some("c".to_string())))); + check_result!(Some(Opt('d', Some("foo".to_string())))); + check_result!(Some(Opt('e', None))); + check_result!(None); + + Ok(()) +} + +#[test] +fn continue_after_error() { + let args: Vec = vec!["x", "-z", "-abc"] + .into_iter() + .map(String::from) + .collect(); + let optstring = "ab:d:e".to_string(); + for _opt in Parser::new(&args, &optstring) { + // do nothing, should not panic + } +}