diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0ff2599 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +*~ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..e34d02cc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: rust +cache: cargo +rust: + - 1.21.0 + - stable + - beta + - nightly +matrix: + include: + - rust: nightly + env: FEATURES="--features nightly" + - rust: stable + env: RUN=FMT + before_script: rustup component add rustfmt-preview + script: cargo fmt --all -- --write-mode diff +script: + - cargo test $FEATURES diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..26413208 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,156 @@ +# v0.2.10 (2018-06-07) + +* 1.21.0 is the minimum required rustc version by + [@TeXitoi](https://github.com/TeXitoi) + +# v0.2.9 (2018-06-05) + +* Fix a bug when using `flatten` by + [@fbenkstein](https://github.com/fbenkstein) +* Update syn, quote and proc_macro2 by + [@TeXitoi](https://github.com/TeXitoi) +* Fix a regression when there is multiple authors by + [@windwardly](https://github.com/windwardly) + +# v0.2.8 (2018-04-28) + +* Add `StructOpt::from_iter_safe()`, which returns an `Error` instead of + killing the program when it fails to parse, or parses one of the + short-circuiting flags. ([#98](https://github.com/TeXitoi/structopt/pull/98) + by [@quodlibetor](https://github.com/quodlibetor)) +* Allow users to enable `clap` features independently by + [@Kerollmops](https://github.com/Kerollmops) +* Fix a bug when flattening an enum + ([#103](https://github.com/TeXitoi/structopt/pull/103) by + [@TeXitoi](https://github.com/TeXitoi) + +# v0.2.7 (2018-04-12) + +* Add flattening, the insertion of options of another StructOpt struct + into another ([#92](https://github.com/TeXitoi/structopt/pull/92)) + by [@birkenfeld](https://github.com/birkenfeld) +* Fail compilation when using `default_value` or `required` with + `Option` ([#88](https://github.com/TeXitoi/structopt/pull/88)) by + [@Kerollmops](https://github.com/Kerollmops) + +# v0.2.6 (2018-03-31) + +* Fail compilation when using `default_value` or `required` with `bool` ([#80](https://github.com/TeXitoi/structopt/issues/80)) by [@TeXitoi](https://github.com/TeXitoi) +* Fix compilation with `#[deny(warnings)]` with the `!` type (https://github.com/rust-lang/rust/pull/49039#issuecomment-376398999) by [@TeXitoi](https://github.com/TeXitoi) +* Improve first example in the documentation ([#82](https://github.com/TeXitoi/structopt/issues/82)) by [@TeXitoi](https://github.com/TeXitoi) + +# v0.2.5 (2018-03-07) + +* Work around breakage when `proc-macro2`'s nightly feature is enabled. ([#77](https://github.com/Texitoi/structopt/pull/77) and [proc-macro2#67](https://github.com/alexcrichton/proc-macro2/issues/67)) by [@fitzgen](https://github.com/fitzgen) + +# v0.2.4 (2018-02-25) + +* Fix compilation with `#![deny(missig_docs]` ([#74](https://github.com/TeXitoi/structopt/issues/74)) by [@TeXitoi](https://github.com/TeXitoi) +* Fix [#76](https://github.com/TeXitoi/structopt/issues/76) by [@TeXitoi](https://github.com/TeXitoi) +* Re-licensed to Apache-2.0/MIT by [@CAD97](https://github.com/cad97) + +# v0.2.3 (2018-02-16) + +* An empty line in a doc comment will result in a double linefeed in the generated about/help call by [@TeXitoi](https://github.com/TeXitoi) + +# v0.2.2 (2018-02-12) + +* Fix [#66](https://github.com/TeXitoi/structopt/issues/66) by [@TeXitoi](https://github.com/TeXitoi) + +# v0.2.1 (2018-02-11) + +* Fix a bug around enum tuple and the about message in the global help by [@TeXitoi](https://github.com/TeXitoi) +* Fix [#65](https://github.com/TeXitoi/structopt/issues/65) by [@TeXitoi](https://github.com/TeXitoi) + +# v0.2.0 (2018-02-10) + +## Breaking changes + +### Don't special case `u64` by [@SergioBenitez](https://github.com/SergioBenitez) + +If you are using a `u64` in your struct to get the number of occurence of a flag, you should now add `parse(from_occurrences)` on the flag. + +For example +```rust +#[structopt(short = "v", long = "verbose")] +verbose: u64, +``` +must be changed by +```rust +#[structopt(short = "v", long = "verbose", parse(from_occurrences))] +verbose: u64, +``` + +This feature was surprising as shown in [#30](https://github.com/TeXitoi/structopt/issues/30). Using the `parse` feature seems much more natural. + +### Change the signature of `Structopt::from_clap` to take its argument by reference by [@TeXitoi](https://github.com/TeXitoi) + +There was no reason to take the argument by value. Most of the StructOpt users will not be impacted by this change. If you are using `StructOpt::from_clap`, just add a `&` before the argument. + +### Fail if attributes are not used by [@TeXitoi](https://github.com/TeXitoi) + +StructOpt was quite fuzzy in its attribute parsing: it was only searching for interresting things, e. g. something like `#[structopt(foo(bar))]` was accepted but not used. It now fails the compilation. + +You should have nothing to do here. This breaking change may highlight some missuse that can be bugs. + +In future versions, if there is cases that are not highlighed, they will be considerated as bugs, not breaking changes. + +### Use `raw()` wrapping instead of `_raw` suffixing by [@TeXitoi](https://github.com/TeXitoi) + +The syntax of raw attributes is changed to improve the syntax. + +You have to change `foo_raw = "bar", baz_raw = "foo"` by `raw(foo = "bar", baz = "foo")` or `raw(foo = "bar"), raw(baz = "foo")`. + +## New features + +* Add `parse(from_occurrences)` parser by [@SergioBenitez](https://github.com/SergioBenitez) +* Support 1-uple enum variant as subcommand by [@TeXitoi](https://github.com/TeXitoi) +* structopt-derive crate is now an implementation detail, structopt reexport the custom derive macro by [@TeXitoi](https://github.com/TeXitoi) +* Add the `StructOpt::from_iter` method by [@Kerollmops](https://github.com/Kerollmops) + +## Documentation + +* Improve doc by [@bestouff](https://github.com/bestouff) +* All the documentation is now on the structopt crate by [@TeXitoi](https://github.com/TeXitoi) + +# v0.1.7 (2018-01-23) + +* Allow opting out of clap default features by [@ski-csis](https://github.com/ski-csis) + +# v0.1.6 (2017-11-25) + +* Improve documentation by [@TeXitoi](https://github.com/TeXitoi) +* Fix bug [#31](https://github.com/TeXitoi/structopt/issues/31) by [@TeXitoi](https://github.com/TeXitoi) + +# v0.1.5 (2017-11-14) + +* Fix a bug with optional subsubcommand and Enum by [@TeXitoi](https://github.com/TeXitoi) + +# v0.1.4 (2017-11-09) + +* Implement custom string parser from either `&str` or `&OsStr` by [@kennytm](https://github.com/kennytm) + +# v0.1.3 (2017-11-01) + +* Improve doc by [@TeXitoi](https://github.com/TeXitoi) + +# v0.1.2 (2017-11-01) + +* Fix bugs [#24](https://github.com/TeXitoi/structopt/issues/24) and [#25](https://github.com/TeXitoi/structopt/issues/25) by [@TeXitoi](https://github.com/TeXitoi) +* Support of methods with something else that a string as argument thanks to `_raw` suffix by [@Flakebi](https://github.com/Flakebi) + +# v0.1.1 (2017-09-22) + +* Better formating of multiple authors by [@killercup](https://github.com/killercup) + +# v0.1.0 (2017-07-17) + +* Subcommand support by [@williamyaoh](https://github.com/williamyaoh) + +# v0.0.5 (2017-06-16) + +* Using doc comment to populate help by [@killercup](https://github.com/killercup) + +# v0.0.3 (2017-02-11) + +* First version with flags, arguments and options support by [@TeXitoi](https://github.com/TeXitoi) diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..91bdbb68 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "structopt" +version = "0.2.10" +authors = ["Guillaume Pinot ", "others"] +description = "Parse command line argument by defining a struct." +documentation = "https://docs.rs/structopt" +repository = "https://github.com/TeXitoi/structopt" +keywords = ["clap", "cli", "derive", "docopt"] +categories = ["command-line-interface"] +license = "Apache-2.0/MIT" +readme = "README.md" + +[features] +default = ["clap/default"] +nightly = ["structopt-derive/nightly"] +suggestions = ["clap/suggestions"] +color = ["clap/color"] +wrap_help = ["clap/wrap_help"] +yaml = ["clap/yaml"] +lints = ["clap/lints"] +debug = ["clap/debug"] +no_cargo = ["clap/no_cargo"] +doc = ["clap/doc"] + +[badges] +travis-ci = { repository = "TeXitoi/structopt" } + +[dependencies] +clap = { version = "2.20", default-features = false } +structopt-derive = { path = "structopt-derive", version = "0.2.10" } + +[workspace] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..e931b83b --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Guillaume Pinot (@TeXitoi) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..7b4fb36b --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# StructOpt [![Build status](https://travis-ci.org/TeXitoi/structopt.svg?branch=master)](https://travis-ci.org/TeXitoi/structopt) [![](https://img.shields.io/crates/v/structopt.svg)](https://crates.io/crates/structopt) [![](https://docs.rs/structopt/badge.svg)](https://docs.rs/structopt) + +Parse command line argument by defining a struct. It combines [clap](https://crates.io/crates/clap) with custom derive. + +## Documentation + +Find it on [Docs.rs](https://docs.rs/structopt). You can also check the [examples](https://github.com/TeXitoi/structopt/tree/master/examples) and the [changelog](https://github.com/TeXitoi/structopt/blob/master/CHANGELOG.md). + +## Example + +Add `structopt` to your dependencies of your `Cargo.toml`: +```toml +[dependencies] +structopt = "0.2" +``` + +And then, in your rust file: +```rust +#[macro_use] +extern crate structopt; + +use std::path::PathBuf; +use structopt::StructOpt; + +/// A basic example +#[derive(StructOpt, Debug)] +#[structopt(name = "basic")] +struct Opt { + // A flag, true if used in the command line. Note doc comment will + // be used for the help message of the flag. + /// Activate debug mode + #[structopt(short = "d", long = "debug")] + debug: bool, + + // The number of occurences of the `v/verbose` flag + /// Verbose mode (-v, -vv, -vvv, etc.) + #[structopt(short = "v", long = "verbose", parse(from_occurrences))] + verbose: u8, + + /// Set speed + #[structopt(short = "s", long = "speed", default_value = "42")] + speed: f64, + + /// Output file + #[structopt(short = "o", long = "output", parse(from_os_str))] + output: PathBuf, + + /// Number of cars + #[structopt(short = "c", long = "nb-cars")] + nb_cars: Option, + + /// admin_level to consider + #[structopt(short = "l", long = "level")] + level: Vec, + + /// Files to process + #[structopt(name = "FILE", parse(from_os_str))] + files: Vec, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} +``` + +Using this example: +``` +$ ./basic +error: The following required arguments were not provided: + --output + +USAGE: + basic --output --speed + +For more information try --help +$ ./basic --help +basic 0.2.0 +Guillaume Pinot +A basic example + +USAGE: + basic [FLAGS] [OPTIONS] --output [--] [FILE]... + +FLAGS: + -d, --debug Activate debug mode + -h, --help Prints help information + -V, --version Prints version information + -v, --verbose Verbose mode + +OPTIONS: + -c, --nb-cars Number of cars + -l, --level ... admin_level to consider + -o, --output Output file + -s, --speed Set speed [default: 42] + +ARGS: + ... Files to process +$ ./basic -o foo.txt +Opt { debug: false, verbose: 0, speed: 42, output: "foo.txt", car: None, level: [], files: [] } +$ ./basic -o foo.txt -dvvvs 1337 -l alice -l bob --nb-cars 4 bar.txt baz.txt +Opt { debug: true, verbose: 3, speed: 1337, output: "foo.txt", nb_cars: Some(4), level: ["alice", "bob"], files: ["bar.txt", "baz.txt"] } +``` + +## StructOpt rustc version policy + +- Minimum rustc version modification must be specified in the [changelog](https://github.com/TeXitoi/structopt/blob/master/CHANGELOG.md) and in the [travis configuration](https://github.com/TeXitoi/structopt/blob/master/.travis.yml). +- Contributors can increment minimum rustc version without any justification if the new version is required by the latest version of one of StructOpt's depedencies (`cargo update` will not fail on StructOpt). +- Contributors can increment minimum rustc version if the library user experience is improved. + +## Why + +I use [docopt](https://crates.io/crates/docopt) since a long time (pre rust 1.0). I really like the fact that you have a structure with the parsed argument: no need to convert `String` to `f64`, no useless `unwrap`. But on the other hand, I don't like to write by hand the usage string. That's like going back to the golden age of WYSIWYG editors. Field naming is also a bit artificial. + +Today, the new standard to read command line arguments in Rust is [clap](https://crates.io/crates/clap). This library is so feature full! But I think there is one downside: even if you can validate argument and expressing that an argument is required, you still need to transform something looking like a hashmap of string vectors to something useful for your application. + +Now, there is stable custom derive. Thus I can add to clap the automatic conversion that I miss. Here is the result. + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + diff --git a/examples/at_least_two.rs b/examples/at_least_two.rs new file mode 100644 index 00000000..ee9578fd --- /dev/null +++ b/examples/at_least_two.rs @@ -0,0 +1,15 @@ +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +struct Opt { + #[structopt(raw(required = "true", min_values = "2"))] + foos: Vec, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 00000000..b05d5ade --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,54 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use std::path::PathBuf; +use structopt::StructOpt; + +/// A basic example +#[derive(StructOpt, Debug)] +#[structopt(name = "basic")] +struct Opt { + // A flag, true if used in the command line. Note doc comment will + // be used for the help message of the flag. + /// Activate debug mode + #[structopt(short = "d", long = "debug")] + debug: bool, + + // The number of occurences of the `v/verbose` flag + /// Verbose mode (-v, -vv, -vvv, etc.) + #[structopt(short = "v", long = "verbose", parse(from_occurrences))] + verbose: u8, + + /// Set speed + #[structopt(short = "s", long = "speed", default_value = "42")] + speed: f64, + + /// Output file + #[structopt(short = "o", long = "output", parse(from_os_str))] + output: PathBuf, + + /// Number of cars + #[structopt(short = "c", long = "nb-cars")] + nb_cars: Option, + + /// admin_level to consider + #[structopt(short = "l", long = "level")] + level: Vec, + + /// Files to process + #[structopt(name = "FILE", parse(from_os_str))] + files: Vec, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/deny_missing_docs.rs b/examples/deny_missing_docs.rs new file mode 100644 index 00000000..dd2ef016 --- /dev/null +++ b/examples/deny_missing_docs.rs @@ -0,0 +1,54 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// This should be in tests but it will not work until +// https://github.com/rust-lang/rust/issues/24584 is fixed + +//! A test to check that structopt compiles with deny(missing_docs) + +#![deny(missing_docs)] + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +/// The options +#[derive(StructOpt, Debug, PartialEq)] +pub struct Opt { + #[structopt(short = "v")] + verbose: bool, + #[structopt(subcommand)] + cmd: Option, +} + +/// Some subcommands +#[derive(StructOpt, Debug, PartialEq)] +pub enum Cmd { + /// command A + A, + /// command B + B { + /// Alice? + #[structopt(short = "a")] + alice: bool, + }, + /// command C + C(COpt), +} + +/// The options for C +#[derive(StructOpt, Debug, PartialEq)] +pub struct COpt { + #[structopt(short = "b")] + bob: bool, +} + +fn main() { + println!("{:?}", Opt::from_args()); +} diff --git a/examples/enum_in_args.rs b/examples/enum_in_args.rs new file mode 100644 index 00000000..482d5631 --- /dev/null +++ b/examples/enum_in_args.rs @@ -0,0 +1,27 @@ +#[macro_use] +extern crate structopt; +#[macro_use] +extern crate clap; + +use structopt::StructOpt; + +arg_enum! { + #[derive(Debug)] + enum Baz { + Foo, + Bar, + FooBar + } +} + +#[derive(StructOpt, Debug)] +struct Opt { + /// Important argument. + #[structopt(raw(possible_values = "&Baz::variants()", case_insensitive = "true"))] + i: Baz, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/enum_tuple.rs b/examples/enum_tuple.rs new file mode 100644 index 00000000..596c060d --- /dev/null +++ b/examples/enum_tuple.rs @@ -0,0 +1,27 @@ +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +pub struct Foo { + pub bar: Option, +} + +#[derive(Debug, StructOpt)] +pub enum Command { + #[structopt(name = "foo")] + Foo(Foo), +} + +#[derive(Debug, StructOpt)] +#[structopt(name = "classify")] +pub struct ApplicationArguments { + #[structopt(subcommand)] + pub command: Command, +} + +fn main() { + let opt = ApplicationArguments::from_args(); + println!("{:?}", opt); +} diff --git a/examples/example.rs b/examples/example.rs new file mode 100644 index 00000000..c188794f --- /dev/null +++ b/examples/example.rs @@ -0,0 +1,38 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "example", about = "An example of StructOpt usage.")] +struct Opt { + /// A flag, true if used in the command line. + #[structopt(short = "d", long = "debug", help = "Activate debug mode")] + debug: bool, + + /// An argument of type float, with a default value. + #[structopt(short = "s", long = "speed", help = "Set speed", default_value = "42")] + speed: f64, + + /// Needed parameter, the first on the command line. + #[structopt(help = "Input file")] + input: String, + + /// An optional parameter, will be `None` if not present on the + /// command line. + #[structopt(help = "Output file, stdout if not present")] + output: Option, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/flatten.rs b/examples/flatten.rs new file mode 100644 index 00000000..cfcbaf8f --- /dev/null +++ b/examples/flatten.rs @@ -0,0 +1,25 @@ +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +struct Cmdline { + #[structopt(short = "v", help = "switch on verbosity")] + verbose: bool, + #[structopt(flatten)] + daemon_opts: DaemonOpts, +} + +#[derive(StructOpt, Debug)] +struct DaemonOpts { + #[structopt(short = "u", help = "daemon user")] + user: String, + #[structopt(short = "g", help = "daemon group")] + group: String, +} + +fn main() { + let opt = Cmdline::from_args(); + println!("{:?}", opt); +} diff --git a/examples/git.rs b/examples/git.rs new file mode 100644 index 00000000..d4f185af --- /dev/null +++ b/examples/git.rs @@ -0,0 +1,40 @@ +//! `git.rs` serves as a demonstration of how to use subcommands, +//! as well as a demonstration of adding documentation to subcommands. +//! Documentation can be added either through doc comments or the +//! `about` attribute. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "git")] +/// the stupid content tracker +enum Opt { + #[structopt(name = "fetch")] + /// fetch branches from remote repository + Fetch { + #[structopt(long = "dry-run")] + dry_run: bool, + #[structopt(long = "all")] + all: bool, + #[structopt(default_value = "origin")] + repository: String, + }, + #[structopt(name = "add")] + /// add files to the staging area + Add { + #[structopt(short = "i")] + interactive: bool, + #[structopt(short = "a")] + all: bool, + files: Vec, + }, +} + +fn main() { + let matches = Opt::from_args(); + + println!("{:?}", matches); +} diff --git a/examples/group.rs b/examples/group.rs new file mode 100644 index 00000000..8ac74e6e --- /dev/null +++ b/examples/group.rs @@ -0,0 +1,40 @@ +// A functional translation of the example at +// https://docs.rs/clap/2.31.2/clap/struct.App.html#method.group + +#[macro_use] +extern crate structopt; + +use structopt::clap::ArgGroup; +use structopt::StructOpt; + +// This function is not needed, we can insert everything in the group +// attribute, but, as it might be long, using a function is more +// lisible. +fn vers_arg_group() -> ArgGroup<'static> { + // As the attributes of the struct are executed before the struct + // fields, we can't use .args(...), but we can use the group + // attribute on the fields. + ArgGroup::with_name("vers").required(true) +} + +#[derive(StructOpt, Debug)] +#[structopt(raw(group = "vers_arg_group()"))] +struct Opt { + /// set the version manually + #[structopt(long = "set-ver", group = "vers")] + set_ver: Option, + /// auto increase major + #[structopt(long = "major", group = "vers")] + major: bool, + /// auto increase minor + #[structopt(long = "minor", group = "vers")] + minor: bool, + /// auto increase patch + #[structopt(long = "patch", group = "vers")] + patch: bool, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/keyvalue.rs b/examples/keyvalue.rs new file mode 100644 index 00000000..85a6270e --- /dev/null +++ b/examples/keyvalue.rs @@ -0,0 +1,28 @@ +#[macro_use] +extern crate structopt; + +use std::error::Error; +use structopt::StructOpt; + +fn parse_key_val(s: &str) -> Result<(T, U), Box> +where + T: std::str::FromStr, + T::Err: Error + 'static, + U: std::str::FromStr, + U::Err: Error + 'static, +{ + let pos = s.find('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?; + Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) +} + +#[derive(StructOpt, Debug)] +struct Opt { + #[structopt(short = "D", parse(try_from_str = "parse_key_val"))] + defines: Vec<(String, i32)>, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/no_version.rs b/examples/no_version.rs new file mode 100644 index 00000000..0dcedf42 --- /dev/null +++ b/examples/no_version.rs @@ -0,0 +1,20 @@ +#[macro_use] +extern crate structopt; + +use structopt::clap::AppSettings; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt( + name = "no_version", + about = "", + version = "", + author = "", + raw(global_settings = "&[AppSettings::DisableVersion]") +)] +struct Opt {} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/raw_attributes.rs b/examples/raw_attributes.rs new file mode 100644 index 00000000..2fb7cc13 --- /dev/null +++ b/examples/raw_attributes.rs @@ -0,0 +1,39 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::clap::AppSettings; +use structopt::StructOpt; + +/// An example of raw attributes +#[derive(StructOpt, Debug)] +#[structopt( + raw(global_settings = "&[AppSettings::ColoredHelp, AppSettings::VersionlessSubcommands]") +)] +struct Opt { + /// Output file + #[structopt(short = "o", long = "output")] + output: String, + + /// admin_level to consider + #[structopt(short = "l", long = "level", raw(aliases = r#"&["set-level", "lvl"]"#))] + level: Vec, + + /// Files to process + /// + /// `level` is required if a file is called `FILE`. + #[structopt(name = "FILE", raw(requires_if = r#""FILE", "level""#))] + files: Vec, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/simple_group.rs b/examples/simple_group.rs new file mode 100644 index 00000000..d3dd5464 --- /dev/null +++ b/examples/simple_group.rs @@ -0,0 +1,31 @@ +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +struct Opt { + /// Set a custom HTTP verb + #[structopt(long = "method", group = "verb")] + method: Option, + /// HTTP GET; default if no other HTTP verb is selected + #[structopt(long = "get", group = "verb")] + get: bool, + /// HTTP HEAD + #[structopt(long = "head", group = "verb")] + head: bool, + /// HTTP POST + #[structopt(long = "post", group = "verb")] + post: bool, + /// HTTP PUT + #[structopt(long = "put", group = "verb")] + put: bool, + /// HTTP DELETE + #[structopt(long = "delete", group = "verb")] + delete: bool, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/examples/subcommand_aliases.rs b/examples/subcommand_aliases.rs new file mode 100644 index 00000000..5629d6ae --- /dev/null +++ b/examples/subcommand_aliases.rs @@ -0,0 +1,22 @@ +#[macro_use] +extern crate structopt; + +use structopt::clap::AppSettings; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +// https://docs.rs/clap/2/clap/enum.AppSettings.html#variant.InferSubcommands +#[structopt(raw(setting = "AppSettings::InferSubcommands"))] +enum Opt { + // https://docs.rs/clap/2/clap/struct.App.html#method.alias + #[structopt(name = "foo", alias = "foobar")] + Foo, + // https://docs.rs/clap/2/clap/struct.App.html#method.aliases + #[structopt(name = "bar", raw(aliases = r#"&["baz", "fizz"]"#))] + Bar, +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..3d153a82 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,445 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![deny(missing_docs)] + +//! This crate defines the `StructOpt` trait and its custom derrive. +//! +//! ## Features +//! +//! If you want to disable all the `clap` features (colors, +//! suggestions, ..) add `default-features = false` to the `structopt` +//! dependency: +//! +//! ```toml +//! [dependencies] +//! structopt = { version = "0.2", default-features = false } +//! ``` +//! +//! ## How to `derive(StructOpt)` +//! +//! First, let's look at an example: +//! +//! ```should_panic +//! #[macro_use] +//! extern crate structopt; +//! +//! use std::path::PathBuf; +//! use structopt::StructOpt; +//! +//! #[derive(Debug, StructOpt)] +//! #[structopt(name = "example", about = "An example of StructOpt usage.")] +//! struct Opt { +//! /// Activate debug mode +//! #[structopt(short = "d", long = "debug")] +//! debug: bool, +//! /// Set speed +//! #[structopt(short = "s", long = "speed", default_value = "42")] +//! speed: f64, +//! /// Input file +//! #[structopt(parse(from_os_str))] +//! input: PathBuf, +//! /// Output file, stdout if not present +//! #[structopt(parse(from_os_str))] +//! output: Option, +//! } +//! +//! fn main() { +//! let opt = Opt::from_args(); +//! println!("{:?}", opt); +//! } +//! ``` +//! +//! So `derive(StructOpt)` tells Rust to generate a command line parser, +//! and the various `structopt` attributes are simply +//! used for additional parameters. +//! +//! First, define a struct, whatever its name. This structure will +//! correspond to a `clap::App`. Every method of `clap::App` in the +//! form of `fn function_name(self, &str)` can be use through attributes +//! placed on the struct. In our example above, the `about` attribute +//! will become an `.about("An example of StructOpt usage.")` call on the +//! generated `clap::App`. There are a few attributes that will default +//! if not specified: +//! +//! - `name`: The binary name displayed in help messages. Defaults +//! to the crate name given by Cargo. +//! - `version`: Defaults to the crate version given by Cargo. +//! - `author`: Defaults to the crate author name given by Cargo. +//! - `about`: Defaults to the crate description given by Cargo. +//! +//! Methods from `clap::App` that don't take an `&str` can be called by +//! wrapping them in `raw()`, e.g. to activate colored help text: +//! +//! ``` +//! #[macro_use] +//! extern crate structopt; +//! +//! use structopt::StructOpt; +//! +//! #[derive(StructOpt, Debug)] +//! #[structopt(raw(setting = "structopt::clap::AppSettings::ColoredHelp"))] +//! struct Opt { +//! #[structopt(short = "s")] +//! speed: bool, +//! #[structopt(short = "d")] +//! debug: bool, +//! } +//! # fn main() {} +//! ``` +//! +//! Then, each field of the struct not marked as a subcommand corresponds +//! to a `clap::Arg`. As with the struct attributes, every method of +//! `clap::Arg` in the form of `fn function_name(self, &str)` can be used +//! through specifying it as an attribute. +//! The `name` attribute can be used to customize the +//! `Arg::with_name()` call (defaults to the field name). +//! For functions that do not take a `&str` as argument, the attribute can be +//! wrapped in `raw()`, e. g. `raw(aliases = r#"&["alias"]"#, next_line_help = "true")`. +//! +//! The type of the field gives the kind of argument: +//! +//! Type | Effect | Added method call to `clap::Arg` +//! ---------------------|---------------------------------------------------|-------------------------------------- +//! `bool` | `true` if the flag is present | `.takes_value(false).multiple(false)` +//! `Option` | optional positional argument or option | `.takes_value(true).multiple(false)` +//! `Vec` | list of options or the other positional arguments | `.takes_value(true).multiple(true)` +//! `T: FromStr` | required option or positional argument | `.takes_value(true).multiple(false).required(!has_default)` +//! +//! The `FromStr` trait is used to convert the argument to the given +//! type, and the `Arg::validator` method is set to a method using +//! `to_string()` (`FromStr::Err` must implement `std::fmt::Display`). +//! If you would like to use a custom string parser other than `FromStr`, see +//! the [same titled section](#custom-string-parsers) below. +//! +//! Thus, the `speed` argument is generated as: +//! +//! ``` +//! # extern crate clap; +//! # fn parse_validator(_: String) -> Result<(), String> { unimplemented!() } +//! # fn main() { +//! clap::Arg::with_name("speed") +//! .takes_value(true) +//! .multiple(false) +//! .required(false) +//! .validator(parse_validator::) +//! .short("s") +//! .long("speed") +//! .help("Set speed") +//! .default_value("42"); +//! # } +//! ``` +//! +//! ## Help messages +//! +//! Help messages for the whole binary or individual arguments can be +//! specified using the `about` attribute on the struct and the `help` +//! attribute on the field, as we've already seen. For convenience, +//! they can also be specified using doc comments. For example: +//! +//! ``` +//! # #[macro_use] extern crate structopt; +//! #[derive(StructOpt)] +//! #[structopt(name = "foo")] +//! /// The help message that will be displayed when passing `--help`. +//! struct Foo { +//! #[structopt(short = "b")] +//! /// The description for the arg that will be displayed when passing `--help`. +//! bar: String +//! } +//! # fn main() {} +//! ``` +//! +//! ## Subcommands +//! +//! Some applications, especially large ones, split their functionality +//! through the use of "subcommands". Each of these act somewhat like a separate +//! command, but is part of the larger group. +//! One example is `git`, which has subcommands such as `add`, `commit`, +//! and `clone`, to mention just a few. +//! +//! `clap` has this functionality, and `structopt` supports it through enums: +//! +//! ``` +//! # #[macro_use] extern crate structopt; +//! # use std::path::PathBuf; +//! #[derive(StructOpt)] +//! #[structopt(name = "git", about = "the stupid content tracker")] +//! enum Git { +//! #[structopt(name = "add")] +//! Add { +//! #[structopt(short = "i")] +//! interactive: bool, +//! #[structopt(short = "p")] +//! patch: bool, +//! #[structopt(parse(from_os_str))] +//! files: Vec +//! }, +//! #[structopt(name = "fetch")] +//! Fetch { +//! #[structopt(long = "dry-run")] +//! dry_run: bool, +//! #[structopt(long = "all")] +//! all: bool, +//! repository: Option +//! }, +//! #[structopt(name = "commit")] +//! Commit { +//! #[structopt(short = "m")] +//! message: Option, +//! #[structopt(short = "a")] +//! all: bool +//! } +//! } +//! # fn main() {} +//! ``` +//! +//! Using `derive(StructOpt)` on an enum instead of a struct will produce +//! a `clap::App` that only takes subcommands. So `git add`, `git fetch`, +//! and `git commit` would be commands allowed for the above example. +//! +//! `structopt` also provides support for applications where certain flags +//! need to apply to all subcommands, as well as nested subcommands: +//! +//! ``` +//! # #[macro_use] extern crate structopt; +//! # fn main() {} +//! #[derive(StructOpt)] +//! #[structopt(name = "make-cookie")] +//! struct MakeCookie { +//! #[structopt(name = "supervisor", default_value = "Puck", long = "supervisor")] +//! supervising_faerie: String, +//! #[structopt(name = "tree")] +//! /// The faerie tree this cookie is being made in. +//! tree: Option, +//! #[structopt(subcommand)] // Note that we mark a field as a subcommand +//! cmd: Command +//! } +//! +//! #[derive(StructOpt)] +//! enum Command { +//! #[structopt(name = "pound")] +//! /// Pound acorns into flour for cookie dough. +//! Pound { +//! acorns: u32 +//! }, +//! #[structopt(name = "sparkle")] +//! /// Add magical sparkles -- the secret ingredient! +//! Sparkle { +//! #[structopt(short = "m", parse(from_occurrences))] +//! magicality: u64, +//! #[structopt(short = "c")] +//! color: String +//! }, +//! #[structopt(name = "finish")] +//! Finish(Finish), +//! } +//! +//! // Subcommand can also be externalized by using a 1-uple enum variant +//! #[derive(StructOpt)] +//! struct Finish { +//! #[structopt(short = "t")] +//! time: u32, +//! #[structopt(subcommand)] // Note that we mark a field as a subcommand +//! finish_type: FinishType +//! } +//! +//! // subsubcommand! +//! #[derive(StructOpt)] +//! enum FinishType { +//! #[structopt(name = "glaze")] +//! Glaze { +//! applications: u32 +//! }, +//! #[structopt(name = "powder")] +//! Powder { +//! flavor: String, +//! dips: u32 +//! } +//! } +//! ``` +//! +//! Marking a field with `structopt(subcommand)` will add the subcommands of the +//! designated enum to the current `clap::App`. The designated enum *must* also +//! be derived `StructOpt`. So the above example would take the following +//! commands: +//! +//! + `make-cookie pound 50` +//! + `make-cookie sparkle -mmm --color "green"` +//! + `make-cookie finish 130 glaze 3` +//! +//! ### Optional subcommands +//! +//! A nested subcommand can be marked optional: +//! +//! ``` +//! # #[macro_use] extern crate structopt; +//! # fn main() {} +//! #[derive(StructOpt)] +//! #[structopt(name = "foo")] +//! struct Foo { +//! file: String, +//! #[structopt(subcommand)] +//! cmd: Option +//! } +//! +//! #[derive(StructOpt)] +//! enum Command { +//! Bar, +//! Baz, +//! Quux +//! } +//! ``` +//! +//! ## Flattening +//! +//! It can sometimes be useful to group related arguments in a substruct, +//! while keeping the command-line interface flat. In these cases you can mark +//! a field as `flatten` and give it another type that derives `StructOpt`: +//! +//! ``` +//! # #[macro_use] extern crate structopt; +//! # use structopt::StructOpt; +//! # fn main() {} +//! #[derive(StructOpt)] +//! struct Cmdline { +//! #[structopt(short = "v", help = "switch on verbosity")] +//! verbose: bool, +//! #[structopt(flatten)] +//! daemon_opts: DaemonOpts, +//! } +//! +//! #[derive(StructOpt)] +//! struct DaemonOpts { +//! #[structopt(short = "u", help = "daemon user")] +//! user: String, +//! #[structopt(short = "g", help = "daemon group")] +//! group: String, +//! } +//! ``` +//! +//! In this example, the derived `Cmdline` parser will support the options `-v`, +//! `-u` and `-g`. +//! +//! This feature also makes it possible to define a `StructOpt` struct in a +//! library, parse the corresponding arguments in the main argument parser, and +//! pass off this struct to a handler provided by that library. +//! +//! ## Custom string parsers +//! +//! If the field type does not have a `FromStr` implementation, or you would +//! like to provide a custom parsing scheme other than `FromStr`, you may +//! provide a custom string parser using `parse(...)` like this: +//! +//! ``` +//! # #[macro_use] extern crate structopt; +//! # fn main() {} +//! use std::num::ParseIntError; +//! use std::path::PathBuf; +//! +//! fn parse_hex(src: &str) -> Result { +//! u32::from_str_radix(src, 16) +//! } +//! +//! #[derive(StructOpt)] +//! struct HexReader { +//! #[structopt(short = "n", parse(try_from_str = "parse_hex"))] +//! number: u32, +//! #[structopt(short = "o", parse(from_os_str))] +//! output: PathBuf, +//! } +//! ``` +//! +//! There are five kinds of custom parsers: +//! +//! | Kind | Signature | Default | +//! |-------------------|---------------------------------------|---------------------------------| +//! | `from_str` | `fn(&str) -> T` | `::std::convert::From::from` | +//! | `try_from_str` | `fn(&str) -> Result` | `::std::str::FromStr::from_str` | +//! | `from_os_str` | `fn(&OsStr) -> T` | `::std::convert::From::from` | +//! | `try_from_os_str` | `fn(&OsStr) -> Result` | (no default function) | +//! | `from_occurrences`| `fn(u64) -> T` | `value as T` | +//! +//! The `from_occurrences` parser is special. Using `parse(from_occurrences)` +//! results in the _number of flags occurrences_ being stored in the relevant +//! field or being passed to the supplied function. In other words, it converts +//! something like `-vvv` to `3`. This is equivalent to +//! `.takes_value(false).multiple(true)`. Note that the default parser can only +//! be used with fields of integer types (`u8`, `usize`, `i64`, etc.). +//! +//! When supplying a custom string parser, `bool` will not be treated specially: +//! +//! Type | Effect | Added method call to `clap::Arg` +//! ------------|-------------------|-------------------------------------- +//! `Option` | optional argument | `.takes_value(true).multiple(false)` +//! `Vec` | list of arguments | `.takes_value(true).multiple(true)` +//! `T` | required argument | `.takes_value(true).multiple(false).required(!has_default)` +//! +//! In the `try_from_*` variants, the function will run twice on valid input: +//! once to validate, and once to parse. Hence, make sure the function is +//! side-effect-free. + +extern crate clap as _clap; + +#[allow(unused_imports)] +#[macro_use] +extern crate structopt_derive; + +#[doc(hidden)] +pub use structopt_derive::*; + +use std::ffi::OsString; + +/// Re-export of clap +pub mod clap { + pub use _clap::*; +} + +/// A struct that is converted from command line arguments. +pub trait StructOpt { + /// Returns the corresponding `clap::App`. + fn clap<'a, 'b>() -> clap::App<'a, 'b>; + + /// Creates the struct from `clap::ArgMatches`. It cannot fail + /// with a parameter generated by `clap` by construction. + fn from_clap(&clap::ArgMatches) -> Self; + + /// Gets the struct from the command line arguments. Print the + /// error message and quit the program in case of failure. + fn from_args() -> Self + where + Self: Sized, + { + Self::from_clap(&Self::clap().get_matches()) + } + + /// Gets the struct from any iterator such as a `Vec` of your making. + /// Print the error message and quit the program in case of failure. + fn from_iter(iter: I) -> Self + where + Self: Sized, + I: IntoIterator, + I::Item: Into + Clone, + { + Self::from_clap(&Self::clap().get_matches_from(iter)) + } + + /// Gets the struct from any iterator such as a `Vec` of your making. + /// + /// Returns a `clap::Error` in case of failure. This does *not* exit in the + /// case of `--help` or `--version`, to achieve the same behavior as + /// `from_iter()` you must call `.exit()` on the error value. + fn from_iter_safe(iter: I) -> Result + where + Self: Sized, + I: IntoIterator, + I::Item: Into + Clone, + { + Ok(Self::from_clap(&Self::clap().get_matches_from_safe(iter)?)) + } +} diff --git a/structopt-derive/Cargo.toml b/structopt-derive/Cargo.toml new file mode 100644 index 00000000..5d294122 --- /dev/null +++ b/structopt-derive/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "structopt-derive" +version = "0.2.10" +authors = ["Guillaume Pinot "] +description = "Parse command line argument by defining a struct, derive crate." +documentation = "https://docs.rs/structopt-derive" +repository = "https://github.com/TeXitoi/structopt" +keywords = ["clap", "cli", "derive", "docopt"] +categories = ["command-line-interface"] +license = "Apache-2.0/MIT" + +[badges] +travis-ci = { repository = "TeXitoi/structopt" } + +[dependencies] +syn = "0.14" +quote = "0.6" +proc-macro2 = "0.4" + +[features] +nightly = ["proc-macro2/nightly"] + +[lib] +proc-macro = true diff --git a/structopt-derive/LICENSE-APACHE b/structopt-derive/LICENSE-APACHE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/structopt-derive/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/structopt-derive/LICENSE-MIT b/structopt-derive/LICENSE-MIT new file mode 100644 index 00000000..e931b83b --- /dev/null +++ b/structopt-derive/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Guillaume Pinot (@TeXitoi) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/structopt-derive/src/attrs.rs b/structopt-derive/src/attrs.rs new file mode 100644 index 00000000..a2d562b0 --- /dev/null +++ b/structopt-derive/src/attrs.rs @@ -0,0 +1,377 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use proc_macro2::{Span, TokenStream}; +use std::{env, mem}; +use syn::Type::Path; +use syn::{self, Attribute, Ident, LitStr, MetaList, MetaNameValue, TypePath}; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Kind { + Arg(Ty), + Subcommand(Ty), + FlattenStruct, +} +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Ty { + Bool, + Vec, + Option, + Other, +} +#[derive(Debug)] +pub struct Attrs { + name: String, + methods: Vec, + parser: (Parser, TokenStream), + has_custom_parser: bool, + kind: Kind, +} +#[derive(Debug)] +struct Method { + name: String, + args: TokenStream, +} +#[derive(Debug, PartialEq)] +pub enum Parser { + FromStr, + TryFromStr, + FromOsStr, + TryFromOsStr, + FromOccurrences, +} +impl ::std::str::FromStr for Parser { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "from_str" => Ok(Parser::FromStr), + "try_from_str" => Ok(Parser::TryFromStr), + "from_os_str" => Ok(Parser::FromOsStr), + "try_from_os_str" => Ok(Parser::TryFromOsStr), + "from_occurrences" => Ok(Parser::FromOccurrences), + _ => Err(format!("unsupported parser {}", s)), + } + } +} + +impl Attrs { + fn new(name: String) -> Attrs { + Attrs { + name: name, + methods: vec![], + parser: (Parser::TryFromStr, quote!(::std::str::FromStr::from_str)), + has_custom_parser: false, + kind: Kind::Arg(Ty::Other), + } + } + fn push_str_method(&mut self, name: &str, arg: &str) { + match (name, arg) { + ("about", "") | ("version", "") | ("author", "") => { + let methods = mem::replace(&mut self.methods, vec![]); + self.methods = methods.into_iter().filter(|m| m.name != name).collect(); + } + ("name", new_name) => self.name = new_name.into(), + (name, arg) => self.methods.push(Method { + name: name.to_string(), + args: quote!(#arg), + }), + } + } + fn push_attrs(&mut self, attrs: &[Attribute]) { + use Lit::*; + use Meta::*; + use NestedMeta::*; + + let iter = attrs + .iter() + .filter_map(|attr| { + let path = &attr.path; + match quote!(#path).to_string() == "structopt" { + true => Some( + attr.interpret_meta() + .expect(&format!("invalid structopt syntax: {}", quote!(attr))), + ), + false => None, + } + }) + .flat_map(|m| match m { + List(l) => l.nested, + tokens => panic!("unsupported syntax: {}", quote!(#tokens).to_string()), + }) + .map(|m| match m { + Meta(m) => m, + ref tokens => panic!("unsupported syntax: {}", quote!(#tokens).to_string()), + }); + for attr in iter { + match attr { + NameValue(MetaNameValue { + ident, + lit: Str(value), + .. + }) => self.push_str_method(&ident.to_string(), &value.value()), + NameValue(MetaNameValue { ident, lit, .. }) => self.methods.push(Method { + name: ident.to_string(), + args: quote!(#lit), + }), + List(MetaList { + ref ident, + ref nested, + .. + }) if ident == "parse" => + { + if nested.len() != 1 { + panic!("parse must have exactly one argument"); + } + self.has_custom_parser = true; + self.parser = match nested[0] { + Meta(NameValue(MetaNameValue { + ref ident, + lit: Str(ref v), + .. + })) => { + let function: syn::Path = v.parse().expect("parser function path"); + let parser = ident.to_string().parse().unwrap(); + (parser, quote!(#function)) + } + Meta(Word(ref i)) => { + use Parser::*; + let parser = i.to_string().parse().unwrap(); + let function = match parser { + FromStr => quote!(::std::convert::From::from), + TryFromStr => quote!(::std::str::FromStr::from_str), + FromOsStr => quote!(::std::convert::From::from), + TryFromOsStr => panic!( + "cannot omit parser function name with `try_from_os_str`" + ), + FromOccurrences => quote!({ |v| v as _ }), + }; + (parser, function) + } + ref l @ _ => panic!("unknown value parser specification: {}", quote!(#l)), + }; + } + List(MetaList { + ref ident, + ref nested, + .. + }) if ident == "raw" => + { + for method in nested { + match *method { + Meta(NameValue(MetaNameValue { + ref ident, + lit: Str(ref v), + .. + })) => self.push_raw_method(&ident.to_string(), v), + ref mi @ _ => panic!("unsupported raw entry: {}", quote!(#mi)), + } + } + } + Word(ref w) if w == "subcommand" => { + self.set_kind(Kind::Subcommand(Ty::Other)); + } + Word(ref w) if w == "flatten" => { + self.set_kind(Kind::FlattenStruct); + } + ref i @ List(..) | ref i @ Word(..) => panic!("unsupported option: {}", quote!(#i)), + } + } + } + fn push_raw_method(&mut self, name: &str, args: &LitStr) { + let ts: TokenStream = args.value().parse().expect(&format!( + "bad parameter {} = {}: the parameter must be valid rust code", + name, + quote!(#args) + )); + self.methods.push(Method { + name: name.to_string(), + args: quote!(#(#ts)*), + }) + } + fn push_doc_comment(&mut self, attrs: &[Attribute], name: &str) { + let doc_comments: Vec<_> = attrs + .iter() + .filter_map(|attr| { + let path = &attr.path; + match quote!(#path).to_string() == "doc" { + true => attr.interpret_meta(), + false => None, + } + }) + .filter_map(|attr| { + use Lit::*; + use Meta::*; + if let NameValue(MetaNameValue { + ident, lit: Str(s), .. + }) = attr + { + if ident != "doc" { + return None; + } + let value = s.value(); + let text = value + .trim_left_matches("//!") + .trim_left_matches("///") + .trim_left_matches("/*!") + .trim_left_matches("/**") + .trim_right_matches("*/") + .trim(); + if text.is_empty() { + Some("\n\n".to_string()) + } else { + Some(text.to_string()) + } + } else { + None + } + }) + .collect(); + if doc_comments.is_empty() { + return; + } + let arg = doc_comments + .join(" ") + .split('\n') + .map(|l| l.trim().to_string()) + .collect::>() + .join("\n"); + self.methods.push(Method { + name: name.to_string(), + args: quote!(#arg), + }); + } + pub fn from_struct(attrs: &[Attribute], name: String) -> Attrs { + let mut res = Self::new(name); + let attrs_with_env = [ + ("version", "CARGO_PKG_VERSION"), + ("about", "CARGO_PKG_DESCRIPTION"), + ("author", "CARGO_PKG_AUTHORS"), + ]; + attrs_with_env + .iter() + .filter_map(|&(m, v)| env::var(v).ok().and_then(|arg| Some((m, arg)))) + .filter(|&(_, ref arg)| !arg.is_empty()) + .for_each(|(name, arg)| { + let new_arg = if name == "author" { + arg.replace(":", ", ") + } else { + arg + }; + res.push_str_method(name, &new_arg); + }); + res.push_doc_comment(attrs, "about"); + res.push_attrs(attrs); + if res.has_custom_parser { + panic!("parse attribute is only allowed on fields"); + } + match res.kind { + Kind::Subcommand(_) => panic!("subcommand is only allowed on fields"), + Kind::FlattenStruct => panic!("flatten is only allowed on fields"), + Kind::Arg(_) => res, + } + } + fn ty_from_field(ty: &syn::Type) -> Ty { + if let Path(TypePath { + path: syn::Path { ref segments, .. }, + .. + }) = *ty + { + match segments.iter().last().unwrap().ident.to_string().as_str() { + "bool" => Ty::Bool, + "Option" => Ty::Option, + "Vec" => Ty::Vec, + _ => Ty::Other, + } + } else { + Ty::Other + } + } + pub fn from_field(field: &syn::Field) -> Attrs { + let name = field.ident.as_ref().unwrap().to_string(); + let mut res = Self::new(name); + res.push_doc_comment(&field.attrs, "help"); + res.push_attrs(&field.attrs); + + match res.kind { + Kind::FlattenStruct => { + if res.has_custom_parser { + panic!("parse attribute is not allowed for flattened entry"); + } + if !res.methods.is_empty() { + panic!("methods and doc comments are not allowed for flattened entry"); + } + } + Kind::Subcommand(_) => { + if res.has_custom_parser { + panic!("parse attribute is not allowed for subcommand"); + } + if !res.methods.iter().all(|m| m.name == "help") { + panic!("methods in attributes is not allowed for subcommand"); + } + res.kind = Kind::Subcommand(Self::ty_from_field(&field.ty)); + } + Kind::Arg(_) => { + let mut ty = Self::ty_from_field(&field.ty); + if res.has_custom_parser { + match ty { + Ty::Option | Ty::Vec => (), + _ => ty = Ty::Other, + } + } + match ty { + Ty::Bool => { + if res.has_method("default_value") { + panic!("default_value is meaningless for bool") + } + if res.has_method("required") { + panic!("required is meaningless for bool") + } + } + Ty::Option => { + if res.has_method("default_value") { + panic!("default_value is meaningless for Option") + } + if res.has_method("required") { + panic!("required is meaningless for Option") + } + } + _ => (), + } + res.kind = Kind::Arg(ty); + } + } + + res + } + fn set_kind(&mut self, kind: Kind) { + if let Kind::Arg(_) = self.kind { + self.kind = kind; + } else { + panic!("subcommands cannot be flattened"); + } + } + pub fn has_method(&self, method: &str) -> bool { + self.methods.iter().find(|m| m.name == method).is_some() + } + pub fn methods(&self) -> TokenStream { + let methods = self.methods.iter().map(|&Method { ref name, ref args }| { + let name = Ident::new(&name, Span::call_site()); + quote!( .#name(#args) ) + }); + quote!( #(#methods)* ) + } + pub fn name(&self) -> &str { + &self.name + } + pub fn parser(&self) -> &(Parser, TokenStream) { + &self.parser + } + pub fn kind(&self) -> Kind { + self.kind + } +} diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs new file mode 100644 index 00000000..08d2b203 --- /dev/null +++ b/structopt-derive/src/lib.rs @@ -0,0 +1,443 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! This crate is custom derive for StructOpt. It should not be used +//! directly. See [structopt documentation](https://docs.rs/structopt) +//! for the usage of `#[derive(StructOpt)]`. + +extern crate proc_macro; +extern crate syn; +#[macro_use] +extern crate quote; +extern crate proc_macro2; + +mod attrs; + +use attrs::{Attrs, Kind, Parser, Ty}; +use proc_macro2::{Span, TokenStream}; +use syn::punctuated::Punctuated; +use syn::token::Comma; +use syn::*; + +/// Generates the `StructOpt` impl. +#[proc_macro_derive(StructOpt, attributes(structopt))] +pub fn structopt(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input: DeriveInput = syn::parse(input).unwrap(); + let gen = impl_structopt(&input); + gen.into() +} + +fn sub_type(t: &syn::Type) -> Option<&syn::Type> { + let segs = match *t { + syn::Type::Path(TypePath { + path: syn::Path { ref segments, .. }, + .. + }) => segments, + _ => return None, + }; + match *segs.iter().last().unwrap() { + PathSegment { + arguments: + PathArguments::AngleBracketed(AngleBracketedGenericArguments { ref args, .. }), + .. + } if args.len() == 1 => + { + if let GenericArgument::Type(ref ty) = args[0] { + Some(ty) + } else { + None + } + } + _ => None, + } +} + +/// Generate a block of code to add arguments/subcommands corresponding to +/// the `fields` to an app. +fn gen_augmentation(fields: &Punctuated, app_var: &Ident) -> TokenStream { + let subcmds: Vec<_> = fields + .iter() + .filter_map(|field| { + let attrs = Attrs::from_field(&field); + if let Kind::Subcommand(ty) = attrs.kind() { + let subcmd_type = match (ty, sub_type(&field.ty)) { + (Ty::Option, Some(sub_type)) => sub_type, + _ => &field.ty, + }; + let required = if ty == Ty::Option { + quote!() + } else { + quote! { + let #app_var = #app_var.setting( + ::structopt::clap::AppSettings::SubcommandRequiredElseHelp + ); + } + }; + + Some(quote!{ + let #app_var = <#subcmd_type>::augment_clap( #app_var ); + #required + }) + } else { + None + } + }) + .collect(); + + assert!( + subcmds.len() <= 1, + "cannot have more than one nested subcommand" + ); + + let args = fields.iter().filter_map(|field| { + let attrs = Attrs::from_field(field); + match attrs.kind() { + Kind::Subcommand(_) => None, + Kind::FlattenStruct => { + let ty = &field.ty; + Some(quote! { + let #app_var = <#ty>::augment_clap(#app_var); + let #app_var = if <#ty>::is_subcommand() { + #app_var.setting(::structopt::clap::AppSettings::SubcommandRequiredElseHelp) + } else { + #app_var + }; + }) + } + Kind::Arg(ty) => { + let convert_type = match ty { + Ty::Vec | Ty::Option => sub_type(&field.ty).unwrap_or(&field.ty), + _ => &field.ty, + }; + + let occurences = attrs.parser().0 == Parser::FromOccurrences; + + let validator = match *attrs.parser() { + (Parser::TryFromStr, ref f) => quote! { + .validator(|s| { + #f(&s) + .map(|_: #convert_type| ()) + .map_err(|e| e.to_string()) + }) + }, + (Parser::TryFromOsStr, ref f) => quote! { + .validator_os(|s| #f(&s).map(|_: #convert_type| ())) + }, + _ => quote!(), + }; + + let modifier = match ty { + Ty::Bool => quote!( .takes_value(false).multiple(false) ), + Ty::Option => quote!( .takes_value(true).multiple(false) #validator ), + Ty::Vec => quote!( .takes_value(true).multiple(true) #validator ), + Ty::Other if occurences => quote!( .takes_value(false).multiple(true) ), + Ty::Other => { + let required = !attrs.has_method("default_value"); + quote!( .takes_value(true).multiple(false).required(#required) #validator ) + } + }; + let methods = attrs.methods(); + let name = attrs.name(); + Some(quote!{ + let #app_var = #app_var.arg( + ::structopt::clap::Arg::with_name(#name) + #modifier + #methods + ); + }) + } + } + }); + + quote! {{ + #( #args )* + #( #subcmds )* + #app_var + }} +} + +fn gen_constructor(fields: &Punctuated) -> TokenStream { + let fields = fields.iter().map(|field| { + let attrs = Attrs::from_field(field); + let field_name = field.ident.as_ref().unwrap(); + match attrs.kind() { + Kind::Subcommand(ty) => { + let subcmd_type = match (ty, sub_type(&field.ty)) { + (Ty::Option, Some(sub_type)) => sub_type, + _ => &field.ty, + }; + let unwrapper = match ty { + Ty::Option => quote!(), + _ => quote!( .unwrap() ), + }; + quote!(#field_name: <#subcmd_type>::from_subcommand(matches.subcommand())#unwrapper) + } + Kind::FlattenStruct => quote!(#field_name: ::structopt::StructOpt::from_clap(matches)), + Kind::Arg(ty) => { + use Parser::*; + let (value_of, values_of, parse) = match *attrs.parser() { + (FromStr, ref f) => (quote!(value_of), quote!(values_of), f.clone()), + (TryFromStr, ref f) => ( + quote!(value_of), + quote!(values_of), + quote!(|s| #f(s).unwrap()), + ), + (FromOsStr, ref f) => (quote!(value_of_os), quote!(values_of_os), f.clone()), + (TryFromOsStr, ref f) => ( + quote!(value_of_os), + quote!(values_of_os), + quote!(|s| #f(s).unwrap()), + ), + (FromOccurrences, ref f) => (quote!(occurrences_of), quote!(), f.clone()), + }; + + let occurences = attrs.parser().0 == Parser::FromOccurrences; + let name = attrs.name(); + let field_value = match ty { + Ty::Bool => quote!(matches.is_present(#name)), + Ty::Option => quote! { + matches.#value_of(#name) + .as_ref() + .map(#parse) + }, + Ty::Vec => quote! { + matches.#values_of(#name) + .map(|v| v.map(#parse).collect()) + .unwrap_or_else(Vec::new) + }, + Ty::Other if occurences => quote! { + #parse(matches.#value_of(#name)) + }, + Ty::Other => quote! { + matches.#value_of(#name) + .map(#parse) + .unwrap() + }, + }; + + quote!( #field_name: #field_value ) + } + } + }); + + quote! {{ + #( #fields ),* + }} +} + +fn gen_from_clap(struct_name: &Ident, fields: &Punctuated) -> TokenStream { + let field_block = gen_constructor(fields); + + quote! { + fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self { + #struct_name #field_block + } + } +} + +fn gen_clap(attrs: &[Attribute]) -> TokenStream { + let name = std::env::var("CARGO_PKG_NAME") + .ok() + .unwrap_or_else(String::default); + let attrs = Attrs::from_struct(attrs, name); + let name = attrs.name(); + let methods = attrs.methods(); + quote!(::structopt::clap::App::new(#name)#methods) +} + +fn gen_clap_struct(struct_attrs: &[Attribute]) -> TokenStream { + let gen = gen_clap(struct_attrs); + quote! { + fn clap<'a, 'b>() -> ::structopt::clap::App<'a, 'b> { + let app = #gen; + Self::augment_clap(app) + } + } +} + +fn gen_augment_clap(fields: &Punctuated) -> TokenStream { + let app_var = Ident::new("app", Span::call_site()); + let augmentation = gen_augmentation(fields, &app_var); + quote! { + pub fn augment_clap<'a, 'b>( + #app_var: ::structopt::clap::App<'a, 'b> + ) -> ::structopt::clap::App<'a, 'b> { + #augmentation + } + } +} + +fn gen_clap_enum(enum_attrs: &[Attribute]) -> TokenStream { + let gen = gen_clap(enum_attrs); + quote! { + fn clap<'a, 'b>() -> ::structopt::clap::App<'a, 'b> { + let app = #gen + .setting(::structopt::clap::AppSettings::SubcommandRequiredElseHelp); + Self::augment_clap(app) + } + } +} + +fn gen_augment_clap_enum(variants: &Punctuated) -> TokenStream { + use syn::Fields::*; + + let subcommands = variants.iter().map(|variant| { + let name = variant.ident.to_string(); + let attrs = Attrs::from_struct(&variant.attrs, name); + let app_var = Ident::new("subcommand", Span::call_site()); + let arg_block = match variant.fields { + Named(ref fields) => gen_augmentation(&fields.named, &app_var), + Unit => quote!( #app_var ), + Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { + let ty = &unnamed[0]; + quote! { + { + let #app_var = <#ty>::augment_clap(#app_var); + if <#ty>::is_subcommand() { + #app_var.setting( + ::structopt::clap::AppSettings::SubcommandRequiredElseHelp + ) + } else { + #app_var + } + } + } + } + Unnamed(..) => panic!("{}: tuple enum are not supported", variant.ident), + }; + + let name = attrs.name(); + let from_attrs = attrs.methods(); + quote! { + .subcommand({ + let #app_var = ::structopt::clap::SubCommand::with_name(#name); + let #app_var = #arg_block; + #app_var#from_attrs + }) + } + }); + + quote! { + pub fn augment_clap<'a, 'b>( + app: ::structopt::clap::App<'a, 'b> + ) -> ::structopt::clap::App<'a, 'b> { + app #( #subcommands )* + } + } +} + +fn gen_from_clap_enum(name: &Ident) -> TokenStream { + quote! { + fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self { + <#name>::from_subcommand(matches.subcommand()) + .unwrap() + } + } +} + +fn gen_from_subcommand(name: &Ident, variants: &Punctuated) -> TokenStream { + use syn::Fields::*; + + let match_arms = variants.iter().map(|variant| { + let attrs = Attrs::from_struct(&variant.attrs, variant.ident.to_string()); + let sub_name = attrs.name(); + let variant_name = &variant.ident; + let constructor_block = match variant.fields { + Named(ref fields) => gen_constructor(&fields.named), + Unit => quote!(), + Unnamed(ref fields) if fields.unnamed.len() == 1 => { + let ty = &fields.unnamed[0]; + quote!( ( <#ty as ::structopt::StructOpt>::from_clap(matches) ) ) + } + Unnamed(..) => panic!("{}: tuple enum are not supported", variant.ident), + }; + + quote! { + (#sub_name, Some(matches)) => + Some(#name :: #variant_name #constructor_block) + } + }); + + quote! { + pub fn from_subcommand<'a, 'b>( + sub: (&'b str, Option<&'b ::structopt::clap::ArgMatches<'a>>) + ) -> Option { + match sub { + #( #match_arms ),*, + _ => None + } + } + } +} + +fn impl_structopt_for_struct( + name: &Ident, + fields: &Punctuated, + attrs: &[Attribute], +) -> TokenStream { + let clap = gen_clap_struct(attrs); + let augment_clap = gen_augment_clap(fields); + let from_clap = gen_from_clap(name, fields); + + quote! { + #[allow(unused_variables)] + impl ::structopt::StructOpt for #name { + #clap + #from_clap + } + + #[allow(dead_code, unreachable_code)] + #[doc(hidden)] + impl #name { + #augment_clap + pub fn is_subcommand() -> bool { false } + } + } +} + +fn impl_structopt_for_enum( + name: &Ident, + variants: &Punctuated, + attrs: &[Attribute], +) -> TokenStream { + let clap = gen_clap_enum(attrs); + let augment_clap = gen_augment_clap_enum(variants); + let from_clap = gen_from_clap_enum(name); + let from_subcommand = gen_from_subcommand(name, variants); + + quote! { + impl ::structopt::StructOpt for #name { + #clap + #from_clap + } + + #[allow(unused_variables, dead_code, unreachable_code)] + #[doc(hidden)] + impl #name { + #augment_clap + #from_subcommand + pub fn is_subcommand() -> bool { true } + } + } +} + +fn impl_structopt(input: &DeriveInput) -> TokenStream { + use syn::Data::*; + + let struct_name = &input.ident; + let inner_impl = match input.data { + Struct(DataStruct { + fields: syn::Fields::Named(ref fields), + .. + }) => impl_structopt_for_struct(struct_name, &fields.named, &input.attrs), + Enum(ref e) => impl_structopt_for_enum(struct_name, &e.variants, &input.attrs), + _ => panic!("structopt only supports non-tuple structs and enums"), + }; + + quote!(#inner_impl) +} diff --git a/tests/arguments.rs b/tests/arguments.rs new file mode 100644 index 00000000..47b57f4c --- /dev/null +++ b/tests/arguments.rs @@ -0,0 +1,111 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::clap; +use structopt::StructOpt; + +#[test] +fn required_argument() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + arg: i32, + } + assert_eq!(Opt { arg: 42 }, Opt::from_iter(&["test", "42"])); + assert!(Opt::clap().get_matches_from_safe(&["test"]).is_err()); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "42", "24"]) + .is_err() + ); +} + +#[test] +fn optional_argument() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + arg: Option, + } + assert_eq!(Opt { arg: Some(42) }, Opt::from_iter(&["test", "42"])); + assert_eq!(Opt { arg: None }, Opt::from_iter(&["test"])); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "42", "24"]) + .is_err() + ); +} + +#[test] +fn argument_with_default() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(default_value = "42")] + arg: i32, + } + assert_eq!(Opt { arg: 24 }, Opt::from_iter(&["test", "24"])); + assert_eq!(Opt { arg: 42 }, Opt::from_iter(&["test"])); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "42", "24"]) + .is_err() + ); +} + +#[test] +fn argument_with_raw_default() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(raw(default_value = r#""42""#))] + arg: i32, + } + assert_eq!(Opt { arg: 24 }, Opt::from_iter(&["test", "24"])); + assert_eq!(Opt { arg: 42 }, Opt::from_iter(&["test"])); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "42", "24"]) + .is_err() + ); +} + +#[test] +fn arguments() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + arg: Vec, + } + assert_eq!(Opt { arg: vec![24] }, Opt::from_iter(&["test", "24"])); + assert_eq!(Opt { arg: vec![] }, Opt::from_iter(&["test"])); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::from_iter(&["test", "24", "42"]) + ); +} + +#[test] +fn arguments_safe() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + arg: Vec, + } + assert_eq!( + Opt { arg: vec![24] }, + Opt::from_iter_safe(&["test", "24"]).unwrap() + ); + assert_eq!(Opt { arg: vec![] }, Opt::from_iter_safe(&["test"]).unwrap()); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::from_iter_safe(&["test", "24", "42"]).unwrap() + ); + + assert_eq!( + clap::ErrorKind::ValueValidation, + Opt::from_iter_safe(&["test", "NOPE"]).err().unwrap().kind + ); +} diff --git a/tests/author_version_about.rs b/tests/author_version_about.rs new file mode 100644 index 00000000..89dad08b --- /dev/null +++ b/tests/author_version_about.rs @@ -0,0 +1,39 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[test] +fn no_author_version_about() { + #[derive(StructOpt, PartialEq, Debug)] + #[structopt(name = "foo", about = "", author = "", version = "")] + struct Opt {} + + let mut output = Vec::new(); + Opt::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + assert!(output.starts_with("foo \n\nUSAGE:")); +} + +#[test] +fn use_env() { + #[derive(StructOpt, PartialEq, Debug)] + #[structopt()] + struct Opt {} + + let mut output = Vec::new(); + Opt::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + assert!(output.starts_with("structopt 0.2.")); + assert!(output.contains("Guillaume Pinot , others")); + assert!(output.contains("Parse command line argument by defining a struct.")); +} diff --git a/tests/custom-string-parsers.rs b/tests/custom-string-parsers.rs new file mode 100644 index 00000000..738f3e21 --- /dev/null +++ b/tests/custom-string-parsers.rs @@ -0,0 +1,290 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +use std::ffi::{OsStr, OsString}; +use std::num::ParseIntError; +use std::path::PathBuf; + +#[derive(StructOpt, PartialEq, Debug)] +struct PathOpt { + #[structopt(short = "p", long = "path", parse(from_os_str))] + path: PathBuf, + + #[structopt(short = "d", default_value = "../", parse(from_os_str))] + default_path: PathBuf, + + #[structopt(short = "v", parse(from_os_str))] + vector_path: Vec, + + #[structopt(short = "o", parse(from_os_str))] + option_path_1: Option, + + #[structopt(short = "q", parse(from_os_str))] + option_path_2: Option, +} + +#[test] +fn test_path_opt_simple() { + assert_eq!( + PathOpt { + path: PathBuf::from("/usr/bin"), + default_path: PathBuf::from("../"), + vector_path: vec![ + PathBuf::from("/a/b/c"), + PathBuf::from("/d/e/f"), + PathBuf::from("/g/h/i"), + ], + option_path_1: None, + option_path_2: Some(PathBuf::from("j.zip")), + }, + PathOpt::from_clap(&PathOpt::clap().get_matches_from(&[ + "test", "-p", "/usr/bin", "-v", "/a/b/c", "-v", "/d/e/f", "-v", "/g/h/i", "-q", + "j.zip", + ])) + ); +} + +fn parse_hex(input: &str) -> Result { + u64::from_str_radix(input, 16) +} + +#[derive(StructOpt, PartialEq, Debug)] +struct HexOpt { + #[structopt(short = "n", parse(try_from_str = "parse_hex"))] + number: u64, +} + +#[test] +fn test_parse_hex() { + assert_eq!( + HexOpt { number: 5 }, + HexOpt::from_clap(&HexOpt::clap().get_matches_from(&["test", "-n", "5"])) + ); + assert_eq!( + HexOpt { number: 0xabcdef }, + HexOpt::from_clap(&HexOpt::clap().get_matches_from(&["test", "-n", "abcdef"])) + ); + + let err = HexOpt::clap() + .get_matches_from_safe(&["test", "-n", "gg"]) + .unwrap_err(); + assert!(err.message.contains("invalid digit found in string"), err); +} + +fn custom_parser_1(_: &str) -> &'static str { + "A" +} +fn custom_parser_2(_: &str) -> Result<&'static str, u32> { + Ok("B") +} +fn custom_parser_3(_: &OsStr) -> &'static str { + "C" +} +fn custom_parser_4(_: &OsStr) -> Result<&'static str, OsString> { + Ok("D") +} + +#[derive(StructOpt, PartialEq, Debug)] +struct NoOpOpt { + #[structopt(short = "a", parse(from_str = "custom_parser_1"))] + a: &'static str, + #[structopt(short = "b", parse(try_from_str = "custom_parser_2"))] + b: &'static str, + #[structopt(short = "c", parse(from_os_str = "custom_parser_3"))] + c: &'static str, + #[structopt(short = "d", parse(try_from_os_str = "custom_parser_4"))] + d: &'static str, +} + +#[test] +fn test_every_custom_parser() { + assert_eq!( + NoOpOpt { + a: "A", + b: "B", + c: "C", + d: "D" + }, + NoOpOpt::from_clap(&NoOpOpt::clap().get_matches_from(&["test", "-a=?", "-b=?", "-c=?", "-d=?"])) + ); +} + +// Note: can't use `Vec` directly, as structopt would instead look for +// conversion function from `&str` to `u8`. +type Bytes = Vec; + +#[derive(StructOpt, PartialEq, Debug)] +struct DefaultedOpt { + #[structopt(short = "b", parse(from_str))] + bytes: Bytes, + + #[structopt(short = "i", parse(try_from_str))] + integer: u64, + + #[structopt(short = "p", parse(from_os_str))] + path: PathBuf, +} + +#[test] +fn test_parser_with_default_value() { + assert_eq!( + DefaultedOpt { + bytes: b"E\xc2\xb2=p\xc2\xb2c\xc2\xb2+m\xc2\xb2c\xe2\x81\xb4".to_vec(), + integer: 9000, + path: PathBuf::from("src/lib.rs"), + }, + DefaultedOpt::from_clap(&DefaultedOpt::clap().get_matches_from(&[ + "test", + "-b", + "E²=p²c²+m²c⁴", + "-i", + "9000", + "-p", + "src/lib.rs", + ])) + ); +} + +#[derive(PartialEq, Debug)] +struct Foo(u8); + +fn foo(value: u64) -> Foo { + Foo(value as u8) +} + +#[derive(StructOpt, PartialEq, Debug)] +struct Occurrences { + #[structopt(short = "s", long = "signed", parse(from_occurrences))] + signed: i32, + + #[structopt(short = "l", parse(from_occurrences))] + little_signed: i8, + + #[structopt(short = "u", parse(from_occurrences))] + unsigned: usize, + + #[structopt(short = "r", parse(from_occurrences))] + little_unsigned: u8, + + #[structopt(short = "c", long = "custom", parse(from_occurrences = "foo"))] + custom: Foo, +} + +#[test] +fn test_parser_occurrences() { + assert_eq!( + Occurrences { + signed: 3, + little_signed: 1, + unsigned: 0, + little_unsigned: 4, + custom: Foo(5), + }, + Occurrences::from_clap(&Occurrences::clap().get_matches_from(&[ + "test", "-s", "--signed", "--signed", "-l", "-rrrr", "-cccc", "--custom", + ])) + ); +} + +#[test] +fn test_custom_bool() { + fn parse_bool(s: &str) -> Result { + match s { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(format!("invalid bool {}", s)), + } + } + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "d", parse(try_from_str = "parse_bool"))] + debug: bool, + #[structopt(short = "v", default_value = "false", parse(try_from_str = "parse_bool"))] + verbose: bool, + #[structopt(short = "t", parse(try_from_str = "parse_bool"))] + tribool: Option, + #[structopt(short = "b", parse(try_from_str = "parse_bool"))] + bitset: Vec, + } + + assert!(Opt::clap().get_matches_from_safe(&["test"]).is_err()); + assert!(Opt::clap().get_matches_from_safe(&["test", "-d"]).is_err()); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-dfoo"]) + .is_err() + ); + assert_eq!( + Opt { + debug: false, + verbose: false, + tribool: None, + bitset: vec![], + }, + Opt::from_iter(&["test", "-dfalse"]) + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: None, + bitset: vec![], + }, + Opt::from_iter(&["test", "-dtrue"]) + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: None, + bitset: vec![], + }, + Opt::from_iter(&["test", "-dtrue", "-vfalse"]) + ); + assert_eq!( + Opt { + debug: true, + verbose: true, + tribool: None, + bitset: vec![], + }, + Opt::from_iter(&["test", "-dtrue", "-vtrue"]) + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: Some(false), + bitset: vec![], + }, + Opt::from_iter(&["test", "-dtrue", "-tfalse"]) + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: Some(true), + bitset: vec![], + }, + Opt::from_iter(&["test", "-dtrue", "-ttrue"]) + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: None, + bitset: vec![false, true, false, false], + }, + Opt::from_iter(&["test", "-dtrue", "-bfalse", "-btrue", "-bfalse", "-bfalse"]) + ); +} diff --git a/tests/deny-warnings.rs b/tests/deny-warnings.rs new file mode 100644 index 00000000..b73d1711 --- /dev/null +++ b/tests/deny-warnings.rs @@ -0,0 +1,52 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![deny(warnings)] +#![cfg(feature = "nightly")] // TODO: remove that when never is stable +#![feature(never_type)] + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +fn try_str(s: &str) -> Result { + Ok(s.into()) +} + +#[test] +fn warning_never_struct() { + #[derive(Debug, PartialEq, StructOpt)] + struct Opt { + #[structopt(parse(try_from_str = "try_str"))] + s: String, + } + assert_eq!( + Opt { + s: "foo".to_string() + }, + Opt::from_iter(&["test", "foo"]) + ); +} + +#[test] +fn warning_never_enum() { + #[derive(Debug, PartialEq, StructOpt)] + enum Opt { + Foo { + #[structopt(parse(try_from_str = "try_str"))] + s: String, + }, + } + assert_eq!( + Opt::Foo { + s: "foo".to_string() + }, + Opt::from_iter(&["test", "Foo", "foo"]) + ); +} diff --git a/tests/doc-comments-help.rs b/tests/doc-comments-help.rs new file mode 100644 index 00000000..ace481dd --- /dev/null +++ b/tests/doc-comments-help.rs @@ -0,0 +1,68 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[test] +fn commets_intead_of_actual_help() { + /// Lorem ipsum + #[derive(StructOpt, PartialEq, Debug)] + struct LoremIpsum { + /// Fooify a bar + /// and a baz + #[structopt(short = "f", long = "foo")] + foo: bool, + } + + let mut output = Vec::new(); + LoremIpsum::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + assert!(output.contains("Lorem ipsum")); + assert!(output.contains("Fooify a bar and a baz")); +} + +#[test] +fn help_is_better_than_comments() { + /// Lorem ipsum + #[derive(StructOpt, PartialEq, Debug)] + #[structopt(name = "lorem-ipsum", about = "Dolor sit amet")] + struct LoremIpsum { + /// Fooify a bar + #[structopt(short = "f", long = "foo", help = "DO NOT PASS A BAR UNDER ANY CIRCUMSTANCES")] + foo: bool, + } + + let mut output = Vec::new(); + LoremIpsum::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + assert!(output.contains("Dolor sit amet")); + assert!(!output.contains("Lorem ipsum")); + assert!(output.contains("DO NOT PASS A BAR")); +} + +#[test] +fn empty_line_in_doc_comment_is_double_linefeed() { + /// Foo. + /// + /// Bar + #[derive(StructOpt, PartialEq, Debug)] + #[structopt(name = "lorem-ipsum", author = "", version = "")] + struct LoremIpsum {} + + let mut output = Vec::new(); + LoremIpsum::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + println!("{}", output); + assert!(output.starts_with("lorem-ipsum \nFoo.\n\nBar\n\nUSAGE:")); +} diff --git a/tests/flags.rs b/tests/flags.rs new file mode 100644 index 00000000..9e8aaf47 --- /dev/null +++ b/tests/flags.rs @@ -0,0 +1,142 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[test] +fn unique_flag() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", long = "alice")] + alice: bool, + } + + assert_eq!( + Opt { alice: false }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert_eq!( + Opt { alice: true }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a"])) + ); + assert_eq!( + Opt { alice: true }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--alice"])) + ); + assert!(Opt::clap().get_matches_from_safe(&["test", "-i"]).is_err()); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a", "foo"]) + .is_err() + ); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a", "-a"]) + .is_err() + ); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a", "--alice"]) + .is_err() + ); +} + +#[test] +fn multiple_flag() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", long = "alice", parse(from_occurrences))] + alice: u64, + #[structopt(short = "b", long = "bob", parse(from_occurrences))] + bob: u8, + } + + assert_eq!( + Opt { alice: 0, bob: 0 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert_eq!( + Opt { alice: 1, bob: 0 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a"])) + ); + assert_eq!( + Opt { alice: 2, bob: 0 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a", "-a"])) + ); + assert_eq!( + Opt { alice: 2, bob: 2 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a", "--alice", "-bb"])) + ); + assert_eq!( + Opt { alice: 3, bob: 1 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-aaa", "--bob"])) + ); + assert!(Opt::clap().get_matches_from_safe(&["test", "-i"]).is_err()); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a", "foo"]) + .is_err() + ); +} + +#[test] +fn combined_flags() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", long = "alice")] + alice: bool, + #[structopt(short = "b", long = "bob", parse(from_occurrences))] + bob: u64, + } + + assert_eq!( + Opt { + alice: false, + bob: 0 + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert_eq!( + Opt { + alice: true, + bob: 0 + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a"])) + ); + assert_eq!( + Opt { + alice: true, + bob: 0 + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a"])) + ); + assert_eq!( + Opt { + alice: false, + bob: 1 + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-b"])) + ); + assert_eq!( + Opt { + alice: true, + bob: 1 + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--alice", "--bob"])) + ); + assert_eq!( + Opt { + alice: true, + bob: 4 + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-bb", "-a", "-bb"])) + ); +} diff --git a/tests/flatten.rs b/tests/flatten.rs new file mode 100644 index 00000000..b1e802be --- /dev/null +++ b/tests/flatten.rs @@ -0,0 +1,102 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[test] +fn flatten() { + #[derive(StructOpt, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(flatten)] + common: Common, + } + assert_eq!( + Opt { + common: Common { arg: 42 } + }, + Opt::from_iter(&["test", "42"]) + ); + assert!(Opt::clap().get_matches_from_safe(&["test"]).is_err()); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "42", "24"]) + .is_err() + ); +} + +#[test] +#[should_panic] +fn flatten_twice() { + #[derive(StructOpt, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(flatten)] + c1: Common, + // Defines "arg" twice, so this should not work. + #[structopt(flatten)] + c2: Common, + } + Opt::from_iter(&["test", "42", "43"]); +} + +#[test] +fn flatten_in_subcommand() { + #[derive(StructOpt, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(StructOpt, PartialEq, Debug)] + struct Add { + #[structopt(short = "i")] + interactive: bool, + #[structopt(flatten)] + common: Common, + } + + #[derive(StructOpt, PartialEq, Debug)] + enum Opt { + #[structopt(name = "fetch")] + Fetch { + #[structopt(short = "a")] + all: bool, + #[structopt(flatten)] + common: Common, + }, + + #[structopt(name = "add")] + Add(Add), + } + + assert_eq!( + Opt::Fetch { + all: false, + common: Common { arg: 42 } + }, + Opt::from_iter(&["test", "fetch", "42"]) + ); + assert_eq!( + Opt::Add(Add { + interactive: true, + common: Common { arg: 43 } + }), + Opt::from_iter(&["test", "add", "-i", "43"]) + ); +} diff --git a/tests/nested-subcommands.rs b/tests/nested-subcommands.rs new file mode 100644 index 00000000..8557e13e --- /dev/null +++ b/tests/nested-subcommands.rs @@ -0,0 +1,206 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, PartialEq, Debug)] +struct Opt { + #[structopt(short = "f", long = "force")] + force: bool, + #[structopt(short = "v", long = "verbose", parse(from_occurrences))] + verbose: u64, + #[structopt(subcommand)] + cmd: Sub, +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Sub { + #[structopt(name = "fetch")] + Fetch {}, + #[structopt(name = "add")] + Add {}, +} + +#[derive(StructOpt, PartialEq, Debug)] +struct Opt2 { + #[structopt(short = "f", long = "force")] + force: bool, + #[structopt(short = "v", long = "verbose", parse(from_occurrences))] + verbose: u64, + #[structopt(subcommand)] + cmd: Option, +} + +#[test] +fn test_no_cmd() { + let result = Opt::clap().get_matches_from_safe(&["test"]); + assert!(result.is_err()); + + assert_eq!( + Opt2 { + force: false, + verbose: 0, + cmd: None + }, + Opt2::from_clap(&Opt2::clap().get_matches_from(&["test"])) + ); +} + +#[test] +fn test_fetch() { + assert_eq!( + Opt { + force: false, + verbose: 3, + cmd: Sub::Fetch {} + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-vvv", "fetch"])) + ); + assert_eq!( + Opt { + force: true, + verbose: 0, + cmd: Sub::Fetch {} + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--force", "fetch"])) + ); +} + +#[test] +fn test_add() { + assert_eq!( + Opt { + force: false, + verbose: 0, + cmd: Sub::Add {} + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "add"])) + ); + assert_eq!( + Opt { + force: false, + verbose: 2, + cmd: Sub::Add {} + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-vv", "add"])) + ); +} + +#[test] +fn test_badinput() { + let result = Opt::clap().get_matches_from_safe(&["test", "badcmd"]); + assert!(result.is_err()); + let result = Opt::clap().get_matches_from_safe(&["test", "add", "--verbose"]); + assert!(result.is_err()); + let result = Opt::clap().get_matches_from_safe(&["test", "--badopt", "add"]); + assert!(result.is_err()); + let result = Opt::clap().get_matches_from_safe(&["test", "add", "--badopt"]); + assert!(result.is_err()); +} + +#[derive(StructOpt, PartialEq, Debug)] +struct Opt3 { + #[structopt(short = "a", long = "all")] + all: bool, + #[structopt(subcommand)] + cmd: Sub2, +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Sub2 { + #[structopt(name = "foo")] + Foo { + file: String, + #[structopt(subcommand)] + cmd: Sub3, + }, + #[structopt(name = "bar")] + Bar {}, +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Sub3 { + #[structopt(name = "baz")] + Baz {}, + #[structopt(name = "quux")] + Quux {}, +} + +#[test] +fn test_subsubcommand() { + assert_eq!( + Opt3 { + all: true, + cmd: Sub2::Foo { + file: "lib.rs".to_string(), + cmd: Sub3::Quux {} + } + }, + Opt3::from_clap(&Opt3::clap().get_matches_from(&["test", "--all", "foo", "lib.rs", "quux"])) + ); +} + +#[derive(StructOpt, PartialEq, Debug)] +enum SubSubCmdWithOption { + #[structopt(name = "remote")] + Remote { + #[structopt(subcommand)] + cmd: Option, + }, + #[structopt(name = "stash")] + Stash { + #[structopt(subcommand)] + cmd: Stash, + }, +} +#[derive(StructOpt, PartialEq, Debug)] +enum Remote { + #[structopt(name = "add")] + Add { name: String, url: String }, + #[structopt(name = "remove")] + Remove { name: String }, +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Stash { + #[structopt(name = "save")] + Save, + #[structopt(name = "pop")] + Pop, +} + +#[test] +fn sub_sub_cmd_with_option() { + fn make(args: &[&str]) -> Option { + SubSubCmdWithOption::clap() + .get_matches_from_safe(args) + .ok() + .map(|m| SubSubCmdWithOption::from_clap(&m)) + } + assert_eq!( + Some(SubSubCmdWithOption::Remote { cmd: None }), + make(&["", "remote"]) + ); + assert_eq!( + Some(SubSubCmdWithOption::Remote { + cmd: Some(Remote::Add { + name: "origin".into(), + url: "http".into() + }) + }), + make(&["", "remote", "add", "origin", "http"]) + ); + assert_eq!( + Some(SubSubCmdWithOption::Stash { cmd: Stash::Save }), + make(&["", "stash", "save"]) + ); + assert_eq!(None, make(&["", "stash"])); +} diff --git a/tests/options.rs b/tests/options.rs new file mode 100644 index 00000000..686113e2 --- /dev/null +++ b/tests/options.rs @@ -0,0 +1,140 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[test] +fn required_option() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", long = "arg")] + arg: i32, + } + assert_eq!( + Opt { arg: 42 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a42"])) + ); + assert_eq!( + Opt { arg: 42 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a", "42"])) + ); + assert_eq!( + Opt { arg: 42 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--arg", "42"])) + ); + assert!(Opt::clap().get_matches_from_safe(&["test"]).is_err()); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a42", "-a24"]) + .is_err() + ); +} + +#[test] +fn optional_option() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a")] + arg: Option, + } + assert_eq!( + Opt { arg: Some(42) }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a42"])) + ); + assert_eq!( + Opt { arg: None }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a42", "-a24"]) + .is_err() + ); +} + +#[test] +fn option_with_default() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", default_value = "42")] + arg: i32, + } + assert_eq!( + Opt { arg: 24 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a24"])) + ); + assert_eq!( + Opt { arg: 42 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a42", "-a24"]) + .is_err() + ); +} + +#[test] +fn option_with_raw_default() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", raw(default_value = r#""42""#))] + arg: i32, + } + assert_eq!( + Opt { arg: 24 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a24"])) + ); + assert_eq!( + Opt { arg: 42 }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert!( + Opt::clap() + .get_matches_from_safe(&["test", "-a42", "-a24"]) + .is_err() + ); +} + +#[test] +fn options() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", long = "arg")] + arg: Vec, + } + assert_eq!( + Opt { arg: vec![24] }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a24"])) + ); + assert_eq!( + Opt { arg: vec![] }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a24", "--arg", "42"])) + ); +} + +#[test] +fn empy_default_value() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a", default_value = "")] + arg: String, + } + assert_eq!(Opt { arg: "".into() }, Opt::from_iter(&["test"])); + assert_eq!( + Opt { arg: "foo".into() }, + Opt::from_iter(&["test", "-afoo"]) + ); +} diff --git a/tests/privacy.rs b/tests/privacy.rs new file mode 100644 index 00000000..67e53a38 --- /dev/null +++ b/tests/privacy.rs @@ -0,0 +1,29 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +mod options { + #[derive(Debug, StructOpt)] + pub struct Options { + #[structopt(subcommand)] + pub subcommand: ::subcommands::SubCommand, + } +} + +mod subcommands { + #[derive(Debug, StructOpt)] + pub enum SubCommand { + #[structopt(name = "foo", about = "foo")] + Foo { + #[structopt(help = "foo")] + bars: Vec, + }, + } +} diff --git a/tests/raw_attributes.rs b/tests/raw_attributes.rs new file mode 100644 index 00000000..cb1989e6 --- /dev/null +++ b/tests/raw_attributes.rs @@ -0,0 +1,121 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::clap::AppSettings; +use structopt::StructOpt; + +// Check if the global settings compile +#[derive(StructOpt, Debug, PartialEq, Eq)] +#[structopt(raw(global_settings = "&[AppSettings::ColoredHelp]"))] +struct Opt { + #[structopt( + long = "x", + raw( + display_order = "2", + next_line_help = "true", + default_value = r#""0""#, + require_equals = "true" + ) + )] + x: i32, + + #[structopt(short = "l", long = "level", raw(aliases = r#"&["set-level", "lvl"]"#))] + level: String, + + #[structopt(long = "values")] + values: Vec, + + #[structopt(name = "FILE", raw(requires_if = r#""FILE", "values""#))] + files: Vec, +} + +#[test] +fn test_raw_slice() { + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-l", "1"])) + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--level", "1"])) + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--set-level", "1"])) + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--lvl", "1"])) + ); +} + +#[test] +fn test_raw_multi_args() { + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: vec!["file".to_string()], + values: vec![], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-l", "1", "file"])) + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: vec!["FILE".to_string()], + values: vec![1], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-l", "1", "--values", "1", "--", "FILE"])) + ); +} + +#[test] +fn test_raw_multi_args_fail() { + let result = Opt::clap().get_matches_from_safe(&["test", "-l", "1", "--", "FILE"]); + assert!(result.is_err()); +} + +#[test] +fn test_raw_bool() { + assert_eq!( + Opt { + x: 1, + level: "1".to_string(), + files: vec![], + values: vec![], + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-l", "1", "--x=1"])) + ); + let result = Opt::clap().get_matches_from_safe(&["test", "-l", "1", "--x", "1"]); + assert!(result.is_err()); +} diff --git a/tests/subcommands.rs b/tests/subcommands.rs new file mode 100644 index 00000000..6ae9b3b2 --- /dev/null +++ b/tests/subcommands.rs @@ -0,0 +1,225 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate structopt; + +use structopt::StructOpt; + +#[derive(StructOpt, PartialEq, Debug)] +enum Opt { + #[structopt(name = "fetch", about = "Fetch stuff from GitHub.")] + Fetch { + #[structopt(long = "all")] + all: bool, + #[structopt(short = "f", long = "force")] + /// Overwrite local branches. + force: bool, + repo: String, + }, + + #[structopt(name = "add")] + Add { + #[structopt(short = "i", long = "interactive")] + interactive: bool, + #[structopt(short = "v", long = "verbose")] + verbose: bool, + }, +} + +#[test] +fn test_fetch() { + assert_eq!( + Opt::Fetch { + all: true, + force: false, + repo: "origin".to_string() + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "fetch", "--all", "origin"])) + ); + assert_eq!( + Opt::Fetch { + all: false, + force: true, + repo: "origin".to_string() + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "fetch", "-f", "origin"])) + ); +} + +#[test] +fn test_add() { + assert_eq!( + Opt::Add { + interactive: false, + verbose: false + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "add"])) + ); + assert_eq!( + Opt::Add { + interactive: true, + verbose: true + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "add", "-i", "-v"])) + ); +} + +#[test] +fn test_no_parse() { + let result = Opt::clap().get_matches_from_safe(&["test", "badcmd", "-i", "-v"]); + assert!(result.is_err()); + + let result = Opt::clap().get_matches_from_safe(&["test", "add", "--badoption"]); + assert!(result.is_err()); + + let result = Opt::clap().get_matches_from_safe(&["test"]); + assert!(result.is_err()); +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Opt2 { + #[structopt(name = "do-something")] + DoSomething { arg: String }, +} + +#[test] +/// This test is specifically to make sure that hyphenated subcommands get +/// processed correctly. +fn test_hyphenated_subcommands() { + assert_eq!( + Opt2::DoSomething { + arg: "blah".to_string() + }, + Opt2::from_clap(&Opt2::clap().get_matches_from(&["test", "do-something", "blah"])) + ); +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Opt3 { + #[structopt(name = "add")] + Add, + #[structopt(name = "init")] + Init, + #[structopt(name = "fetch")] + Fetch, +} + +#[test] +fn test_null_commands() { + assert_eq!( + Opt3::Add, + Opt3::from_clap(&Opt3::clap().get_matches_from(&["test", "add"])) + ); + assert_eq!( + Opt3::Init, + Opt3::from_clap(&Opt3::clap().get_matches_from(&["test", "init"])) + ); + assert_eq!( + Opt3::Fetch, + Opt3::from_clap(&Opt3::clap().get_matches_from(&["test", "fetch"])) + ); +} + +#[derive(StructOpt, PartialEq, Debug)] +#[structopt(about = "Not shown")] +struct Add { + file: String, +} +/// Not shown +#[derive(StructOpt, PartialEq, Debug)] +struct Fetch { + remote: String, +} +#[derive(StructOpt, PartialEq, Debug)] +enum Opt4 { + /// Not shown + #[structopt(name = "add", about = "Add a file")] + Add(Add), + #[structopt(name = "init")] + Init, + /// download history from remote + #[structopt(name = "fetch")] + Fetch(Fetch), +} + +#[test] +fn test_tuple_commands() { + assert_eq!( + Opt4::Add(Add { + file: "f".to_string() + }), + Opt4::from_clap(&Opt4::clap().get_matches_from(&["test", "add", "f"])) + ); + assert_eq!( + Opt4::Init, + Opt4::from_clap(&Opt4::clap().get_matches_from(&["test", "init"])) + ); + assert_eq!( + Opt4::Fetch(Fetch { + remote: "origin".to_string() + }), + Opt4::from_clap(&Opt4::clap().get_matches_from(&["test", "fetch", "origin"])) + ); + + let mut output = Vec::new(); + Opt4::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + assert!(output.contains("download history from remote")); + assert!(output.contains("Add a file")); + assert!(!output.contains("Not shown")); +} + +#[test] +fn enum_in_enum_subsubcommand() { + #[derive(StructOpt, Debug, PartialEq)] + pub enum Opt { + #[structopt(name = "daemon")] + Daemon(DaemonCommand), + } + + #[derive(StructOpt, Debug, PartialEq)] + pub enum DaemonCommand { + #[structopt(name = "start")] + Start, + #[structopt(name = "stop")] + Stop, + } + + let result = Opt::clap().get_matches_from_safe(&["test"]); + assert!(result.is_err()); + + let result = Opt::clap().get_matches_from_safe(&["test", "daemon"]); + assert!(result.is_err()); + + let result = Opt::from_iter(&["test", "daemon", "start"]); + assert_eq!(Opt::Daemon(DaemonCommand::Start), result); +} + +#[test] +fn flatten_enum() { + #[derive(StructOpt, Debug, PartialEq)] + struct Opt { + #[structopt(flatten)] + sub_cmd: SubCmd, + } + #[derive(StructOpt, Debug, PartialEq)] + enum SubCmd { + Foo, + Bar, + } + + assert!(Opt::from_iter_safe(&["test"]).is_err()); + assert_eq!( + Opt::from_iter(&["test", "Foo"]), + Opt { + sub_cmd: SubCmd::Foo + } + ); +}