From e36af0266635f23e85e951b9088d561e9a5d1bf6 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Sat, 12 Mar 2016 15:47:57 -0500 Subject: [PATCH] feat(Help Message): can auto wrap and aligning help text to term width By default `clap` now automatically wraps and aligns help strings to the term width. i.e. ``` -o, --option some really long help text that should be auto aligned but isn't righ t now ``` Now looks like this: ``` -o, --option some really long help text that should be auto aligned but isn't right now ``` The wrapping also respects words, and wraps at spaces so as to not cut words in the middle. This requires the `libc` dep which is enabled (by default) with the `wrap_help` cargo feature flag. Closes #428 --- Cargo.toml | 4 +- README.md | 16 +- src/app/parser.rs | 13 +- src/args/any_arg.rs | 6 + src/args/arg_builder/flag.rs | 18 +- src/args/arg_builder/macros.rs | 126 -------------- src/args/arg_builder/mod.rs | 2 - src/args/arg_builder/option.rs | 41 +++-- src/args/arg_builder/positional.rs | 15 +- src/args/help_writer.rs | 256 +++++++++++++++++++++++++++++ src/args/mod.rs | 2 + src/lib.rs | 3 + src/term.rs | 82 +++++++++ 13 files changed, 413 insertions(+), 171 deletions(-) delete mode 100644 src/args/arg_builder/macros.rs create mode 100644 src/args/help_writer.rs create mode 100644 src/term.rs diff --git a/Cargo.toml b/Cargo.toml index bcec105a298..4202451a6eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 c3d707e0180..ab030124377 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,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 +433,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 +472,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 +553,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/src/app/parser.rs b/src/app/parser.rs index 32bccd14ab9..d084108dc2e 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -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/args/any_arg.rs b/src/args/any_arg.rs index 3e737680344..8ea3320994d 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 7f17cddaa7e..33ad0f6dce3 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,13 @@ 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 +104,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 +115,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 d641930814e..00000000000 --- 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 eafca0289b6..2f96a5c7818 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 b60bbaeb968..a8c4fd4b601 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 { - 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, "...")); + } 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 { + 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 b2ae2c1eee9..289af2f713a 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 00000000000..65d25d1f976 --- /dev/null +++ b/src/args/help_writer.rs @@ -0,0 +1,256 @@ +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 98b2330b8f5..365eaf91599 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 43b29a33cd4..dabec14f784 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 00000000000..fca7d4ac294 --- /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(feature = "wrap_help")] +use std::mem::zeroed; +#[cfg(feature = "wrap_help")] +use libc::{c_int, c_ushort, c_ulong, STDOUT_FILENO}; + + +/// The number of rows and columns of a terminal. +#[cfg(feature = "wrap_help")] +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(feature = "wrap_help")] + 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(feature = "wrap_help")] +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(feature = "wrap_help")] +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(not(feature = "wrap_help"))] +pub fn dimensions() -> Option<(usize, usize)> { + None +}