feat(generate): Add fig autocomplete generator

This commit is contained in:
grant0417 2021-10-06 19:33:06 -04:00
parent 5512c90380
commit f3611ad6b9
10 changed files with 923 additions and 6 deletions

View file

@ -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

View file

@ -13,7 +13,7 @@
//! ./target/debug/examples/value_hints_derive --<TAB>
//! ```
use clap::{App, AppSettings, ArgEnum, Clap, IntoApp, ValueHint};
use clap_generate::generators::{Bash, Elvish, Fish, PowerShell, Zsh};
use clap_generate::generators::{Bash, Elvish, Fig, Fish, PowerShell, Zsh};
use clap_generate::{generate, Generator};
use std::ffi::OsString;
use std::io;
@ -23,6 +23,7 @@ use std::path::PathBuf;
enum GeneratorChoice {
Bash,
Elvish,
Fig,
Fish,
#[clap(name = "powershell")]
PowerShell,
@ -82,6 +83,7 @@ fn main() {
match generator {
GeneratorChoice::Bash => print_completions::<Bash>(&mut app),
GeneratorChoice::Elvish => print_completions::<Elvish>(&mut app),
GeneratorChoice::Fig => print_completions::<Fig>(&mut app),
GeneratorChoice::Fish => print_completions::<Fish>(&mut app),
GeneratorChoice::PowerShell => print_completions::<PowerShell>(&mut app),
GeneratorChoice::Zsh => print_completions::<Zsh>(&mut app),

View file

@ -13,7 +13,7 @@
//! ./target/debug/examples/value_hints --<TAB>
//! ```
use clap::{App, AppSettings, Arg, ValueHint};
use clap_generate::generators::{Bash, Elvish, Fish, PowerShell, Zsh};
use clap_generate::generators::{Bash, Elvish, Fig, Fish, PowerShell, Zsh};
use clap_generate::{generate, Generator};
use std::io;
@ -25,6 +25,7 @@ fn build_cli() -> App<'static> {
.arg(Arg::new("generator").long("generate").possible_values([
"bash",
"elvish",
"fig",
"fish",
"powershell",
"zsh",
@ -109,6 +110,7 @@ fn main() {
match generator {
"bash" => print_completions::<Bash>(&mut app),
"elvish" => print_completions::<Elvish>(&mut app),
"fig" => print_completions::<Fig>(&mut app),
"fish" => print_completions::<Fish>(&mut app),
"powershell" => print_completions::<PowerShell>(&mut app),
"zsh" => print_completions::<Zsh>(&mut app),

View file

@ -0,0 +1,305 @@
// Std
use std::io::Write;
// Internal
use crate::Generator;
use clap::*;
/// 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");
w!(buf, buffer.as_bytes());
}
}
// 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,
) {
debug!("gen_fig_inner: parent_commands={:?}", parent_commands);
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::<Vec<_>>();
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::<Vec<_>>()
.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::<Vec<_>>()
.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
}

View file

@ -1,11 +1,13 @@
mod bash;
mod elvish;
mod fig;
mod fish;
mod powershell;
mod zsh;
pub use bash::Bash;
pub use elvish::Elvish;
pub use fig::Fig;
pub use fish::Fish;
pub use powershell::PowerShell;
pub use zsh::Zsh;

View file

@ -9,6 +9,8 @@ pub enum Shell {
Bash,
/// Elvish shell
Elvish,
/// Fig autocomplete
Fig,
/// Friendly Interactive SHell (fish)
Fish,
/// PowerShell
@ -19,8 +21,8 @@ pub enum Shell {
impl Shell {
/// A list of supported shells in `[&'static str]` form.
pub fn variants() -> [&'static str; 5] {
["bash", "elvish", "fish", "powershell", "zsh"]
pub fn variants() -> [&'static str; 6] {
["bash", "elvish", "fig", "fish", "powershell", "zsh"]
}
}
@ -29,6 +31,7 @@ impl Display for Shell {
match *self {
Shell::Bash => write!(f, "bash"),
Shell::Elvish => write!(f, "elvish"),
Shell::Fig => write!(f, "fig"),
Shell::Fish => write!(f, "fish"),
Shell::PowerShell => write!(f, "powershell"),
Shell::Zsh => write!(f, "zsh"),
@ -43,11 +46,12 @@ impl FromStr for Shell {
match s.to_ascii_lowercase().as_str() {
"bash" => Ok(Shell::Bash),
"elvish" => Ok(Shell::Elvish),
"fig" => Ok(Shell::Fig),
"fish" => Ok(Shell::Fish),
"powershell" => Ok(Shell::PowerShell),
"zsh" => Ok(Shell::Zsh),
_ => Err(String::from(
"[valid values: bash, elvish, fish, powershell, zsh]",
"[valid values: bash, elvish, fig, fish, powershell, zsh]",
)),
}
}

View file

@ -0,0 +1,466 @@
use super::*;
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::<Fig>(&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::<Fig>(&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::<Fig>(&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::<Fig>(&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::<Fig>(&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;
"#;

View file

@ -4,6 +4,7 @@ use std::fmt;
mod bash;
mod elvish;
mod fig;
mod fish;
mod powershell;
mod zsh;

View file

@ -19,6 +19,7 @@ fn generate_completions() {
);
generate::<Bash, _>(&mut app, "test_app", &mut io::sink());
generate::<Fig, _>(&mut app, "test_app", &mut io::sink());
generate::<Fish, _>(&mut app, "test_app", &mut io::sink());
generate::<PowerShell, _>(&mut app, "test_app", &mut io::sink());
generate::<Elvish, _>(&mut app, "test_app", &mut io::sink());

View file

@ -147,6 +147,134 @@ complete -c my_app -l email -r -f
complete -c my_app -l help -d 'Print help information'
"#;
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 zsh_with_value_hints() {
let mut app = build_app_with_value_hints();
@ -158,3 +286,9 @@ fn fish_with_value_hints() {
let mut app = build_app_with_value_hints();
common::<Fish>(&mut app, "my_app", FISH_VALUE_HINTS);
}
#[test]
fn fig_with_value_hints() {
let mut app = build_app_with_value_hints();
common::<Fig>(&mut app, "my_app", FIG_VALUE_HINTS);
}