Compare commits

...

26 commits

Author SHA1 Message Date
Zach Dziura
831f65b27e Merge pull request #4 from joelself/master
Updated `env::args().collect()` to output a Vec
2016-05-17 23:52:02 -04:00
Joel
3563377a5e Updated env::args().collect() to output a Vec
It seems `collect` defaults to collecting into an array which doesn't work with a collection of `String`s because `String`s vary in size and their size cannot be known at compile time. To work around this I collect the args into a `Vec` instead and pass that in. Also I think the first call to `matches` was missing an `&` on the `args` parameter.
2016-05-17 11:39:24 -06:00
Zach Dziura
426c0c9ef7 Fixed formatting and spelling errors in README 2015-06-30 01:36:53 +00:00
Zach Dziura
1c75b5354e Version 1.0 2015-06-29 07:55:53 -04:00
Zach Dziura
14b5cad8c1 Finished test cases for matches 2015-06-24 17:24:06 -04:00
Zach Dziura
e3d1176fa6 Fixed broken tests 2015-06-24 17:02:16 -04:00
Zach Dziura
cc6f53979a Fixing some of the failing test cases
Still more to finish, however...
2015-06-24 16:04:50 -04:00
Zach Dziura
75c277db3a Fixed usage display formatting 2015-06-23 11:09:56 -04:00
Zach Dziura
77f65c79c2 Fixed bug where the correct usage format wasn't being used.
TODO: Fix a spacing issue with the usage display.
2015-06-23 12:00:50 +00:00
Zach Dziura
3c9dae504b Matches is now a Type Alias, rather than its own struct
Since the original Matches structure was nothing more than a wrapper around a HashMap<String,
String>, the code has been updated to simply be a type alias to that type. Makes things easier.
2015-06-23 03:31:02 +00:00
Zach Dziura
632850c58c Finally (FINALLY!) finished the Token integration 2015-06-16 14:28:14 -04:00
Zach Dziura
ee56d7c081 Added extra Token methods 2015-06-11 16:59:41 -04:00
Zach Dziura
87675e409a Creating test cases for each module 2015-06-10 18:14:11 +00:00
Zach Dziura
0bab8dc9ca Finished the Tokens refactor
Now I just need to test the damn thing...
2015-06-09 16:50:16 -04:00
Zach Dziura
d1de4d9431 Tokens are now in Opts 2015-06-08 16:26:12 -04:00
Zach Dziura
c0d0a99c25 Begin bringing Tokens into Opt
I've made an executive decision: Opt will be in charge of displaying the
usage (help) information for the program. That will involve bringing the
functionality delivered by Token into Opt. I've begun that process now.
2015-06-04 15:37:59 -04:00
Zach Dziura
346c17c517 Actually finished lexer integration
There were some previous bugs in the original lexer implementation that had to
be fixed, along with some extraneous code that had to be removed. Luckily,
that's all been taken care of!
2015-05-28 21:04:00 +00:00
Zach Dziura
2ba09a17b6 Fixed a few outstanding integration errors 2015-05-28 09:19:19 -04:00
Zach Dziura
ad4a8ee4b3 Integrated the lexer into the existing workflow
Next up, generate the help-generator functions!
2015-05-27 15:49:50 -04:00
Zach Dziura
97a0d856b2 Finished lexer stub
Still need to test...
2015-05-27 14:21:03 -04:00
Zach Dziura
2423cd7d19 Implemented lexer stub 2015-05-26 15:53:47 -04:00
Zach Dziura
be28b0307c Continuous Integration now tests on all 3 rustc channels 2015-05-25 11:48:01 -04:00
Zach Dziura
297aaa0ad3 Updated examples with better example
Now we'll encourage users to create an actual vector, instead of an array slice.
It's better form to do so, anyways.
2015-05-25 11:14:13 -04:00
Zach Dziura
29077d64d6 Merge pull request #1 from filsmick/master
The code example in README.md used `has_match`, which doesn't exist, instead of `has_arg`
2015-05-23 14:49:28 -04:00
filsmick
fb603d35ab Code example used has_match, which doesn't exist, instead of has_arg 2015-05-23 10:45:57 +02:00
Zach Dziura
1b71c6c876 Updated "Installation" section with correct version 2015-04-30 18:06:49 +00:00
11 changed files with 713 additions and 312 deletions

View file

@ -1 +1,5 @@
language: rust
rust:
- nightly
- beta
- stable

View file

@ -1,7 +1,7 @@
[package]
name = "pirate"
version = "0.2.0"
version = "1.0.0"
authors = ["Zach Dziura <zcdziura@gmail.com>"]
description = "A simple arrrguments parser"
repository = "https://github.com/zcdziura/pirate"

187
README.md
View file

@ -6,28 +6,20 @@ 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 call
different functions repeatedly in order to add different command-line options to their programs. 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:
@ -39,50 +31,76 @@ 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
-------
@ -91,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 generating `--help` output, rather than having the user create it
manually.
- [ ] Also create helper functions for defining the description section of the `--help` output.
- [x] 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
-------

View file

@ -22,7 +22,7 @@ use std::fmt::{Display, Formatter, Result};
pub struct Error {
kind: ErrorKind,
offender: String,
desc: String,
desc: String
}
impl Error {
@ -49,15 +49,17 @@ impl Display for Error {
#[derive(Clone, Debug)]
pub enum ErrorKind {
InvalidOption,
InvalidArgument,
MissingArgument,
TokenFormat
}
impl ErrorKind {
fn description(&self) -> String {
match *self {
ErrorKind::InvalidOption => String::from("An invalid option was passed to the program:"),
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:")
}
}
}

View file

@ -15,104 +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::{Error, ErrorKind};
pub use matches::Matches;
use opts::Opts;
pub fn parse(mut args: Args, options: &[&'static str]) -> Result<Matches, Error> {
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 &current_arg[..1] == "-" { // Probably a opt
if current_arg.len() == 2 { // Short form opt
arg = String::from(&current_arg[1..]);
} else { // Assuming it's a long form opt
//TODO: Handle cases where it may be a opt group
arg = String::from(&current_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(Error::new(ErrorKind::MissingArgument, arg));
},
Some(a) => a
};
matches.insert(&arg, &current_arg);
} else {
matches.insert(&arg, "");
}
} else {
return Err(Error::new(ErrorKind::InvalidOption, arg));
}
} else { // Probably a required arg
let arg_name: String = opts.get_arg().unwrap();
matches.insert(&arg_name, &current_arg);
}
next_arg = args.next();
}
match opts.arg_len() {
0 => Ok(matches),
_ => Err(Error::new(ErrorKind::MissingArgument, opts.get_arg().unwrap())),
}
}
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;

View file

@ -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 &current_arg[..1] == "-" {
if &current_arg[..2] == "--" { // Long form opt
arg_vec.push(String::from(&current_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)
};
}
}

View file

@ -1,57 +0,0 @@
/* 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;
use std::collections::VecDeque;
pub struct Opts {
pub opts: HashMap<String, bool>,
pub args: VecDeque<String>,
}
impl Opts {
pub fn new() -> Opts {
Opts {
opts: HashMap::new(),
args: VecDeque::new()
}
}
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()
}
pub fn arg_len(&self) -> usize {
self.args.len()
}
}

299
src/token.rs Normal file
View 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");
}
}

33
src/usage.rs Normal file
View file

@ -0,0 +1,33 @@
/* 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 vars::Vars;
pub fn usage(vars: &Vars) {
print!("Usage: {} ", vars.program_name);
for token in vars.tokens() {
if let Some(usage) = token.usage() {
print!("{} ", usage);
}
}
println!("\nOptions:");
for token in vars.tokens() {
println!("{}", token);
}
}

126
src/vars.rs Normal file
View 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()
}
}

View file

@ -1,50 +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);
}