From 65c2350aa376eeaeb27d33a7f2aba49d684b6436 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 04:07:44 -0400 Subject: [PATCH 01/16] feat: colors dont get sent to pipes by default Color are now only used when outputting to a termainal/TTY. There are three new settings as well which can be used to control color output, they are: * `AppSettings::ColorAuto`: The default, and will only output color when outputting to a terminal or TTY * `AppSettings::ColorAlways`: Outputs color no matter where the output is going * `AppSettings::ColorNever`: Never colors output This now allows one to use things like command line options, or environmental variables to turn colored output on/off. Closes #512 --- ' | 147 +++++++++++++++++++++++++++++ Cargo.toml | 4 +- README.md | 8 +- src/app/help.rs | 138 ++++++++++++++++++--------- src/app/macros.rs | 2 +- src/app/parser.rs | 79 +++++++++++----- src/app/settings.rs | 109 +++++++++++++++++----- src/errors.rs | 223 ++++++++++++++++++++++++++++++-------------- src/fmt.rs | 102 ++++++++++++++++++-- src/macros.rs | 16 ++-- 10 files changed, 647 insertions(+), 181 deletions(-) create mode 100644 ' diff --git a/' b/' new file mode 100644 index 00000000..594d8548 --- /dev/null +++ b/' @@ -0,0 +1,147 @@ +use std::fmt; + +#[cfg(all(feature = "color", not(target_os = "windows")))] +use ansi_term::Colour::{Green, Red, Yellow}; +#[cfg(all(feature = "color", not(target_os = "windows")))] +use ansi_term::ANSIString; + +#[cfg(color)] +use libc; + +#[cfg(color)] +const STDERR: i32 = libc::STDERR_FILENO; +#[cfg(color)] +const STDOUT: i32 = libc::STDOUT_FILENO; +#[cfg(not(color))] +const STDERR: i32 = 0; +#[cfg(not(color))] +const STDOUT: i32 = 0; + +#[doc(hidden)] +#[derive(Debug, PartialEq)] +pub enum ColorWhen { + Auto, // Default + Always, + Never +} + +#[cfg(color)] +pub fn is_a_tty(stderr: bool) -> bool { + let fd = if stderr { STDERR } else { STDOUT }; + unsafe { libc::isatty(fd) != 0 } +} + +#[cfg(not(color))] +pub fn is_a_tty(stderr: bool) -> bool { + false +} + +#[doc(hidden)] +pub struct Colorizer { + use_stderr: bool, + when: ColorWhen +} + +macro_rules! color { + ($_self:ident, $c:ident, $m:expr) => { + match $_self.when { + ColorWhen::Auto => if is_a_tty($_self.use_stderr) { + Format::$c($m) + } else { + $m + }, + ColorWhen::Always => Format::$c($m), + ColorWhen::Never => $m, + } + }; +} + +impl Colorizer { + pub fn good(&self, msg: T) -> &fmt::Display where T: fmt::Display { + use Format::Good; + color!(self, Good, msg) + } + pub fn warning(&self, msg: T) -> &fmt::Display where T: fmt::Display { + use Format::Warning; + color!(self, Warning, msg) + } + pub fn error(&self, msg: T) -> &fmt::Display where T: fmt::Display { + use Format::Error; + color!(self, Error, msg) + } +} + +impl Default for Colorizer { + fn default() -> Self { + Colorizer { + use_stderr: true, + when: ColorWhen::Auto + } + } +} + +/// Defines styles for different types of error messages. Defaults to Error=Red, Warning=Yellow, +/// and Good=Green +#[derive(Debug)] +#[doc(hidden)] +pub enum Format { + /// Defines the style used for errors, defaults to Red + Error(T), + /// Defines the style used for warnings, defaults to Yellow + Warning(T), + /// Defines the style used for good values, defaults to Green + Good(T), +} + +#[cfg(all(feature = "color", not(target_os = "windows")))] +impl> Format { + fn format(&self) -> ANSIString { + match *self { + Format::Error(ref e) => Red.bold().paint(e.as_ref()), + Format::Warning(ref e) => Yellow.paint(e.as_ref()), + Format::Good(ref e) => Green.paint(e.as_ref()), + } + } +} + +#[cfg(all(feature = "color", not(target_os = "windows")))] +impl> fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.format()) + } +} + +#[cfg(any(not(feature = "color"), target_os = "windows"))] +impl Format { + fn format(&self) -> &T { + match *self { + Format::Error(ref e) => e, + Format::Warning(ref e) => e, + Format::Good(ref e) => e, + } + } +} + +#[cfg(any(not(feature = "color"), target_os = "windows"))] +impl fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.format()) + } +} + +#[cfg(all(test, feature = "color", not(target_os = "windows")))] +mod test { + use super::Format; + use ansi_term::Colour::{Green, Red, Yellow}; + + #[test] + fn colored_output() { + let err = Format::Error("error"); + assert_eq!(&*format!("{}", err), + &*format!("{}", Red.bold().paint("error"))); + let good = Format::Good("good"); + assert_eq!(&*format!("{}", good), &*format!("{}", Green.paint("good"))); + let warn = Format::Warning("warn"); + assert_eq!(&*format!("{}", warn), &*format!("{}", Yellow.paint("warn"))); + } +} diff --git a/Cargo.toml b/Cargo.toml index 76e1e723..87efa862 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "clap" -version = "2.5.1" +version = "2.6.0" authors = ["Kevin K. "] exclude = ["examples/*", "clap-test/*", "tests/*", "benches/*", "*.png", "clap-perf/*", "*.dot"] description = "A simple to use, efficient, and full featured Command Line Argument Parser" @@ -27,7 +27,7 @@ regex = "~0.1.69" [features] default = ["suggestions", "color", "wrap_help"] suggestions = ["strsim"] -color = ["ansi_term"] +color = ["ansi_term", "libc"] yaml = ["yaml-rust"] wrap_help = ["libc", "unicode-width"] lints = ["clippy", "nightly"] diff --git a/README.md b/README.md index 62cce930..94a56c06 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,12 @@ Here's what's new in v2.4.3 * Improvements * Positional arguments which are part of a group are now formatted in a more readable way (fewer brackets) * Positional arguments use the standard `<>` brackets to reduce confusion - * The default help string for the `help` subcommand has been shortened to fit in 80 columns + * The default help string for the `help` subcommand has been shortened to fit in 80 columns Here's the highlights from v2.4.0 -* **Before Help:** adds support for displaying info before help message -* **Required Unless:** adds support for allowing args that are required unless certain other args are present +* **Before Help:** adds support for displaying info before help message +* **Required Unless:** adds support for allowing args that are required unless certain other args are present * Bug fixes Here's the highlights from v2.3.0 @@ -487,7 +487,7 @@ 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. (builds dependency `strsim`) -* **"color"**: Turns on colored error messages. This feature only works on non-Windows OSs. (builds dependency `ansi-term`) +* **"color"**: Turns on colored error messages. This feature only works on non-Windows OSs. (builds dependency `ansi-term` and `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. diff --git a/src/app/help.rs b/src/app/help.rs index 7cbc2d81..15299c10 100644 --- a/src/app/help.rs +++ b/src/app/help.rs @@ -10,7 +10,7 @@ use errors::{Error, Result as ClapResult}; use args::{AnyArg, ArgSettings, DispOrder}; use app::{App, AppSettings}; use app::parser::Parser; -use fmt::Format; +use fmt::{Format, Colorizer}; use term; @@ -57,18 +57,18 @@ impl<'b, 'c> DispOrder for App<'b, 'c> { } macro_rules! color { - ($_self:ident, $nc:expr, $c:ident) => { + ($_self:ident, $s:expr, $c:ident) => { if $_self.color { - write!($_self.writer, "{}", Format::$c($nc)) + write!($_self.writer, "{}", $_self.cizer.$c($s)) } else { - write!($_self.writer, "{}", $nc) + write!($_self.writer, "{}", $s) } }; - ($_self:ident, $nc:expr, $i:expr, $c:ident) => { + ($_self:ident, $fmt_s:expr, $v:expr, $c:ident) => { if $_self.color { - write!($_self.writer, "{}", Format::$c(format!($nc, $i))) + write!($_self.writer, "{}", $_self.cizer.$c(format!($fmt_s, $v))) } else { - write!($_self.writer, $nc, $i) + write!($_self.writer, $fmt_s, $v) } }; } @@ -82,40 +82,64 @@ pub struct Help<'a> { hide_pv: bool, term_w: Option, color: bool, + cizer: Colorizer, } // Public Functions impl<'a> Help<'a> { /// Create a new `Help` instance. - pub fn new(w: &'a mut Write, next_line_help: bool, hide_pv: bool, color: bool) -> Self { + pub fn new(w: &'a mut Write, next_line_help: bool, hide_pv: bool, color: bool, cizer: Colorizer) -> Self { + debugln!("fn=Help::new;"); Help { writer: w, next_line_help: next_line_help, hide_pv: hide_pv, term_w: term::dimensions().map(|(w, _)| w), color: color, + cizer: cizer, } } /// Reads help settings from an App /// and write its help to the wrapped stream. pub fn write_app_help(w: &'a mut Write, app: &App) -> ClapResult<()> { + debugln!("fn=Help::write_app_help;"); Self::write_parser_help(w, &app.p) } /// Reads help settings from a Parser /// and write its help to the wrapped stream. pub fn write_parser_help(w: &'a mut Write, parser: &Parser) -> ClapResult<()> { + debugln!("fn=Help::write_parser_help;"); + Self::_write_parser_help(w, parser, false) + } + + /// Reads help settings from a Parser + /// and write its help to the wrapped stream which will be stderr. This method prevents + /// formatting when required. + pub fn write_parser_help_to_stderr(w: &'a mut Write, parser: &Parser) -> ClapResult<()> { + debugln!("fn=Help::write_parser_help;"); + Self::_write_parser_help(w, parser, true) + } + + #[doc(hidden)] + pub fn _write_parser_help(w: &'a mut Write, parser: &Parser, stderr: bool) -> ClapResult<()> { + debugln!("fn=Help::write_parser_help;"); let nlh = parser.is_set(AppSettings::NextLineHelp); let hide_v = parser.is_set(AppSettings::HidePossibleValuesInHelp); let color = parser.is_set(AppSettings::ColoredHelp); - Self::new(w, nlh, hide_v, color).write_help(&parser) + let cizer = Colorizer { + use_stderr: stderr, + when: parser.color(), + }; + Self::new(w, nlh, hide_v, color, cizer).write_help(&parser) } /// Writes the parser help to the wrapped stream. pub fn write_help(&mut self, parser: &Parser) -> ClapResult<()> { + debugln!("fn=Help::write_help;"); if let Some(h) = parser.meta.help_str { - try!(writeln!(self.writer, "{}", h).map_err(Error::from)); + try!(write!(self.writer, "{}", h).map_err(Error::from)); } else if let Some(ref tmpl) = parser.meta.template { try!(self.write_templated_help(&parser, tmpl)); } else { @@ -131,6 +155,7 @@ impl<'a> Help<'a> { fn write_args_unsorted<'b: 'd, 'c: 'd, 'd, I: 'd>(&mut self, args: I) -> io::Result<()> where I: Iterator> { + debugln!("fn=write_args_unsorted;"); let mut longest = 0; let mut arg_v = Vec::with_capacity(10); for arg in args.filter(|arg| { @@ -143,7 +168,13 @@ impl<'a> Help<'a> { arg_v.push(arg) } } + let mut first = true; for arg in arg_v { + if !first { + try!(self.writer.write(b"\n")); + } else { + first = false; + }; try!(self.write_arg(arg.as_base(), longest)); } Ok(()) @@ -153,6 +184,7 @@ impl<'a> Help<'a> { fn write_args<'b: 'd, 'c: 'd, 'd, I: 'd>(&mut self, args: I) -> io::Result<()> where I: Iterator> { + debugln!("fn=write_args;"); let mut longest = 0; let mut ord_m = VecMap::new(); for arg in args.filter(|arg| { @@ -166,8 +198,14 @@ impl<'a> Help<'a> { btm.insert(arg.name(), arg); } } + let mut first = true; for (_, btm) in ord_m.into_iter() { for (_, arg) in btm.into_iter() { + if !first { + try!(self.writer.write(b"\n")); + } else { + first = false; + } try!(self.write_arg(arg.as_base(), longest)); } } @@ -179,12 +217,11 @@ impl<'a> Help<'a> { arg: &ArgWithDisplay<'b, 'c>, longest: usize) -> io::Result<()> { - debugln!("fn=write_to;"); + debugln!("fn=write_arg;"); try!(self.short(arg)); try!(self.long(arg, longest)); try!(self.val(arg, longest)); try!(self.help(arg, longest)); - try!(self.writer.write(b"\n")); Ok(()) } @@ -193,7 +230,7 @@ impl<'a> Help<'a> { debugln!("fn=short;"); try!(write!(self.writer, "{}", TAB)); if let Some(s) = arg.short() { - color!(self, "-{}", s, Good) + color!(self, "-{}", s, good) } else if arg.has_switch() { write!(self.writer, "{}", TAB) } else { @@ -212,7 +249,7 @@ impl<'a> Help<'a> { if arg.short().is_some() { try!(write!(self.writer, ", ")); } - try!(color!(self, "--{}", l, Good)) + try!(color!(self, "--{}", l, good)) } try!(write!(self.writer, " ")); } else { @@ -220,7 +257,7 @@ impl<'a> Help<'a> { if arg.short().is_some() { try!(write!(self.writer, ", ")); } - try!(color!(self, "--{}", l, Good)); + try!(color!(self, "--{}", l, good)); if !self.next_line_help || !arg.is_set(ArgSettings::NextLineHelp) { write_nspaces!(self.writer, (longest + 4) - (l.len() + 2)); } @@ -243,27 +280,27 @@ impl<'a> Help<'a> { if let Some(ref vec) = arg.val_names() { let mut it = vec.iter().peekable(); while let Some((_, val)) = it.next() { - try!(color!(self, "<{}>", val, Good)); + try!(color!(self, "<{}>", val, good)); if it.peek().is_some() { try!(write!(self.writer, " ")); } } let num = vec.len(); if arg.is_set(ArgSettings::Multiple) && num == 1 { - try!(color!(self, "...", Good)); + try!(color!(self, "...", good)); } } else if let Some(num) = arg.num_vals() { let mut it = (0..num).peekable(); while let Some(_) = it.next() { - try!(color!(self, "<{}>", arg.name(), Good)); + try!(color!(self, "<{}>", arg.name(), good)); if it.peek().is_some() { try!(write!(self.writer, " ")); } } } else if arg.has_switch() { - try!(color!(self, "<{}>", arg.name(), Good)); + try!(color!(self, "<{}>", arg.name(), good)); } else { - try!(color!(self, "{}", arg, Good)); + try!(color!(self, "{}", arg, good)); } if arg.has_switch() { if !(self.next_line_help || arg.is_set(ArgSettings::NextLineHelp)) { @@ -404,9 +441,9 @@ impl<'a> Help<'a> { debugln!("Writing defaults"); return format!(" [default: {}] {}", if self.color { - format!("{}", Format::Good(pv)) + self.cizer.good(pv) } else { - pv.to_string() + Format::None(pv) }, if self.hide_pv { "".into() @@ -415,7 +452,7 @@ impl<'a> Help<'a> { if self.color { format!(" [values: {}]", pv.iter() - .map(|v| format!("{}", Format::Good(v))) + .map(|v| format!("{}", self.cizer.good(v))) .collect::>() .join(", ")) } else { @@ -432,7 +469,7 @@ impl<'a> Help<'a> { return if self.color { format!(" [values: {}]", pv.iter() - .map(|v| format!("{}", Format::Good(v))) + .map(|v| format!("{}", self.cizer.good(v))) .collect::>() .join(", ")) } else { @@ -464,23 +501,20 @@ impl<'a> Help<'a> { let opts_flags = parser.iter_flags() .map(as_arg_trait) .chain(parser.iter_opts().map(as_arg_trait)); - try!(color!(self, "OPTIONS:\n", Warning)); + try!(color!(self, "OPTIONS:\n", warning)); try!(self.write_args(opts_flags)); - first = false; } else { if flags { - try!(color!(self, "FLAGS:\n", Warning)); + try!(color!(self, "FLAGS:\n", warning)); try!(self.write_args(parser.iter_flags() .map(as_arg_trait))); - first = false; } if opts { if !first { try!(self.writer.write(b"\n")); } - try!(color!(self, "OPTIONS:\n", Warning)); + try!(color!(self, "OPTIONS:\n", warning)); try!(self.write_args(parser.iter_opts().map(as_arg_trait))); - first = false; } } @@ -490,14 +524,11 @@ impl<'a> Help<'a> { } try!(color!(self, "ARGS:\n", Warning)); try!(self.write_args_unsorted(parser.iter_positionals().map(as_arg_trait))); - first = false; } if subcmds { - if !first { - try!(self.writer.write(b"\n")); - } - try!(color!(self, "SUBCOMMANDS:\n", Warning)); + try!(self.writer.write(b"\n\n")); + try!(color!(self, "SUBCOMMANDS:\n", warning)); try!(self.write_subcommands(&parser)); } @@ -506,6 +537,7 @@ impl<'a> Help<'a> { /// Writes help for subcommands of a Parser Object to the wrapped stream. fn write_subcommands(&mut self, parser: &Parser) -> io::Result<()> { + debugln!("exec=write_subcommands;"); let mut longest = 0; let mut ord_m = VecMap::new(); @@ -515,8 +547,16 @@ impl<'a> Help<'a> { longest = cmp::max(longest, sc.p.meta.name.len()); } + let mut first = true; for (_, btm) in ord_m.into_iter() { for (_, sc) in btm.into_iter() { + if !first { + debugln!("Writing newline..."); + try!(self.writer.write(b"\n")); + } else { + first = false; + } + debugln!("Writing sc...{}", sc); try!(self.write_arg(sc, longest)); } } @@ -534,18 +574,23 @@ impl<'a> Help<'a> { if let Some(bn) = parser.meta.bin_name.as_ref() { if bn.contains(' ') { // Incase we're dealing with subcommands i.e. git mv is translated to git-mv - try!(color!(self, bn.replace(" ", "-"), Good)) + try!(color!(self, bn.replace(" ", "-"), good)) } else { - try!(color!(self, &parser.meta.name[..], Good)) + try!(color!(self, &parser.meta.name[..], good)) } } else { - try!(color!(self, &parser.meta.name[..], Good)) + try!(color!(self, &parser.meta.name[..], good)) } Ok(()) } /// Writes default help for a Parser Object to the wrapped stream. pub fn write_default_help(&mut self, parser: &Parser) -> ClapResult<()> { + debugln!("fn=write_default_help;"); + if let Some(h) = parser.meta.pre_help { + try!(write!(self.writer, "{}", h)); + try!(self.writer.write(b"\n\n")); + } // Print the version try!(self.write_bin_name(&parser)); @@ -559,7 +604,7 @@ impl<'a> Help<'a> { try!(write!(self.writer, "{}\n", about)); } - try!(color!(self, "\nUSAGE:", Warning)); + try!(color!(self, "\nUSAGE:", warning)); try!(write!(self.writer, "\n{}{}\n\n", TAB, @@ -575,7 +620,10 @@ impl<'a> Help<'a> { } if let Some(h) = parser.meta.more_help { - try!(write!(self.writer, "{}\n", h)); + if flags || opts || pos || subcmds { + try!(self.writer.write(b"\n\n")); + } + try!(write!(self.writer, "{}", h)); } self.writer.flush().map_err(Error::from) @@ -621,7 +669,7 @@ fn copy_until(r: &mut R, w: &mut W, delimiter_byte: u8) -> Co /// Copies the contents of a reader into a writer until a {tag} is found, /// copying the tag content to a buffer and returning its size. -/// In addition to Errors, there are three possible outputs: +/// In addition to errors, there are three possible outputs: /// - None: The reader was consumed. /// - Some(Ok(0)): No tag was captured but the reader still contains data. /// - Some(Ok(length>0)): a tag with `length` was captured to the tag_buffer. @@ -708,6 +756,7 @@ impl<'a> Help<'a> { /// The template system is, on purpose, very simple. Therefore the tags have to writen /// in the lowercase and without spacing. fn write_templated_help(&mut self, parser: &Parser, template: &str) -> ClapResult<()> { + debugln!("fn=write_templated_help;"); let mut tmplr = Cursor::new(&template); let mut tag_buf = Cursor::new(vec![0u8; 15]); @@ -725,6 +774,12 @@ impl<'a> Help<'a> { _ => continue, }; + debugln!("iter;tag_buf={};", unsafe { + String::from_utf8_unchecked(tag_buf.get_ref()[0..tag_length] + .iter() + .map(|&i|i) + .collect::>()) + }); match &tag_buf.get_ref()[0..tag_length] { b"?" => { try!(self.writer.write(b"Could not decode tag name")); @@ -791,7 +846,6 @@ impl<'a> Help<'a> { try!(self.writer.write(b"}")); } } - } } } diff --git a/src/app/macros.rs b/src/app/macros.rs index 56b612f0..35f6367b 100644 --- a/src/app/macros.rs +++ b/src/app/macros.rs @@ -102,7 +102,7 @@ macro_rules! validate_multiples { debugln!("macro=validate_multiples!;"); if $m.contains(&$a.name) && !$a.settings.is_set(ArgSettings::Multiple) { // Not the first time, and we don't allow multiples - return Err(Error::unexpected_multiple_usage($a, &*$_self.create_current_usage($m))) + return Err(Error::unexpected_multiple_usage($a, &*$_self.create_current_usage($m), $_self.color())) } }; } diff --git a/src/app/parser.rs b/src/app/parser.rs index ecdaaa85..1dd68a69 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -20,7 +20,7 @@ use INVALID_UTF8; use suggestions; use INTERNAL_ERROR_MSG; use SubCommand; -use fmt::Format; +use fmt::{Format, ColorWhen}; use osstringext::OsStrExt2; use app::meta::AppMeta; use args::MatchedArg; @@ -527,7 +527,8 @@ impl<'a, 'b> Parser<'a, 'b> self.meta .bin_name .as_ref() - .unwrap_or(&self.meta.name))); + .unwrap_or(&self.meta.name), + self.color())); } } sc.clone() @@ -547,7 +548,8 @@ impl<'a, 'b> Parser<'a, 'b> .bin_name .as_ref() .unwrap_or(&self.meta.name), - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } @@ -561,7 +563,7 @@ impl<'a, 'b> Parser<'a, 'b> if let None = a.to_str() { if !self.settings.is_set(AppSettings::StrictUtf8) { return Err( - Error::invalid_utf8(&*self.create_current_usage(matcher)) + Error::invalid_utf8(&*self.create_current_usage(matcher), self.color()) ); } } @@ -575,7 +577,8 @@ impl<'a, 'b> Parser<'a, 'b> } else { return Err(Error::unknown_argument(&*arg_os.to_string_lossy(), "", - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } } @@ -591,7 +594,7 @@ impl<'a, 'b> Parser<'a, 'b> true }; if should_err { - return Err(Error::empty_value(o, &*self.create_current_usage(matcher))); + return Err(Error::empty_value(o, &*self.create_current_usage(matcher), self.color())); } } else { return Err(Error::empty_value(self.positionals @@ -599,7 +602,8 @@ impl<'a, 'b> Parser<'a, 'b> .filter(|p| &p.name == &a) .next() .expect(INTERNAL_ERROR_MSG), - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } @@ -635,10 +639,10 @@ impl<'a, 'b> Parser<'a, 'b> try!(self.parse_subcommand(sc_name, matcher, it)); } else if self.is_set(AppSettings::SubcommandRequired) { let bn = self.meta.bin_name.as_ref().unwrap_or(&self.meta.name); - return Err(Error::missing_subcommand(bn, &self.create_current_usage(matcher))); + return Err(Error::missing_subcommand(bn, &self.create_current_usage(matcher), self.color())); } else if self.is_set(AppSettings::SubcommandRequiredElseHelp) { let mut out = vec![]; - try!(self.write_help(&mut out)); + try!(self.write_help_err(&mut out)); return Err(Error { message: String::from_utf8_lossy(&*out).into_owned(), kind: ErrorKind::MissingArgumentOrSubcommand, @@ -648,7 +652,7 @@ impl<'a, 'b> Parser<'a, 'b> if matcher.is_empty() && matcher.subcommand_name().is_none() && self.is_set(AppSettings::ArgRequiredElseHelp) { let mut out = vec![]; - try!(self.write_help(&mut out)); + try!(self.write_help_err(&mut out)); return Err(Error { message: String::from_utf8_lossy(&*out).into_owned(), kind: ErrorKind::MissingArgumentOrSubcommand, @@ -1070,7 +1074,8 @@ impl<'a, 'b> Parser<'a, 'b> arg.push(c); return Err(Error::unknown_argument(&*arg, "", - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } Ok(None) @@ -1089,7 +1094,7 @@ impl<'a, 'b> Parser<'a, 'b> let v = fv.trim_left_matches(b'='); if !opt.is_set(ArgSettings::EmptyValues) && v.len_() == 0 { sdebugln!("Found Empty - Error"); - return Err(Error::empty_value(opt, &*self.create_current_usage(matcher))); + return Err(Error::empty_value(opt, &*self.create_current_usage(matcher), self.color())); } sdebugln!("Found - {:?}, len: {}", v, v.len_()); try!(self.add_val_to_arg(opt, v, matcher)); @@ -1161,7 +1166,7 @@ impl<'a, 'b> Parser<'a, 'b> { debugln!("fn=validate_value; val={:?}", val); if self.is_set(AppSettings::StrictUtf8) && val.to_str().is_none() { - return Err(Error::invalid_utf8(&*self.create_current_usage(matcher))); + return Err(Error::invalid_utf8(&*self.create_current_usage(matcher), self.color())); } if let Some(ref p_vals) = arg.possible_vals() { let val_str = val.to_string_lossy(); @@ -1169,16 +1174,17 @@ impl<'a, 'b> Parser<'a, 'b> return Err(Error::invalid_value(val_str, p_vals, arg, - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } if !arg.is_set(ArgSettings::EmptyValues) && val.is_empty_() && matcher.contains(&*arg.name()) { - return Err(Error::empty_value(arg, &*self.create_current_usage(matcher))); + return Err(Error::empty_value(arg, &*self.create_current_usage(matcher), self.color())); } if let Some(ref vtor) = arg.validator() { if let Err(e) = vtor(val.to_string_lossy().into_owned()) { - return Err(Error::value_validation(e)); + return Err(Error::value_validation(e, self.color())); } } if matcher.needs_more_vals(arg) { @@ -1212,19 +1218,19 @@ impl<'a, 'b> Parser<'a, 'b> let usg = $me.create_current_usage($matcher); if let Some(f) = $me.flags.iter().filter(|f| f.name == $name).next() { debugln!("It was a flag..."); - Error::argument_conflict(f, c_with, &*usg) + Error::argument_conflict(f, c_with, &*usg, self.color()) } else if let Some(o) = $me.opts.iter() .filter(|o| o.name == $name) .next() { debugln!("It was an option..."); - Error::argument_conflict(o, c_with, &*usg) + Error::argument_conflict(o, c_with, &*usg, self.color()) } else { match $me.positionals.values() .filter(|p| p.name == $name) .next() { Some(p) => { debugln!("It was a positional..."); - Error::argument_conflict(p, c_with, &*usg) + Error::argument_conflict(p, c_with, &*usg, self.color()) }, None => panic!(INTERNAL_ERROR_MSG) } @@ -1300,7 +1306,8 @@ impl<'a, 'b> Parser<'a, 'b> } else { "ere" }, - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } if let Some(num) = a.max_vals() { @@ -1316,7 +1323,8 @@ impl<'a, 'b> Parser<'a, 'b> .to_str() .expect(INVALID_UTF8), a, - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } if let Some(num) = a.min_vals() { @@ -1326,7 +1334,8 @@ impl<'a, 'b> Parser<'a, 'b> return Err(Error::too_few_values(a, num, ma.vals.len(), - &*self.create_current_usage(matcher))); + &*self.create_current_usage(matcher), + self.color())); } } Ok(()) @@ -1371,7 +1380,8 @@ impl<'a, 'b> Parser<'a, 'b> .iter() .fold(String::new(), |acc, s| acc + &format!("\n {}", Format::Error(s))[..]), - &*self.create_current_usage(matcher)) + &*self.create_current_usage(matcher), + self.color()) }; return Err(err); } @@ -1435,7 +1445,7 @@ impl<'a, 'b> Parser<'a, 'b> } let used_arg = format!("--{}", arg); - Err(Error::unknown_argument(&*used_arg, &*suffix.0, &*self.create_current_usage(matcher))) + Err(Error::unknown_argument(&*used_arg, &*suffix.0, &*self.create_current_usage(matcher), self.color())) } // Creates a usage string if one was not provided by the user manually. This happens just @@ -1575,6 +1585,10 @@ impl<'a, 'b> Parser<'a, 'b> Help::write_parser_help(w, &self) } + pub fn write_help_err(&self, w: &mut W) -> ClapResult<()> { + Help::write_parser_help_to_stderr(w, &self) + } + fn add_defaults(&mut self, matcher: &mut ArgMatcher<'a>) -> ClapResult<()> { macro_rules! add_val { ($_self:ident, $a:ident, $m:ident) => { @@ -1606,6 +1620,23 @@ impl<'a, 'b> Parser<'a, 'b> pub fn iter_positionals(&self) -> vec_map::Values { self.positionals.values() } + + // Should we color the output? None=determined by output location, true=yes, false=no + #[doc(hidden)] + pub fn color(&self) -> ColorWhen { + debugln!("exec=color;"); + debug!("Color setting..."); + if self.is_set(AppSettings::ColorNever) { + sdebugln!("Never"); + ColorWhen::Never + } else if self.is_set(AppSettings::ColorAlways) { + sdebugln!("Always"); + ColorWhen::Always + } else { + sdebugln!("Auto"); + ColorWhen::Auto + } + } } impl<'a, 'b> Clone for Parser<'a, 'b> diff --git a/src/app/settings.rs b/src/app/settings.rs index 752ddb27..62b09c13 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -3,29 +3,32 @@ use std::ascii::AsciiExt; bitflags! { flags Flags: u32 { - const SC_NEGATE_REQS = 0b00000000000000000000001, - const SC_REQUIRED = 0b00000000000000000000010, - const A_REQUIRED_ELSE_HELP = 0b00000000000000000000100, - const GLOBAL_VERSION = 0b00000000000000000001000, - const VERSIONLESS_SC = 0b00000000000000000010000, - const UNIFIED_HELP = 0b00000000000000000100000, - const WAIT_ON_ERROR = 0b00000000000000001000000, - const SC_REQUIRED_ELSE_HELP= 0b00000000000000010000000, - const NEEDS_LONG_HELP = 0b00000000000000100000000, - const NEEDS_LONG_VERSION = 0b00000000000001000000000, - const NEEDS_SC_HELP = 0b00000000000010000000000, - const DISABLE_VERSION = 0b00000000000100000000000, - const HIDDEN = 0b00000000001000000000000, - const TRAILING_VARARG = 0b00000000010000000000000, - const NO_BIN_NAME = 0b00000000100000000000000, - const ALLOW_UNK_SC = 0b00000001000000000000000, - const UTF8_STRICT = 0b00000010000000000000000, - const UTF8_NONE = 0b00000100000000000000000, - const LEADING_HYPHEN = 0b00001000000000000000000, - const NO_POS_VALUES = 0b00010000000000000000000, - const NEXT_LINE_HELP = 0b00100000000000000000000, - const DERIVE_DISP_ORDER = 0b01000000000000000000000, - const COLORED_HELP = 0b10000000000000000000000, + const SC_NEGATE_REQS = 0b00000000000000000000000001, + const SC_REQUIRED = 0b00000000000000000000000010, + const A_REQUIRED_ELSE_HELP = 0b00000000000000000000000100, + const GLOBAL_VERSION = 0b00000000000000000000001000, + const VERSIONLESS_SC = 0b00000000000000000000010000, + const UNIFIED_HELP = 0b00000000000000000000100000, + const WAIT_ON_ERROR = 0b00000000000000000001000000, + const SC_REQUIRED_ELSE_HELP= 0b00000000000000000010000000, + const NEEDS_LONG_HELP = 0b00000000000000000100000000, + const NEEDS_LONG_VERSION = 0b00000000000000001000000000, + const NEEDS_SC_HELP = 0b00000000000000010000000000, + const DISABLE_VERSION = 0b00000000000000100000000000, + const HIDDEN = 0b00000000000001000000000000, + const TRAILING_VARARG = 0b00000000000010000000000000, + const NO_BIN_NAME = 0b00000000000100000000000000, + const ALLOW_UNK_SC = 0b00000000001000000000000000, + const UTF8_STRICT = 0b00000000010000000000000000, + const UTF8_NONE = 0b00000000100000000000000000, + const LEADING_HYPHEN = 0b00000001000000000000000000, + const NO_POS_VALUES = 0b00000010000000000000000000, + const NEXT_LINE_HELP = 0b00000100000000000000000000, + const DERIVE_DISP_ORDER = 0b00001000000000000000000000, + const COLORED_HELP = 0b00010000000000000000000000, + const COLOR_ALWAYS = 0b00100000000000000000000000, + const COLOR_AUTO = 0b01000000000000000000000000, + const COLOR_NEVER = 0b10000000000000000000000000, } } @@ -41,7 +44,7 @@ impl Clone for AppFlags { impl Default for AppFlags { fn default() -> Self { - AppFlags(NEEDS_LONG_VERSION | NEEDS_LONG_HELP | NEEDS_SC_HELP | UTF8_NONE) + AppFlags(NEEDS_LONG_VERSION | NEEDS_LONG_HELP | NEEDS_SC_HELP | UTF8_NONE | COLOR_AUTO) } } @@ -73,7 +76,10 @@ impl AppFlags { HidePossibleValuesInHelp => NO_POS_VALUES, NextLineHelp => NEXT_LINE_HELP, ColoredHelp => COLORED_HELP, - DeriveDisplayOrder => DERIVE_DISP_ORDER + DeriveDisplayOrder => DERIVE_DISP_ORDER, + ColorAlways => COLOR_ALWAYS, + ColorAuto => COLOR_AUTO, + ColorNever => COLOR_NEVER } } @@ -492,6 +498,59 @@ pub enum AppSettings { /// .get_matches(); /// ``` ColoredHelp, + /// Enables colored output only when the output is going to a terminal or TTY. + /// + /// **NOTE:** This is the default behavior of `clap` + /// + /// **NOTE:** Must be compiled with the `color` cargo feature + /// + /// # Platform Specific + /// + /// This setting only applies to Unix, Linux, and OSX (i.e. non-Windows platforms) + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, SubCommand, AppSettings}; + /// App::new("myprog") + /// .setting(AppSettings::ColorAuto) + /// .get_matches(); + /// ``` + ColorAuto, + /// Enables colored output regardless of whether or not the output is going to a terminal/TTY. + /// + /// **NOTE:** Must be compiled with the `color` cargo feature + /// + /// # Platform Specific + /// + /// This setting only applies to Unix, Linux, and OSX (i.e. non-Windows platforms) + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, SubCommand, AppSettings}; + /// App::new("myprog") + /// .setting(AppSettings::ColorAlways) + /// .get_matches(); + /// ``` + ColorAlways, + /// Disables colored output no matter if the output is going to a terminal/TTY, or not. + /// + /// **NOTE:** Must be compiled with the `color` cargo feature + /// + /// # Platform Specific + /// + /// This setting only applies to Unix, Linux, and OSX (i.e. non-Windows platforms) + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, SubCommand, AppSettings}; + /// App::new("myprog") + /// .setting(AppSettings::ColorNever) + /// .get_matches(); + /// ``` + ColorNever, #[doc(hidden)] NeedsLongVersion, #[doc(hidden)] diff --git a/src/errors.rs b/src/errors.rs index aa5db714..9ef18e98 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,7 +6,7 @@ use std::io::{self, Write}; use std::convert::From; use std::result::Result as StdResult; -use fmt::Format; +use fmt; use suggestions; use args::any_arg::AnyArg; @@ -14,6 +14,7 @@ use args::any_arg::AnyArg; /// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html pub type Result = StdResult; + /// Command line argument parser kind of error #[derive(Debug, Copy, Clone, PartialEq)] pub enum ErrorKind { @@ -382,54 +383,62 @@ impl Error { } #[doc(hidden)] - pub fn argument_conflict<'a, 'b, A, O, U>(arg: &A, other: Option, usage: U) -> Self + pub fn argument_conflict<'a, 'b, A, O, U>(arg: &A, other: Option, usage: U, color: fmt::ColorWhen) -> Self where A: AnyArg<'a, 'b> + Display, O: Into, U: Display { let mut v = vec![arg.name().to_owned()]; + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The argument '{}' cannot be used with {}\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(arg.to_string()), + c.error("error:"), + c.warning(&*arg.to_string()), match other { Some(name) => { let n = name.into(); v.push(n.clone()); - format!("'{}'", Format::Warning(n)) + c.warning(format!("'{}'", n)) } - None => "one or more of the other specified arguments".to_owned(), + None => c.none("one or more of the other specified arguments".to_owned()), }, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::ArgumentConflict, - info: Some(v), + info: Some(v) } } #[doc(hidden)] - pub fn empty_value<'a, 'b, A, U>(arg: &A, usage: U) -> Self + pub fn empty_value<'a, 'b, A, U>(arg: &A, usage: U, color: fmt::ColorWhen) -> Self where A: AnyArg<'a, 'b> + Display, U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The argument '{}' requires a value but none was supplied\ \n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(arg.to_string()), + c.error("error:"), + c.warning(arg.to_string()), usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::EmptyValue, info: Some(vec![arg.name().to_owned()]), } } #[doc(hidden)] - pub fn invalid_value<'a, 'b, B, G, A, U>(bad_val: B, good_vals: &[G], arg: &A, usage: U) -> Self + pub fn invalid_value<'a, 'b, B, G, A, U>(bad_val: B, good_vals: &[G], arg: &A, usage: U, color: fmt::ColorWhen) -> Self where B: AsRef, G: AsRef + Display, A: AnyArg<'a, 'b> + Display, @@ -446,32 +455,40 @@ impl Error { } sorted.sort(); let valid_values = sorted.join(" "); + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} '{}' isn't a valid value for '{}'\n\t\ [values:{}]\n\ {}\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(bad_val.as_ref()), - Format::Warning(arg.to_string()), + c.error("error:"), + c.warning(bad_val.as_ref()), + c.warning(arg.to_string()), valid_values, suffix.0, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::InvalidValue, info: Some(vec![arg.name().to_owned(), bad_val.as_ref().to_owned()]), } } #[doc(hidden)] - pub fn invalid_subcommand(subcmd: S, did_you_mean: D, name: N, usage: U) -> Self + pub fn invalid_subcommand(subcmd: S, did_you_mean: D, name: N, usage: U, color: fmt::ColorWhen) -> Self where S: Into, D: AsRef + Display, N: Display, U: Display { let s = subcmd.into(); + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The subcommand '{}' wasn't recognized\n\t\ Did you mean '{}' ?\n\n\ @@ -479,70 +496,82 @@ impl Error { re-running with '{} {} {}'\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(&*s), - Format::Good(did_you_mean.as_ref()), + c.error("error:"), + c.warning(&*s), + c.good(did_you_mean.as_ref()), name, - Format::Good("--"), + c.good("--"), &*s, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::InvalidSubcommand, info: Some(vec![s]), } } #[doc(hidden)] - pub fn unrecognized_subcommand(subcmd: S, name: N) -> Self + pub fn unrecognized_subcommand(subcmd: S, name: N, color: fmt::ColorWhen) -> Self where S: Into, N: Display { let s = subcmd.into(); + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The subcommand '{}' wasn't recognized\n\n\ USAGE:\n\t\ {} help ...\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(&*s), + c.error("error:"), + c.warning(&*s), name, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::UnrecognizedSubcommand, info: Some(vec![s]), } } #[doc(hidden)] - pub fn missing_required_argument(required: R, usage: U) -> Self + pub fn missing_required_argument(required: R, usage: U, color: fmt::ColorWhen) -> Self where R: Display, U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The following required arguments were not provided:{}\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), + c.error("error:"), required, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::MissingRequiredArgument, info: None, } } #[doc(hidden)] - pub fn missing_subcommand(name: N, usage: U) -> Self + pub fn missing_subcommand(name: N, usage: U, color: fmt::ColorWhen) -> Self where N: AsRef + Display, U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} '{}' requires a subcommand, but one was not provided\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(name), + c.error("error:"), + c.warning(name), usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::MissingSubcommand, info: None, } @@ -550,158 +579,208 @@ impl Error { #[doc(hidden)] - pub fn invalid_utf8(usage: U) -> Self + pub fn invalid_utf8(usage: U, color: fmt::ColorWhen) -> Self where U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} Invalid UTF-8 was detected in one or more arguments\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), + c.error("error:"), usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::InvalidUtf8, info: None, } } #[doc(hidden)] - pub fn too_many_values<'a, 'b, V, A, U>(val: V, arg: &A, usage: U) -> Self + pub fn too_many_values<'a, 'b, V, A, U>(val: V, arg: &A, usage: U, color: fmt::ColorWhen) -> Self where V: AsRef + Display + ToOwned, A: AnyArg<'a, 'b> + Display, U: Display { let v = val.as_ref(); + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The value '{}' was provided to '{}', but it wasn't expecting \ any more values\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(v), - Format::Warning(arg.to_string()), + c.error("error:"), + c.warning(v), + c.warning(arg.to_string()), usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::TooManyValues, info: Some(vec![arg.name().to_owned(), v.to_owned()]), } } #[doc(hidden)] - pub fn too_few_values<'a, 'b, A, U>(arg: &A, min_vals: u64, curr_vals: usize, usage: U) -> Self + pub fn too_few_values<'a, 'b, A, U>(arg: &A, min_vals: u64, curr_vals: usize, usage: U, color: fmt::ColorWhen) -> Self where A: AnyArg<'a, 'b> + Display, U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The argument '{}' requires at least {} values, but only {} w{} \ provided\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(arg.to_string()), - Format::Warning(min_vals.to_string()), - Format::Warning(curr_vals.to_string()), + c.error("error:"), + c.warning(arg.to_string()), + c.warning(min_vals.to_string()), + c.warning(curr_vals.to_string()), if curr_vals > 1 { "ere" } else { "as" }, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::TooFewValues, info: Some(vec![arg.name().to_owned()]), } } #[doc(hidden)] - pub fn value_validation(err: String) -> Self { + pub fn value_validation(err: String, color: fmt::ColorWhen) -> Self { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { - message: format!("{} {}", Format::Error("error:"), err), + message: format!("{} {}", c.error("error:"), err), kind: ErrorKind::ValueValidation, info: None, } } + #[doc(hidden)] + pub fn value_validation_auto(err: String) -> Self { + Error::value_validation(err, fmt::ColorWhen::Auto) + } + #[doc(hidden)] pub fn wrong_number_of_values<'a, 'b, A, S, U>(arg: &A, num_vals: u64, curr_vals: usize, suffix: S, - usage: U) + usage: U, color: fmt::ColorWhen) -> Self where A: AnyArg<'a, 'b> + Display, S: Display, U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The argument '{}' requires {} values, but {} w{} \ provided\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(arg.to_string()), - Format::Warning(num_vals.to_string()), - Format::Warning(curr_vals.to_string()), + c.error("error:"), + c.warning(arg.to_string()), + c.warning(num_vals.to_string()), + c.warning(curr_vals.to_string()), suffix, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::WrongNumberOfValues, info: Some(vec![arg.name().to_owned()]), } } #[doc(hidden)] - pub fn unexpected_multiple_usage<'a, 'b, A, U>(arg: &A, usage: U) -> Self + pub fn unexpected_multiple_usage<'a, 'b, A, U>(arg: &A, usage: U, color: fmt::ColorWhen) -> Self where A: AnyArg<'a, 'b> + Display, U: Display { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} The argument '{}' was provided more than once, but cannot \ be used multiple times\n\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(arg.to_string()), + c.error("error:"), + c.warning(arg.to_string()), usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::UnexpectedMultipleUsage, info: Some(vec![arg.name().to_owned()]), } } #[doc(hidden)] - pub fn unknown_argument(arg: A, did_you_mean: &str, usage: U) -> Self + pub fn unknown_argument(arg: A, did_you_mean: &str, usage: U, color: fmt::ColorWhen) -> Self where A: Into, U: Display { let a = arg.into(); + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; Error { message: format!("{} Found argument '{}' which wasn't expected, or isn't valid in \ this context{}\n\ {}\n\n\ For more information try {}", - Format::Error("error:"), - Format::Warning(&*a), + c.error("error:"), + c.warning(&*a), if did_you_mean.is_empty() { "\n".to_owned() } else { format!("{}\n", did_you_mean) }, usage, - Format::Good("--help")), + c.good("--help")), kind: ErrorKind::UnknownArgument, info: Some(vec![a]), } } #[doc(hidden)] - pub fn argument_not_found(arg: A) -> Self + pub fn io_error(e: &Error, color: fmt::ColorWhen) -> Self { + let c = fmt::Colorizer { + use_stderr: true, + when: color + }; + Error { + message: format!("{} {}", c.error("error:"), e.description()), + kind: ErrorKind::Io, + info: None, + } + } + + #[doc(hidden)] + pub fn argument_not_found_auto(arg: A) -> Self where A: Into { let a = arg.into(); + let c = fmt::Colorizer { + use_stderr: true, + when: fmt::ColorWhen::Auto + }; Error { message: format!("{} The argument '{}' wasn't found", - Format::Error("error:"), + c.error("error:"), a.clone()), kind: ErrorKind::ArgumentNotFound, info: Some(vec![a]), @@ -723,8 +802,12 @@ impl Display for Error { impl From for Error { fn from(e: io::Error) -> Self { + let c = fmt::Colorizer { + use_stderr: true, + when: fmt::ColorWhen::Auto + }; Error { - message: format!("{} {}", Format::Error("error:"), e.description()), + message: format!("{} {}", c.error("error:"), e.description()), kind: ErrorKind::Io, info: None, } @@ -733,8 +816,12 @@ impl From for Error { impl From for Error { fn from(e: std_fmt::Error) -> Self { + let c = fmt::Colorizer { + use_stderr: true, + when: fmt::ColorWhen::Auto + }; Error { - message: format!("{} {}", Format::Error("error:"), e), + message: format!("{} {}", c.error("error:"), e), kind: ErrorKind::Format, info: None, } diff --git a/src/fmt.rs b/src/fmt.rs index f7e2a3be..86fedf76 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -5,6 +5,86 @@ use ansi_term::Colour::{Green, Red, Yellow}; #[cfg(all(feature = "color", not(target_os = "windows")))] use ansi_term::ANSIString; +#[cfg(feature = "color")] +use libc; + +#[cfg(feature = "color")] +const STDERR: i32 = libc::STDERR_FILENO; +#[cfg(feature = "color")] +const STDOUT: i32 = libc::STDOUT_FILENO; + +#[doc(hidden)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ColorWhen { + Auto, + Always, + Never +} + +#[cfg(feature = "color")] +pub fn is_a_tty(stderr: bool) -> bool { + debugln!("exec=is_a_tty;"); + debugln!("Use stderr...{:?}", stderr); + let fd = if stderr { STDERR } else { STDOUT }; + unsafe { libc::isatty(fd) != 0 } +} + +#[cfg(not(feature = "color"))] +pub fn is_a_tty(_: bool) -> bool { + debugln!("exec=is_a_tty;"); + false +} + +#[doc(hidden)] +pub struct Colorizer { + pub use_stderr: bool, + pub when: ColorWhen +} + +macro_rules! color { + ($_self:ident, $c:ident, $m:expr) => { + match $_self.when { + ColorWhen::Auto => if is_a_tty($_self.use_stderr) { + Format::$c($m) + } else { + Format::None($m) + }, + ColorWhen::Always => Format::$c($m), + ColorWhen::Never => Format::None($m), + } + }; +} + +impl Colorizer { + pub fn good(&self, msg: T) -> Format where T: fmt::Display + AsRef { + debugln!("exec=good;"); + color!(self, Good, msg) + } + + pub fn warning(&self, msg: T) -> Format where T: fmt::Display + AsRef { + debugln!("exec=warning;"); + color!(self, Warning, msg) + } + + pub fn error(&self, msg: T) -> Format where T: fmt::Display + AsRef { + debugln!("exec=error;"); + color!(self, Error, msg) + } + + pub fn none(&self, msg: T) -> Format where T: fmt::Display + AsRef { + debugln!("exec=none;"); + Format::None(msg) + } +} + +impl Default for Colorizer { + fn default() -> Self { + Colorizer { + use_stderr: true, + when: ColorWhen::Auto + } + } +} /// Defines styles for different types of error messages. Defaults to Error=Red, Warning=Yellow, /// and Good=Green @@ -17,6 +97,8 @@ pub enum Format { Warning(T), /// Defines the style used for good values, defaults to Green Good(T), + /// Defines no formatting style + None(T), } #[cfg(all(feature = "color", not(target_os = "windows")))] @@ -26,17 +108,11 @@ impl> Format { Format::Error(ref e) => Red.bold().paint(e.as_ref()), Format::Warning(ref e) => Yellow.paint(e.as_ref()), Format::Good(ref e) => Green.paint(e.as_ref()), + Format::None(ref e) => ANSIString::from(e.as_ref()), } } } -#[cfg(all(feature = "color", not(target_os = "windows")))] -impl> fmt::Display for Format { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", &self.format()) - } -} - #[cfg(any(not(feature = "color"), target_os = "windows"))] impl Format { fn format(&self) -> &T { @@ -44,10 +120,19 @@ impl Format { Format::Error(ref e) => e, Format::Warning(ref e) => e, Format::Good(ref e) => e, + Format::None(ref e) => e, } } } + +#[cfg(all(feature = "color", not(target_os = "windows")))] +impl> fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.format()) + } +} + #[cfg(any(not(feature = "color"), target_os = "windows"))] impl fmt::Display for Format { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -59,6 +144,7 @@ impl fmt::Display for Format { mod test { use super::Format; use ansi_term::Colour::{Green, Red, Yellow}; + use ansi_term::ANSIString; #[test] fn colored_output() { @@ -69,5 +155,7 @@ mod test { assert_eq!(&*format!("{}", good), &*format!("{}", Green.paint("good"))); let warn = Format::Warning("warn"); assert_eq!(&*format!("{}", warn), &*format!("{}", Yellow.paint("warn"))); + let none = Format::None("none"); + assert_eq!(&*format!("{}", none), &*format!("{}", ANSIString::from("none"))); } } diff --git a/src/macros.rs b/src/macros.rs index 77cd13b7..155ee903 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -63,11 +63,11 @@ macro_rules! value_t { match v.parse::<$t>() { Ok(val) => Ok(val), Err(_) => - Err(::clap::Error::value_validation( + Err(::clap::Error::value_validation_auto( format!("The argument '{}' isn't a valid value", v))), } } else { - Err(::clap::Error::argument_not_found($v)) + Err(::clap::Error::argument_not_found_auto($v)) } }; } @@ -108,11 +108,11 @@ macro_rules! value_t_or_exit { match v.parse::<$t>() { Ok(val) => val, Err(_) => - ::clap::Error::value_validation( + ::clap::Error::value_validation_auto( format!("The argument '{}' isn't a valid value", v)).exit(), } } else { - ::clap::Error::argument_not_found($v).exit() + ::clap::Error::argument_not_found_auto($v).exit() } }; } @@ -159,7 +159,7 @@ macro_rules! values_t { match pv.parse::<$t>() { Ok(rv) => tmp.push(rv), Err(..) => { - err = Some(::clap::Error::value_validation( + err = Some(::clap::Error::value_validation_auto( format!("The argument '{}' isn't a valid value", pv))); break } @@ -170,7 +170,7 @@ macro_rules! values_t { None => Ok(tmp), } } else { - Err(::clap::Error::argument_not_found($v)) + Err(::clap::Error::argument_not_found_auto($v)) } }; } @@ -215,11 +215,11 @@ macro_rules! values_t_or_exit { ($m:ident.values_of($v:expr), $t:ty) => { if let Some(vals) = $m.values_of($v) { vals.map(|v| v.parse::<$t>().unwrap_or_else(|_|{ - ::clap::Error::value_validation( + ::clap::Error::value_validation_auto( format!("One or more arguments aren't valid values")).exit() })).collect::>() } else { - ::clap::Error::argument_not_found($v).exit() + ::clap::Error::argument_not_found_auto($v).exit() } }; } From cda27469cd8e0ff408360f7701b6fc577ed87a65 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 04:48:13 -0400 Subject: [PATCH 02/16] imp: removes extra newline when printing version --- src/app/parser.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/parser.rs b/src/app/parser.rs index 1dd68a69..00b3072d 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -1552,22 +1552,22 @@ impl<'a, 'b> Parser<'a, 'b> w.flush().map_err(Error::from) } - fn write_version(&self, w: &mut W) -> io::Result<()> { + pub fn write_version(&self, w: &mut W) -> io::Result<()> { if let Some(bn) = self.meta.bin_name.as_ref() { if bn.contains(' ') { // Incase we're dealing with subcommands i.e. git mv is translated to git-mv - writeln!(w, + write!(w, "{} {}", bn.replace(" ", "-"), self.meta.version.unwrap_or("".into())) } else { - writeln!(w, + write!(w, "{} {}", &self.meta.name[..], self.meta.version.unwrap_or("".into())) } } else { - writeln!(w, + write!(w, "{} {}", &self.meta.name[..], self.meta.version.unwrap_or("".into())) From a7401dc6a1a9a43eaf5632d3c8dfec2690435a82 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 04:48:47 -0400 Subject: [PATCH 03/16] imp: allows printing version to any io::Write object --- src/app/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 87c2888c..0bc4102a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -796,6 +796,22 @@ impl<'a, 'b> App<'a, 'b> { Help::write_app_help(w, &self) } + /// Writes the version message to the user to a [`io::Write`] object + /// + /// # Examples + /// + /// ```rust + /// # use clap::App; + /// use std::io; + /// let mut app = App::new("myprog"); + /// let mut out = io::stdout(); + /// app.write_vesrion(&mut out).ok().expect("failed to write to stdout"); + /// ``` + /// [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html + pub fn write_version(&self, w: &mut W) -> ClapResult<()> { + self.p.write_version(w).map_err(From::from) + } + /// Starts the parsing process, upon a failed parse an error will be displayed to the user and /// the process with exit with the appropriate error code. By default this method gets all user /// provided arguments from [`env::args_os`] in order to allow for invalid UTF-8 code points, From c5b24c0eb079ae0a7318ce9a32ffdb83567960f9 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 04:49:13 -0400 Subject: [PATCH 04/16] tests: removes extra newline from version output tests --- clap-test.rs | 10 ++++++++++ src/app/mod.rs | 2 +- tests/version.rs | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/clap-test.rs b/clap-test.rs index d2becbe3..e3425d60 100644 --- a/clap-test.rs +++ b/clap-test.rs @@ -60,6 +60,16 @@ mod test { assert_eq!(str::from_utf8(&help).unwrap(), out); } + pub fn check_version(mut a: App, out: &str) { + // We call a get_matches method to cause --help and --version to be built + let _ = a.get_matches_from_safe_borrow(vec![""]); + + // Now we check the output of print_version() + let mut ver = vec![]; + a.write_version(&mut ver).ok().expect("failed to print help"); + assert_eq!(str::from_utf8(&ver).unwrap(), out); + } + pub fn check_complex_output(args: &str, out: &str) { let mut w = vec![]; let matches = complex_app().get_matches_from(args.split(' ').collect::>()); diff --git a/src/app/mod.rs b/src/app/mod.rs index 0bc4102a..283c8791 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -805,7 +805,7 @@ impl<'a, 'b> App<'a, 'b> { /// use std::io; /// let mut app = App::new("myprog"); /// let mut out = io::stdout(); - /// app.write_vesrion(&mut out).ok().expect("failed to write to stdout"); + /// app.write_version(&mut out).ok().expect("failed to write to stdout"); /// ``` /// [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html pub fn write_version(&self, w: &mut W) -> ClapResult<()> { diff --git a/tests/version.rs b/tests/version.rs index c5fc9154..c51a55ee 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -1,7 +1,12 @@ extern crate clap; +extern crate regex; use clap::{App, ErrorKind}; +include!("../clap-test.rs"); + +static VERSION: &'static str = "clap-test v1.4.8"; + #[test] fn version_short() { let m = App::new("test") @@ -25,3 +30,8 @@ fn version_long() { assert!(m.is_err()); assert_eq!(m.unwrap_err().kind, ErrorKind::VersionDisplayed); } + +#[test] +fn complex_version_output() { + test::check_version(test::complex_app(), VERSION); +} From 054f8cb433e17e40ddade1c6eee52385b30d809a Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 05:39:29 -0400 Subject: [PATCH 05/16] imp: removes extra newline from help output --- src/app/help.rs | 31 +++++++++++++++++++------------ src/app/parser.rs | 1 - 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/app/help.rs b/src/app/help.rs index 15299c10..30751a2f 100644 --- a/src/app/help.rs +++ b/src/app/help.rs @@ -503,31 +503,37 @@ impl<'a> Help<'a> { .chain(parser.iter_opts().map(as_arg_trait)); try!(color!(self, "OPTIONS:\n", warning)); try!(self.write_args(opts_flags)); + first = false; } else { if flags { try!(color!(self, "FLAGS:\n", warning)); try!(self.write_args(parser.iter_flags() .map(as_arg_trait))); + first = false; } if opts { if !first { - try!(self.writer.write(b"\n")); + try!(self.writer.write(b"\n\n")); } try!(color!(self, "OPTIONS:\n", warning)); try!(self.write_args(parser.iter_opts().map(as_arg_trait))); + first = false; } } if pos { if !first { - try!(self.writer.write(b"\n")); + try!(self.writer.write(b"\n\n")); } - try!(color!(self, "ARGS:\n", Warning)); + try!(color!(self, "ARGS:\n", warning)); try!(self.write_args_unsorted(parser.iter_positionals().map(as_arg_trait))); + first = false; } if subcmds { - try!(self.writer.write(b"\n\n")); + if !first { + try!(self.writer.write(b"\n\n")); + } try!(color!(self, "SUBCOMMANDS:\n", warning)); try!(self.write_subcommands(&parser)); } @@ -551,12 +557,12 @@ impl<'a> Help<'a> { for (_, btm) in ord_m.into_iter() { for (_, sc) in btm.into_iter() { if !first { - debugln!("Writing newline..."); + debugln!("Writing newline..."); try!(self.writer.write(b"\n")); } else { first = false; } - debugln!("Writing sc...{}", sc); + debugln!("Writing sc...{}", sc); try!(self.write_arg(sc, longest)); } } @@ -774,12 +780,12 @@ impl<'a> Help<'a> { _ => continue, }; - debugln!("iter;tag_buf={};", unsafe { - String::from_utf8_unchecked(tag_buf.get_ref()[0..tag_length] - .iter() - .map(|&i|i) - .collect::>()) - }); + debugln!("iter;tag_buf={};", unsafe { + String::from_utf8_unchecked(tag_buf.get_ref()[0..tag_length] + .iter() + .map(|&i|i) + .collect::>()) + }); match &tag_buf.get_ref()[0..tag_length] { b"?" => { try!(self.writer.write(b"Could not decode tag name")); @@ -846,6 +852,7 @@ impl<'a> Help<'a> { try!(self.writer.write(b"}")); } } + } } } diff --git a/src/app/parser.rs b/src/app/parser.rs index 00b3072d..773f2605 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -1580,7 +1580,6 @@ impl<'a, 'b> Parser<'a, 'b> self.write_help(&mut buf_w) } - #[cfg_attr(feature = "lints", allow(for_kv_map))] pub fn write_help(&self, w: &mut W) -> ClapResult<()> { Help::write_parser_help(w, &self) } From c8cc87ac1022e507e9fbca90ba651ecce3226556 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 05:39:54 -0400 Subject: [PATCH 06/16] tests: updates help tests to remove extra newline --- tests/app_settings.rs | 4 +-- tests/help.rs | 6 ++-- tests/hidden_args.rs | 2 +- tests/template_help.rs | 79 +++++++++++++++++++++++++++++++----------- 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/tests/app_settings.rs b/tests/app_settings.rs index 54df0c15..7acbbe4b 100644 --- a/tests/app_settings.rs +++ b/tests/app_settings.rs @@ -102,7 +102,7 @@ OPTIONS: -V, --version Prints version information ARGS: - some pos arg\n")); + some pos arg")); } #[test] @@ -135,5 +135,5 @@ OPTIONS: -o, --opt some option ARGS: - some pos arg\n")); + some pos arg")); } diff --git a/tests/help.rs b/tests/help.rs index 41ae717d..2f0925bd 100644 --- a/tests/help.rs +++ b/tests/help.rs @@ -34,8 +34,7 @@ ARGS: SUBCOMMANDS: help Prints this message or the help of the given subcommand(s) - subcmd tests subcommands -"; + subcmd tests subcommands"; static SC_HELP: &'static str = "subcmd 0.1 Kevin K. @@ -51,8 +50,7 @@ OPTIONS: -o, --option ... tests options ARGS: - tests positionals -"; + tests positionals"; #[test] fn help_short() { diff --git a/tests/hidden_args.rs b/tests/hidden_args.rs index 91eced4e..cf52e485 100644 --- a/tests/hidden_args.rs +++ b/tests/hidden_args.rs @@ -30,5 +30,5 @@ FLAGS: -V, --version Prints version information OPTIONS: - --option some option\n")); + --option some option")); } diff --git a/tests/template_help.rs b/tests/template_help.rs index 313479c3..420111e1 100644 --- a/tests/template_help.rs +++ b/tests/template_help.rs @@ -7,6 +7,41 @@ use clap::{App, SubCommand}; static EXAMPLE1_TMPL_S : &'static str = include_str!("example1_tmpl_simple.txt"); static EXAMPLE1_TMPS_F : &'static str = include_str!("example1_tmpl_full.txt"); +static CUSTOM_TEMPL_HELP: &'static str = "MyApp 1.0 +Kevin K. +Does awesome things + +USAGE: + MyApp [FLAGS] [OPTIONS] [SUBCOMMAND] + +FLAGS: + -d Turn debugging information on +OPTIONS: + -c, --config Sets a custom config file +ARGS: + Sets an optional output file +SUBCOMMANDS: + test does testing things"; + +static SIMPLE_TEMPLATE: &'static str = "MyApp 1.0 +Kevin K. +Does awesome things + +USAGE: + MyApp [FLAGS] [OPTIONS] [SUBCOMMAND] + +FLAGS: + -d Turn debugging information on + +OPTIONS: + -c, --config Sets a custom config file + +ARGS: + Sets an optional output file + +SUBCOMMANDS: + test does testing things"; + fn build_new_help(app: &App) -> String { let mut buf = Cursor::new(Vec::with_capacity(50)); app.write_help(&mut buf).unwrap(); @@ -14,27 +49,29 @@ fn build_new_help(app: &App) -> String { String::from_utf8(content).unwrap() } -fn compare2(app1: &App, app2: &App) -> bool { - let hlp1f = build_new_help(&app1); - let hlp1 = hlp1f.trim(); - let hlp2f = build_new_help(&app2); - let hlp2 = hlp2f.trim(); - let b = hlp1 == hlp2; +fn compare_app_str(l: &App, right: &str) -> bool { + let left = build_new_help(&l); + let b = left.trim() == right; if !b { println!(""); - println!("--> hlp1"); - println!("{}", hlp1); - println!("--> hlp2"); - println!("{}", hlp2); + println!("--> left"); + println!("{}", left); + println!("--> right"); + println!("{}", right); println!("--") } b } #[test] -fn comparison_with_template() { - assert!(compare2(&app_example1(), &app_example1().template(EXAMPLE1_TMPL_S))); - assert!(compare2(&app_example1(), &app_example1().template(EXAMPLE1_TMPS_F))); +fn with_template() { + assert!(compare_app_str(&app_example1().template(EXAMPLE1_TMPL_S), SIMPLE_TEMPLATE)); +} + +#[test] +fn custom_template() { + let app = app_example1().template(EXAMPLE1_TMPS_F); + assert!(compare_app_str(&app, CUSTOM_TEMPL_HELP)); } #[test] @@ -44,7 +81,7 @@ fn template_empty() { .author("Kevin K. ") .about("Does awesome things") .template(""); - assert_eq!(build_new_help(&app), ""); + assert!(compare_app_str(&app, "")); } #[test] @@ -53,8 +90,8 @@ fn template_notag() { .version("1.0") .author("Kevin K. ") .about("Does awesome things") - .template(" no tag "); - assert_eq!(build_new_help(&app), " no tag "); + .template("test no tag test"); + assert!(compare_app_str(&app, "test no tag test")); } #[test] @@ -63,8 +100,8 @@ fn template_unknowntag() { .version("1.0") .author("Kevin K. ") .about("Does awesome things") - .template(" {unknown_tag} "); - assert_eq!(build_new_help(&app), " {unknown_tag} "); + .template("test {unknown_tag} test"); + assert!(compare_app_str(&app, "test {unknown_tag} test")); } #[test] @@ -74,7 +111,7 @@ fn template_author_version() { .author("Kevin K. ") .about("Does awesome things") .template("{author}\n{version}\n{about}\n{bin}"); - assert_eq!(build_new_help(&app), "Kevin K. \n1.0\nDoes awesome things\nMyApp"); + assert!(compare_app_str(&app, "Kevin K. \n1.0\nDoes awesome things\nMyApp")); } fn app_example1<'b, 'c>() -> App<'b, 'c> { @@ -83,8 +120,8 @@ fn app_example1<'b, 'c>() -> App<'b, 'c> { .author("Kevin K. ") .about("Does awesome things") .args_from_usage("-c, --config=[FILE] 'Sets a custom config file' - 'Sets an optional output file' - -d... 'Turn debugging information on'") + 'Sets an optional output file' + -d... 'Turn debugging information on'") .subcommand(SubCommand::with_name("test") .about("does testing things") .arg_from_usage("-l, --list 'lists test values'")) From 330e5edf049f8422ac8340ddf878387780fc3a4c Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 06:00:42 -0400 Subject: [PATCH 07/16] fix: fixes issue where before_help wasn't printed --- src/app/help.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/help.rs b/src/app/help.rs index 30751a2f..6cacd100 100644 --- a/src/app/help.rs +++ b/src/app/help.rs @@ -543,7 +543,7 @@ impl<'a> Help<'a> { /// Writes help for subcommands of a Parser Object to the wrapped stream. fn write_subcommands(&mut self, parser: &Parser) -> io::Result<()> { - debugln!("exec=write_subcommands;"); + debugln!("exec=write_subcommands;"); let mut longest = 0; let mut ord_m = VecMap::new(); From 9582b8278952d0e874ca7184cd7f18dc59296e42 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 06:01:00 -0400 Subject: [PATCH 08/16] tests: adds test to check for before_help --- tests/help.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/help.rs b/tests/help.rs index 2f0925bd..30cce9a8 100644 --- a/tests/help.rs +++ b/tests/help.rs @@ -36,6 +36,20 @@ SUBCOMMANDS: help Prints this message or the help of the given subcommand(s) subcmd tests subcommands"; +static AFTER_HELP: &'static str = "some text that comes before the help + +clap-test v1.4.8 +tests clap library + +USAGE: + clap-test [FLAGS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +some text that comes after the help"; + static SC_HELP: &'static str = "subcmd 0.1 Kevin K. tests subcommands @@ -135,6 +149,16 @@ fn complex_help_output() { test::check_help(test::complex_app(), HELP); } +#[test] +fn after_and_before_help_output() { + let app = App::new("clap-test") + .version("v1.4.8") + .about("tests clap library") + .before_help("some text that comes before the help") + .after_help("some text that comes after the help"); + test::check_help(app, AFTER_HELP); +} + #[test] fn complex_subcommand_help_output() { let mut a = test::complex_app(); From 8f630c6a07e0deb53412c081a5e074b76b9cd292 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 06:12:21 -0400 Subject: [PATCH 09/16] fix: fixes bug where one can't override version or help flags Closes #514 --- src/app/parser.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/parser.rs b/src/app/parser.rs index 773f2605..0dfb0b62 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -115,9 +115,9 @@ impl<'a, 'b> Parser<'a, 'b> l)); self.long_list.push(l); if l == "help" { - self.set(AppSettings::NeedsLongHelp); + self.unset(AppSettings::NeedsLongHelp); } else if l == "version" { - self.set(AppSettings::NeedsLongVersion); + self.unset(AppSettings::NeedsLongVersion); } } if a.is_set(ArgSettings::Required) { @@ -378,6 +378,10 @@ impl<'a, 'b> Parser<'a, 'b> self.settings.set(s) } + pub fn unset(&mut self, s: AppSettings) { + self.settings.unset(s) + } + pub fn verify_positionals(&mut self) { // Because you must wait until all arguments have been supplied, this is the first chance // to make assertions on positional argument indexes @@ -933,13 +937,13 @@ impl<'a, 'b> Parser<'a, 'b> debug!("Checking if -{} is help or version...", arg); if let Some(h) = self.help_short { sdebugln!("Help"); - if arg == h { + if arg == h && self.settings.is_set(AppSettings::NeedsLongHelp) { try!(self._help()); } } if let Some(v) = self.version_short { sdebugln!("Help"); - if arg == v { + if arg == v && self.settings.is_set(AppSettings::NeedsLongVersion) { try!(self._version()); } } From 04d27ce803959d0db10bd06ec8ec0a088f6fb4f2 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 30 May 2016 06:18:54 -0400 Subject: [PATCH 10/16] chore: increase version --- CHANGELOG.md | 23 +++++++++++++++++++++-- README.md | 10 ++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0d75d0..2d7d4395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + +### v2.5.2 (2016-05-31) + + +#### Improvements + +* removes extra newline from help output ([86e61d19](https://github.com/kbknapp/clap-rs/commit/86e61d19a748fb9870fcf1175308984e51ca1115)) +* allows printing version to any io::Write object ([921f5f79](https://github.com/kbknapp/clap-rs/commit/921f5f7916597f1d028cd4a65bfe76a01c801724)) +* removes extra newline when printing version ([7e2e2cbb](https://github.com/kbknapp/clap-rs/commit/7e2e2cbb4a8a0f050bb8072a376f742fc54b8589)) + +#### Bug Fixes + +* fixes bug where args are printed out of order with templates ([3935431d](https://github.com/kbknapp/clap-rs/commit/3935431d5633f577c0826ae2142794b301f4b8ca)) +* fixes bug where one can't override version or help flags ([90d7d6a2](https://github.com/kbknapp/clap-rs/commit/90d7d6a2ea8240122dd9bf8d82d3c4f5ebb5c703), closes [#514](https://github.com/kbknapp/clap-rs/issues/514)) +* fixes issue where before_help wasn't printed ([b3faff60](https://github.com/kbknapp/clap-rs/commit/b3faff6030f76a23f26afcfa6a90169002ed7106)) + +#### Documentation + +* inter-links all types and pages ([3312893d](https://github.com/kbknapp/clap-rs/commit/3312893ddaef3f44d68d8d26ed3d08010be50d97), closes [#505](https://github.com/kbknapp/clap-rs/issues/505)) +* makes all publicly available types viewable in docs ([52ca6505](https://github.com/kbknapp/clap-rs/commit/52ca6505b4fec7b5c2d53d160c072d395eb21da6)) + ### v2.5.1 (2016-05-11) @@ -6,8 +27,6 @@ * **Subcommand Aliases**: fixes lifetime issue when setting multiple aliases at once ([ac42f6cf0](https://github.com/kbknapp/clap-rs/commit/ac42f6cf0de6c4920f703807d63061803930b18d)) - - ## v2.5.0 (2016-05-10) diff --git a/README.md b/README.md index 94a56c06..4d5e130d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,16 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) ## What's New +Here's what's new in v.2.5.2 + +* Removes trailing newlines from help and version output +* Allows printing version to any io::Write object +* Inter-links all types and pages +* Makes all publicly available types viewable in docs +* Fixes bug where one can't override version or help flags +* Fixes bug where args are printed out of order when using templates +* Fixes issue where `App::before_help` wasn't printed properly + Here's what's new in v.2.5.0 * Subcommands now support aliases - think of them as hidden subcommands that dispatch to said subcommand automatically From cd4408065014ede411d26055c580249ec4a25cd3 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Tue, 31 May 2016 00:48:02 -0400 Subject: [PATCH 11/16] fix: fixes bug where args are printed out of order with templates --- src/app/help.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/help.rs b/src/app/help.rs index 6cacd100..2957a731 100644 --- a/src/app/help.rs +++ b/src/app/help.rs @@ -155,7 +155,6 @@ impl<'a> Help<'a> { fn write_args_unsorted<'b: 'd, 'c: 'd, 'd, I: 'd>(&mut self, args: I) -> io::Result<()> where I: Iterator> { - debugln!("fn=write_args_unsorted;"); let mut longest = 0; let mut arg_v = Vec::with_capacity(10); for arg in args.filter(|arg| { @@ -852,7 +851,6 @@ impl<'a> Help<'a> { try!(self.writer.write(b"}")); } } - } } } From e23418351a3b98bf08dfd7744bc14377c70d59ee Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 3 Jun 2016 22:44:58 -0400 Subject: [PATCH 12/16] feat(Settings): one can now set an AppSetting which is propogated down through child subcommands Closes #519 --- src/app/mod.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/app/parser.rs | 7 +++++++ 2 files changed, 54 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 283c8791..f75e07ff 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -435,6 +435,53 @@ impl<'a, 'b> App<'a, 'b> { self } + /// Enables a single setting that is propogated *down* through all child [`SubCommand`]s. + /// + /// See [`AppSettings`] for a full list of possibilities and examples. + /// + /// **NOTE**: The setting is *only* propogated *down* and not up through parent commands. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, AppSettings}; + /// App::new("myprog") + /// .global_setting(AppSettings::SubcommandRequired) + /// # ; + /// ``` + /// [`SubCommand`]: ./struct.SubCommand.html + /// [`AppSettings`]: ./enum.AppSettings.html + pub fn global_setting(mut self, setting: AppSettings) -> Self { + self.p.set(setting); + self.p.g_settings.push(setting); + self + } + + /// Enables multiple settings which are propogated *down* through all child [`SubCommand`]s. + /// + /// See [`AppSettings`] for a full list of possibilities and examples. + /// + /// **NOTE**: The setting is *only* propogated *down* and not up through parent commands. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, AppSettings}; + /// App::new("myprog") + /// .global_settings(&[AppSettings::SubcommandRequired, + /// AppSettings::ColoredHelp]) + /// # ; + /// ``` + /// [`SubCommand`]: ./struct.SubCommand.html + /// [`AppSettings`]: ./enum.AppSettings.html + pub fn global_settings(mut self, settings: &[AppSettings]) -> Self { + for s in settings { + self.p.set(*s); + self.p.g_settings.push(*s) + } + self + } + /// Adds an [argument] to the list of valid possibilties. /// /// # Examples diff --git a/src/app/parser.rs b/src/app/parser.rs index 0dfb0b62..9718f6e4 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -49,6 +49,7 @@ pub struct Parser<'a, 'b> help_short: Option, version_short: Option, settings: AppFlags, + pub g_settings: Vec, pub meta: AppMeta<'b>, } @@ -68,6 +69,7 @@ impl<'a, 'b> Default for Parser<'a, 'b> { groups: HashMap::new(), global_args: vec![], overrides: vec![], + g_settings: vec![], settings: AppFlags::new(), meta: AppMeta::new(), } @@ -217,6 +219,10 @@ impl<'a, 'b> Parser<'a, 'b> if self.settings.is_set(AppSettings::DeriveDisplayOrder) { subcmd.p.meta.disp_ord = self.subcommands.len(); } + for s in &self.g_settings { + subcmd.p.set(*s); + subcmd.p.g_settings.push(*s); + } self.subcommands.push(subcmd); } @@ -1661,6 +1667,7 @@ impl<'a, 'b> Clone for Parser<'a, 'b> help_short: self.help_short, version_short: self.version_short, settings: self.settings.clone(), + g_settings: self.g_settings.clone(), meta: self.meta.clone(), } } From 17bc17c69910d9217b923231a435ba595ba8d98e Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 3 Jun 2016 22:45:55 -0400 Subject: [PATCH 13/16] tests(Settings): adds tests for global subcommands --- tests/app_settings.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/app_settings.rs b/tests/app_settings.rs index 7acbbe4b..ff746333 100644 --- a/tests/app_settings.rs +++ b/tests/app_settings.rs @@ -137,3 +137,48 @@ OPTIONS: ARGS: some pos arg")); } + +#[test] +fn global_setting() { + let app = App::new("test") + .global_setting(AppSettings::ColoredHelp) + .subcommand(SubCommand::with_name("subcmd")); + assert!(app.p + .subcommands + .iter() + .filter(|s| s.p + .meta + .name == "subcmd") + .next() + .unwrap() + .p + .is_set(AppSettings::ColoredHelp)); +} + +#[test] +fn global_settings() { + let app = App::new("test") + .global_settings(&[AppSettings::ColoredHelp, AppSettings::TrailingVarArg]) + .subcommand(SubCommand::with_name("subcmd")); + assert!(app.p + .subcommands + .iter() + .filter(|s| s.p + .meta + .name == "subcmd") + .next() + .unwrap() + .p + .is_set(AppSettings::ColoredHelp)); + assert!(app.p + .subcommands + .iter() + .filter(|s| s.p + .meta + .name == "subcmd") + .next() + .unwrap() + .p + .is_set(AppSettings::TrailingVarArg)); + +} From ec86f2dada1545a63fc72355e22fcdc4c466c215 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 3 Jun 2016 23:14:02 -0400 Subject: [PATCH 14/16] imp(Usage Strings): improves the default usage string when only a single positional arg is present Instead of blindly printing `[ARGS]` when only a single positional arg is present, it will now print `[NAME]` (or `[NAME]...` for multiple values allowed) Closes #518 --- src/app/parser.rs | 12 +++++++++++- src/args/arg_builder/positional.rs | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/app/parser.rs b/src/app/parser.rs index 9718f6e4..cee80f41 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -1514,7 +1514,17 @@ impl<'a, 'b> Parser<'a, 'b> } if self.has_positionals() && self.positionals.values().any(|a| !a.settings.is_set(ArgSettings::Required)) { - usage.push_str(" [ARGS]"); + if self.positionals.len() == 1 { + let p = self.positionals.values().next().expect(INTERNAL_ERROR_MSG); + if !self.groups.values().any(|g| g.args.iter().any(|a| a == &p.name)) { + usage.push_str(&*format!(" [{}]{}", p.name_no_brackets(), + p.multiple_str())); + } else { + usage.push_str(" [ARGS]"); + } + } else { + usage.push_str(" [ARGS]"); + } } diff --git a/src/args/arg_builder/positional.rs b/src/args/arg_builder/positional.rs index 5a265991..6524e600 100644 --- a/src/args/arg_builder/positional.rs +++ b/src/args/arg_builder/positional.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter, Result}; use std::result::Result as StdResult; use std::rc::Rc; +use std::borrow::Cow; use vec_map::VecMap; @@ -108,6 +109,25 @@ impl<'n, 'e> PosBuilder<'n, 'e> { } pb } + + pub fn multiple_str(&self) -> &str { + if self.settings.is_set(ArgSettings::Multiple) && self.val_names.is_none() { + "..." + } else { + "" + } + } + + pub fn name_no_brackets(&self) -> Cow { + if let Some(ref names) = self.val_names { + Cow::Owned(names.values() + .map(|n| format!("<{}>", n)) + .collect::>() + .join(" ")) + } else { + Cow::Borrowed(self.name) + } + } } impl<'n, 'e> Display for PosBuilder<'n, 'e> { From 6e7f3e0556150a6198228641cdfc5ba951b91523 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 3 Jun 2016 23:16:02 -0400 Subject: [PATCH 15/16] tests(Usage Strings): adds tests for single positional arg usage strings --- tests/app_settings.rs | 4 ++-- tests/help.rs | 2 +- tests/positionals.rs | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/app_settings.rs b/tests/app_settings.rs index ff746333..554ac071 100644 --- a/tests/app_settings.rs +++ b/tests/app_settings.rs @@ -93,7 +93,7 @@ Kevin K. tests stuff USAGE: - test [OPTIONS] [ARGS] + test [OPTIONS] [arg1] OPTIONS: -f, --flag some flag @@ -125,7 +125,7 @@ Kevin K. tests stuff USAGE: - test [FLAGS] [OPTIONS] [ARGS] + test [FLAGS] [OPTIONS] [arg1] FLAGS: -h, --help Prints help information diff --git a/tests/help.rs b/tests/help.rs index 30cce9a8..99253402 100644 --- a/tests/help.rs +++ b/tests/help.rs @@ -55,7 +55,7 @@ Kevin K. tests subcommands USAGE: - subcmd [FLAGS] [OPTIONS] [--] [ARGS] + subcmd [FLAGS] [OPTIONS] [--] [scpositional] FLAGS: -f, --flag tests flags diff --git a/tests/positionals.rs b/tests/positionals.rs index 987ef14c..ae98c922 100644 --- a/tests/positionals.rs +++ b/tests/positionals.rs @@ -176,3 +176,41 @@ fn default_values_user_value() { assert!(m.is_present("arg")); assert_eq!(m.value_of("arg").unwrap(), "value"); } + +#[test] +fn single_positional_usage_string() { + let m = App::new("test").arg_from_usage("[FILE] 'some file'").get_matches_from(vec!["test"]); + assert_eq!(m.usage(), "USAGE:\n test [FLAGS] [FILE]"); +} + +#[test] +fn single_positional_multiple_usage_string() { + let m = App::new("test").arg_from_usage("[FILE]... 'some file'").get_matches_from(vec!["test"]); + assert_eq!(m.usage(), "USAGE:\n test [FLAGS] [FILE]..."); +} + +#[test] +fn multiple_positional_usage_string() { + let m = App::new("test") + .arg_from_usage("[FILE] 'some file'") + .arg_from_usage("[FILES]... 'some file'") + .get_matches_from(vec!["test"]); + assert_eq!(m.usage(), "USAGE:\n test [FLAGS] [ARGS]"); +} + +#[test] +fn multiple_positional_one_required_usage_string() { + let m = App::new("test") + .arg_from_usage(" 'some file'") + .arg_from_usage("[FILES]... 'some file'") + .get_matches_from(vec!["test", "file"]); + assert_eq!(m.usage(), "USAGE:\n test [FLAGS] [ARGS]"); +} + +#[test] +fn single_positional_required_usage_string() { + let m = App::new("test") + .arg_from_usage(" 'some file'") + .get_matches_from(vec!["test", "file"]); + assert_eq!(m.usage(), "USAGE:\n test [FLAGS] "); +} From 01e7dfd6c07228c0be6695b3c7bf9370d82860d4 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Mon, 6 Jun 2016 18:43:35 -0400 Subject: [PATCH 16/16] fix(Windows): fixes a failing windows build --- src/fmt.rs | 9 +++++++-- src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/fmt.rs b/src/fmt.rs index 86fedf76..f2afcba7 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -8,11 +8,16 @@ use ansi_term::ANSIString; #[cfg(feature = "color")] use libc; -#[cfg(feature = "color")] +#[cfg(all(feature = "color", not(target_os = "windows")))] const STDERR: i32 = libc::STDERR_FILENO; -#[cfg(feature = "color")] +#[cfg(all(feature = "color", not(target_os = "windows")))] const STDOUT: i32 = libc::STDOUT_FILENO; +#[cfg(any(not(feature = "color"), target_os = "windows"))] +const STDERR: i32 = 0; +#[cfg(any(not(feature = "color"), target_os = "windows"))] +const STDOUT: i32 = 0; + #[doc(hidden)] #[derive(Debug, Copy, Clone, PartialEq)] pub enum ColorWhen { diff --git a/src/lib.rs b/src/lib.rs index 9571b346..4538b4af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -407,7 +407,7 @@ extern crate strsim; extern crate ansi_term; #[cfg(feature = "yaml")] extern crate yaml_rust; -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] +#[cfg(any(feature = "wrap_help", feature = "color"))] extern crate libc; #[cfg(all(feature = "wrap_help", not(target_os = "windows")))] extern crate unicode_width;