Auto merge of #554 - kbknapp:issue-376, r=kbknapp

Issue 376
This commit is contained in:
Homu 2016-07-01 20:51:21 +09:00
commit 70fa5f780a
7 changed files with 427 additions and 40 deletions

View file

@ -1,3 +1,16 @@
<a name="v2.9.0"></a>
## v2.9.0 (2016-07-01)
#### Documentation
* **Completions:** adds documentation for completion scripts ([c6c519e4](https://github.com/kbknapp/clap-rs/commit/c6c519e40efd6c4533a9ef5efe8e74fd150391b7))
#### Features
* **Completions:** one can now generate a bash completions script at compile time! ([e75b6c7b](https://github.com/kbknapp/clap-rs/commit/e75b6c7b75f729afb9eb1d2a2faf61dca7674634), closes [#376](https://github.com/kbknapp/clap-rs/issues/376))
<a name="v2.8.0"></a> <a name="v2.8.0"></a>
## v2.8.0 (2016-06-30) ## v2.8.0 (2016-06-30)

View file

@ -1,7 +1,7 @@
[package] [package]
name = "clap" name = "clap"
version = "2.8.0" version = "2.9.0"
authors = ["Kevin K. <kbknapp@gmail.com>"] authors = ["Kevin K. <kbknapp@gmail.com>"]
exclude = ["examples/*", "clap-test/*", "tests/*", "benches/*", "*.png", "clap-perf/*", "*.dot"] exclude = ["examples/*", "clap-test/*", "tests/*", "benches/*", "*.png", "clap-perf/*", "*.dot"]
description = "A simple to use, efficient, and full featured Command Line Argument Parser" description = "A simple to use, efficient, and full featured Command Line Argument Parser"

View file

@ -1,4 +1,5 @@
# clap clap
====
[![Crates.io](https://img.shields.io/crates/v/clap.svg)](https://crates.io/crates/clap) [![Crates.io](https://img.shields.io/crates/d/clap.svg)](https://crates.io/crates/clap) [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kbknapp/clap-rs/blob/master/LICENSE-MIT) [![Coverage Status](https://coveralls.io/repos/kbknapp/clap-rs/badge.svg?branch=master&service=github)](https://coveralls.io/github/kbknapp/clap-rs?branch=master) [![Join the chat at https://gitter.im/kbknapp/clap-rs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kbknapp/clap-rs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Crates.io](https://img.shields.io/crates/v/clap.svg)](https://crates.io/crates/clap) [![Crates.io](https://img.shields.io/crates/d/clap.svg)](https://crates.io/crates/clap) [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kbknapp/clap-rs/blob/master/LICENSE-MIT) [![Coverage Status](https://coveralls.io/repos/kbknapp/clap-rs/badge.svg?branch=master&service=github)](https://coveralls.io/github/kbknapp/clap-rs?branch=master) [![Join the chat at https://gitter.im/kbknapp/clap-rs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kbknapp/clap-rs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@ -38,6 +39,10 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
## What's New ## What's New
Here's the highlights for v2.9.0
* **Completions:** one can now generate a bash completions script at compile time!
Here's the highlights for v2.8.0 Here's the highlights for v2.8.0
* **Arg:** adds new optional setting `Arg::require_delimiter` which requires val delimiter to parse multiple values * **Arg:** adds new optional setting `Arg::require_delimiter` which requires val delimiter to parse multiple values
@ -192,6 +197,8 @@ Below are a few of the features which `clap` supports, full descriptions and usa
* **Auto-generated Help, Version, and Usage information** * **Auto-generated Help, Version, and Usage information**
- Can optionally be fully, or partially overridden if you want a custom help, version, or usage - Can optionally be fully, or partially overridden if you want a custom help, version, or usage
* **Auto-generated bash completion scripts at compile time**
- Even works through many multiple levels of subcommands
* **Flags / Switches** (i.e. bool fields) * **Flags / Switches** (i.e. bool fields)
- Both short and long versions supported (i.e. `-f` and `--flag` respectively) - Both short and long versions supported (i.e. `-f` and `--flag` respectively)
- Supports combining short versions (i.e. `-fBgoZ` is the same as `-f -B -g -o -Z`) - Supports combining short versions (i.e. `-fBgoZ` is the same as `-f -B -g -o -Z`)
@ -340,37 +347,6 @@ fn main() {
} }
``` ```
The following combines the previous two examples by using the less verbose `from_usage` methods and the performance of the Builder Pattern.
```rust
// (Full example with detailed comments in examples/01c_quick_example.rs)
// Must be compiled with `--features unstable`
//
// This example demonstrates clap's "usage strings" method of creating arguments which is less
// less verbose
#[macro_use]
extern crate clap;
fn main() {
let matches = clap_app!(myapp =>
(version: "1.0")
(author: "Kevin K. <kbknapp@gmail.com>")
(about: "Does awesome things")
(@arg config: -c --config +takes_value "Sets a custom config file")
(@arg INPUT: +required "Sets the input file to use")
(@arg verbose: -v ... "Sets the level of verbosity")
(@subcommand test =>
(about: "controls testing features")
(version: "1.3")
(author: "Someone E. <someone_else@other.com>")
(@arg verbose: -d --debug "Print debug information")
)
).get_matches();
// Same as previous examples...
}
```
This final method shows how you can use a YAML file to build your CLI and keep your Rust source tidy or support multiple localized translations by having different YAML files for each localization. First, create the `cli.yml` file to hold your CLI options, but it could be called anything we like (we'll use the same both examples above to keep it functionally equivalent): This final method shows how you can use a YAML file to build your CLI and keep your Rust source tidy or support multiple localized translations by having different YAML files for each localization. First, create the `cli.yml` file to hold your CLI options, but it could be called anything we like (we'll use the same both examples above to keep it functionally equivalent):
```yaml ```yaml

View file

@ -2,7 +2,7 @@
mod settings; mod settings;
#[macro_use] #[macro_use]
mod macros; mod macros;
mod parser; pub mod parser;
mod meta; mod meta;
mod help; mod help;
@ -27,6 +27,7 @@ use app::parser::Parser;
use app::help::Help; use app::help::Help;
use errors::Error; use errors::Error;
use errors::Result as ClapResult; use errors::Result as ClapResult;
use shell::Shell;
/// Used to create a representation of a command line program and all possible command line /// Used to create a representation of a command line program and all possible command line
/// arguments. Application settings are set using the "builder pattern" with the /// arguments. Application settings are set using the "builder pattern" with the
@ -939,6 +940,87 @@ impl<'a, 'b> App<'a, 'b> {
self.p.write_version(w).map_err(From::from) self.p.write_version(w).map_err(From::from)
} }
/// Generate a completions file for a specified shell at compile time.
///
/// **NOTE:** to generate the this file at compile time you must use a `build.rs` "Build Script"
///
/// # Examples
///
/// The following example generates a bash completion script via a `build.rs` script. In this
/// simple example, we'll demo a very small application with only a single subcommand and two
/// args. Real applications could be many multiple levels deep in subcommands, and have tens or
/// potentiall hundreds of arguments.
///
/// First, it helps if we separate out our `App` definition into a seperate file. Whether you
/// do this as a function, or bare App definition is a matter of personal preference.
///
/// ```ignore
/// // src/cli.rs
///
/// use clap::{App, Arg, SubCommand};
///
/// fn build_cli() -> App<'static, 'static> {
/// App::new("compl")
/// .about("Tests completions")
/// .arg(Arg::with_name("file")
/// .help("some input file"))
/// .subcommand(SubCommand::with_name("test")
/// .about("tests things")
/// .arg(Arg::with_name("case")
/// .long("case")
/// .takes_value(true)
/// .help("the case to test")))
/// }
/// ```
///
/// In our regular code, we can simply call this `build_cli()` function, then call
/// `get_mathces()`, or any of the other normal methods directly after. For example:
///
/// ```ignore
/// src/main.rs
///
/// use cli;
///
/// fn main() {
/// let m = cli::build_cli().get_matches();
///
/// // normal logic continues...
/// }
/// ```
/// Next, we set up our `Cargo.toml` to use a `build.rs` build script.
/// ```ignore
/// # Cargo.toml
/// build = "build.rs"
///
/// [build-dependencies]
/// clap = "2.9"
/// ```
///
/// Next, we place a `build.rs` in our project root.
///
/// ```ignore
/// extern crate clap;
///
/// use clap::Shell;
///
/// include!("src/cli.rs");
///
/// fn main() {
/// let mut app = build_cli();
/// app.gen_completions("myapp", // We need to specify the bin name manually
/// Shell::Bash, // Then say which shell to build completions for
/// env!("OUT_DIR")); // Then say where write the completions to
/// }
/// ```
/// Now, once we combile there will be a `bash.sh` file in the directory. Assuming we compiled
/// with debug mode, it would be somewhere similar to
/// `<project>/target/debug/build/myapp-<hash>/out/bash.sh`
pub fn gen_completions<T: Into<OsString>, S: Into<String>>(&mut self, bin_name: S, for_shell: Shell, out_dir: T) {
self.p.meta.bin_name = Some(bin_name.into());
self.p.gen_completions(for_shell, out_dir.into());
}
/// Starts the parsing process, upon a failed parse an error will be displayed to the user and /// Starts the parsing process, upon a failed parse an error will be displayed to the user and
/// the process will exit with the appropriate error code. By default this method gets all user /// the process will 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, /// provided arguments from [`env::args_os`] in order to allow for invalid UTF-8 code points,

View file

@ -24,6 +24,8 @@ use fmt::{Format, ColorWhen};
use osstringext::OsStrExt2; use osstringext::OsStrExt2;
use app::meta::AppMeta; use app::meta::AppMeta;
use args::MatchedArg; use args::MatchedArg;
use shell::Shell;
use completions::ComplGen;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
#[doc(hidden)] #[doc(hidden)]
@ -31,15 +33,15 @@ pub struct Parser<'a, 'b>
where 'a: 'b where 'a: 'b
{ {
required: Vec<&'b str>, required: Vec<&'b str>,
short_list: Vec<char>, pub short_list: Vec<char>,
long_list: Vec<&'b str>, pub long_list: Vec<&'b str>,
blacklist: Vec<&'b str>, blacklist: Vec<&'b str>,
// A list of possible flags // A list of possible flags
flags: Vec<FlagBuilder<'a, 'b>>, flags: Vec<FlagBuilder<'a, 'b>>,
// A list of possible options // A list of possible options
opts: Vec<OptBuilder<'a, 'b>>, pub opts: Vec<OptBuilder<'a, 'b>>,
// A list of positional arguments // A list of positional arguments
positionals: VecMap<PosBuilder<'a, 'b>>, pub positionals: VecMap<PosBuilder<'a, 'b>>,
// A list of subcommands // A list of subcommands
#[doc(hidden)] #[doc(hidden)]
pub subcommands: Vec<App<'a, 'b>>, pub subcommands: Vec<App<'a, 'b>>,
@ -97,6 +99,12 @@ impl<'a, 'b> Parser<'a, 'b>
.nth(0); .nth(0);
} }
pub fn gen_completions(&mut self, for_shell: Shell, od: OsString) {
self.propogate_help_version();
self.build_bin_names();
ComplGen::new(self, od).generate(for_shell)
}
// actually adds the arguments // actually adds the arguments
pub fn add_arg(&mut self, a: &Arg<'a, 'b>) { pub fn add_arg(&mut self, a: &Arg<'a, 'b>) {
debug_assert!(!(self.flags.iter().any(|f| &f.name == &a.name) || debug_assert!(!(self.flags.iter().any(|f| &f.name == &a.name) ||
@ -236,6 +244,7 @@ impl<'a, 'b> Parser<'a, 'b>
self.required.iter() self.required.iter()
} }
#[cfg_attr(feature = "lints", allow(for_kv_map))] #[cfg_attr(feature = "lints", allow(for_kv_map))]
pub fn get_required_from(&self, pub fn get_required_from(&self,
reqs: &[&'a str], reqs: &[&'a str],
@ -652,7 +661,7 @@ impl<'a, 'b> Parser<'a, 'b>
self.meta self.meta
.bin_name .bin_name
.as_ref() .as_ref()
.unwrap_or(&String::new()), .unwrap_or(&self.meta.name.clone()),
if self.meta.bin_name.is_some() { if self.meta.bin_name.is_some() {
" " " "
} else { } else {
@ -788,6 +797,41 @@ impl<'a, 'b> Parser<'a, 'b>
Ok(()) Ok(())
} }
fn propogate_help_version(&mut self) {
debugln!("exec=propogate_help_version;");
self.create_help_and_version();
for sc in self.subcommands.iter_mut() {
sc.p.propogate_help_version();
}
}
fn build_bin_names(&mut self) {
debugln!("exec=build_bin_names;");
for sc in self.subcommands.iter_mut() {
debug!("bin_name set...");
if sc.p.meta.bin_name.is_none() {
sdebugln!("No");
let bin_name = format!("{}{}{}",
self.meta
.bin_name
.as_ref()
.unwrap_or(&self.meta.name.clone()),
if self.meta.bin_name.is_some() {
" "
} else {
""
},
&*sc.p.meta.name);
debugln!("Setting bin_name of {} to {}", self.meta.name, bin_name);
sc.p.meta.bin_name = Some(bin_name);
} else {
sdebugln!("yes ({:?})", sc.p.meta.bin_name);
}
debugln!("Calling build_bin_names from...{}", sc.p.meta.name);
sc.p.build_bin_names();
}
}
fn parse_subcommand<I, T>(&mut self, fn parse_subcommand<I, T>(&mut self,
sc_name: String, sc_name: String,
matcher: &mut ArgMatcher<'a>, matcher: &mut ArgMatcher<'a>,

261
src/completions.rs Normal file
View file

@ -0,0 +1,261 @@
use std::path::PathBuf;
use std::fs::File;
use std::ffi::OsString;
use std::io::Write;
use app::parser::Parser;
use shell::Shell;
use args::{ArgSettings, OptBuilder};
macro_rules! w {
($_self:ident, $f:ident, $to_w:expr) => {
match $f.write_all($to_w) {
Ok(..) => (),
Err(..) => panic!(format!("Failed to write to file completions file")),
}
};
}
pub struct ComplGen<'a, 'b> where 'a: 'b {
p: &'b Parser<'a, 'b>,
out_dir: OsString,
}
impl<'a, 'b> ComplGen<'a, 'b> {
pub fn new(p: &'b Parser<'a, 'b>, od: OsString) -> Self {
ComplGen {
p: p,
out_dir: od,
}
}
pub fn generate(&self, for_shell: Shell) {
match for_shell {
Shell::Bash => self.gen_bash(),
}
}
fn gen_bash(&self) {
use std::error::Error;
let out_dir = PathBuf::from(&self.out_dir);
let mut file = match File::create(out_dir.join("bash.sh")) {
Err(why) => panic!("couldn't create bash completion file: {}",
why.description()),
Ok(file) => file,
};
w!(self, file, format!(
"_{name}() {{
local i cur prev opts cmds
COMPREPLY=()
cur=\"${{COMP_WORDS[COMP_CWORD]}}\"
prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\"
cmd=\"\"
opts=\"\"
for i in ${{COMP_WORDS[@]}}
do
case \"${{i}}\" in
{name})
cmd=\"{name}\"
;;
{subcmds}
*)
;;
esac
done
case \"${{cmd}}\" in
{name})
opts=\"{name_opts}\"
if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W \"${{opts}}\" -- ${{cur}}) )
return 0
fi
case \"${{prev}}\" in
{name_opts_details}
*)
COMPREPLY=()
;;
esac
;;
{subcmd_details}
esac
}}
complete -F _{name} {name}
",
name=self.p.meta.bin_name.as_ref().unwrap(),
name_opts=self.all_options_for_path(self.p.meta.bin_name.as_ref().unwrap()),
name_opts_details=self.option_details_for_path(self.p.meta.bin_name.as_ref().unwrap()),
subcmds=self.all_subcommands(),
subcmd_details=self.subcommand_details()
).as_bytes());
}
fn all_subcommands(&self) -> String {
let mut subcmds = String::new();
let mut scs = get_all_subcommands(self.p);
scs.sort();
scs.dedup();
for sc in &scs {
subcmds = format!(
"{}
{name})
cmd+=\"_{name}\"
;;",
subcmds,
name=sc.replace("-", "_"));
}
subcmds
}
fn subcommand_details(&self) -> String {
let mut subcmd_dets = String::new();
let mut scs = get_all_subcommand_paths(self.p, true);
scs.sort();
scs.dedup();
for sc in &scs {
subcmd_dets = format!(
"{}
{subcmd})
opts=\"{sc_opts}\"
if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then
COMPREPLY=( $(compgen -W \"${{opts}}\" -- ${{cur}}) )
return 0
fi
case \"${{prev}}\" in
{opts_details}
*)
COMPREPLY=()
;;
esac
;;",
subcmd_dets,
subcmd=sc.replace("-", "_"),
sc_opts=self.all_options_for_path(&*sc),
level=sc.split("_").map(|_|1).fold(0, |acc, n| acc + n),
opts_details=self.option_details_for_path(&*sc)
);
}
subcmd_dets
}
fn all_options_for_path(&self, path: &str) -> String {
let mut p = self.p;
for sc in path.split("_").skip(1) {
debugln!("iter;sc={}", sc);
p = &p.subcommands.iter().filter(|s| s.p.meta.name == sc).next().unwrap().p;
}
let mut opts = p.short_list.iter().fold(String::new(), |acc, s| format!("{} -{}", acc, s));
opts = format!("{} {}", opts, p.long_list.iter()
.fold(String::new(), |acc, l| format!("{} --{}", acc, l)));
opts = format!("{} {}", opts, p.positionals.values()
.fold(String::new(), |acc, p| format!("{} {}", acc, p)));
opts = format!("{} {}", opts, p.subcommands.iter()
.fold(String::new(), |acc, s| format!("{} {}", acc, s.p.meta.name)));
opts
}
fn option_details_for_path(&self, path: &str) -> String {
let mut p = self.p;
for sc in path.split("_").skip(1) {
debugln!("iter;sc={}", sc);
p = &p.subcommands.iter().filter(|s| s.p.meta.name == sc).next().unwrap().p;
}
let mut opts = String::new();
for o in &p.opts {
if let Some(l) = o.long {
opts = format!("{}
--{})
COMPREPLY=(\"{}\")
;;", opts, l, vals_for(o));
}
if let Some(s) = o.short {
opts = format!("{}
-{})
COMPREPLY=(\"{}\")
;;", opts, s, vals_for(o));
}
}
opts
}
}
pub fn get_all_subcommands(p: &Parser) -> Vec<String> {
let mut subcmds = vec![];
if !p.has_subcommands() {
return vec![p.meta.name.clone()]
}
for sc in p.subcommands.iter().map(|ref s| s.p.meta.name.clone()) {
subcmds.push(sc);
}
for sc_v in p.subcommands.iter().map(|ref s| get_all_subcommands(&s.p)) {
subcmds.extend(sc_v);
}
subcmds
}
pub fn get_all_subcommand_paths(p: &Parser, first: bool) -> Vec<String> {
let mut subcmds = vec![];
if !p.has_subcommands() {
if !first {
return vec![p.meta.bin_name.as_ref().unwrap().clone().replace(" ", "_")]
}
return vec![];
}
for sc in p.subcommands.iter()
.map(|ref s| s.p.meta.bin_name.as_ref()
.unwrap()
.clone()
.replace(" ", "_")) {
subcmds.push(sc);
}
for sc_v in p.subcommands.iter().map(|ref s| get_all_subcommand_paths(&s.p, false)) {
subcmds.extend(sc_v);
}
subcmds
}
fn vals_for(o: &OptBuilder) -> String {
use args::AnyArg;
let mut ret = String::new();
if let Some(ref vec) = o.val_names() {
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
ret = format!("{}<{}>{}", ret, val,
if it.peek().is_some() {
" "
} else {
""
});
}
let num = vec.len();
if o.is_set(ArgSettings::Multiple) && num == 1 {
ret = format!("{}...", ret);
}
} else if let Some(num) = o.num_vals() {
let mut it = (0..num).peekable();
while let Some(_) = it.next() {
ret = format!("{}<{}>{}", ret, o.name(),
if it.peek().is_some() {
" "
} else {
""
});
}
if o.is_set(ArgSettings::Multiple) && num == 1 {
ret = format!("{}...", ret);
}
} else {
ret = format!("<{}>", o.name());
if o.is_set(ArgSettings::Multiple) {
ret = format!("{}...", ret);
}
}
ret
}

View file

@ -423,6 +423,7 @@ pub use args::{Arg, ArgGroup, ArgMatches, ArgSettings, SubCommand, Values, OsVal
pub use app::{App, AppSettings}; pub use app::{App, AppSettings};
pub use fmt::Format; pub use fmt::Format;
pub use errors::{Error, ErrorKind, Result}; pub use errors::{Error, ErrorKind, Result};
pub use shell::Shell;
#[macro_use] #[macro_use]
mod macros; mod macros;
@ -434,7 +435,17 @@ mod suggestions;
mod errors; mod errors;
mod osstringext; mod osstringext;
mod strext; mod strext;
mod completions;
const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \ const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \
report at https://github.com/kbknapp/clap-rs/issues"; report at https://github.com/kbknapp/clap-rs/issues";
const INVALID_UTF8: &'static str = "unexpected invalid UTF-8 code point"; const INVALID_UTF8: &'static str = "unexpected invalid UTF-8 code point";
mod shell {
/// Describes which shell to produce a completions file for
#[derive(Debug, Copy, Clone)]
pub enum Shell {
/// Generates a .sh completion file for the Bourne Again SHell (BASH)
Bash
}
}