From 9abd2f88cfc7e23e4498dd4429da3f1418eefef0 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 29 Sep 2025 16:30:16 -0600 Subject: [PATCH] feat: Added dynamic tab completions for the profile, providers, and the secrets in any given secret manager --- Cargo.lock | 37 ++++++++++++++++++++++++++++++++ Cargo.toml | 4 +++- README.md | 16 ++++++++++++++ src/bin/gman/cli.rs | 51 ++++++++++++++++++++++++++++++++++++++++++-- src/bin/gman/main.rs | 17 ++++++++++----- 5 files changed, 117 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe3e604..65a7df1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,6 +926,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a" dependencies = [ "clap", + "clap_lex", + "is_executable", + "shlex", ] [[package]] @@ -1306,6 +1309,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.2" @@ -1604,6 +1613,7 @@ dependencies = [ "indoc", "log", "log4rs", + "once_cell", "openssl", "predicates", "pretty_assertions", @@ -1618,6 +1628,7 @@ dependencies = [ "tempfile", "tokio", "validator", + "which", "zeroize", ] @@ -2117,6 +2128,15 @@ dependencies = [ "serde", ] +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -4258,6 +4278,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +dependencies = [ + "env_home", + "rustix", + "winsafe", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4510,6 +4541,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index b5b07f7..615dfba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ clap = { version = "4.5.47", features = [ "env", "wrap_help", ] } -clap_complete = "4.5.57" +clap_complete = { version = "4.5.57", features = ["unstable-dynamic"] } confy = { version = "1.0.0", default-features = false, features = [ "yaml_conf", ] } @@ -59,6 +59,8 @@ crc32c = "0.6.8" azure_identity = "0.27.0" azure_security_keyvault_secrets = "0.6.0" aws-lc-sys = { version = "0.31.0", features = ["bindgen"] } +which = "8.0.0" +once_cell = "1.21.3" [target.'cfg(all(target_os="linux", target_env="musl"))'.dependencies] openssl = { version = "0.10", features = ["vendored"] } diff --git a/README.md b/README.md index c07674e..eed63ef 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,22 @@ To use a binary from the releases page on Linux/MacOS, do the following: 3. Extract the binary with `tar -C /usr/local/bin -xzf gman-.tar.gz` (Note: This may require `sudo`) 4. Now you can run `gman`! +### Enable Tab Completion +`gman` supports shell tab completion for `bash`, `zsh`, and `fish`. To enable it, run the following command for your +shell: + +```shell +# Bash +echo 'source <(COMPLETE=bash gman)' >> ~/.bashrc +# Zsh +echo 'source <(COMPLETE=zsh gman)' >> ~/.zshrc +# Fish +echo 'COMPLETE=fish gman | source' >> ~/.config/fish/config.fish +``` + +Then restart your shell or `source` the appropriate config file. + + ## Configuration `gman` reads a YAML configuration file located at an OS-specific path: diff --git a/src/bin/gman/cli.rs b/src/bin/gman/cli.rs index 2dd1210..0e2c201 100644 --- a/src/bin/gman/cli.rs +++ b/src/bin/gman/cli.rs @@ -1,14 +1,16 @@ use crate::command::preview_command; use anyhow::{Context, Result, anyhow}; use futures::future::join_all; -use gman::config::{Config, RunConfig}; +use gman::config::{load_config, Config, RunConfig}; use log::{debug, error}; use regex::Regex; use std::collections::HashMap; -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::fs; use std::path::PathBuf; use std::process::Command; +use clap_complete::CompletionCandidate; +use tokio::runtime::Handle; const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}"; const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}"; @@ -252,6 +254,51 @@ pub fn parse_args( Ok(args) } +pub fn run_config_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + match load_config() { + Ok(config) => { + if let Some(run_configs) = config.run_configs { + run_configs + .iter() + .filter(|rc| { + rc.name + .as_ref() + .expect("run config has no name") + .starts_with(&*cur) + }) + .map(|rc| { + CompletionCandidate::new(rc.name.as_ref().expect("run config has no name")) + }) + .collect() + } else { + vec![] + } + } + Err(_) => vec![], + } +} + +pub fn secrets_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + match load_config() { + Ok(config) => { + let mut provider_config = match config.extract_provider_config(None) { + Ok(pc) => pc, + Err(_) => return vec![], + }; + let secrets_provider = provider_config.extract_provider(); + let h = Handle::current(); + tokio::task::block_in_place(|| h.block_on(secrets_provider.list_secrets())).unwrap_or_default() + .into_iter() + .filter(|s| s.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect() + } + Err(_) => vec![], + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/bin/gman/main.rs b/src/bin/gman/main.rs index 0ea0dc8..13c62d7 100644 --- a/src/bin/gman/main.rs +++ b/src/bin/gman/main.rs @@ -1,11 +1,14 @@ +use crate::cli::run_config_completer; +use crate::cli::secrets_completer; use anyhow::{Context, Result}; use clap::Subcommand; use clap::{ - CommandFactory, Parser, ValueEnum, crate_authors, crate_description, crate_name, crate_version, + crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, ValueEnum, }; +use clap_complete::{ArgValueCompleter, CompleteEnv}; use crossterm::execute; -use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode}; -use gman::config::{Config, get_config_file_path, load_config}; +use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen}; +use gman::config::{get_config_file_path, load_config, Config}; use std::ffi::OsString; use std::io::{self, IsTerminal, Read, Write}; use std::panic::PanicHookInfo; @@ -48,11 +51,11 @@ struct Cli { output: Option, /// Specify the secret provider to use (defaults to 'default_provider' in config (usually 'local')) - #[arg(long, value_enum, global = true, env = "GMAN_PROVIDER")] + #[arg(long, global = true, env = "GMAN_PROVIDER", value_parser = ["local", "aws_secrets_manager", "azure_key_vault", "gcp_secret_manager", "gopass"])] provider: Option, /// Specify a run profile to use when wrapping a command - #[arg(long, short)] + #[arg(long, short, add = ArgValueCompleter::new(run_config_completer))] profile: Option, /// Output the command that will be run instead of executing it @@ -82,6 +85,7 @@ enum Commands { /// Decrypt a secret and print the plaintext Get { /// Name of the secret to retrieve + #[arg(add = ArgValueCompleter::new(secrets_completer))] name: String, }, @@ -89,12 +93,14 @@ enum Commands { /// If a provider does not support updating secrets, this command will return an error. Update { /// Name of the secret to update + #[arg(add = ArgValueCompleter::new(secrets_completer))] name: String, }, /// Delete a secret from the configured secret provider Delete { /// Name of the secret to delete + #[arg(add = ArgValueCompleter::new(secrets_completer))] name: String, }, @@ -129,6 +135,7 @@ async fn main() -> Result<()> { panic::set_hook(Box::new(|info| { panic_hook(info); })); + CompleteEnv::with_factory(Cli::command).complete(); let cli = Cli::parse(); if cli.show_log_path {