Compare commits
34 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
831f65b27e | ||
![]() |
3563377a5e | ||
![]() |
426c0c9ef7 | ||
![]() |
1c75b5354e | ||
![]() |
14b5cad8c1 | ||
![]() |
e3d1176fa6 | ||
![]() |
cc6f53979a | ||
![]() |
75c277db3a | ||
![]() |
77f65c79c2 | ||
![]() |
3c9dae504b | ||
![]() |
632850c58c | ||
![]() |
ee56d7c081 | ||
![]() |
87675e409a | ||
![]() |
0bab8dc9ca | ||
![]() |
d1de4d9431 | ||
![]() |
c0d0a99c25 | ||
![]() |
346c17c517 | ||
![]() |
2ba09a17b6 | ||
![]() |
ad4a8ee4b3 | ||
![]() |
97a0d856b2 | ||
![]() |
2423cd7d19 | ||
![]() |
be28b0307c | ||
![]() |
297aaa0ad3 | ||
![]() |
29077d64d6 | ||
![]() |
fb603d35ab | ||
![]() |
1b71c6c876 | ||
![]() |
f8809979ff | ||
![]() |
25327fa441 | ||
![]() |
da684ab0f7 | ||
![]() |
78a56f27f8 | ||
![]() |
11a9fc97f7 | ||
![]() |
e5924be826 | ||
![]() |
8aca4b42ac | ||
![]() |
7ef5a2ceb8 |
10 changed files with 727 additions and 292 deletions
5
.travis.yml
Normal file
5
.travis.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
language: rust
|
||||
rust:
|
||||
- nightly
|
||||
- beta
|
||||
- stable
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
|
||||
name = "pirate"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
authors = ["Zach Dziura <zcdziura@gmail.com>"]
|
||||
description = "A simple arrrguments parser"
|
||||
repository = "https://github.com/zcdziura/pirate"
|
||||
keywords = ["opts", "args", "parser", "getopts", "options", "arguments"]
|
||||
keywords = ["parser", "getopts", "options", "arguments"]
|
||||
license = "LGPL-3.0+"
|
||||
|
|
190
README.md
190
README.md
|
@ -1,4 +1,4 @@
|
|||
Pirate
|
||||
Pirate [](https://travis-ci.org/zcdziura/pirate)
|
||||
======
|
||||
|
||||
A command-line arrrrguments parser, written in Rust.
|
||||
|
@ -6,82 +6,101 @@ A command-line arrrrguments parser, written in Rust.
|
|||
Synopsis
|
||||
--------
|
||||
|
||||
Most programs that provide a command-line interface use a special-purpose library to make the
|
||||
process easier, such as the GNU Project's `getopt` library. The Rust team provides their own
|
||||
alternative to `getopt`: `getopts`, which should win an award for the Most Originally Named Project
|
||||
Ever.
|
||||
Most programs that provide a command-line interface use a special-purpose library to make the process easier, such as the GNU Project's `getopt` library. The Rust team provides their own alternative, `getopts`, which deserves an award for the Most Originally Named Project Ever.
|
||||
|
||||
In all seriousness, `getopts` is a fantastic library that gives the developers all of the tools
|
||||
necessary to create and interface with command-line arguments. However, with all that power comes
|
||||
complexity. `getopts`, while straight forward to use, is verbose. The developer has to make repeated
|
||||
method calls to add different command-line options. And while the only victim here is the
|
||||
developer's wrists due to carpal tunnel, I felt that there was a better way to do things.
|
||||
In all seriousness, `getopts` is a fantastic library that gives the developers all of the power necessary to create and interface with command-line arguments. However, with all that power comes complexity. `getopts` -- while straight forward to use -- is verbose. The developer has to call different functions repeatedly in order to add different command-line options to their programs. While the only victim here is the developer's wrists due to carpal tunnel, I felt that there was a better way to do things.
|
||||
|
||||
Enter Pirate (which should totally usurp `getopts` for the award of Most Originally Named Project Ever).
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Add this to your `Cargo.toml:
|
||||
Add this to your project's `Cargo.toml` file:
|
||||
|
||||
```
|
||||
[dependencies]
|
||||
pirate = "0.1.0"
|
||||
pirate = "1.0.0"
|
||||
```
|
||||
|
||||
and this to your crate root:
|
||||
|
||||
```rust
|
||||
extern crate getopts;
|
||||
extern crate pirate;
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Using Pirate is simple. First, create an array slice defining all of the valid opts that your
|
||||
program accepts:
|
||||
|
||||
`let opts = &["o:", "l/long", ":arg"];`
|
||||
|
||||
Opts are defined in a specific format:
|
||||
|
||||
* Opts that have an associated argument must be followed by a colon (:).
|
||||
* Opts with both a short and long form are separated by a slash (/). If an opt has an associated
|
||||
argument, the colon must come after the long form, e.g. `"l/long:"`.
|
||||
* Required program arguments have a preceding colon, e.g. `":arg"`.
|
||||
* All other opts are defined normally, e.g. `"l"` is an opt in short form, `"long"` is an opt in
|
||||
long form.
|
||||
|
||||
Next, call the `pirate::parse()` function, passing in the environment arguments along with the slice
|
||||
of opts that you defined:
|
||||
|
||||
`let matches = pirate::parse(env::args(), opts);`
|
||||
|
||||
Now, handle any errors that may have arisen from parsing:
|
||||
Using Pirate is simple. First, create a vector defining all of the valid options that your program accepts:
|
||||
|
||||
```rust
|
||||
let options = vec![
|
||||
"a/addend#The right side of the addition equation; default=1:",
|
||||
"#Required Arguments",
|
||||
":/augend#The left side of an addition equation"
|
||||
];
|
||||
```
|
||||
let matches: Matches = match pirate::parse(env::args(), opts) {
|
||||
Err(ref e) => {
|
||||
println!("Error: {}", e);
|
||||
help();
|
||||
|
||||
Options are defined in a very specific format:
|
||||
|
||||
* Options that have an associated argument must be followed by a colon (:). The colon must be the last character of the option (see above for example).
|
||||
* Long-form options are denoted by a preceding slash (/). Options are able to have short- and long-forms. Options which are only long-form still need a preceding slash, e.g. `"/addend"`.
|
||||
* Required program arguments must have a preceding colon as the first character of the opt, e.g. `":/augend"`.
|
||||
* Option descriptions are denoted by a proceding hash (#). Descriptions are optional and are used to display helpful information about the option when displaying a program's usage information (typically when the `--help` flag is passed). Options with **only** a description (i.e. no short- or long-form name) are called "Groups", and are used to group options together when displaying usage.
|
||||
|
||||
Next, create a `Vars` struct, which is responsible for keeping track of all of the options, along with the program's name, defined for the program:
|
||||
|
||||
```rust
|
||||
let vars: Vars = match pirate::vars("program-name", &options) {
|
||||
Ok(v) => v,
|
||||
Err(why) => panic!("Error: {}", why)
|
||||
}
|
||||
```
|
||||
|
||||
Next, call the `pirate::matches()` function, passing in a vector of the program's environment arguments, along with a mutable reference to the `Vars` struct that you previously defined:
|
||||
|
||||
```rust
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let matches: Matches = match pirate::matches(&args, &mut vars) {
|
||||
Ok(m) => m,
|
||||
Err(why) => {
|
||||
println!("Error: {}", why);
|
||||
pirate::usage(&vars);
|
||||
return;
|
||||
},
|
||||
Ok(m) => m
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
`Matches` is nothing more than a type alias to a `HashMap<String, String>`. All of the custom methods that make the type easier to use are defined by the `Match` trait.
|
||||
|
||||
Finally, you may want to check which arguments were passed to the program. Luckily, the `Matches`
|
||||
struct provides several helpful methods for querying whether an argument was passed to the program
|
||||
and what its value is.
|
||||
And finally, check which arguments were passed to the program.
|
||||
|
||||
```
|
||||
fn get(arg: &str) -> Option<&String> // Returns a reference to the given arg, or None if not found
|
||||
// Returns a reference to the given arg, or None if not found
|
||||
fn get(arg: &str) -> Option<&String>;
|
||||
|
||||
fn has_arg(arg: &str) -> bool // Returns true if the arg exists, false if not
|
||||
// Returns true if the match exists, false if not
|
||||
fn has_match(arg: &str) -> bool;
|
||||
|
||||
fn keys() -> Keys<String, String> // An iterator over all args passed to the program
|
||||
// An iterator over all matches found
|
||||
fn keys() -> Keys<String, String>;
|
||||
```
|
||||
|
||||
Something to remember when using the `get()` function: by default, the `pirate::matches()` function stores the opt's long-form name as the key, by default, should the long-form exist; otherwise the short-form is used. So, should you define an opt with both a short- and long-form name, when querying for it, pass the long-form as the argument. For example:
|
||||
|
||||
```rust
|
||||
let options = vec!["l/long#An example opt"];
|
||||
let vars = pirate::vars("program-name", &options);
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let matches = pirate::matches(&args, &mut vars).unwrap();
|
||||
|
||||
let short = matches.get("l").unwrap(); // Error! This won't work!
|
||||
let long = matches.get("long").unwrap(); // Success!
|
||||
|
||||
// Usage: program-name -l
|
||||
```
|
||||
|
||||
As shown in a previous example, should you ever want to display the program's usage data, simply call the `pirate::usage()` function, passing in a reverence to your `Vars` struct as an argument. E.g. `pirate::usage(&vars)`
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
|
@ -90,64 +109,51 @@ Here is a trivial example that gives a general idea about how to use `pirate`:
|
|||
```rust
|
||||
extern crate pirate;
|
||||
|
||||
use std::env;
|
||||
|
||||
use pirate::Matches;
|
||||
use pirate::{Matches, Match, matches, usage, vars};
|
||||
|
||||
fn main() {
|
||||
let opts = &["n:", "b/boop", ":input"];
|
||||
|
||||
let matches: Matches = match pirate::parse(env::args(), opts) {
|
||||
Err(ref e) => {
|
||||
println!("Error: {}", e);
|
||||
help();
|
||||
let env_args: Vec<String> = vec![
|
||||
String::from("test"),
|
||||
String::from("-a"), String::from("2"),
|
||||
String::from("3")
|
||||
];
|
||||
let options = vec![
|
||||
"a/addend#The right side of the addition equation; default=1:",
|
||||
"#Required Arguments",
|
||||
":/augend#The left side of an addition equation"
|
||||
];
|
||||
let mut vars = vars("test", &options).unwrap();
|
||||
|
||||
let matches: Matches = match matches(&env_args, &mut vars) {
|
||||
Ok(m) => m,
|
||||
Err(why) => {
|
||||
println!("Error: {}", why);
|
||||
usage(&vars);
|
||||
return;
|
||||
},
|
||||
Ok(m) => m
|
||||
}
|
||||
};
|
||||
|
||||
// Print the program help if necessary
|
||||
if matches.has_match("h") || matches.has_match("help") {
|
||||
help();
|
||||
|
||||
if matches.has_match("help") {
|
||||
usage(&vars);
|
||||
return;
|
||||
}
|
||||
|
||||
let augend: i32 = matches.get("augend")
|
||||
.unwrap()
|
||||
.parse::<i32>()
|
||||
.unwrap();
|
||||
|
||||
let input = matches.get("input").unwrap().parse::<i32>().unwrap();
|
||||
let num = match matches.get("n") {
|
||||
Some(n) => n.parse::<i32>().unwrap(),
|
||||
let addend: i32 = match matches.get("addend") {
|
||||
Some(a) => a.parse::<i32>().unwrap(),
|
||||
None => 1
|
||||
};
|
||||
|
||||
let sum = input + num;
|
||||
|
||||
println!("{} + {} = {}", input, num, sum);
|
||||
|
||||
if matches.has_match("b") || matches.has_match("boop") {
|
||||
println!("Boop!!");
|
||||
}
|
||||
}
|
||||
|
||||
fn help() {
|
||||
println!("usage: pirate-test [-n NUM] [-b|--boop] INPUT\n");
|
||||
|
||||
println!("Options:");
|
||||
println!(" -n NUM\tChange the default number that's added to the input");
|
||||
println!(" -b, --boop\tIt's a surprise!");
|
||||
|
||||
println!("\nRequired arguments:");
|
||||
println!(" INPUT\tWe're gonna manipulate this somehow, you'll see!");
|
||||
|
||||
let sum = augend + addend;
|
||||
|
||||
println!("{} + {} = {}", augend, addend, sum);
|
||||
}
|
||||
```
|
||||
|
||||
To Do
|
||||
-----
|
||||
|
||||
- [ ] Create a helper function for generation `--help` output, rather than having the user create it
|
||||
manually.
|
||||
- [ ] Also create helper functions for defining the description section of the `--help` output.
|
||||
- [ ] Refactor the `ErrorKind` enum into a struct that is able to represent more complex data (such
|
||||
giving the value of the invalid argument passed to the program).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
|
|
@ -15,26 +15,52 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::error::Error;
|
||||
use std::error;
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorKind {
|
||||
InvalidOption,
|
||||
MissingArgument,
|
||||
pub struct Error {
|
||||
kind: ErrorKind,
|
||||
offender: String,
|
||||
desc: String
|
||||
}
|
||||
|
||||
impl Error for ErrorKind {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
ErrorKind::InvalidOption => "An invalid option was passed to the program",
|
||||
ErrorKind::MissingArgument => "A required argument is missing",
|
||||
impl Error {
|
||||
pub fn new(kind: ErrorKind, offender: String) -> Error {
|
||||
Error {
|
||||
kind: kind.clone(),
|
||||
offender: offender.clone(),
|
||||
desc: format!("{} {}", kind.description(), offender.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f, "{}", self.description())
|
||||
impl error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
&self.desc
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f, "{}", self.desc)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ErrorKind {
|
||||
InvalidArgument,
|
||||
MissingArgument,
|
||||
TokenFormat
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
fn description(&self) -> String {
|
||||
match *self {
|
||||
ErrorKind::InvalidArgument => String::from("An invalid option was passed to the program:"),
|
||||
ErrorKind::MissingArgument => String::from("A required argument is missing:"),
|
||||
ErrorKind::TokenFormat => String::from("A token was created in the wrong format:")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
105
src/lib.rs
105
src/lib.rs
|
@ -15,101 +15,12 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod errors;
|
||||
pub mod matches;
|
||||
mod opts;
|
||||
mod errors;
|
||||
mod matches;
|
||||
mod token;
|
||||
mod usage;
|
||||
mod vars;
|
||||
|
||||
use std::env::Args;
|
||||
|
||||
pub use errors::ErrorKind;
|
||||
pub use matches::Matches;
|
||||
use opts::Opts;
|
||||
|
||||
pub fn parse(mut args: Args, options: &[&'static str]) -> Result<Matches, errors::ErrorKind> {
|
||||
let mut matches: Matches = Matches::new();
|
||||
|
||||
let mut opts: Opts = opts(options); // Jesus, this is redundant...
|
||||
|
||||
args.next(); // Remove the program name from the list of program arguments
|
||||
|
||||
let mut next_arg = args.next();
|
||||
while next_arg.is_some() {
|
||||
let mut current_arg = next_arg.unwrap();
|
||||
let arg: String;
|
||||
|
||||
if ¤t_arg[..1] == "-" { // Probably a opt
|
||||
if current_arg.len() == 2 { // Short form opt
|
||||
arg = String::from(¤t_arg[1..]);
|
||||
} else { // Assuming it's a long form opt
|
||||
//TODO: Handle cases where it may be a opt group
|
||||
arg = String::from(¤t_arg[2..]);
|
||||
}
|
||||
|
||||
if opts.contains_opt(&arg) {
|
||||
let has_arg: bool = *opts.get_opt(&arg).unwrap();
|
||||
|
||||
if has_arg {
|
||||
// NOTE: The corresponding arg MUST be immediately following
|
||||
current_arg = match args.next() {
|
||||
None => {
|
||||
return Err(ErrorKind::MissingArgument);
|
||||
},
|
||||
Some(a) => a
|
||||
};
|
||||
|
||||
matches.insert(&arg, ¤t_arg);
|
||||
} else {
|
||||
matches.insert(&arg, "");
|
||||
}
|
||||
} else {
|
||||
return Err(ErrorKind::InvalidOption);
|
||||
}
|
||||
|
||||
} else { // Probably a required arg
|
||||
let arg_name: String = opts.get_arg().unwrap();
|
||||
matches.insert(&arg_name, ¤t_arg);
|
||||
}
|
||||
|
||||
next_arg = args.next();
|
||||
}
|
||||
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
fn opts(opts: &[&'static str]) -> Opts {
|
||||
let mut options = Opts::new();
|
||||
|
||||
for opt in opts.iter() {
|
||||
let is_arg: bool = match &opt[..1] {
|
||||
":" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
let has_arg: bool = match &opt[(opt.len() - 1)..] {
|
||||
":" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
if is_arg {
|
||||
options.insert_arg(&opt[1..]);
|
||||
} else {
|
||||
let option: &str;
|
||||
|
||||
if has_arg {
|
||||
option = &opt[..(opt.len() - 1)];
|
||||
} else {
|
||||
option = *opt;
|
||||
}
|
||||
|
||||
for form in option.split("/") {
|
||||
options.insert_opt(form, has_arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push the obligatory "-h/--help" options
|
||||
options.insert_opt("h", false);
|
||||
options.insert_opt("help", false);
|
||||
|
||||
options
|
||||
}
|
||||
pub use matches::{Matches, Match, matches};
|
||||
pub use vars::{Vars, vars};
|
||||
pub use usage::usage;
|
136
src/matches.rs
136
src/matches.rs
|
@ -18,31 +18,123 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Keys;
|
||||
|
||||
pub struct Matches {
|
||||
matches: HashMap<String, String>,
|
||||
}
|
||||
use errors::{Error, ErrorKind};
|
||||
use vars::Vars;
|
||||
|
||||
impl Matches {
|
||||
pub fn new() -> Matches {
|
||||
Matches {
|
||||
matches: HashMap::new()
|
||||
pub type Matches = HashMap<String, String>;
|
||||
|
||||
pub fn matches(env_args: &[String], vars: &mut Vars) -> Result<Matches, Error> {
|
||||
let mut matches: Matches = HashMap::new();
|
||||
let mut args = env_args.iter();
|
||||
|
||||
args.next(); // Remove the program name
|
||||
|
||||
while let Some(mut current_arg) = args.next() {
|
||||
let mut arg_vec: Vec<String> = Vec::new();
|
||||
|
||||
// Determine if current opt is in short, long, or arg form
|
||||
if ¤t_arg[..1] == "-" {
|
||||
if ¤t_arg[..2] == "--" { // Long form opt
|
||||
arg_vec.push(String::from(¤t_arg[2..]));
|
||||
} else { // Short form opt
|
||||
// Assuming it's a group of short-form vars; e.g. tar -xzf
|
||||
for c in current_arg[1..].chars() {
|
||||
let mut s = String::new();
|
||||
s.push(c);
|
||||
arg_vec.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
for arg in arg_vec.iter() {
|
||||
if vars.contains_opt(arg) {
|
||||
let token = vars.get_opt(arg).unwrap();
|
||||
|
||||
if token.has_arg {
|
||||
// NOTE: The corresponding arg MUST be immediately following
|
||||
current_arg = match args.next() {
|
||||
None => return Err(Error::new(ErrorKind::MissingArgument, arg.clone())),
|
||||
Some(a) => a
|
||||
};
|
||||
|
||||
matches.insert(token.name(), (*current_arg).clone());
|
||||
} else {
|
||||
matches.insert(token.name(), String::new());
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(ErrorKind::InvalidArgument, arg.clone()));
|
||||
}
|
||||
}
|
||||
} else { // Probably a required arg
|
||||
let arg = vars.get_arg().unwrap();
|
||||
matches.insert(arg.name(), (*current_arg).clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, arg: &str, value: &str) {
|
||||
self.matches.insert(String::from(arg), String::from(value));
|
||||
}
|
||||
|
||||
pub fn get(&self, arg: &str) -> Option<&String> {
|
||||
self.matches.get(arg)
|
||||
}
|
||||
|
||||
pub fn has_arg(&self, arg: &str) -> bool {
|
||||
let arg = String::from(arg);
|
||||
self.matches.contains_key(&arg)
|
||||
}
|
||||
|
||||
pub fn args(&self) -> Keys<String, String> {
|
||||
self.matches.keys()
|
||||
match vars.arg_len() {
|
||||
0 => Ok( matches ),
|
||||
_ => Err(Error::new(ErrorKind::MissingArgument, vars.get_arg().unwrap().name())),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Match {
|
||||
fn get(&self, arg: &str) -> Option<&String>;
|
||||
|
||||
fn has_match(&self, arg: &str) -> bool;
|
||||
|
||||
fn matches(&self) -> Keys<String, String>;
|
||||
}
|
||||
|
||||
impl Match for Matches {
|
||||
fn get(&self, arg: &str) -> Option<&String> {
|
||||
self.get(arg)
|
||||
}
|
||||
|
||||
fn has_match(&self, arg: &str) -> bool {
|
||||
let arg = String::from(arg);
|
||||
self.contains_key(&arg)
|
||||
}
|
||||
|
||||
fn matches(&self) -> Keys<String, String> {
|
||||
self.keys()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Match, matches};
|
||||
use super::super::vars::vars;
|
||||
|
||||
#[test]
|
||||
fn test_matches_good() {
|
||||
let env_args = vec![String::from("test"), String::from("-a"), String::from("Test")];
|
||||
let opts = vec!["o/opt#An option", "a#An argument:"];
|
||||
|
||||
let mut var = match vars("Test", &opts) {
|
||||
Ok(m) => m,
|
||||
Err(why) => panic!("An error occurred: {}", why)
|
||||
};
|
||||
|
||||
let matches = matches(&env_args, &mut var).unwrap();
|
||||
|
||||
let has_opt = match matches.get("opt") {
|
||||
Some(_) => true,
|
||||
None => false
|
||||
};
|
||||
let argument = matches.get("a").unwrap();
|
||||
assert_eq!(*argument, String::from("Test"));
|
||||
assert_eq!(has_opt, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_matches_bad() {
|
||||
let env_args = vec![String::from("test"), String::from("-a")];
|
||||
let opts = vec!["o/opt#An option", "a#An argument:"];
|
||||
|
||||
let mut vars = vars("Test", &opts).unwrap();
|
||||
match matches(&env_args, &mut vars) {
|
||||
Ok(m) => m,
|
||||
Err(why) => panic!("An error occurred: {}", why)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
299
src/token.rs
Normal file
299
src/token.rs
Normal file
|
@ -0,0 +1,299 @@
|
|||
/* Pirate - A command-line arrrrguments parser, written in Rust.
|
||||
* Copyright (C) 2015 Zachary Dziura
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use errors::{Error, ErrorKind};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Token {
|
||||
pub short_name: String,
|
||||
pub long_name: String,
|
||||
pub description: String,
|
||||
pub is_arg: bool,
|
||||
pub has_arg: bool,
|
||||
pub is_group: bool,
|
||||
pub padding: usize
|
||||
}
|
||||
|
||||
pub fn token(input: &str) -> Result<Token, Error> {
|
||||
let mut short_name = String::new();
|
||||
let mut long_name = String::new();
|
||||
let mut description = String::new();
|
||||
let last_char = input.len() - 1;
|
||||
|
||||
let is_arg = match &input[..1] {
|
||||
":" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
let has_arg = match &input[last_char..] {
|
||||
":" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
if is_arg && has_arg {
|
||||
return Err(Error::new(ErrorKind::TokenFormat, String::from(input)));
|
||||
}
|
||||
|
||||
let option = if is_arg {
|
||||
&input[1..]
|
||||
} else if has_arg {
|
||||
&input[..last_char]
|
||||
} else {
|
||||
input
|
||||
};
|
||||
|
||||
let mut current_stage = AnalysisStage::ShortName;
|
||||
for c in option.chars() {
|
||||
match c {
|
||||
'/' => current_stage = AnalysisStage::LongName,
|
||||
'#' => current_stage = AnalysisStage::Description,
|
||||
_ => {
|
||||
match current_stage {
|
||||
AnalysisStage::ShortName => short_name.push(c),
|
||||
AnalysisStage::LongName => long_name.push(c),
|
||||
AnalysisStage::Description => description.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_group = if short_name.is_empty() && long_name.is_empty() {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(Token {
|
||||
short_name: short_name,
|
||||
long_name: long_name,
|
||||
is_arg: is_arg,
|
||||
has_arg: has_arg,
|
||||
is_group: is_group,
|
||||
description: description,
|
||||
padding: 0
|
||||
})
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn adjust_padding(&mut self, padding: usize) {
|
||||
self.padding = padding;
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let short_name_empty = self.short_name.is_empty();
|
||||
let long_name_empty = self.long_name.is_empty();
|
||||
|
||||
let repr = if !short_name_empty && !long_name_empty {
|
||||
format!("-{}, --{}", self.short_name, self.long_name)
|
||||
} else if !short_name_empty && long_name_empty {
|
||||
format!("-{}", self.short_name)
|
||||
} else if short_name_empty && !long_name_empty {
|
||||
format!("--{}", self.long_name)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
repr.len()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
if !self.long_name.is_empty() {
|
||||
self.long_name.clone()
|
||||
} else if !self.short_name.is_empty() {
|
||||
self.short_name.clone()
|
||||
} else {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn usage(&self) -> Option<String> {
|
||||
let mut repr = String::new();
|
||||
|
||||
if !self.is_group {
|
||||
if !self.is_arg {
|
||||
repr.push('[');
|
||||
|
||||
if !self.short_name.is_empty() {
|
||||
repr.push('-');
|
||||
repr.push_str(&self.short_name);
|
||||
}
|
||||
|
||||
if !self.long_name.is_empty() {
|
||||
repr.push_str("|--");
|
||||
repr.push_str(&self.long_name);
|
||||
}
|
||||
|
||||
if self.has_arg {
|
||||
let name = String::from(self.name());
|
||||
repr.push(' ');
|
||||
repr.push_str(&name);
|
||||
}
|
||||
|
||||
repr.push(']');
|
||||
} else {
|
||||
let name = String::from(self.name());
|
||||
repr.push_str(&name);
|
||||
}
|
||||
|
||||
Some(repr)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Token {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let mut spacing = String::new();
|
||||
for _ in 0..self.padding {
|
||||
spacing.push(' ');
|
||||
}
|
||||
|
||||
let short_name_empty = self.short_name.is_empty();
|
||||
let long_name_empty = self.long_name.is_empty();
|
||||
|
||||
let repr = if self.is_group {
|
||||
format!("\n{}:", self.description)
|
||||
} else if !short_name_empty && !long_name_empty {
|
||||
format!(" -{}, --{}{} {}", self.short_name, self.long_name, spacing, self.description)
|
||||
} else if short_name_empty && !long_name_empty {
|
||||
format!(" --{}{} {}", self.long_name, spacing, self.description)
|
||||
} else {
|
||||
format!(" -{}{} {}", self.short_name, spacing, self.description)
|
||||
};
|
||||
|
||||
write!(f, "{}", repr)
|
||||
}
|
||||
}
|
||||
|
||||
enum AnalysisStage {
|
||||
ShortName,
|
||||
LongName,
|
||||
Description
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Token, token};
|
||||
|
||||
#[test]
|
||||
fn test_new_token() {
|
||||
let opt = "h/help#Display the program usage";
|
||||
let token = match token(opt) {
|
||||
Ok(t) => t,
|
||||
Err(why) => panic!("Received error: {}", why)
|
||||
};
|
||||
let control_token = Token {
|
||||
short_name: String::from("h"),
|
||||
long_name: String::from("help"),
|
||||
description: String::from("Display the program usage"),
|
||||
is_arg: false,
|
||||
has_arg: false,
|
||||
is_group: false,
|
||||
padding: 0
|
||||
};
|
||||
|
||||
assert_eq!(token, control_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_group() {
|
||||
let opt = "#This is a group";
|
||||
let token = match token(opt) {
|
||||
Ok(t) => t,
|
||||
Err(why) => panic!("Received error: {}", why)
|
||||
};
|
||||
let control_token = Token {
|
||||
short_name: String::new(),
|
||||
long_name: String::new(),
|
||||
description: String::from("This is a group"),
|
||||
is_arg: false,
|
||||
has_arg: false,
|
||||
is_group: true,
|
||||
padding: 0
|
||||
};
|
||||
|
||||
assert_eq!(token, control_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_token_with_arg() {
|
||||
let opt = "o/option#An option with an argument:";
|
||||
let token = match token(opt) {
|
||||
Ok(t) => t,
|
||||
Err(why) => panic!("Received error: {}", why)
|
||||
};
|
||||
let control_token = Token {
|
||||
short_name: String::from("o"),
|
||||
long_name: String::from("option"),
|
||||
description: String::from("An option with an argument"),
|
||||
is_arg: false,
|
||||
has_arg: true,
|
||||
is_group: false,
|
||||
padding: 0
|
||||
};
|
||||
|
||||
assert_eq!(token, control_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_token_as_arg() {
|
||||
let opt = ":a/arg#An argument";
|
||||
let token = match token(opt) {
|
||||
Ok(t) => t,
|
||||
Err(why) => panic!("Received error: {}", why)
|
||||
};
|
||||
let control_token = Token {
|
||||
short_name: String::from("a"),
|
||||
long_name: String::from("arg"),
|
||||
description: String::from("An argument"),
|
||||
is_arg: true,
|
||||
has_arg: false,
|
||||
is_group: false,
|
||||
padding: 0
|
||||
};
|
||||
|
||||
assert_eq!(token, control_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_token_format() {
|
||||
let input = ":w/wrong#Wrong format:";
|
||||
match token(input) {
|
||||
Ok(t) => t,
|
||||
Err(why) => panic!("Received error: {}", why)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name() {
|
||||
let short_name = "o";
|
||||
let long_name = "/out";
|
||||
let group = "#Output";
|
||||
|
||||
let short_token = token(short_name).unwrap();
|
||||
let long_token = token(long_name).unwrap();
|
||||
let group_token = token(group).unwrap();
|
||||
|
||||
assert_eq!(short_token.name(), "o");
|
||||
assert_eq!(long_token.name(), "out");
|
||||
assert_eq!(group_token.name(), "Output");
|
||||
}
|
||||
}
|
|
@ -15,39 +15,19 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use vars::Vars;
|
||||
|
||||
pub struct Opts {
|
||||
pub opts: HashMap<String, bool>,
|
||||
pub args: VecDeque<String>,
|
||||
}
|
||||
pub fn usage(vars: &Vars) {
|
||||
print!("Usage: {} ", vars.program_name);
|
||||
|
||||
impl Opts {
|
||||
pub fn new() -> Opts {
|
||||
Opts {
|
||||
opts: HashMap::new(),
|
||||
args: VecDeque::new()
|
||||
for token in vars.tokens() {
|
||||
if let Some(usage) = token.usage() {
|
||||
print!("{} ", usage);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_opt(&mut self, key: &str, value: bool) {
|
||||
self.opts.insert(String::from(key), value);
|
||||
}
|
||||
|
||||
pub fn get_opt(&self, opt_name: &String) -> Option<&bool> {
|
||||
self.opts.get(opt_name)
|
||||
}
|
||||
|
||||
pub fn contains_opt(&self, opt: &String) -> bool {
|
||||
self.opts.contains_key(opt)
|
||||
}
|
||||
|
||||
pub fn insert_arg(&mut self, arg: &str) {
|
||||
self.args.push_back(String::from(arg));
|
||||
}
|
||||
|
||||
pub fn get_arg(&mut self) -> Option<String> {
|
||||
self.args.pop_front()
|
||||
|
||||
println!("\nOptions:");
|
||||
for token in vars.tokens() {
|
||||
println!("{}", token);
|
||||
}
|
||||
}
|
126
src/vars.rs
Normal file
126
src/vars.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
/* Pirate - A command-line arrrrguments parser, written in Rust.
|
||||
* Copyright (C) 2015 Zachary Dziura
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::slice::Iter;
|
||||
|
||||
use errors::Error;
|
||||
use token::{Token, token};
|
||||
|
||||
pub struct Vars {
|
||||
tokens: Vec<Token>,
|
||||
opts: HashMap<String, usize>,
|
||||
args: VecDeque<usize>,
|
||||
pub program_name: String
|
||||
}
|
||||
|
||||
pub fn vars(program_name: &str, options: &[&str]) -> Result<Vars, Error> {
|
||||
let mut tokens: Vec<Token> = Vec::new();
|
||||
let mut opts: HashMap<String, usize> = HashMap::new();
|
||||
let mut args: VecDeque<usize> = VecDeque::new();
|
||||
let mut index: usize = 0;
|
||||
|
||||
// The first option should be the help option, i.e. -h, --help
|
||||
let help_token = Token {
|
||||
short_name: String::from("h"),
|
||||
long_name: String::from("help"),
|
||||
description: String::from("Display usage information"),
|
||||
is_arg: false,
|
||||
has_arg: false,
|
||||
is_group: false,
|
||||
padding: 0
|
||||
};
|
||||
opts.insert(help_token.short_name.clone(), index);
|
||||
opts.insert(help_token.long_name.clone(), index);
|
||||
let mut longest_token_len: usize = help_token.len();
|
||||
tokens.push(help_token);
|
||||
index += 1;
|
||||
|
||||
// Second, add the other, user defined options
|
||||
for opt in options.iter() {
|
||||
let token = match token(opt) {
|
||||
Ok(t) => t,
|
||||
Err(why) => return Err(why)
|
||||
};
|
||||
|
||||
if !token.is_group {
|
||||
if token.is_arg {
|
||||
args.push_back(index);
|
||||
} else {
|
||||
if !token.short_name.is_empty() {
|
||||
opts.insert(token.short_name.clone(), index);
|
||||
}
|
||||
|
||||
if !token.long_name.is_empty() {
|
||||
opts.insert(token.long_name.clone(), index);
|
||||
}
|
||||
}
|
||||
|
||||
if token.len() > longest_token_len {
|
||||
longest_token_len = token.len();
|
||||
}
|
||||
}
|
||||
tokens.push(token);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// Finally, adjust the padding for each token so they display properly
|
||||
for t in tokens.iter_mut() {
|
||||
let token_len = t.len();
|
||||
if token_len < longest_token_len {
|
||||
let delta = longest_token_len - token_len;
|
||||
t.adjust_padding(delta);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Vars {
|
||||
opts: opts,
|
||||
args: args,
|
||||
tokens: tokens,
|
||||
program_name: String::from(program_name)
|
||||
})
|
||||
}
|
||||
|
||||
impl Vars {
|
||||
pub fn get_opt(&self, opt_name: &str) -> Option<&Token> {
|
||||
if let Some(&index) = self.opts.get(opt_name) {
|
||||
self.tokens.get(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_opt(&self, opt: &str) -> bool {
|
||||
self.opts.contains_key(opt)
|
||||
}
|
||||
|
||||
pub fn get_arg(&mut self) -> Option<&Token> {
|
||||
if let Some(index) = self.args.pop_front() {
|
||||
self.tokens.get(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg_len(&self) -> usize {
|
||||
self.args.len()
|
||||
}
|
||||
|
||||
pub fn tokens(&self) -> Iter<Token>{
|
||||
self.tokens.iter()
|
||||
}
|
||||
}
|
|
@ -1,49 +1,39 @@
|
|||
extern crate pirate;
|
||||
|
||||
use std::env;
|
||||
|
||||
use pirate::Matches;
|
||||
use pirate::{Matches, Match, matches, usage, vars};
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
let opts = &["n:", "b/boop", ":input"];
|
||||
|
||||
let matches: Matches = match pirate::parse(env::args(), opts) {
|
||||
Err(ref e) => {
|
||||
println!("Error: {}", e);
|
||||
help();
|
||||
let env_args: Vec<String> = vec![String::from("test"), String::from("-a"), String::from("2"),
|
||||
String::from("3")];
|
||||
let opts = vec!["a/addend#The right side of the addition equation; default=1:", "#Required Arguments",
|
||||
":augend#The left side of an addition equation"];
|
||||
let mut vars = vars("test", &opts).unwrap();
|
||||
|
||||
let matches: Matches = match matches(&env_args, &mut vars) {
|
||||
Ok(m) => m,
|
||||
Err(why) => {
|
||||
println!("Error: {}", why);
|
||||
usage(&vars);
|
||||
return;
|
||||
},
|
||||
Ok(m) => m
|
||||
}
|
||||
};
|
||||
|
||||
// Print the program help if necessary
|
||||
if matches.has_arg("h") || matches.has_arg("help") {
|
||||
help();
|
||||
|
||||
if matches.has_match("help") {
|
||||
usage(&vars);
|
||||
return;
|
||||
}
|
||||
|
||||
let augend: i32 = matches.get("augend").unwrap().parse::<i32>().unwrap();
|
||||
|
||||
let input = matches.get("input").unwrap().parse::<i32>().unwrap();
|
||||
let num = match matches.get("n") {
|
||||
Some(n) => n.parse::<i32>().unwrap(),
|
||||
let addend: i32 = match matches.get("addend") {
|
||||
Some(a) => a.parse::<i32>().unwrap(),
|
||||
None => 1
|
||||
};
|
||||
|
||||
let sum = input + num;
|
||||
|
||||
println!("{} + {} = {}", input, num, sum);
|
||||
|
||||
if matches.has_arg("b") || matches.has_arg("boop") {
|
||||
println!("Boop!!");
|
||||
}
|
||||
}
|
||||
|
||||
fn help() {
|
||||
println!("usage: pirate-test [-n NUM] [-b|--boop] INPUT\n");
|
||||
|
||||
println!("Options:");
|
||||
println!(" -n NUM\tChange the default number that's added to the input");
|
||||
println!(" -b, --boop\tIt's a surprise!");
|
||||
|
||||
println!("\nRequired arguments:");
|
||||
println!(" INPUT\tWe're gonna manipulate this somehow, you'll see!");
|
||||
|
||||
let sum = augend + addend;
|
||||
|
||||
assert_eq!(augend, 3);
|
||||
assert_eq!(addend, 2);
|
||||
assert_eq!(sum, 5);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue