From 7a5d5851ab567a2c2c5ee8c6681ff46ab484ba57 Mon Sep 17 00:00:00 2001 From: Dustin Speckhals Date: Mon, 26 Oct 2020 13:44:58 -0400 Subject: [PATCH] feat: Generate completions during build process This commit adds a `build.rs` which generates completion files for Bash, Elvish, Fish, and zsh shells. In order to accomplish this, the `build_cli` command was moved into a separate module: `cli`. The CI build script was also modified to package the four scripts into the outputted build artifact. One downside of pre-generating the completion scripts is that it won't be able to take user-specified profiles, as those are dependent on each system. That would require custom generations, and would probably no longer able to be auto-generated with clap. --- Cargo.toml | 7 ++- build.rs | 20 +++++++ ci/action.sh | 9 ++- src/bin/bombadil.rs | 141 ++++++-------------------------------------- src/cli.rs | 111 ++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 6 files changed, 163 insertions(+), 126 deletions(-) create mode 100644 build.rs create mode 100644 src/cli.rs diff --git a/Cargo.toml b/Cargo.toml index 163d8dac..51015c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ A dotfile manager. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +build = "build.rs" + [dependencies] config = "^0" dirs = "^3" @@ -34,5 +36,8 @@ name = "bombadil" path = "src/bin/bombadil.rs" required-features = ["clap"] +[build-dependencies] +clap = "^2" + [dev-dependencies] -temp_testdir = "0.2" \ No newline at end of file +temp_testdir = "0.2" diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..9d0b342a --- /dev/null +++ b/build.rs @@ -0,0 +1,20 @@ +extern crate clap; + +use std::env; + +use clap::Shell; + +include!("src/cli.rs"); + +fn main() { + let outdir = match env::var_os("OUT_DIR") { + None => return, + Some(outdir) => outdir, + }; + let mut app = build_cli(vec![]); + vec![Shell::Bash, Shell::Elvish, Shell::Fish, Shell::Zsh] + .iter() + .for_each(|shell| { + app.gen_completions("bombadil", *shell, outdir.clone()); + }); +} diff --git a/ci/action.sh b/ci/action.sh index 6e47503f..ac1ae5ad 100755 --- a/ci/action.sh +++ b/ci/action.sh @@ -10,6 +10,7 @@ echoerr() { release() { TAR_DIR="${BOMBADIL_HOME}/target/tar" + COMPLETIONS_DIR = "${TAR_DIR}/completions" target="${1:-}" if [[ $target == *"osx"* ]]; then @@ -32,10 +33,16 @@ release() { bin_path="${BOMBADIL_HOME}/target/${bin_folder}/bombadil" chmod +x "$bin_path" - mkdir -p "$TAR_DIR" 2> /dev/null || true + mkdir -p "$COMPLETIONS_DIR" 2> /dev/null || true cp "$bin_path" "$TAR_DIR" + # Copy completion files + cp "${BOMBADIL_HOME}/target/${bin_folder}/build/toml-bombadil/"-*/out/_bombadil "$COMPLETIONS_DIR" + cp "${BOMBADIL_HOME}/target/${bin_folder}/build/toml-bombadil/"-*/out/bombadil.bash "$COMPLETIONS_DIR" + cp "${BOMBADIL_HOME}/target/${bin_folder}/build/toml-bombadil/"-*/out/bombadil.elv "$COMPLETIONS_DIR" + cp "${BOMBADIL_HOME}/target/${bin_folder}/build/toml-bombadil/"-*/out/bombadil.fish "$COMPLETIONS_DIR" + cd "$TAR_DIR" tar -czf bombadil.tar.gz * diff --git a/src/bin/bombadil.rs b/src/bin/bombadil.rs index 7cf192ef..222c9135 100644 --- a/src/bin/bombadil.rs +++ b/src/bin/bombadil.rs @@ -1,16 +1,10 @@ -use clap::{App, AppSettings, Arg, Shell, SubCommand}; +use clap::Shell; use std::io::BufRead; use std::path::PathBuf; +use toml_bombadil::cli; use toml_bombadil::settings::Settings; use toml_bombadil::{Bombadil, MetadataType, Mode}; -const LINK: &str = "link"; -const UNLINK: &str = "unlink"; -const INSTALL: &str = "install"; -const ADD_SECRET: &str = "add-secret"; -const GET: &str = "get"; -const GENERATE_COMPLETIONS: &str = "generate-completions"; - macro_rules! fatal { ($($tt:tt)*) => {{ use std::io::Write; @@ -19,109 +13,6 @@ macro_rules! fatal { }} } -fn build_cli<'a, 'b>(profile_names: Vec<&'a str>) -> App<'a, 'b> -where - 'a: 'b, -{ - let app_settings = &[ - AppSettings::SubcommandRequiredElseHelp, - AppSettings::UnifiedHelpMessage, - AppSettings::ColoredHelp, - AppSettings::VersionlessSubcommands, - ]; - - let subcommand_settings = &[ - AppSettings::UnifiedHelpMessage, - AppSettings::ColoredHelp, - AppSettings::VersionlessSubcommands, - ]; - - App::new("Toml Bombadil") - .settings(app_settings) - .version(env!("CARGO_PKG_VERSION")) - .author("Paul D. ") - .about("A dotfile template manager") - .long_about("Toml is a dotfile template manager, written in rust. \ - For more info on how to configure it please go to https://github.com/oknozor/toml-bombadil") - .subcommand(SubCommand::with_name(INSTALL) - .settings(subcommand_settings) - .about("Link a given bombadil config to XDG_CONFIG_DIR/bombadil.toml") - .arg(Arg::with_name("CONFIG") - .help("path to your bombadil.toml config file inside your dotfiles directory") - .short("c") - .long("config") - .takes_value(true) - .required(true))) - .subcommand(SubCommand::with_name(LINK) - .settings(subcommand_settings) - .about("Symlink a copy of your dotfiles and inject variables according to bombadil.toml config") - .arg(Arg::with_name("profiles") - .help("A list of comma separated profiles to activate") - .short("p") - .long("profiles") - .possible_values(profile_names.as_slice()) - .takes_value(true) - .multiple(true) - .required(false))) - .subcommand(SubCommand::with_name(UNLINK) - .settings(subcommand_settings) - .about("Remove all symlinks defined in your bombadil.toml")) - .subcommand(SubCommand::with_name(ADD_SECRET) - .settings(subcommand_settings) - .about("Add a secret var to bombadil environment") - .arg(Arg::with_name("key") - .help("Key of the secret variable to create") - .short("k") - .long("key") - .takes_value(true) - .required(true)) - .arg(Arg::with_name("value") - .help("Value of the secret variable to create") - .short("v") - .long("value") - .takes_value(true) - .required_unless("ask")) - .arg(Arg::with_name("ask") - .help("Get the secret value from stdin") - .short("a") - .long("ask") - .takes_value(false) - .required_unless("value")) - .arg(Arg::with_name("file") - .help("Path of the var file to modify") - .short("f") - .long("file") - .takes_value(true) - .required(true))) - .subcommand(SubCommand::with_name(GET) - .settings(subcommand_settings) - .about("Get metadata about dots, hooks, path, profiles, or vars") - .arg(Arg::with_name("value") - .possible_values(&["dots", "hooks", "path", "profiles", "vars", "secrets"]) - .default_value("dots") - .takes_value(true) - .help("Metadata to get")) - .arg(Arg::with_name("profiles") - .short("p") - .long("profiles") - .takes_value(true) - .possible_values(profile_names.as_slice()) - .multiple(true) - .help("Get metadata for specific profiles") - ) - ) - .subcommand(SubCommand::with_name(GENERATE_COMPLETIONS) - .settings(subcommand_settings) - .about("Generate shell completions") - .arg(Arg::with_name("type") - .possible_values(&["bash", "elvish", "fish", "zsh"]) - .required(true) - .takes_value(true) - .help("Type of completions to generate") - ) - ) -} - fn main() { let profiles = Settings::get() .map(|settings| settings.profiles) @@ -132,21 +23,21 @@ fn main() { .map(|profile| profile.0.as_str()) .collect::>(); - let matches = build_cli(profile_names.clone()).get_matches(); + let matches = cli::build_cli(profile_names.clone()).get_matches(); if let Some(subcommand) = matches.subcommand_name() { match subcommand { - INSTALL => { - let install_commmand = matches.subcommand_matches(INSTALL).unwrap(); + cli::INSTALL => { + let install_commmand = matches.subcommand_matches(cli::INSTALL).unwrap(); let config_path = install_commmand.value_of("CONFIG").map(PathBuf::from); Bombadil::link_self_config(config_path).unwrap_or_else(|err| fatal!("{}", err)); } - LINK => { + cli::LINK => { let mut bombadil = Bombadil::from_settings(Mode::Gpg).unwrap_or_else(|err| fatal!("{}", err)); - let link_command = matches.subcommand_matches(LINK).unwrap(); + let link_command = matches.subcommand_matches(cli::LINK).unwrap(); if link_command.is_present("profiles") { let profiles: Vec<_> = link_command.values_of("profiles").unwrap().collect(); @@ -157,13 +48,13 @@ fn main() { bombadil.install().unwrap_or_else(|err| fatal!("{}", err)); } - UNLINK => { + cli::UNLINK => { let bombadil = Bombadil::from_settings(Mode::NoGpg).unwrap_or_else(|err| fatal!("{}", err)); bombadil.uninstall().unwrap_or_else(|err| fatal!("{}", err)); } - ADD_SECRET => { - let add_secret_subcommand = matches.subcommand_matches(ADD_SECRET).unwrap(); + cli::ADD_SECRET => { + let add_secret_subcommand = matches.subcommand_matches(cli::ADD_SECRET).unwrap(); let key = add_secret_subcommand.value_of("key").unwrap(); let value = if add_secret_subcommand.is_present("ask") { @@ -182,8 +73,8 @@ fn main() { .add_secret(key, &value, var_file) .unwrap_or_else(|err| fatal!("{}", err)); } - GET => { - let get_subcommand = matches.subcommand_matches(GET).unwrap(); + cli::GET => { + let get_subcommand = matches.subcommand_matches(cli::GET).unwrap(); let metadata_type = match get_subcommand.value_of("value").unwrap() { "dots" => MetadataType::Dots, "hooks" => MetadataType::Hooks, @@ -209,8 +100,10 @@ fn main() { bombadil.print_metadata(metadata_type); } - GENERATE_COMPLETIONS => { - let generate_subcommand = matches.subcommand_matches(GENERATE_COMPLETIONS).unwrap(); + cli::GENERATE_COMPLETIONS => { + let generate_subcommand = matches + .subcommand_matches(cli::GENERATE_COMPLETIONS) + .unwrap(); let for_shell = match generate_subcommand.value_of("type").unwrap() { "bash" => Shell::Bash, "elvish" => Shell::Elvish, @@ -218,7 +111,7 @@ fn main() { "zsh" => Shell::Zsh, _ => unreachable!(), }; - build_cli(profile_names).gen_completions_to( + cli::build_cli(profile_names).gen_completions_to( "bombadil", for_shell, &mut std::io::stdout(), diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..5ca4310a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,111 @@ +use clap::{App, AppSettings, Arg, SubCommand}; + +pub const LINK: &str = "link"; +pub const UNLINK: &str = "unlink"; +pub const INSTALL: &str = "install"; +pub const ADD_SECRET: &str = "add-secret"; +pub const GET: &str = "get"; +pub const GENERATE_COMPLETIONS: &str = "generate-completions"; + +pub fn build_cli<'a, 'b>(profile_names: Vec<&'a str>) -> App<'a, 'b> +where + 'a: 'b, +{ + let app_settings = &[ + AppSettings::SubcommandRequiredElseHelp, + AppSettings::UnifiedHelpMessage, + AppSettings::ColoredHelp, + AppSettings::VersionlessSubcommands, + ]; + + let subcommand_settings = &[ + AppSettings::UnifiedHelpMessage, + AppSettings::ColoredHelp, + AppSettings::VersionlessSubcommands, + ]; + + App::new("Toml Bombadil") + .settings(app_settings) + .version(env!("CARGO_PKG_VERSION")) + .author("Paul D. ") + .about("A dotfile template manager") + .long_about("Toml is a dotfile template manager, written in rust. \ + For more info on how to configure it please go to https://github.com/oknozor/toml-bombadil") + .subcommand(SubCommand::with_name(INSTALL) + .settings(subcommand_settings) + .about("Link a given bombadil config to XDG_CONFIG_DIR/bombadil.toml") + .arg(Arg::with_name("CONFIG") + .help("path to your bombadil.toml config file inside your dotfiles directory") + .short("c") + .long("config") + .takes_value(true) + .required(true))) + .subcommand(SubCommand::with_name(LINK) + .settings(subcommand_settings) + .about("Symlink a copy of your dotfiles and inject variables according to bombadil.toml config") + .arg(Arg::with_name("profiles") + .help("A list of comma separated profiles to activate") + .short("p") + .long("profiles") + .possible_values(profile_names.as_slice()) + .takes_value(true) + .multiple(true) + .required(false))) + .subcommand(SubCommand::with_name(UNLINK) + .settings(subcommand_settings) + .about("Remove all symlinks defined in your bombadil.toml")) + .subcommand(SubCommand::with_name(ADD_SECRET) + .settings(subcommand_settings) + .about("Add a secret var to bombadil environment") + .arg(Arg::with_name("key") + .help("Key of the secret variable to create") + .short("k") + .long("key") + .takes_value(true) + .required(true)) + .arg(Arg::with_name("value") + .help("Value of the secret variable to create") + .short("v") + .long("value") + .takes_value(true) + .required_unless("ask")) + .arg(Arg::with_name("ask") + .help("Get the secret value from stdin") + .short("a") + .long("ask") + .takes_value(false) + .required_unless("value")) + .arg(Arg::with_name("file") + .help("Path of the var file to modify") + .short("f") + .long("file") + .takes_value(true) + .required(true))) + .subcommand(SubCommand::with_name(GET) + .settings(subcommand_settings) + .about("Get metadata about dots, hooks, path, profiles, or vars") + .arg(Arg::with_name("value") + .possible_values(&["dots", "hooks", "path", "profiles", "vars", "secrets"]) + .default_value("dots") + .takes_value(true) + .help("Metadata to get")) + .arg(Arg::with_name("profiles") + .short("p") + .long("profiles") + .takes_value(true) + .possible_values(profile_names.as_slice()) + .multiple(true) + .help("Get metadata for specific profiles") + ) + ) + .subcommand(SubCommand::with_name(GENERATE_COMPLETIONS) + .settings(subcommand_settings) + .about("Generate shell completions") + .arg(Arg::with_name("type") + .possible_values(&["bash", "elvish", "fish", "zsh"]) + .required(true) + .takes_value(true) + .help("Type of completions to generate") + ) + ) +} diff --git a/src/lib.rs b/src/lib.rs index 714f2b99..75f98305 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ use std::fs; use std::os::unix; use std::path::{Path, PathBuf}; +pub mod cli; mod dots; mod gpg; mod hook;