diff --git a/CHANGELOG.md b/CHANGELOG.md
index 338e3b77ec3..ff37933dc2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,13 @@
+
+### v2.2.2 (2016-03-27)
+
+
+#### Bug Fixes
+
+* **Help Message:** fixes bug with wrapping in the middle of a unicode sequence ([05365ddc](https://github.com/kbknapp/clap-rs/commit/05365ddcc252e4b49e7a75e199d6001a430bd84d), closes [#456](https://github.com/kbknapp/clap-rs/issues/456))
+* **Usage Strings:** fixes small bug where -- would appear needlessly in usage strings ([6933b849](https://github.com/kbknapp/clap-rs/commit/6933b8491c2a7e28cdb61b47dcf10caf33c2f78a), closes [#461](https://github.com/kbknapp/clap-rs/issues/461))
+
+
### 2.2.1 (2016-03-16)
diff --git a/Cargo.toml b/Cargo.toml
index 20333622ec4..1b5c2d430f0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "clap"
-version = "2.2.1"
+version = "2.2.2"
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"
@@ -19,13 +19,14 @@ 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.55", optional = true }
+unicode-width = { version = "~0.1.3", optional = true }
[features]
default = ["suggestions", "color", "wrap_help"]
suggestions = ["strsim"]
color = ["ansi_term"]
yaml = ["yaml-rust"]
-wrap_help = ["libc"]
+wrap_help = ["libc", "unicode-width"]
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 0838039d0bf..5ca44e3429b 100644
--- a/README.md
+++ b/README.md
@@ -457,7 +457,7 @@ The following is a list of optional `clap` features:
* **"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`)
+* **"wrap_help"**: Automatically detects terminal width and wraps long help text lines with proper indentation alignment (builds dependency `libc` and 'unicode-width')
* **"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. (builds dependency `yaml-rust`)
diff --git a/src/app/parser.rs b/src/app/parser.rs
index cc1adcd6b37..b9d63be19fd 100644
--- a/src/app/parser.rs
+++ b/src/app/parser.rs
@@ -341,19 +341,19 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
pub fn has_flags(&self) -> bool {
- self.flags.is_empty()
+ !self.flags.is_empty()
}
pub fn has_opts(&self) -> bool {
- self.opts.is_empty()
+ !self.opts.is_empty()
}
pub fn has_positionals(&self) -> bool {
- self.positionals.is_empty()
+ !self.positionals.is_empty()
}
pub fn has_subcommands(&self) -> bool {
- self.subcommands.is_empty()
+ !self.subcommands.is_empty()
}
pub fn is_set(&self, s: AppSettings) -> bool {
@@ -1318,13 +1318,14 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
.iter()
.fold(String::new(), |a, s| a + &format!(" {}", s)[..]);
- if !self.has_flags() && !self.is_set(AppSettings::UnifiedHelpMessage) {
+ if self.has_flags() && !self.is_set(AppSettings::UnifiedHelpMessage) {
usage.push_str(" [FLAGS]");
} else {
usage.push_str(" [OPTIONS]");
}
- if !self.is_set(AppSettings::UnifiedHelpMessage) && !self.has_opts() &&
- self.opts.iter().any(|a| !a.settings.is_set(ArgSettings::Required)) {
+ if !self.is_set(AppSettings::UnifiedHelpMessage)
+ && self.has_opts()
+ && self.opts.iter().any(|a| !a.settings.is_set(ArgSettings::Required)) {
usage.push_str(" [OPTIONS]");
}
@@ -1332,22 +1333,22 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
// places a '--' in the usage string if there are args and options
// supporting multiple values
- if !self.has_positionals()
- && (self.opts.iter().any(|a| a.settings.is_set(ArgSettings::Multiple))
- || self.positionals.values().any(|a| a.settings.is_set(ArgSettings::Multiple)))
- && !self.opts.iter().any(|a| a.settings.is_set(ArgSettings::Required))
- && self.has_subcommands() {
+ if self.has_positionals()
+ && self.opts.iter().any(|a| a.settings.is_set(ArgSettings::Multiple))
+ // || self.positionals.values().any(|a| a.settings.is_set(ArgSettings::Multiple)))
+ && self.positionals.values().any(|a| !a.settings.is_set(ArgSettings::Required))
+ && !self.has_subcommands() {
usage.push_str(" [--]")
}
- if !self.has_positionals()
+ if self.has_positionals()
&& self.positionals.values().any(|a| !a.settings.is_set(ArgSettings::Required)) {
usage.push_str(" [ARGS]");
}
- if !self.has_subcommands() && !self.is_set(AppSettings::SubcommandRequired) {
+ if self.has_subcommands() && !self.is_set(AppSettings::SubcommandRequired) {
usage.push_str(" [SUBCOMMAND]");
- } else if self.is_set(AppSettings::SubcommandRequired) && !self.has_subcommands() {
+ } else if self.is_set(AppSettings::SubcommandRequired) && self.has_subcommands() {
usage.push_str(" ");
}
} else {
@@ -1417,10 +1418,10 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
try!(write!(w, "\n{}", self.create_usage(&[])));
- let flags = !self.has_flags();
- let pos = !self.has_positionals();
- let opts = !self.has_opts();
- let subcmds = !self.has_subcommands();
+ let flags = self.has_flags();
+ let pos = self.has_positionals();
+ let opts = self.has_opts();
+ let subcmds = self.has_subcommands();
let unified_help = self.is_set(AppSettings::UnifiedHelpMessage);
let mut longest_flag = 0;
diff --git a/src/args/help_writer.rs b/src/args/help_writer.rs
index 94247b99879..bb1806abaf7 100644
--- a/src/args/help_writer.rs
+++ b/src/args/help_writer.rs
@@ -1,9 +1,23 @@
use std::io;
use std::fmt::Display;
+#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
+use unicode_width::UnicodeWidthStr;
+
use args::AnyArg;
use args::settings::ArgSettings;
use term;
+use strext::_StrExt;
+
+#[cfg(any(not(feature = "wrap_help"), target_os = "windows"))]
+fn str_width(s: &str) -> usize {
+ s.len()
+}
+
+#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
+fn str_width(s: &str) -> usize {
+ UnicodeWidthStr::width(s)
+}
const TAB: &'static str = " ";
@@ -139,7 +153,7 @@ impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> + Display {
// determine if our help fits or needs to wrap
let width = self.term_w.unwrap_or(0);
debugln!("Term width...{}", width);
- let too_long = self.term_w.is_some() && (spcs + h.len() + spec_vals.len() >= width);
+ let too_long = self.term_w.is_some() && (spcs + str_width(h) + str_width(&*spec_vals) >= width);
debugln!("Too long...{:?}", too_long);
// Is help on next line, if so newline + 2x tab
@@ -153,13 +167,13 @@ impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> + Display {
help.push_str(h);
help.push_str(&*spec_vals);
debugln!("help: {}", help);
- debugln!("help len: {}", help.len());
+ debugln!("help width: {}", str_width(help));
// Determine how many newlines we need to insert
let avail_chars = width - spcs;
debugln!("Usable space: {}", avail_chars);
let longest_w = {
let mut lw = 0;
- for l in help.split(' ').map(|s| s.len()) {
+ for l in help.split(' ').map(|s| str_width(s)) {
if l > lw {
lw = l;
}
@@ -167,7 +181,7 @@ impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> + Display {
lw
};
debugln!("Longest word...{}", longest_w);
- debug!("Enough space...");
+ debug!("Enough space to wrap...");
if longest_w < avail_chars {
sdebugln!("Yes");
let mut indices = vec![];
@@ -182,13 +196,13 @@ impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> + Display {
debugln!("Adding idx: {}", idx);
debugln!("At {}: {:?}", idx, help.chars().nth(idx));
indices.push(idx);
- if &help[idx..].len() <= &avail_chars {
+ if str_width(&help[idx..]) <= avail_chars {
break;
}
}
for (i, idx) in indices.iter().enumerate() {
debugln!("iter;i={},idx={}", i, idx);
- let j = idx+(2*i);
+ let j = idx + (2 * i);
debugln!("removing: {}", j);
debugln!("at {}: {:?}", j, help.chars().nth(j));
help.remove(j);
@@ -252,15 +266,20 @@ impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> + Display {
}
}
-fn find_idx_of_space(full: &str, start: usize) -> usize {
+fn find_idx_of_space(full: &str, mut start: usize) -> usize {
debugln!("fn=find_idx_of_space;");
- let haystack = &full[..start];
+ let haystack = if full._is_char_boundary(start) {
+ &full[..start]
+ } else {
+ while !full._is_char_boundary(start) { start -= 1; }
+ &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);
+ debugln!("Found space returning start-i...{}", start - (i + 1));
+ return start - (i + 1);
}
}
0
diff --git a/src/lib.rs b/src/lib.rs
index 8901e9d9afd..73907c1f895 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -406,8 +406,10 @@ extern crate strsim;
extern crate ansi_term;
#[cfg(feature = "yaml")]
extern crate yaml_rust;
-#[cfg(feature = "wrap_help")]
+#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
extern crate libc;
+#[cfg(all(feature = "wrap_help", not(target_os = "windows")))]
+extern crate unicode_width;
#[macro_use]
extern crate bitflags;
extern crate vec_map;
@@ -429,6 +431,7 @@ mod suggestions;
mod errors;
mod osstringext;
mod term;
+mod strext;
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/strext.rs b/src/strext.rs
new file mode 100644
index 00000000000..f82aa7bdb6d
--- /dev/null
+++ b/src/strext.rs
@@ -0,0 +1,14 @@
+pub trait _StrExt {
+ fn _is_char_boundary(&self, index: usize) -> bool;
+}
+
+impl _StrExt for str {
+ #[inline]
+ fn _is_char_boundary(&self, index: usize) -> bool {
+ if index == self.len() { return true; }
+ match self.as_bytes().get(index) {
+ None => false,
+ Some(&b) => b < 128 || b >= 192,
+ }
+ }
+}
\ No newline at end of file