diff --git a/CHANGELOG.md b/CHANGELOG.md index bbafdde5cfcb..ad89c1128577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + +## v2.8.0 (2016-06-30) + + +#### Features + +* **Arg:** adds new setting `Arg::require_delimiter` which requires val delimiter to parse multiple values ([920b5595](https://github.com/kbknapp/clap-rs/commit/920b5595ed72abfb501ce054ab536067d8df2a66)) + +#### Bug Fixes + +* Declare term::Winsize as repr(C) ([5d663d90](https://github.com/kbknapp/clap-rs/commit/5d663d905c9829ce6e7a164f1f0896cdd70236dd)) + +#### Documentation + +* **Arg:** adds docs for ([49af4e38](https://github.com/kbknapp/clap-rs/commit/49af4e38a5dae2ab0a7fc3b4147e2c053d532484)) + + + ### v2.7.1 (2016-06-29) diff --git a/Cargo.toml b/Cargo.toml index fe9a4c55c613..e1a0b980fa36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "clap" -version = "2.7.1" +version = "2.8.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" @@ -20,6 +20,7 @@ strsim = { version = "~0.4.0", optional = true } yaml-rust = { version = "~0.3.2", optional = true } clippy = { version = "~0.0.74", optional = true } unicode-width = { version = "~0.1.3", optional = true } +term_size = { version = "~0.1.0", optional = true } [dev-dependencies] regex = "~0.1.69" @@ -29,7 +30,7 @@ default = ["suggestions", "color", "wrap_help"] suggestions = ["strsim"] color = ["ansi_term", "libc"] yaml = ["yaml-rust"] -wrap_help = ["libc", "unicode-width"] +wrap_help = ["libc", "unicode-width", "term_size"] 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 1163997fa495..c923fe316c41 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,13 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) ## What's New +Here's the highlights for v2.8.0 + +* **Arg:** adds new optional setting `Arg::require_delimiter` which requires val delimiter to parse multiple values +* The terminal sizing portion has been factored out into a separate crate, [term_size](https://crates.io/crates/term_size) +* Minor bug fixes + + Here's the highlights for v2.7.1 * **Options:** diff --git a/src/app/help.rs b/src/app/help.rs index b77e47539d8a..2c60f20a5312 100644 --- a/src/app/help.rs +++ b/src/app/help.rs @@ -12,7 +12,14 @@ use app::{App, AppSettings}; use app::parser::Parser; use fmt::{Format, Colorizer}; -use term; +#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] +use term_size; +#[cfg(any(not(feature = "wrap_help"), target_os = "windows"))] +mod term_size { + pub fn dimensions() -> Option<(usize, usize)> { + None + } +} #[cfg(all(feature = "wrap_help", not(target_os = "windows")))] use unicode_width::UnicodeWidthStr; @@ -96,7 +103,7 @@ impl<'a> Help<'a> { hide_pv: hide_pv, term_w: match term_w { Some(width) => width, - None => term::dimensions().map(|(w, _)| w).unwrap_or(120), + None => term_size::dimensions().map(|(w, _)| w).unwrap_or(120), }, color: color, cizer: cizer, diff --git a/src/app/parser.rs b/src/app/parser.rs index cb5725be43e1..d51017fc0407 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -1256,9 +1256,9 @@ impl<'a, 'b> Parser<'a, 'b> ret = try!(self.add_single_val_to_arg(arg, v, matcher)); } // If there was a delimiter used, we're not looking for more values - if val.contains_byte(delim as u32 as u8) { + if val.contains_byte(delim as u32 as u8) || arg.is_set(ArgSettings::RequireDelimiter) { ret = None; - } + } } } else { ret = try!(self.add_single_val_to_arg(arg, val, matcher)); diff --git a/src/args/arg.rs b/src/args/arg.rs index ac0c5dc8f5a5..aabe4f2f34b4 100644 --- a/src/args/arg.rs +++ b/src/args/arg.rs @@ -1919,6 +1919,88 @@ impl<'a, 'b> Arg<'a, 'b> { } } + /// Specifies that *multiple values* may only be set using the delimiter. This means if an + /// if an option is encountered, and no delimiter is found, it automatically assumed that no + /// additional values for that option follow. This is unlike the default, where it is generally + /// assumed that more values will follow regardless of whether or not a delimiter is used. + /// + /// **NOTE:** The default is `false`. + /// + /// **NOTE:** It's a good idea to inform the user that use of a delimiter is required, either + /// through help text or other means. + /// + /// # Examples + /// + /// These examples demonstrate what happens when `require_delimiter(true)` is used. Notice + /// everything works in this first example, as we use a delimiter, as expected. + /// + /// ```rust + /// # use clap::{App, Arg}; + /// let delims = App::new("reqdelims") + /// .arg(Arg::with_name("opt") + /// .short("o") + /// .takes_value(true) + /// .multiple(true) + /// .require_delimiter(true)) + /// // Simulate "$ reqdelims -o val1,val2,val3" + /// .get_matches_from(vec![ + /// "reqdelims", "-o", "val1,val2,val3", + /// ]); + /// + /// assert!(delims.is_present("opt")); + /// assert_eq!(delims.values_of("opt").unwrap().collect::>(), ["val1", "val2", "val3"]); + /// ``` + /// In this next example, we will *not* use a delimiter. Notice it's now an error. + /// + /// ```rust + /// # use clap::{App, Arg, ErrorKind}; + /// let res = App::new("reqdelims") + /// .arg(Arg::with_name("opt") + /// .short("o") + /// .takes_value(true) + /// .multiple(true) + /// .require_delimiter(true)) + /// // Simulate "$ reqdelims -o val1 val2 val3" + /// .get_matches_from_safe(vec![ + /// "reqdelims", "-o", "val1", "val2", "val3", + /// ]); + /// + /// assert!(res.is_err()); + /// let err = res.unwrap_err(); + /// assert_eq!(err.kind, ErrorKind::UnknownArgument); + /// ``` + /// What's happening is `-o` is getting `val1`, and because delimiters are required yet none + /// were present, it stops parsing `-o`. At this point it reaches `val2` and because no + /// positional arguments have been defined, it's an error of an unexpected argument. + /// + /// In this final example, we contrast the above with `clap`'s default behavior where the above + /// is *not* an error. + /// + /// ```rust + /// # use clap::{App, Arg}; + /// let delims = App::new("reqdelims") + /// .arg(Arg::with_name("opt") + /// .short("o") + /// .takes_value(true) + /// .multiple(true)) + /// // Simulate "$ reqdelims -o val1 val2 val3" + /// .get_matches_from(vec![ + /// "reqdelims", "-o", "val1", "val2", "val3", + /// ]); + /// + /// assert!(delims.is_present("opt")); + /// assert_eq!(delims.values_of("opt").unwrap().collect::>(), ["val1", "val2", "val3"]); + /// ``` + pub fn require_delimiter(mut self, d: bool) -> Self { + if d { + self.setb(ArgSettings::UseValueDelimiter); + self.set(ArgSettings::RequireDelimiter) + } else { + self.unsetb(ArgSettings::UseValueDelimiter); + self.unset(ArgSettings::RequireDelimiter) + } + } + /// Specifies the separator to use when values are clumped together, defaults to `,` (comma). /// /// **NOTE:** implicitly sets [`Arg::use_delimiter(true)`] diff --git a/src/args/settings.rs b/src/args/settings.rs index c4573c07184a..4921fcd17d34 100644 --- a/src/args/settings.rs +++ b/src/args/settings.rs @@ -3,15 +3,16 @@ use std::ascii::AsciiExt; bitflags! { flags Flags: u16 { - const REQUIRED = 0b000000001, - const MULTIPLE = 0b000000010, - const EMPTY_VALS = 0b000000100, - const GLOBAL = 0b000001000, - const HIDDEN = 0b000010000, - const TAKES_VAL = 0b000100000, - const USE_DELIM = 0b001000000, - const NEXT_LINE_HELP = 0b010000000, - const R_UNLESS_ALL = 0b100000000, + const REQUIRED = 0b0000000001, + const MULTIPLE = 0b0000000010, + const EMPTY_VALS = 0b0000000100, + const GLOBAL = 0b0000001000, + const HIDDEN = 0b0000010000, + const TAKES_VAL = 0b0000100000, + const USE_DELIM = 0b0001000000, + const NEXT_LINE_HELP = 0b0010000000, + const R_UNLESS_ALL = 0b0100000000, + const REQ_DELIM = 0b1000000000, } } @@ -33,7 +34,8 @@ impl ArgFlags { TakesValue => TAKES_VAL, UseValueDelimiter => USE_DELIM, NextLineHelp => NEXT_LINE_HELP, - RequiredUnlessAll => R_UNLESS_ALL + RequiredUnlessAll => R_UNLESS_ALL, + RequireDelimiter => REQ_DELIM } } @@ -67,6 +69,8 @@ pub enum ArgSettings { UseValueDelimiter, /// Prints the help text on the line after the argument NextLineHelp, + /// Requires the use of a value delimiter for all multiple values + RequireDelimiter, #[doc(hidden)] RequiredUnlessAll, } @@ -84,6 +88,7 @@ impl FromStr for ArgSettings { "usevaluedelimiter" => Ok(ArgSettings::UseValueDelimiter), "nextlinehelp" => Ok(ArgSettings::NextLineHelp), "requiredunlessall" => Ok(ArgSettings::RequiredUnlessAll), + "requiredelimiter" => Ok(ArgSettings::RequireDelimiter), _ => Err("unknown ArgSetting, cannot convert from str".to_owned()), } } diff --git a/src/lib.rs b/src/lib.rs index 4538b4af1774..22074ad94403 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -414,6 +414,8 @@ extern crate unicode_width; #[macro_use] extern crate bitflags; extern crate vec_map; +#[cfg(feature = "wrap_help")] +extern crate term_size; #[cfg(feature = "yaml")] pub use yaml_rust::YamlLoader; @@ -431,7 +433,6 @@ mod fmt; mod suggestions; mod errors; mod osstringext; -mod term; mod strext; const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \ diff --git a/src/term.rs b/src/term.rs deleted file mode 100644 index f4ac97a57b7b..000000000000 --- a/src/term.rs +++ /dev/null @@ -1,81 +0,0 @@ -// The following was taken and adapated from exa source -// repo: https://github.com/ogham/exa -// commit: b9eb364823d0d4f9085eb220233c704a13d0f611 -// license: MIT - Copyright (c) 2014 Benjamin Sago - -//! System calls for getting the terminal size. -//! -//! Getting the terminal size is performed using an ioctl command that takes -//! the file handle to the terminal -- which in this case, is stdout -- and -//! populates a structure containing the values. -//! -//! The size is needed when the user wants the output formatted into columns: -//! the default grid view, or the hybrid grid-details view. - -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] -use std::mem::zeroed; -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] -use libc::{STDOUT_FILENO, c_int, c_ulong, c_ushort}; - - -/// The number of rows and columns of a terminal. -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] -#[repr(C)] -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 "C" { -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] - pub fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int; -} - -/// Runs the ioctl command. Returns (0, 0) if output is not to a terminal, or -/// there is an error. (0, 0) is an invalid size to have anyway, which is why -/// it can be used as a nil value. -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] -unsafe fn get_dimensions() -> Winsize { - let mut window: Winsize = zeroed(); - let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window); - - if result == -1 { - zeroed() - } else { - window - } -} - -/// Query the current processes's output, returning its width and height as a -/// number of characters. Returns `None` if the output isn't to a terminal. -#[cfg(all(feature = "wrap_help", not(target_os = "windows")))] -pub fn dimensions() -> Option<(usize, usize)> { - let w = unsafe { get_dimensions() }; - - if w.ws_col == 0 || w.ws_row == 0 { - None - } else { - Some((w.ws_col as usize, w.ws_row as usize)) - } -} - -#[cfg(any(not(feature = "wrap_help"), target_os = "windows"))] -pub fn dimensions() -> Option<(usize, usize)> { - None -} diff --git a/tests/opts.rs b/tests/opts.rs index 97ba46499c6b..a2c58dd4366e 100644 --- a/tests/opts.rs +++ b/tests/opts.rs @@ -3,7 +3,7 @@ extern crate regex; include!("../clap-test.rs"); -use clap::{App, Arg}; +use clap::{App, Arg, ErrorKind}; #[test] fn stdin_char() { @@ -200,6 +200,31 @@ fn multiple_vals_pos_arg_delim() { assert_eq!(m.value_of("file").unwrap(), "some"); } +#[test] +fn require_delims_no_delim() { + let r = App::new("mvae") + .arg( Arg::from_usage("-o [opt]... 'some opt'").require_delimiter(true) ) + .arg( Arg::from_usage("[file] 'some file'") ) + .get_matches_from_safe(vec!["mvae", "-o", "1", "2", "some"]); + assert!(r.is_err()); + let err = r.unwrap_err(); + assert_eq!(err.kind, ErrorKind::UnknownArgument); +} + +#[test] +fn require_delims() { + let r = App::new("mvae") + .arg( Arg::from_usage("-o [opt]... 'some opt'").require_delimiter(true) ) + .arg( Arg::from_usage("[file] 'some file'") ) + .get_matches_from_safe(vec!["", "-o", "1,2", "some"]); + assert!(r.is_ok()); + let m = r.unwrap(); + assert!(m.is_present("o")); + assert_eq!(m.values_of("o").unwrap().collect::>(), &["1", "2"]); + assert!(m.is_present("file")); + assert_eq!(m.value_of("file").unwrap(), "some"); +} + #[test] fn did_you_mean() { test::check_err_output(test::complex_app(), "clap-test --optio=foo",