diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d3187e5..6310bc72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+
+## 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))
+
+
+
### v2.1.2 (2016-02-24)
diff --git a/Cargo.toml b/Cargo.toml
index bcec105a..c2c3fe1d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "clap"
-version = "2.1.2"
+version = "2.2.0"
authors = ["Kevin K. "]
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
diff --git a/README.md b/README.md
index c3d707e0..96dd16ca 100644
--- a/README.md
+++ b/README.md
@@ -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!
diff --git a/clap-tests/run_tests.py b/clap-tests/run_tests.py
index 952bb977..3e55a8eb 100755
--- a/clap-tests/run_tests.py
+++ b/clap-tests/run_tests.py
@@ -13,7 +13,7 @@ Kevin K.
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 ...
+ claptests --option ...
For more information try --help'''
@@ -65,30 +65,30 @@ _pv_dym_usage = '''error: 'slo' isn't a valid value for '--Option '
Did you mean 'slow' ?
USAGE:
-\tclaptests --Option
+ claptests --Option
For more information try --help'''
_excluded = '''error: The argument '--flag' cannot be used with '-F'
USAGE:
-\tclaptests [positional2] -F --long-option-2
+ claptests [positional2] -F --long-option-2
For more information try --help'''
_excluded_l = '''error: The argument '-f' cannot be used with '-F'
USAGE:
-\tclaptests [positional2] -F --long-option-2
+ claptests [positional2] -F --long-option-2
For more information try --help'''
_required = '''error: The following required arguments were not provided:
-\t[positional2]
-\t--long-option-2
+ [positional2]
+ --long-option-2
USAGE:
-\tclaptests [positional2] -F --long-option-2
+ claptests [positional2] -F --long-option-2
For more information try --help'''
@@ -141,7 +141,7 @@ Kevin K.
tests subcommands
USAGE:
-\tclaptests subcmd [FLAGS] [OPTIONS] [--] [ARGS]
+ claptests subcmd [FLAGS] [OPTIONS] [--] [ARGS]
FLAGS:
-f, --flag tests flags
diff --git a/clap_dep_graph.png b/clap_dep_graph.png
index 9674306c..1cb59b74 100644
Binary files a/clap_dep_graph.png and b/clap_dep_graph.png differ
diff --git a/src/app/parser.rs b/src/app/parser.rs
index 32bccd14..05e373d0 100644
--- a/src/app/parser.rs
+++ b/src/app/parser.rs
@@ -1235,7 +1235,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
&*self.get_required_from(&*self.required.iter().map(|&r| &*r).collect::>(), 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}") {
diff --git a/src/app/settings.rs b/src/app/settings.rs
index 73d54727..3c08f8f5 100644
--- a/src/app/settings.rs
+++ b/src/app/settings.rs
@@ -476,3 +476,29 @@ impl FromStr for AppSettings {
}
}
}
+
+#[cfg(test)]
+mod test {
+ use super::AppSettings;
+
+ #[test]
+ fn app_settings_fromstr() {
+ assert_eq!("subcommandsnegatereqs".parse::().unwrap(), AppSettings::SubcommandsNegateReqs);
+ assert_eq!("subcommandsrequired".parse::().unwrap(), AppSettings::SubcommandRequired);
+ assert_eq!("argrequiredelsehelp".parse::().unwrap(), AppSettings::ArgRequiredElseHelp);
+ assert_eq!("globalversion".parse::().unwrap(), AppSettings::GlobalVersion);
+ assert_eq!("versionlesssubcommands".parse::().unwrap(), AppSettings::VersionlessSubcommands);
+ assert_eq!("unifiedhelpmessage".parse::().unwrap(), AppSettings::UnifiedHelpMessage);
+ assert_eq!("waitonerror".parse::().unwrap(), AppSettings::WaitOnError);
+ assert_eq!("subcommandrequiredelsehelp".parse::().unwrap(), AppSettings::SubcommandRequiredElseHelp);
+ assert_eq!("allowexternalsubcommands".parse::().unwrap(), AppSettings::AllowExternalSubcommands);
+ assert_eq!("trailingvararg".parse::().unwrap(), AppSettings::TrailingVarArg);
+ assert_eq!("nobinaryname".parse::().unwrap(), AppSettings::NoBinaryName);
+ assert_eq!("strictutf8".parse::().unwrap(), AppSettings::StrictUtf8);
+ assert_eq!("allowinvalidutf8".parse::().unwrap(), AppSettings::AllowInvalidUtf8);
+ assert_eq!("allowleadinghyphen".parse::().unwrap(), AppSettings::AllowLeadingHyphen);
+ assert_eq!("hidepossiblevaluesinhelp".parse::().unwrap(), AppSettings::HidePossibleValuesInHelp);
+ assert_eq!("hidden".parse::().unwrap(), AppSettings::Hidden);
+ assert!("hahahaha".parse::().is_err());
+ }
+}
diff --git a/src/args/any_arg.rs b/src/args/any_arg.rs
index 3e737680..8ea33209 100644
--- a/src/args/any_arg.rs
+++ b/src/args/any_arg.rs
@@ -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;
fn long(&self) -> Option<&'e str>;
fn val_delim(&self) -> Option;
+ 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>;
}
diff --git a/src/args/arg_builder/flag.rs b/src/args/arg_builder/flag.rs
index 7f17cdda..452a0e37 100644
--- a/src/args/arg_builder/flag.rs
+++ b/src/args/arg_builder/flag.rs
@@ -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(&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(&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 { None }
+ fn val_names(&self) -> Option<&VecMap<&'e str>> { None }
fn num_vals(&self) -> Option { None }
fn possible_vals(&self) -> Option<&[&'e str]> { None }
fn validator(&self) -> Option<&Rc StdResult<(), String>>> { None }
@@ -107,6 +111,8 @@ impl<'n, 'e> AnyArg<'n, 'e> for FlagBuilder<'n, 'e> {
fn short(&self) -> Option { self.short }
fn long(&self) -> Option<&'e str> { self.long }
fn val_delim(&self) -> Option { None }
+ fn help(&self) -> Option<&'e str> { self.help }
+ fn default_val(&self) -> Option<&'n str> { None }
}
#[cfg(test)]
diff --git a/src/args/arg_builder/macros.rs b/src/args/arg_builder/macros.rs
deleted file mode 100644
index d6419308..00000000
--- a/src/args/arg_builder/macros.rs
+++ /dev/null
@@ -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));
- }
- };
-}
diff --git a/src/args/arg_builder/mod.rs b/src/args/arg_builder/mod.rs
index eafca028..2f96a5c7 100644
--- a/src/args/arg_builder/mod.rs
+++ b/src/args/arg_builder/mod.rs
@@ -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)]
diff --git a/src/args/arg_builder/option.rs b/src/args/arg_builder/option.rs
index b60bbaeb..a8c4fd4b 100644
--- a/src/args/arg_builder/option.rs
+++ b/src/args/arg_builder/option.rs
@@ -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(&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(&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
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 { self.short }
fn long(&self) -> Option<&'e str> { self.long }
fn val_delim(&self) -> Option { 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)]
diff --git a/src/args/arg_builder/positional.rs b/src/args/arg_builder/positional.rs
index b2ae2c1e..289af2f7 100644
--- a/src/args/arg_builder/positional.rs
+++ b/src/args/arg_builder/positional.rs
@@ -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(&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(&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 { None }
fn long(&self) -> Option<&'e str> { None }
fn val_delim(&self) -> Option { 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)]
diff --git a/src/args/help_writer.rs b/src/args/help_writer.rs
new file mode 100644
index 00000000..4f9b2263
--- /dev/null
+++ b/src/args/help_writer.rs
@@ -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,
+}
+
+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(&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(&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(&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(&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(&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
+}
diff --git a/src/args/mod.rs b/src/args/mod.rs
index 98b2330b..365eaf91 100644
--- a/src/args/mod.rs
+++ b/src/args/mod.rs
@@ -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;
diff --git a/src/lib.rs b/src/lib.rs
index 43b29a33..dabec14f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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";
diff --git a/src/term.rs b/src/term.rs
new file mode 100644
index 00000000..cbdceb60
--- /dev/null
+++ b/src/term.rs
@@ -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
+}
diff --git a/tests/app_settings.rs b/tests/app_settings.rs
index d25a9abe..54df0c15 100644
--- a/tests/app_settings.rs
+++ b/tests/app_settings.rs
@@ -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:
some pos arg\n"));
}
-
-#[test]
-fn app_settings_fromstr() {
- assert_eq!("subcommandsnegatereqs".parse::().unwrap(), AppSettings::SubcommandsNegateReqs);
- assert_eq!("subcommandsrequired".parse::().unwrap(), AppSettings::SubcommandRequired);
- assert_eq!("argrequiredelsehelp".parse::().unwrap(), AppSettings::ArgRequiredElseHelp);
- assert_eq!("globalversion".parse::().unwrap(), AppSettings::GlobalVersion);
- assert_eq!("versionlesssubcommands".parse::().unwrap(), AppSettings::VersionlessSubcommands);
- assert_eq!("unifiedhelpmessage".parse::().unwrap(), AppSettings::UnifiedHelpMessage);
- assert_eq!("waitonerror".parse::().unwrap(), AppSettings::WaitOnError);
- assert_eq!("subcommandrequiredelsehelp".parse::().unwrap(), AppSettings::SubcommandRequiredElseHelp);
- assert_eq!("allowexternalsubcommands".parse::().unwrap(), AppSettings::AllowExternalSubcommands);
- assert_eq!("trailingvararg".parse::().unwrap(), AppSettings::TrailingVarArg);
- assert_eq!("nobinaryname".parse::().unwrap(), AppSettings::NoBinaryName);
- assert_eq!("strictutf8".parse::().unwrap(), AppSettings::StrictUtf8);
- assert_eq!("allowinvalidutf8".parse::().unwrap(), AppSettings::AllowInvalidUtf8);
- assert_eq!("allowleadinghyphen".parse::().unwrap(), AppSettings::AllowLeadingHyphen);
- assert_eq!("hidepossiblevaluesinhelp".parse::().unwrap(), AppSettings::HidePossibleValuesInHelp);
- assert_eq!("hidden".parse::().unwrap(), AppSettings::Hidden);
- assert!("hahahaha".parse::().is_err());
-}
diff --git a/tests/help.rs b/tests/help.rs
index 676d3100..f2082bf6 100644
--- a/tests/help.rs
+++ b/tests/help.rs
@@ -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
diff --git a/tests/hidden_args.rs b/tests/hidden_args.rs
index 51a5f984..91eced4e 100644
--- a/tests/hidden_args.rs
+++ b/tests/hidden_args.rs
@@ -22,7 +22,7 @@ Kevin K.
tests stuff
USAGE:
-\ttest [FLAGS] [OPTIONS]
+ test [FLAGS] [OPTIONS]
FLAGS:
-F, --flag2 some other flag