Auto merge of #449 - kbknapp:issue-428-redux, r=kbknapp

Issue 428 redux
This commit is contained in:
Homu 2016-03-15 20:48:13 +09:00
commit f6d1685000
20 changed files with 486 additions and 211 deletions

View file

@ -1,3 +1,24 @@
<a name="v2.2.0"></a>
## v2.2.0 (2016-03-14)
#### Documentation
* **Groups:** explains required ArgGroups better ([4ff0205b](https://github.com/kbknapp/clap-rs/commit/4ff0205b85a45151b59bbaf090a89df13438380f), closes [#439](https://github.com/kbknapp/clap-rs/issues/439))
#### Features
* **Help Message:** can auto wrap and aligning help text to term width ([e36af026](https://github.com/kbknapp/clap-rs/commit/e36af0266635f23e85e951b9088d561e9a5d1bf6), closes [#428](https://github.com/kbknapp/clap-rs/issues/428))
* **Opts and Flags:** adds support for custom ordering in help messages ([9803b51e](https://github.com/kbknapp/clap-rs/commit/9803b51e799904c0befaac457418ee766ccc1ab9))
* **Settings:** adds support for automatically deriving custom display order of args ([ad86e433](https://github.com/kbknapp/clap-rs/commit/ad86e43334c4f70e86909689a088fb87e26ff95a), closes [#444](https://github.com/kbknapp/clap-rs/issues/444))
* **Subcommands:** adds support for custom ordering in help messages ([7d2a2ed4](https://github.com/kbknapp/clap-rs/commit/7d2a2ed413f5517d45988eef0765cdcd663b6372), closes [#442](https://github.com/kbknapp/clap-rs/issues/442))
#### Bug Fixes
* **From Usage:** fixes a bug where adding empty lines werent ignored ([c5c58c86](https://github.com/kbknapp/clap-rs/commit/c5c58c86b9c503d8de19da356a5a5cffb59fbe84))
<a name="v2.1.2"></a>
### v2.1.2 (2016-02-24)

View file

@ -1,7 +1,7 @@
[package]
name = "clap"
version = "2.1.2"
version = "2.2.0"
authors = ["Kevin K. <kbknapp@gmail.com>"]
exclude = ["examples/*", "clap-tests/*", "tests/*", "benches/*", "*.png", "clap-perf/*"]
description = "A simple to use, efficient, and full featured Command Line Argument Parser"
@ -14,16 +14,18 @@ keywords = ["argument", "command", "arg", "parser", "parse"]
[dependencies]
bitflags = "~0.4"
vec_map = "~0.6"
libc = { version = "~0.2.8", optional = true }
ansi_term = { version = "~0.7.2", optional = true }
strsim = { version = "~0.4.0", optional = true }
yaml-rust = { version = "~0.3", optional = true }
clippy = { version = "~0.0.48", optional = true }
[features]
default = ["suggestions", "color"]
default = ["suggestions", "color", "wrap_help"]
suggestions = ["strsim"]
color = ["ansi_term"]
yaml = ["yaml-rust"]
wrap_help = ["libc"]
lints = ["clippy", "nightly"]
nightly = [] # for building with nightly and unstable features
unstable = [] # for building with unstable features on stable Rust

View file

@ -38,7 +38,20 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
## What's New
In v2.1.1
Here's the highlights from v2.2.0
#### Features
* **Help text auto wraps and aligns at term width!** - Long help strings will now properly wrap and align to term width on Linux and OSX (and resumably Unix too). This can be turned off as well.
* **Can customize the order of opts, flags, and subcommands in help messages** - Instead of using the default alphabetical order, you can now re-arange the order of your args and subcommands in help message. This helps to emphasize more popular or important options.
* **Can auto-derive the order from declaration order** - Have a bunch of args or subcommmands to re-order? You can now just derive the order from the declaration order!
* Other minor bug fixes
An example of the help text wrapping at term width:
![screenshot](http://i.imgur.com/PAJzJJG.png)
In v2.1.2
#### New Features
@ -47,7 +60,7 @@ In v2.1.1
#### Improvements
* **Documenation Examples**: The examples in the documentation have been vastly improved
* **Documentation Examples**: The examples in the documentation have been vastly improved
For full details, see [CHANGELOG.md](https://github.com/kbknapp/clap-rs/blob/master/CHANGELOG.md)
@ -433,14 +446,14 @@ default-features = false
# Cherry-pick the features you'd like to use
features = [ "suggestions", "color" ]
```
The following is a list of optional `clap` features:
* **"suggestions"**: Turns on the `Did you mean '--myoption' ?` feature for when users make typos.
* **"color"**: Turns on colored error messages. This feature only works on non-Windows OSs.
* **"lints"**: This is **not** included by default and should only be used while developing to run basic lints against changes. This can only be used on Rust nightly.
* **"suggestions"**: Turns on the `Did you mean '--myoption' ?` feature for when users make typos. (builds dependency `strsim`)
* **"color"**: Turns on colored error messages. This feature only works on non-Windows OSs. (builds dependency `ansi-term`)
* **"wrap_help"**: Automatically detects terminal width and wraps long help text lines with proper indentation alignment (builds dependency `libc`)
* **"lints"**: This is **not** included by default and should only be used while developing to run basic lints against changes. This can only be used on Rust nightly. (builds dependency `clippy`)
* **"debug"**: This is **not** included by default and should only be used while developing to display debugging information.
* **"yaml"**: This is **not** included by default. Enables building CLIs from YAML documents.
* **"yaml"**: This is **not** included by default. Enables building CLIs from YAML documents. (builds dependency `yaml-rust`)
* **"unstable"**: This is **not** included by default. Enables unstable features, unstable refers to whether or not they may change, not performance stability.
### Dependencies Tree
@ -472,7 +485,7 @@ Contributions are always welcome! And there is a multitude of ways in which you
Another really great way to help is if you find an interesting, or helpful way in which to use `clap`. You can either add it to the [examples/](examples) directory, or file an issue and tell me. I'm all about giving credit where credit is due :)
Please read [CONTRIBUTING.md](CONTRIBUTING.md) before you start contributing.
Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start contributing.
### Running the tests
@ -553,6 +566,6 @@ As of 2.0.0 (From 1.x)
Old method names will be left around for several minor version bumps, or one major version bump.
As of 2.1.1:
As of 2.2.0:
* None!

View file

@ -13,7 +13,7 @@ Kevin K. <kbknapp@gmail.com>
tests clap library
USAGE:
\tclaptests [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
claptests [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
FLAGS:
-f, --flag tests flags
@ -47,7 +47,7 @@ _sc_dym_usage = '''error: The subcommand 'subcm' wasn't recognized
If you believe you received this message in error, try re-running with 'claptests -- subcm'
USAGE:
\tclaptests [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
claptests [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
For more information try --help'''
@ -55,7 +55,7 @@ _arg_dym_usage = '''error: Found argument '--optio' which wasn't expected, or is
\tDid you mean --option ?
USAGE:
\tclaptests --option <opt>...
claptests --option <opt>...
For more information try --help'''
@ -65,30 +65,30 @@ _pv_dym_usage = '''error: 'slo' isn't a valid value for '--Option <option3>'
Did you mean 'slow' ?
USAGE:
\tclaptests --Option <option3>
claptests --Option <option3>
For more information try --help'''
_excluded = '''error: The argument '--flag' cannot be used with '-F'
USAGE:
\tclaptests [positional2] -F --long-option-2 <option2>
claptests [positional2] -F --long-option-2 <option2>
For more information try --help'''
_excluded_l = '''error: The argument '-f' cannot be used with '-F'
USAGE:
\tclaptests [positional2] -F --long-option-2 <option2>
claptests [positional2] -F --long-option-2 <option2>
For more information try --help'''
_required = '''error: The following required arguments were not provided:
\t[positional2]
\t--long-option-2 <option2>
[positional2]
--long-option-2 <option2>
USAGE:
\tclaptests [positional2] -F --long-option-2 <option2>
claptests [positional2] -F --long-option-2 <option2>
For more information try --help'''
@ -141,7 +141,7 @@ Kevin K. <kbknapp@gmail.com>
tests subcommands
USAGE:
\tclaptests subcmd [FLAGS] [OPTIONS] [--] [ARGS]
claptests subcmd [FLAGS] [OPTIONS] [--] [ARGS]
FLAGS:
-f, --flag tests flags

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -1235,7 +1235,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
&*self.get_required_from(&*self.required.iter().map(|&r| &*r).collect::<Vec<_>>(), Some(matcher))
.iter()
.fold(String::new(),
|acc, s| acc + &format!("\n\t{}", Format::Error(s))[..]),
|acc, s| acc + &format!("\n {}", Format::Error(s))[..]),
&*self.create_current_usage(matcher))
};
return Err(err);
@ -1290,7 +1290,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
fn create_usage(&self, used: &[&str]) -> String {
debugln!("fn=create_usage;");
let mut usage = String::with_capacity(75);
usage.push_str("USAGE:\n\t");
usage.push_str("USAGE:\n ");
if let Some(u) = self.meta.usage_str {
usage.push_str(&*u);
} else if used.is_empty() {
@ -1448,7 +1448,6 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
try!(write!(w, "\n"));
}
let tab = " ";
let longest = if !unified_help || longest_opt == 0 {
longest_flag
} else {
@ -1461,13 +1460,13 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
for f in self.flags.iter().filter(|f| !f.settings.is_set(ArgSettings::Hidden)) {
let btm = ord_m.entry(f.disp_ord).or_insert(BTreeMap::new());
let mut v = vec![];
try!(f.write_help(&mut v, tab, longest, nlh));
try!(f.write_help(&mut v, longest, nlh));
btm.insert(f.name, v);
}
for o in self.opts.iter().filter(|o| !o.settings.is_set(ArgSettings::Hidden)) {
let btm = ord_m.entry(o.disp_ord).or_insert(BTreeMap::new());
let mut v = vec![];
try!(o.write_help(&mut v, tab, longest, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
try!(o.write_help(&mut v, longest, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
btm.insert(o.name, v);
}
for (_, btm) in ord_m.into_iter() {
@ -1486,7 +1485,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
for (_, btm) in ord_m.into_iter() {
for (_, f) in btm.into_iter() {
try!(f.write_help(w, tab, longest, nlh));
try!(f.write_help(w, longest, nlh));
}
}
}
@ -1499,7 +1498,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
for (_, btm) in ord_m.into_iter() {
for (_, o) in btm.into_iter() {
try!(o.write_help(w, tab, longest_opt, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
try!(o.write_help(w, longest_opt, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
}
}
}
@ -1508,7 +1507,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
try!(write!(w, "\nARGS:\n"));
for v in self.positionals.values()
.filter(|p| !p.settings.is_set(ArgSettings::Hidden)) {
try!(v.write_help(w, tab, longest_pos, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
try!(v.write_help(w, longest_pos, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
}
}
if subcmds {
@ -1520,7 +1519,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
for (_, btm) in ord_m.into_iter() {
for (name, sc) in btm.into_iter() {
try!(write!(w, "{}{}", tab, name));
try!(write!(w, " {}", name));
write_spaces!((longest_sc + 4) - (name.len()), w);
if let Some(a) = sc.p.meta.about {
if a.contains("{n}") {

View file

@ -476,3 +476,29 @@ impl FromStr for AppSettings {
}
}
}
#[cfg(test)]
mod test {
use super::AppSettings;
#[test]
fn app_settings_fromstr() {
assert_eq!("subcommandsnegatereqs".parse::<AppSettings>().unwrap(), AppSettings::SubcommandsNegateReqs);
assert_eq!("subcommandsrequired".parse::<AppSettings>().unwrap(), AppSettings::SubcommandRequired);
assert_eq!("argrequiredelsehelp".parse::<AppSettings>().unwrap(), AppSettings::ArgRequiredElseHelp);
assert_eq!("globalversion".parse::<AppSettings>().unwrap(), AppSettings::GlobalVersion);
assert_eq!("versionlesssubcommands".parse::<AppSettings>().unwrap(), AppSettings::VersionlessSubcommands);
assert_eq!("unifiedhelpmessage".parse::<AppSettings>().unwrap(), AppSettings::UnifiedHelpMessage);
assert_eq!("waitonerror".parse::<AppSettings>().unwrap(), AppSettings::WaitOnError);
assert_eq!("subcommandrequiredelsehelp".parse::<AppSettings>().unwrap(), AppSettings::SubcommandRequiredElseHelp);
assert_eq!("allowexternalsubcommands".parse::<AppSettings>().unwrap(), AppSettings::AllowExternalSubcommands);
assert_eq!("trailingvararg".parse::<AppSettings>().unwrap(), AppSettings::TrailingVarArg);
assert_eq!("nobinaryname".parse::<AppSettings>().unwrap(), AppSettings::NoBinaryName);
assert_eq!("strictutf8".parse::<AppSettings>().unwrap(), AppSettings::StrictUtf8);
assert_eq!("allowinvalidutf8".parse::<AppSettings>().unwrap(), AppSettings::AllowInvalidUtf8);
assert_eq!("allowleadinghyphen".parse::<AppSettings>().unwrap(), AppSettings::AllowLeadingHyphen);
assert_eq!("hidepossiblevaluesinhelp".parse::<AppSettings>().unwrap(), AppSettings::HidePossibleValuesInHelp);
assert_eq!("hidden".parse::<AppSettings>().unwrap(), AppSettings::Hidden);
assert!("hahahaha".parse::<AppSettings>().is_err());
}
}

View file

@ -1,6 +1,8 @@
use std::rc::Rc;
use std::fmt::Display;
use vec_map::VecMap;
use args::settings::ArgSettings;
#[doc(hidden)]
@ -20,4 +22,8 @@ pub trait AnyArg<'n, 'e>: Display {
fn short(&self) -> Option<char>;
fn long(&self) -> Option<&'e str>;
fn val_delim(&self) -> Option<char>;
fn takes_value(&self) -> bool;
fn val_names(&self) -> Option<&VecMap<&'e str>>;
fn help(&self) -> Option<&'e str>;
fn default_val(&self) -> Option<&'n str>;
}

View file

@ -5,8 +5,10 @@ use std::io;
use std::rc::Rc;
use std::result::Result as StdResult;
use vec_map::VecMap;
use Arg;
use args::AnyArg;
use args::{AnyArg, HelpWriter};
use args::settings::{ArgFlags, ArgSettings};
#[derive(Debug)]
@ -47,9 +49,9 @@ impl<'n, 'e> FlagBuilder<'n, 'e> {
}
}
pub fn write_help<W: io::Write>(&self, w: &mut W, tab: &str, longest: usize, nlh: bool) -> io::Result<()> {
write_arg_help!(@flag self, w, tab, longest, nlh);
write!(w, "\n")
pub fn write_help<W: io::Write>(&self, w: &mut W, longest: usize, nlh: bool) -> io::Result<()> {
let hw = HelpWriter::new(self, longest, nlh);
hw.write_to(w)
}
}
@ -98,8 +100,10 @@ impl<'n, 'e> AnyArg<'n, 'e> for FlagBuilder<'n, 'e> {
fn blacklist(&self) -> Option<&[&'e str]> { self.blacklist.as_ref().map(|o| &o[..]) }
fn is_set(&self, s: ArgSettings) -> bool { self.settings.is_set(s) }
fn has_switch(&self) -> bool { true }
fn takes_value(&self) -> bool { false }
fn set(&mut self, s: ArgSettings) { self.settings.set(s) }
fn max_vals(&self) -> Option<u64> { None }
fn val_names(&self) -> Option<&VecMap<&'e str>> { None }
fn num_vals(&self) -> Option<u64> { None }
fn possible_vals(&self) -> Option<&[&'e str]> { None }
fn validator(&self) -> Option<&Rc<Fn(String) -> StdResult<(), String>>> { None }
@ -107,6 +111,8 @@ impl<'n, 'e> AnyArg<'n, 'e> for FlagBuilder<'n, 'e> {
fn short(&self) -> Option<char> { self.short }
fn long(&self) -> Option<&'e str> { self.long }
fn val_delim(&self) -> Option<char> { None }
fn help(&self) -> Option<&'e str> { self.help }
fn default_val(&self) -> Option<&'n str> { None }
}
#[cfg(test)]

View file

@ -1,126 +0,0 @@
macro_rules! write_arg_help {
(@opt $_self:ident, $w:ident, $tab:ident, $longest:ident, $skip_pv:ident, $nlh:ident) => {
write_arg_help!(@short $_self, $w, $tab);
write_arg_help!(@opt_long $_self, $w, $nlh, $longest);
write_arg_help!(@val $_self, $w);
if !($nlh || $_self.settings.is_set(ArgSettings::NextLineHelp)) {
write_spaces!(if $_self.long.is_some() { $longest + 4 } else { $longest + 8 } - ($_self.to_string().len()), $w);
}
if let Some(h) = $_self.help {
write_arg_help!(@help $_self, $w, h, $tab, $longest, $nlh);
write_arg_help!(@spec_vals $_self, $w, $skip_pv);
}
};
(@flag $_self:ident, $w:ident, $tab:ident, $longest:ident, $nlh:ident) => {
write_arg_help!(@short $_self, $w, $tab);
write_arg_help!(@flag_long $_self, $w, $longest, $nlh);
if let Some(h) = $_self.help {
write_arg_help!(@help $_self, $w, h, $tab, $longest, $nlh);
}
};
(@pos $_self:ident, $w:ident, $tab:ident, $longest:ident, $skip_pv:ident, $nlh:ident) => {
try!(write!($w, "{}", $tab));
write_arg_help!(@val $_self, $w);
if !($nlh || $_self.settings.is_set(ArgSettings::NextLineHelp)) {
write_spaces!($longest + 4 - ($_self.to_string().len()), $w);
}
if let Some(h) = $_self.help {
write_arg_help!(@help $_self, $w, h, $tab, $longest, $nlh);
write_arg_help!(@spec_vals $_self, $w, $skip_pv);
}
};
(@short $_self:ident, $w:ident, $tab:ident) => {
try!(write!($w, "{}", $tab));
if let Some(s) = $_self.short {
try!(write!($w, "-{}", s));
} else {
try!(write!($w, "{}", $tab));
}
};
(@flag_long $_self:ident, $w:ident, $longest:ident, $nlh:ident) => {
if let Some(l) = $_self.long {
write_arg_help!(@long $_self, $w, l);
if !$nlh || !$_self.settings.is_set(ArgSettings::NextLineHelp) {
write_spaces!(($longest + 4) - (l.len() + 2), $w);
}
} else {
if !$nlh || !$_self.settings.is_set(ArgSettings::NextLineHelp) {
// 6 is tab (4) + -- (2)
write_spaces!(($longest + 6), $w);
}
}
};
(@opt_long $_self:ident, $w:ident, $nlh:ident, $longest:ident) => {
if let Some(l) = $_self.long {
write_arg_help!(@long $_self, $w, l);
}
try!(write!($w, " "));
};
(@long $_self:ident, $w:ident, $l:ident) => {
try!(write!($w,
"{}--{}",
if $_self.short.is_some() {
", "
} else {
""
},
$l));
};
(@val $_self:ident, $w:ident) => {
if let Some(ref vec) = $_self.val_names {
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!($w, "<{}>", val));
if it.peek().is_some() { try!(write!($w, " ")); }
}
let num = vec.len();
if $_self.settings.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!($w, "..."));
}
} else if let Some(num) = $_self.num_vals {
for _ in 0..num {
try!(write!($w, "<{}>", $_self.name));
}
} else {
try!(write!($w,
"<{}>{}",
$_self.name,
if $_self.settings.is_set(ArgSettings::Multiple) {
"..."
} else {
""
}));
}
};
(@spec_vals $_self:ident, $w:ident, $skip_pv:ident) => {
if let Some(ref pv) = $_self.default_val {
try!(write!($w, " [default: {}]", pv));
}
if !$skip_pv {
if let Some(ref pv) = $_self.possible_vals {
try!(write!($w, " [values: {}]", pv.join(", ")));
}
}
};
(@help $_self:ident, $w:ident, $h:ident, $tab:ident, $longest:expr, $nlh:ident) => {
if $nlh || $_self.settings.is_set(ArgSettings::NextLineHelp) {
try!(write!($w, "\n{}{}", $tab, $tab));
}
if $h.contains("{n}") {
if let Some(part) = $h.split("{n}").next() {
try!(write!($w, "{}", part));
}
for part in $h.split("{n}").skip(1) {
try!(write!($w, "\n"));
if $nlh || $_self.settings.is_set(ArgSettings::NextLineHelp) {
try!(write!($w, "{}{}", $tab, $tab));
} else {
write_spaces!($longest + 12, $w);
}
try!(write!($w, "{}", part));
}
} else {
try!(write!($w, "{}", $h));
}
};
}

View file

@ -2,8 +2,6 @@ pub use self::flag::FlagBuilder;
pub use self::option::OptBuilder;
pub use self::positional::PosBuilder;
#[macro_use]
mod macros;
#[allow(dead_code)]
mod flag;
#[allow(dead_code)]

View file

@ -5,7 +5,7 @@ use std::io;
use vec_map::VecMap;
use args::{AnyArg, Arg};
use args::{AnyArg, Arg, HelpWriter};
use args::settings::{ArgFlags, ArgSettings};
#[allow(missing_debug_implementations)]
@ -104,10 +104,10 @@ impl<'n, 'e> OptBuilder<'n, 'e> {
ob
}
pub fn write_help<W: io::Write>(&self, w: &mut W, tab: &str, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
debugln!("fn=write_help");
write_arg_help!(@opt self, w, tab, longest, skip_pv, nlh);
write!(w, "\n")
pub fn write_help<W: io::Write>(&self, w: &mut W, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
let mut hw = HelpWriter::new(self, longest, nlh);
hw.skip_pv = skip_pv;
hw.write_to(w)
}
}
@ -116,29 +116,30 @@ impl<'n, 'e> Display for OptBuilder<'n, 'e> {
debugln!("fn=fmt");
// Write the name such --long or -l
if let Some(l) = self.long {
try!(write!(f, "--{}", l));
try!(write!(f, "--{} ", l));
} else {
try!(write!(f, "-{}", self.short.unwrap()));
try!(write!(f, "-{} ", self.short.unwrap()));
}
// Write the values such as <name1> <name2>
if let Some(ref vec) = self.val_names {
for (_, n) in vec {
debugln!("writing val_name: {}", n);
try!(write!(f, " <{}>", n));
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!(f, "<{}>", val));
if it.peek().is_some() { try!(write!(f, " ")); }
}
let num = vec.len();
if self.settings.is_set(ArgSettings::Multiple) && num == 1 {
if self.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(f, "..."));
}
} else if let Some(num) = self.num_vals {
let mut it = (0..num).peekable();
while let Some(_) = it.next() {
try!(write!(f, "<{}>", self.name));
if it.peek().is_some() { try!(write!(f, " ")); }
}
} else {
let num = self.num_vals.unwrap_or(1);
for _ in 0..num {
try!(write!(f, " <{}>", self.name));
}
if self.settings.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(f, "..."));
}
try!(write!(f, "<{}>{}", self.name, if self.is_set(ArgSettings::Multiple) { "..." } else { "" }));
}
Ok(())
@ -150,6 +151,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for OptBuilder<'n, 'e> {
fn overrides(&self) -> Option<&[&'e str]> { self.overrides.as_ref().map(|o| &o[..]) }
fn requires(&self) -> Option<&[&'e str]> { self.requires.as_ref().map(|o| &o[..]) }
fn blacklist(&self) -> Option<&[&'e str]> { self.blacklist.as_ref().map(|o| &o[..]) }
fn val_names(&self) -> Option<&VecMap<&'e str>> { self.val_names.as_ref().map(|o| o) }
fn is_set(&self, s: ArgSettings) -> bool { self.settings.is_set(s) }
fn has_switch(&self) -> bool { true }
fn set(&mut self, s: ArgSettings) { self.settings.set(s) }
@ -163,6 +165,9 @@ impl<'n, 'e> AnyArg<'n, 'e> for OptBuilder<'n, 'e> {
fn short(&self) -> Option<char> { self.short }
fn long(&self) -> Option<&'e str> { self.long }
fn val_delim(&self) -> Option<char> { self.val_delim }
fn takes_value(&self) -> bool { true }
fn help(&self) -> Option<&'e str> { self.help }
fn default_val(&self) -> Option<&'n str> { self.default_val }
}
#[cfg(test)]

View file

@ -6,7 +6,7 @@ use std::io;
use vec_map::VecMap;
use Arg;
use args::AnyArg;
use args::{AnyArg, HelpWriter};
use args::settings::{ArgFlags, ArgSettings};
#[allow(missing_debug_implementations)]
@ -63,7 +63,7 @@ impl<'n, 'e> PosBuilder<'n, 'e> {
}
pub fn from_arg(a: &Arg<'n, 'e>, idx: u64, reqs: &mut Vec<&'e str>) -> Self {
assert!(a.short.is_none() || a.long.is_none(),
debug_assert!(a.short.is_none() || a.long.is_none(),
format!("Argument \"{}\" has conflicting requirements, both index() and short(), \
or long(), were supplied", a.name));
@ -105,9 +105,10 @@ impl<'n, 'e> PosBuilder<'n, 'e> {
pb
}
pub fn write_help<W: io::Write>(&self, w: &mut W, tab: &str, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
write_arg_help!(@pos self, w, tab, longest, skip_pv, nlh);
write!(w, "\n")
pub fn write_help<W: io::Write>(&self, w: &mut W, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
let mut hw = HelpWriter::new(self, longest, nlh);
hw.skip_pv = skip_pv;
hw.write_to(w)
}
}
@ -139,6 +140,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for PosBuilder<'n, 'e> {
fn overrides(&self) -> Option<&[&'e str]> { self.overrides.as_ref().map(|o| &o[..]) }
fn requires(&self) -> Option<&[&'e str]> { self.requires.as_ref().map(|o| &o[..]) }
fn blacklist(&self) -> Option<&[&'e str]> { self.blacklist.as_ref().map(|o| &o[..]) }
fn val_names(&self) -> Option<&VecMap<&'e str>> { self.val_names.as_ref() }
fn is_set(&self, s: ArgSettings) -> bool { self.settings.is_set(s) }
fn set(&mut self, s: ArgSettings) { self.settings.set(s) }
fn has_switch(&self) -> bool { false }
@ -152,6 +154,9 @@ impl<'n, 'e> AnyArg<'n, 'e> for PosBuilder<'n, 'e> {
fn short(&self) -> Option<char> { None }
fn long(&self) -> Option<&'e str> { None }
fn val_delim(&self) -> Option<char> { self.val_delim }
fn takes_value(&self) -> bool { true }
fn help(&self) -> Option<&'e str> { self.help }
fn default_val(&self) -> Option<&'n str> { self.default_val }
}
#[cfg(test)]

254
src/args/help_writer.rs Normal file
View file

@ -0,0 +1,254 @@
use std::io;
use args::AnyArg;
use args::settings::ArgSettings;
use term;
const TAB: &'static str = " ";
pub struct HelpWriter<'a, A> where A: 'a {
a: &'a A,
l: usize,
nlh: bool,
pub skip_pv: bool,
term_w: Option<usize>,
}
impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> {
pub fn new(a: &'a A, l: usize, nlh: bool) -> Self {
HelpWriter {
a: a,
l: l,
nlh: nlh,
skip_pv: false,
term_w: term::dimensions().map(|(w, _)| w),
}
}
pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
debugln!("fn=write_to;");
try!(self.short(w));
try!(self.long(w));
try!(self.val(w));
try!(self.help(w));
write!(w, "\n")
}
fn short<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=short;");
try!(write!(w, "{}", TAB));
if let Some(s) = self.a.short() {
write!(w, "-{}", s)
} else if self.a.has_switch() {
write!(w, "{}", TAB)
} else {
Ok(())
}
}
fn long<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=long;");
if !self.a.has_switch() {
return Ok(());
}
if self.a.takes_value() {
if let Some(l) = self.a.long() {
try!(write!(w, "{}--{}", if self.a.short().is_some() { ", " } else { "" }, l));
}
try!(write!(w, " "));
} else {
if let Some(l) = self.a.long() {
try!(write!(w, "{}--{}", if self.a.short().is_some() { ", " } else { "" }, l));
if !self.nlh || !self.a.is_set(ArgSettings::NextLineHelp) {
write_spaces!((self.l + 4) - (l.len() + 2), w);
}
} else {
if !self.nlh || !self.a.is_set(ArgSettings::NextLineHelp) {
// 6 is tab (4) + -- (2)
write_spaces!((self.l + 6), w);
}
}
}
Ok(())
}
fn val<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=val;");
if !self.a.takes_value() {
return Ok(());
}
if let Some(ref vec) = self.a.val_names() {
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!(w, "<{}>", val));
if it.peek().is_some() { try!(write!(w, " ")); }
}
let num = vec.len();
if self.a.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(w, "..."));
}
} else if let Some(num) = self.a.num_vals() {
let mut it = (0..num).peekable();
while let Some(_) = it.next() {
try!(write!(w, "<{}>", self.a.name()));
if it.peek().is_some() { try!(write!(w, " ")); }
}
} else {
try!(write!(w, "<{}>{}", self.a.name(), if self.a.is_set(ArgSettings::Multiple) { "..." } else { "" }));
}
if self.a.has_switch() {
if !(self.nlh || self.a.is_set(ArgSettings::NextLineHelp)) {
let self_len = self.a.to_string().len();
// subtract ourself
let mut spcs = self.l - self_len;
// Since we're writing spaces from the tab point we first need to know if we
// had a long and short, or just short
if self.a.long().is_some() {
// Only account 4 after the val
spcs += 4;
} else {
// Only account for ', --' + 4 after the val
spcs += 8;
}
write_spaces!(spcs, w);
}
} else {
if !(self.nlh || self.a.is_set(ArgSettings::NextLineHelp)) {
write_spaces!(self.l + 4 - (self.a.to_string().len()), w);
}
}
Ok(())
}
fn help<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=help;");
let spec_vals = self.spec_vals();
let mut help = String::new();
let h = self.a.help().unwrap_or("");
let spcs = if self.nlh || self.a.is_set(ArgSettings::NextLineHelp) {
8 // "tab" + "tab"
} else {
self.l + 12
};
// determine if our help fits or needs to wrap
let too_long = self.term_w.is_some() && (spcs + h.len() + spec_vals.len() >= self.term_w.unwrap_or(0));
// Is help on next line, if so newline + 2x tab
if self.nlh || self.a.is_set(ArgSettings::NextLineHelp) {
try!(write!(w, "\n{}{}", TAB, TAB));
}
debug!("Too long...");
if too_long {
sdebugln!("Yes");
if let Some(width) = self.term_w {
help.push_str(h);
help.push_str(&*spec_vals);
debugln!("term width: {}", width);
debugln!("help: {}", help);
debugln!("help len: {}", help.len());
// Determine how many newlines we need to insert
let avail_chars = width - spcs;
debugln!("Usable space: {}", avail_chars);
let mut indices = vec![];
let mut idx = 0;
loop {
idx += avail_chars - 1;
if idx >= help.len() { break; }
// 'a' arbitrary non space char
if help.chars().nth(idx).unwrap_or('a') != ' ' {
idx = find_idx_of_space(&*help, idx);
}
debugln!("Adding idx: {}", idx);
debugln!("At {}: {:?}", idx, help.chars().nth(idx));
indices.push(idx);
if &help[idx..].len() <= &avail_chars {
break;
}
}
for (i, idx) in indices.iter().enumerate() {
debugln!("iter;i={},idx={}", i, idx);
let j = idx+(2*i);
debugln!("removing: {}", j);
debugln!("at {}: {:?}", j, help.chars().nth(j));
help.remove(j);
help.insert(j, '{');
help.insert(j + 1 , 'n');
help.insert(j + 2, '}');
}
}
} else { sdebugln!("No"); }
let help = if !help.is_empty() {
&*help
} else if !spec_vals.is_empty() {
help.push_str(h);
help.push_str(&*spec_vals);
&*help
} else {
h
};
if help.contains("{n}") {
if let Some(part) = help.split("{n}").next() {
try!(write!(w, "{}", part));
}
for part in help.split("{n}").skip(1) {
try!(write!(w, "\n"));
if self.nlh || self.a.is_set(ArgSettings::NextLineHelp) {
try!(write!(w, "{}{}", TAB, TAB));
} else {
if self.a.has_switch() {
write_spaces!(self.l + 12, w);
} else {
write_spaces!(self.l + 8, w);
}
}
try!(write!(w, "{}", part));
}
} else {
try!(write!(w, "{}", help));
}
Ok(())
}
fn spec_vals(&self) -> String {
debugln!("fn=spec_vals;");
if let Some(ref pv) = self.a.default_val() {
debugln!("Writing defaults");
return format!(" [default: {}] {}", pv,
if !self.skip_pv {
if let Some(ref pv) = self.a.possible_vals() {
format!(" [values: {}]", pv.join(", "))
} else { "".into() }
} else { "".into() }
);
} else if !self.skip_pv {
debugln!("Writing values");
if let Some(ref pv) = self.a.possible_vals() {
debugln!("Possible vals...{:?}", pv);
return format!(" [values: {}]", pv.join(", "));
}
}
String::new()
}
}
fn find_idx_of_space(full: &str, start: usize) -> usize {
debugln!("fn=find_idx_of_space;");
let haystack = &full[..start];
debugln!("haystack: {}", haystack);
for (i, c) in haystack.chars().rev().enumerate() {
debugln!("iter;c={},i={}", c, i);
if c == ' ' {
debugln!("Found space returning start-i...{}", start - (i+1));
return start - (i+1);
}
}
0
}

View file

@ -7,6 +7,7 @@ pub use self::matched_arg::MatchedArg;
pub use self::group::ArgGroup;
pub use self::any_arg::AnyArg;
pub use self::settings::ArgSettings;
pub use self::help_writer::HelpWriter;
mod arg;
pub mod any_arg;
@ -18,3 +19,4 @@ mod matched_arg;
mod group;
#[allow(dead_code)]
pub mod settings;
mod help_writer;

View file

@ -405,6 +405,8 @@ extern crate strsim;
extern crate ansi_term;
#[cfg(feature = "yaml")]
extern crate yaml_rust;
#[cfg(feature = "wrap_help")]
extern crate libc;
#[macro_use]
extern crate bitflags;
extern crate vec_map;
@ -425,6 +427,7 @@ mod fmt;
mod suggestions;
mod errors;
mod osstringext;
mod term;
const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \
report at https://github.com/kbknapp/clap-rs/issues";

82
src/term.rs Normal file
View file

@ -0,0 +1,82 @@
// The following was taken and adapated from exa source
// repo: https://github.com/ogham/exa
// commit: b9eb364823d0d4f9085eb220233c704a13d0f611
// license: MIT - Copyright (c) 2014 Benjamin Sago
//! System calls for getting the terminal size.
//!
//! Getting the terminal size is performed using an ioctl command that takes
//! the file handle to the terminal -- which in this case, is stdout -- and
//! populates a structure containing the values.
//!
//! The size is needed when the user wants the output formatted into columns:
//! the default grid view, or the hybrid grid-details view.
#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
use std::mem::zeroed;
#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
use libc::{c_int, c_ushort, c_ulong, STDOUT_FILENO};
/// The number of rows and columns of a terminal.
#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
struct Winsize {
ws_row: c_ushort,
ws_col: c_ushort,
}
// Unfortunately the actual command is not standardised...
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(feature = "wrap_help")]
static TIOCGWINSZ: c_ulong = 0x5413;
#[cfg(any(target_os = "macos",
target_os = "ios",
target_os = "bitrig",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"))]
#[cfg(feature = "wrap_help")]
static TIOCGWINSZ: c_ulong = 0x40087468;
extern {
#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
pub fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
}
/// Runs the ioctl command. Returns (0, 0) if output is not to a terminal, or
/// there is an error. (0, 0) is an invalid size to have anyway, which is why
/// it can be used as a nil value.
#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
unsafe fn get_dimensions() -> Winsize {
let mut window: Winsize = zeroed();
let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window);
if result == -1 {
zeroed()
}
else {
window
}
}
/// Query the current processes's output, returning its width and height as a
/// number of characters. Returns `None` if the output isn't to a terminal.
#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
pub fn dimensions() -> Option<(usize, usize)> {
let w = unsafe { get_dimensions() };
if w.ws_col == 0 || w.ws_row == 0 {
None
}
else {
Some((w.ws_col as usize, w.ws_row as usize))
}
}
#[cfg(any(not(feature = "wrap_help"), target_os = "windows"))]
pub fn dimensions() -> Option<(usize, usize)> {
None
}

View file

@ -93,7 +93,7 @@ Kevin K.
tests stuff
USAGE:
\ttest [OPTIONS] [ARGS]
test [OPTIONS] [ARGS]
OPTIONS:
-f, --flag some flag
@ -125,7 +125,7 @@ Kevin K.
tests stuff
USAGE:
\ttest [FLAGS] [OPTIONS] [ARGS]
test [FLAGS] [OPTIONS] [ARGS]
FLAGS:
-h, --help Prints help information
@ -137,24 +137,3 @@ OPTIONS:
ARGS:
<arg1> some pos arg\n"));
}
#[test]
fn app_settings_fromstr() {
assert_eq!("subcommandsnegatereqs".parse::<AppSettings>().unwrap(), AppSettings::SubcommandsNegateReqs);
assert_eq!("subcommandsrequired".parse::<AppSettings>().unwrap(), AppSettings::SubcommandRequired);
assert_eq!("argrequiredelsehelp".parse::<AppSettings>().unwrap(), AppSettings::ArgRequiredElseHelp);
assert_eq!("globalversion".parse::<AppSettings>().unwrap(), AppSettings::GlobalVersion);
assert_eq!("versionlesssubcommands".parse::<AppSettings>().unwrap(), AppSettings::VersionlessSubcommands);
assert_eq!("unifiedhelpmessage".parse::<AppSettings>().unwrap(), AppSettings::UnifiedHelpMessage);
assert_eq!("waitonerror".parse::<AppSettings>().unwrap(), AppSettings::WaitOnError);
assert_eq!("subcommandrequiredelsehelp".parse::<AppSettings>().unwrap(), AppSettings::SubcommandRequiredElseHelp);
assert_eq!("allowexternalsubcommands".parse::<AppSettings>().unwrap(), AppSettings::AllowExternalSubcommands);
assert_eq!("trailingvararg".parse::<AppSettings>().unwrap(), AppSettings::TrailingVarArg);
assert_eq!("nobinaryname".parse::<AppSettings>().unwrap(), AppSettings::NoBinaryName);
assert_eq!("strictutf8".parse::<AppSettings>().unwrap(), AppSettings::StrictUtf8);
assert_eq!("allowinvalidutf8".parse::<AppSettings>().unwrap(), AppSettings::AllowInvalidUtf8);
assert_eq!("allowleadinghyphen".parse::<AppSettings>().unwrap(), AppSettings::AllowLeadingHyphen);
assert_eq!("hidepossiblevaluesinhelp".parse::<AppSettings>().unwrap(), AppSettings::HidePossibleValuesInHelp);
assert_eq!("hidden".parse::<AppSettings>().unwrap(), AppSettings::Hidden);
assert!("hahahaha".parse::<AppSettings>().is_err());
}

View file

@ -72,7 +72,7 @@ Kevin K.
tests stuff
USAGE:
\ttest [FLAGS] [OPTIONS]
test [FLAGS] [OPTIONS]
FLAGS:
-f, --flag some flag
@ -102,7 +102,7 @@ Kevin K.
tests stuff
USAGE:
\ttest [FLAGS] [OPTIONS] [ARGS]
test [FLAGS] [OPTIONS] [ARGS]
FLAGS:
-h, --help Prints help information

View file

@ -22,7 +22,7 @@ Kevin K.
tests stuff
USAGE:
\ttest [FLAGS] [OPTIONS]
test [FLAGS] [OPTIONS]
FLAGS:
-F, --flag2 some other flag