diff --git a/Cargo.toml b/Cargo.toml index f1037201..161ef508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ targets = ["x86_64-unknown-linux-gnu"] members = [ "clap_derive", "clap_generate", + "clap_generate_fig", "clap_up", ] default-members = [ diff --git a/README.md b/README.md index 44135ce5..0c120c45 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Below are a few of the features which `clap` supports, full descriptions and usa * Generate a CLI simply by defining a struct! * **Auto-generated Help, Version, and Usage information** - Can optionally be fully, or partially overridden if you want a custom help, version, or usage statements -* **Auto-generated completion scripts (Bash, Zsh, Fish, Elvish and PowerShell)** +* **Auto-generated completion scripts (Bash, Zsh, Fish, Fig, Elvish and PowerShell)** - Using [`clap_generate`](https://github.com/clap-rs/clap/tree/master/clap_generate) - Even works through many multiple levels of subcommands - Works with options which only accept certain values diff --git a/clap_generate_fig/Cargo.toml b/clap_generate_fig/Cargo.toml new file mode 100644 index 00000000..0b8b04d4 --- /dev/null +++ b/clap_generate_fig/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "clap_generate_fig" +version = "3.0.0-beta.4" +edition = "2018" +authors = [ + "Clap Maintainers" +] +include = [ + "src/**/*", + "Cargo.toml", + "README.md" +] +description = "A generator library used with clap for Fig completion scripts" +repository = "https://github.com/clap-rs/clap/tree/master/clap_generate_fig" +documentation = "https://docs.rs/clap_generate_fig" +homepage = "https://clap.rs/" +keywords = [ + "clap", + "cli", + "generate", + "completion", + "manpage", + "fig", +] +categories = ["command-line-interface"] +license = "MIT OR Apache-2.0" +readme = "README.md" + +[lib] +bench = false + +[dependencies] +clap = { path = "../", version = "=3.0.0-beta.4" } +clap_generate = { path = "../clap_generate", version = "3.0.0-beta.4" } + +[dev-dependencies] +pretty_assertions = "0.7" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/clap_generate_fig/LICENSE-APACHE b/clap_generate_fig/LICENSE-APACHE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/clap_generate_fig/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/clap_generate_fig/LICENSE-MIT b/clap_generate_fig/LICENSE-MIT new file mode 100644 index 00000000..5acedf04 --- /dev/null +++ b/clap_generate_fig/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2016 Kevin B. Knapp + +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/clap_generate_fig/README.md b/clap_generate_fig/README.md new file mode 100644 index 00000000..d6d932eb --- /dev/null +++ b/clap_generate_fig/README.md @@ -0,0 +1,9 @@ +# clap_generate_fig + +Generates [Fig](https://github.com/withfig/autocomplete) completions for [`clap`](https://github.com/clap-rs/clap) based CLIs + + +* [Questions & Discussions](https://github.com/clap-rs/clap/discussions) +* [Website](https://clap.rs/) + + diff --git a/clap_generate_fig/examples/fig_completion.rs b/clap_generate_fig/examples/fig_completion.rs new file mode 100644 index 00000000..aac6d275 --- /dev/null +++ b/clap_generate_fig/examples/fig_completion.rs @@ -0,0 +1,12 @@ +use clap::App; +use clap_generate::generate; +use clap_generate_fig::Fig; +use std::io; + +fn main() { + let mut app = App::new("myapp") + .subcommand(App::new("test").subcommand(App::new("config"))) + .subcommand(App::new("hello")); + + generate::(&mut app, "myapp", &mut io::stdout()); +} diff --git a/clap_generate_fig/src/fig.rs b/clap_generate_fig/src/fig.rs new file mode 100644 index 00000000..d10db221 --- /dev/null +++ b/clap_generate_fig/src/fig.rs @@ -0,0 +1,304 @@ +// Std +use std::io::Write; + +// Internal +use clap::*; +use clap_generate::*; + +/// Generate fig completion file +pub struct Fig; + +impl Generator for Fig { + fn file_name(name: &str) -> String { + format!("{}.ts", name) + } + + fn generate(app: &App, buf: &mut dyn Write) { + let command = app.get_bin_name().unwrap(); + let mut buffer = String::new(); + + buffer.push_str(&format!( + "const completion: Fig.Spec = {{\n name: \"{}\",\n", + command + )); + + buffer.push_str(&format!( + " description: \"{}\",\n", + app.get_about().unwrap_or_default() + )); + + gen_fig_inner(command, &[], 2, app, &mut buffer); + + buffer.push_str("};\n\nexport default completion;\n"); + + buf.write_all(buffer.as_bytes()) + .expect("Failed to write to generated file"); + } +} + +// Escape string inside double quotes +fn escape_string(string: &str) -> String { + string.replace("\\", "\\\\").replace("\"", "\\\"") +} + +fn gen_fig_inner( + root_command: &str, + parent_commands: &[&str], + indent: usize, + app: &App, + buffer: &mut String, +) { + if app.has_subcommands() { + buffer.push_str(&format!("{:indent$}subcommands: [\n", "", indent = indent)); + // generate subcommands + for subcommand in app.get_subcommands() { + buffer.push_str(&format!( + "{:indent$}{{\n{:indent$} name: \"{}\",\n", + "", + "", + subcommand.get_name(), + indent = indent + 2 + )); + + if let Some(data) = subcommand.get_about() { + buffer.push_str(&format!( + "{:indent$}description: \"{}\",\n", + "", + escape_string(data), + indent = indent + 4 + )); + } + + let mut parent_commands: Vec<_> = parent_commands.into(); + parent_commands.push(subcommand.get_name()); + gen_fig_inner( + root_command, + &parent_commands, + indent + 4, + subcommand, + buffer, + ); + + buffer.push_str(&format!("{:indent$}}},\n", "", indent = indent + 2)); + } + buffer.push_str(&format!("{:indent$}],\n", "", indent = indent)); + } + + buffer.push_str(&gen_options(app, indent)); + + let args = app.get_positionals().collect::>(); + + match args.len() { + 0 => {} + 1 => { + buffer.push_str(&format!("{:indent$}args: ", "", indent = indent)); + + buffer.push_str(&gen_args(args[0], indent)); + } + _ => { + buffer.push_str(&format!("{:indent$}args: [\n", "", indent = indent)); + for arg in args { + buffer.push_str(&gen_args(arg, indent)); + } + buffer.push_str(&format!("{:indent$}]\n", "", indent = indent)); + } + }; +} + +fn gen_options(app: &App, indent: usize) -> String { + let mut buffer = String::new(); + + buffer.push_str(&format!("{:indent$}options: [\n", "", indent = indent)); + + for option in app.get_opts() { + buffer.push_str(&format!("{:indent$}{{\n", "", indent = indent + 2)); + + let mut names = vec![]; + + if let Some(shorts) = option.get_short_and_visible_aliases() { + names.extend(shorts.iter().map(|short| format!("-{}", short))); + } + + if let Some(longs) = option.get_long_and_visible_aliases() { + names.extend(longs.iter().map(|long| format!("--{}", long))); + } + + if names.len() > 1 { + buffer.push_str(&format!("{:indent$}name: [", "", indent = indent + 4)); + + buffer.push_str( + &names + .iter() + .map(|name| format!("\"{}\"", name)) + .collect::>() + .join(", "), + ); + + buffer.push_str("],\n"); + } else { + buffer.push_str(&format!( + "{:indent$}name: \"{}\",\n", + "", + names[0], + indent = indent + 4 + )); + } + + if let Some(data) = option.get_about() { + buffer.push_str(&format!( + "{:indent$}description: \"{}\",\n", + "", + escape_string(data), + indent = indent + 4 + )); + } + + buffer.push_str(&format!("{:indent$}args: ", "", indent = indent + 4)); + + buffer.push_str(&gen_args(option, indent + 4)); + + buffer.push_str(&format!("{:indent$}}},\n", "", indent = indent + 2)); + } + + for flag in Fig::flags(app) { + buffer.push_str(&format!("{:indent$}{{\n", "", indent = indent + 2)); + + let mut flags = vec![]; + + if let Some(shorts) = flag.get_short_and_visible_aliases() { + flags.extend(shorts.iter().map(|s| format!("-{}", s))); + } + + if let Some(longs) = flag.get_long_and_visible_aliases() { + flags.extend(longs.iter().map(|s| format!("--{}", s))); + } + + if flags.len() > 1 { + buffer.push_str(&format!("{:indent$}name: [", "", indent = indent + 4)); + + buffer.push_str( + &flags + .iter() + .map(|name| format!("\"{}\"", name)) + .collect::>() + .join(", "), + ); + + buffer.push_str("],\n"); + } else { + buffer.push_str(&format!( + "{:indent$}name: \"{}\",\n", + "", + flags[0], + indent = indent + 4 + )); + } + + if let Some(data) = flag.get_about() { + buffer.push_str(&format!( + "{:indent$}description: \"{}\",\n", + "", + escape_string(data).as_str(), + indent = indent + 4 + )); + } + + buffer.push_str(&format!("{:indent$}}},\n", "", indent = indent + 2)); + } + + buffer.push_str(&format!("{:indent$}],\n", "", indent = indent)); + + buffer +} + +fn gen_args(arg: &Arg, indent: usize) -> String { + if !arg.is_set(ArgSettings::TakesValue) { + return "".to_string(); + } + + let mut buffer = String::new(); + + buffer.push_str(&format!( + "{{\n{:indent$} name: \"{}\",\n", + "", + arg.get_name(), + indent = indent + )); + + if arg.is_set(ArgSettings::MultipleValues) { + buffer.push_str(&format!( + "{:indent$}isVariadic: true,\n", + "", + indent = indent + 2 + )); + } + + if !arg.is_set(ArgSettings::Required) { + buffer.push_str(&format!( + "{:indent$}isOptional: true,\n", + "", + indent = indent + 2 + )); + } + + if let Some(data) = arg.get_possible_values() { + buffer.push_str(&format!( + "{:indent$}suggestions: [\n", + "", + indent = indent + 2 + )); + + for value in data { + buffer.push_str(&format!( + "{:indent$}{{\n{:indent$} name: \"{}\",\n", + "", + "", + value.get_name(), + indent = indent + 4, + )); + + if let Some(about) = value.get_about() { + buffer.push_str(&format!( + "{:indent$}description: \"{}\",\n", + "", + escape_string(about), + indent = indent + 4 + )); + } + + buffer.push_str(&format!("{:indent$}}},\n", "", indent = indent + 4)); + } + + buffer.push_str(&format!("{:indent$}]\n", "", indent = indent + 2)); + } else { + match arg.get_value_hint() { + ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => { + buffer.push_str(&format!( + "{:indent$}template: \"filepaths\",\n", + "", + indent = indent + 2 + )); + } + ValueHint::DirPath => { + buffer.push_str(&format!( + "{:indent$}template: \"folders\",\n", + "", + indent = indent + 2 + )); + } + ValueHint::CommandString | ValueHint::CommandName | ValueHint::CommandWithArguments => { + buffer.push_str(&format!( + "{:indent$}isCommand: true,\n", + "", + indent = indent + 2 + )); + } + // Disable completion for others + _ => (), + }; + }; + + buffer.push_str(&format!("{:indent$}}},\n", "", indent = indent)); + + buffer +} diff --git a/clap_generate_fig/src/lib.rs b/clap_generate_fig/src/lib.rs new file mode 100644 index 00000000..67935e04 --- /dev/null +++ b/clap_generate_fig/src/lib.rs @@ -0,0 +1,15 @@ +// `clap_generate_fig` is distributed under the terms of both the MIT license and the Apache License +// (Version 2.0). +// See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files in this repository +// for more information. + +//! Generates [Fig](https://github.com/withfig/autocomplete) completions for [`clap`](https://github.com/clap-rs/clap) based CLIs + +#![doc(html_logo_url = "https://clap.rs/images/media/clap.png")] +#![doc(html_root_url = "https://docs.rs/clap_generate_fig/3.0.0-beta.4")] +#![deny(missing_docs, trivial_casts, unused_allocation, trivial_numeric_casts)] +#![forbid(unsafe_code)] +#![allow(clippy::needless_doctest_main)] + +mod fig; +pub use fig::Fig; diff --git a/clap_generate_fig/tests/completions/fig.rs b/clap_generate_fig/tests/completions/fig.rs new file mode 100644 index 00000000..c21388d8 --- /dev/null +++ b/clap_generate_fig/tests/completions/fig.rs @@ -0,0 +1,467 @@ +use super::*; +use crate::Fig; + +fn build_app() -> App<'static> { + build_app_with_name("myapp") +} + +fn build_app_with_name(s: &'static str) -> App<'static> { + App::new(s) + .about("Tests completions") + .arg( + Arg::new("file") + .value_hint(ValueHint::FilePath) + .about("some input file"), + ) + .subcommand( + App::new("test").about("tests things").arg( + Arg::new("case") + .long("case") + .takes_value(true) + .about("the case to test"), + ), + ) +} + +#[test] +fn fig() { + let mut app = build_app(); + common::(&mut app, "myapp", FIG); +} + +static FIG: &str = r#"const completion: Fig.Spec = { + name: "myapp", + description: "Tests completions", + subcommands: [ + { + name: "test", + description: "tests things", + options: [ + { + name: "--case", + description: "the case to test", + args: { + name: "case", + isOptional: true, + }, + }, + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + { + name: "help", + description: "Print this message or the help of the given subcommand(s)", + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + ], + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + args: { + name: "file", + isOptional: true, + template: "filepaths", + }, +}; + +export default completion; +"#; + +#[test] +fn fig_with_special_commands() { + let mut app = build_app_special_commands(); + common::(&mut app, "my_app", FIG_SPECIAL_CMDS); +} + +fn build_app_special_commands() -> App<'static> { + build_app_with_name("my_app") + .subcommand( + App::new("some_cmd").about("tests other things").arg( + Arg::new("config") + .long("--config") + .takes_value(true) + .about("the other case to test"), + ), + ) + .subcommand(App::new("some-cmd-with-hypens").alias("hyphen")) +} + +static FIG_SPECIAL_CMDS: &str = r#"const completion: Fig.Spec = { + name: "my_app", + description: "Tests completions", + subcommands: [ + { + name: "test", + description: "tests things", + options: [ + { + name: "--case", + description: "the case to test", + args: { + name: "case", + isOptional: true, + }, + }, + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + { + name: "some_cmd", + description: "tests other things", + options: [ + { + name: "--config", + description: "the other case to test", + args: { + name: "config", + isOptional: true, + }, + }, + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + { + name: "some-cmd-with-hypens", + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + { + name: "help", + description: "Print this message or the help of the given subcommand(s)", + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + ], + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + args: { + name: "file", + isOptional: true, + template: "filepaths", + }, +}; + +export default completion; +"#; + +#[test] +fn fig_with_special_help() { + let mut app = build_app_special_help(); + common::(&mut app, "my_app", FIG_SPECIAL_HELP); +} + +fn build_app_special_help() -> App<'static> { + App::new("my_app") + .arg( + Arg::new("single-quotes") + .long("single-quotes") + .about("Can be 'always', 'auto', or 'never'"), + ) + .arg( + Arg::new("double-quotes") + .long("double-quotes") + .about("Can be \"always\", \"auto\", or \"never\""), + ) + .arg( + Arg::new("backticks") + .long("backticks") + .about("For more information see `echo test`"), + ) + .arg(Arg::new("backslash").long("backslash").about("Avoid '\\n'")) + .arg( + Arg::new("brackets") + .long("brackets") + .about("List packages [filter]"), + ) + .arg( + Arg::new("expansions") + .long("expansions") + .about("Execute the shell command with $SHELL"), + ) +} + +static FIG_SPECIAL_HELP: &str = r#"const completion: Fig.Spec = { + name: "my_app", + description: "", + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + { + name: "--single-quotes", + description: "Can be 'always', 'auto', or 'never'", + }, + { + name: "--double-quotes", + description: "Can be \"always\", \"auto\", or \"never\"", + }, + { + name: "--backticks", + description: "For more information see `echo test`", + }, + { + name: "--backslash", + description: "Avoid '\\n'", + }, + { + name: "--brackets", + description: "List packages [filter]", + }, + { + name: "--expansions", + description: "Execute the shell command with $SHELL", + }, + ], +}; + +export default completion; +"#; + +#[test] +fn fig_with_aliases() { + let mut app = build_app_with_aliases(); + common::(&mut app, "cmd", FIG_ALIASES); +} + +fn build_app_with_aliases() -> App<'static> { + App::new("cmd") + .about("testing bash completions") + .arg( + Arg::new("flag") + .short('f') + .visible_short_alias('F') + .long("flag") + .visible_alias("flg") + .about("cmd flag"), + ) + .arg( + Arg::new("option") + .short('o') + .visible_short_alias('O') + .long("option") + .visible_alias("opt") + .about("cmd option") + .takes_value(true), + ) + .arg(Arg::new("positional")) +} + +static FIG_ALIASES: &str = r#"const completion: Fig.Spec = { + name: "cmd", + description: "testing bash completions", + options: [ + { + name: ["-o", "-O", "--option", "--opt"], + description: "cmd option", + args: { + name: "option", + isOptional: true, + }, + }, + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + { + name: ["-f", "-F", "--flag", "--flg"], + description: "cmd flag", + }, + ], + args: { + name: "positional", + isOptional: true, + }, +}; + +export default completion; +"#; + +#[test] +fn fig_with_sub_subcommands() { + let mut app = build_app_sub_subcommands(); + common::(&mut app, "my_app", FIG_SUB_SUBCMDS); +} + +fn build_app_sub_subcommands() -> App<'static> { + build_app_with_name("my_app").subcommand( + App::new("some_cmd") + .about("top level subcommand") + .subcommand( + App::new("sub_cmd").about("sub-subcommand").arg( + Arg::new("config") + .long("--config") + .takes_value(true) + .about("the other case to test"), + ), + ), + ) +} + +static FIG_SUB_SUBCMDS: &str = r#"const completion: Fig.Spec = { + name: "my_app", + description: "Tests completions", + subcommands: [ + { + name: "test", + description: "tests things", + options: [ + { + name: "--case", + description: "the case to test", + args: { + name: "case", + isOptional: true, + }, + }, + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + { + name: "some_cmd", + description: "top level subcommand", + subcommands: [ + { + name: "sub_cmd", + description: "sub-subcommand", + options: [ + { + name: "--config", + description: "the other case to test", + args: { + name: "config", + isOptional: true, + }, + }, + { + name: "--help", + description: "Print help information", + }, + { + name: "--version", + description: "Print version information", + }, + ], + }, + ], + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + { + name: "help", + description: "Print this message or the help of the given subcommand(s)", + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + ], + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + args: { + name: "file", + isOptional: true, + template: "filepaths", + }, +}; + +export default completion; +"#; diff --git a/clap_generate_fig/tests/completions/mod.rs b/clap_generate_fig/tests/completions/mod.rs new file mode 100644 index 00000000..b149f51f --- /dev/null +++ b/clap_generate_fig/tests/completions/mod.rs @@ -0,0 +1,28 @@ +use clap::{App, Arg, ValueHint}; +use clap_generate::{generate, generators::*}; +use std::fmt; + +mod fig; + +#[derive(PartialEq, Eq)] +pub struct PrettyString<'a>(pub &'a str); + +impl<'a> fmt::Debug for PrettyString<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.0) + } +} + +macro_rules! assert_eq { + ($left:expr, $right:expr) => { + pretty_assertions::assert_eq!(PrettyString($left), PrettyString($right)); + }; +} + +pub fn common(app: &mut App, name: &str, fixture: &str) { + let mut buf = vec![]; + generate::(app, name, &mut buf); + let string = String::from_utf8(buf).unwrap(); + + assert_eq!(&string, fixture); +} diff --git a/clap_generate_fig/tests/generate_completions.rs b/clap_generate_fig/tests/generate_completions.rs new file mode 100644 index 00000000..b1203f39 --- /dev/null +++ b/clap_generate_fig/tests/generate_completions.rs @@ -0,0 +1,23 @@ +use clap::{App, Arg}; +use clap_generate::generate; +use clap_generate_fig::Fig; +use std::io; + +#[test] +fn generate_completions() { + let mut app = App::new("test_app") + .arg( + Arg::new("config") + .short('c') + .conflicts_with("v") + .global(true), + ) + .arg(Arg::new("v").short('v')) + .subcommand( + App::new("test") + .about("Subcommand") + .arg(Arg::new("debug").short('d')), + ); + + generate::(&mut app, "test_app", &mut io::sink()); +} diff --git a/clap_generate_fig/tests/value_hints.rs b/clap_generate_fig/tests/value_hints.rs new file mode 100644 index 00000000..eaea7cf9 --- /dev/null +++ b/clap_generate_fig/tests/value_hints.rs @@ -0,0 +1,215 @@ +use clap::{App, AppSettings, Arg, ValueHint}; +use clap_generate_fig::Fig; +use completions::common; + +mod completions; + +pub fn build_app_with_value_hints() -> App<'static> { + App::new("my_app") + .setting(AppSettings::DisableVersionFlag) + .setting(AppSettings::TrailingVarArg) + .arg( + Arg::new("choice") + .long("choice") + .possible_values(["bash", "fish", "zsh"]), + ) + .arg( + Arg::new("unknown") + .long("unknown") + .value_hint(ValueHint::Unknown), + ) + .arg(Arg::new("other").long("other").value_hint(ValueHint::Other)) + .arg( + Arg::new("path") + .long("path") + .short('p') + .value_hint(ValueHint::AnyPath), + ) + .arg( + Arg::new("file") + .long("file") + .short('f') + .value_hint(ValueHint::FilePath), + ) + .arg( + Arg::new("dir") + .long("dir") + .short('d') + .value_hint(ValueHint::DirPath), + ) + .arg( + Arg::new("exe") + .long("exe") + .short('e') + .value_hint(ValueHint::ExecutablePath), + ) + .arg( + Arg::new("cmd_name") + .long("cmd-name") + .value_hint(ValueHint::CommandName), + ) + .arg( + Arg::new("cmd") + .long("cmd") + .short('c') + .value_hint(ValueHint::CommandString), + ) + .arg( + Arg::new("command_with_args") + .takes_value(true) + .multiple_values(true) + .value_hint(ValueHint::CommandWithArguments), + ) + .arg( + Arg::new("user") + .short('u') + .long("user") + .value_hint(ValueHint::Username), + ) + .arg( + Arg::new("host") + .short('h') + .long("host") + .value_hint(ValueHint::Hostname), + ) + .arg(Arg::new("url").long("url").value_hint(ValueHint::Url)) + .arg( + Arg::new("email") + .long("email") + .value_hint(ValueHint::EmailAddress), + ) +} + +static FIG_VALUE_HINTS: &str = r#"const completion: Fig.Spec = { + name: "my_app", + description: "", + options: [ + { + name: "--choice", + args: { + name: "choice", + isOptional: true, + suggestions: [ + { + name: "bash", + }, + { + name: "fish", + }, + { + name: "zsh", + }, + ] + }, + }, + { + name: "--unknown", + args: { + name: "unknown", + isOptional: true, + }, + }, + { + name: "--other", + args: { + name: "other", + isOptional: true, + }, + }, + { + name: ["-p", "--path"], + args: { + name: "path", + isOptional: true, + template: "filepaths", + }, + }, + { + name: ["-f", "--file"], + args: { + name: "file", + isOptional: true, + template: "filepaths", + }, + }, + { + name: ["-d", "--dir"], + args: { + name: "dir", + isOptional: true, + template: "folders", + }, + }, + { + name: ["-e", "--exe"], + args: { + name: "exe", + isOptional: true, + template: "filepaths", + }, + }, + { + name: "--cmd-name", + args: { + name: "cmd_name", + isOptional: true, + isCommand: true, + }, + }, + { + name: ["-c", "--cmd"], + args: { + name: "cmd", + isOptional: true, + isCommand: true, + }, + }, + { + name: ["-u", "--user"], + args: { + name: "user", + isOptional: true, + }, + }, + { + name: ["-h", "--host"], + args: { + name: "host", + isOptional: true, + }, + }, + { + name: "--url", + args: { + name: "url", + isOptional: true, + }, + }, + { + name: "--email", + args: { + name: "email", + isOptional: true, + }, + }, + { + name: "--help", + description: "Print help information", + }, + ], + args: { + name: "command_with_args", + isVariadic: true, + isOptional: true, + isCommand: true, + }, +}; + +export default completion; +"#; + +#[test] +fn fig_with_value_hints() { + let mut app = build_app_with_value_hints(); + common::(&mut app, "my_app", FIG_VALUE_HINTS); +}