feat: Added dynamic tab completions for the profile, providers, and the secrets in any given secret manager

This commit is contained in:
2025-09-29 16:30:16 -06:00
parent 29acad5eed
commit 9abd2f88cf
5 changed files with 117 additions and 8 deletions
Generated
+37
View File
@@ -926,6 +926,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a" checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a"
dependencies = [ dependencies = [
"clap", "clap",
"clap_lex",
"is_executable",
"shlex",
] ]
[[package]] [[package]]
@@ -1306,6 +1309,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -1604,6 +1613,7 @@ dependencies = [
"indoc", "indoc",
"log", "log",
"log4rs", "log4rs",
"once_cell",
"openssl", "openssl",
"predicates", "predicates",
"pretty_assertions", "pretty_assertions",
@@ -1618,6 +1628,7 @@ dependencies = [
"tempfile", "tempfile",
"tokio", "tokio",
"validator", "validator",
"which",
"zeroize", "zeroize",
] ]
@@ -2117,6 +2128,15 @@ dependencies = [
"serde", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@@ -4258,6 +4278,17 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -4510,6 +4541,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.46.0" version = "0.46.0"
+3 -1
View File
@@ -25,7 +25,7 @@ clap = { version = "4.5.47", features = [
"env", "env",
"wrap_help", "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 = [ confy = { version = "1.0.0", default-features = false, features = [
"yaml_conf", "yaml_conf",
] } ] }
@@ -59,6 +59,8 @@ crc32c = "0.6.8"
azure_identity = "0.27.0" azure_identity = "0.27.0"
azure_security_keyvault_secrets = "0.6.0" azure_security_keyvault_secrets = "0.6.0"
aws-lc-sys = { version = "0.31.0", features = ["bindgen"] } 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] [target.'cfg(all(target_os="linux", target_env="musl"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
+16
View File
@@ -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-<arch>.tar.gz` (Note: This may require `sudo`) 3. Extract the binary with `tar -C /usr/local/bin -xzf gman-<arch>.tar.gz` (Note: This may require `sudo`)
4. Now you can run `gman`! 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 ## Configuration
`gman` reads a YAML configuration file located at an OS-specific path: `gman` reads a YAML configuration file located at an OS-specific path:
+49 -2
View File
@@ -1,14 +1,16 @@
use crate::command::preview_command; use crate::command::preview_command;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use futures::future::join_all; use futures::future::join_all;
use gman::config::{Config, RunConfig}; use gman::config::{load_config, Config, RunConfig};
use log::{debug, error}; use log::{debug, error};
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsString; use std::ffi::{OsStr, OsString};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use clap_complete::CompletionCandidate;
use tokio::runtime::Handle;
const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}"; const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}";
const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}"; const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}";
@@ -252,6 +254,51 @@ pub fn parse_args(
Ok(args) Ok(args)
} }
pub fn run_config_completer(current: &OsStr) -> Vec<CompletionCandidate> {
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<CompletionCandidate> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+12 -5
View File
@@ -1,11 +1,14 @@
use crate::cli::run_config_completer;
use crate::cli::secrets_completer;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::Subcommand; use clap::Subcommand;
use clap::{ 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::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode}; use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
use gman::config::{Config, get_config_file_path, load_config}; use gman::config::{get_config_file_path, load_config, Config};
use std::ffi::OsString; use std::ffi::OsString;
use std::io::{self, IsTerminal, Read, Write}; use std::io::{self, IsTerminal, Read, Write};
use std::panic::PanicHookInfo; use std::panic::PanicHookInfo;
@@ -48,11 +51,11 @@ struct Cli {
output: Option<OutputFormat>, output: Option<OutputFormat>,
/// Specify the secret provider to use (defaults to 'default_provider' in config (usually 'local')) /// 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<String>, provider: Option<String>,
/// Specify a run profile to use when wrapping a command /// Specify a run profile to use when wrapping a command
#[arg(long, short)] #[arg(long, short, add = ArgValueCompleter::new(run_config_completer))]
profile: Option<String>, profile: Option<String>,
/// Output the command that will be run instead of executing it /// Output the command that will be run instead of executing it
@@ -82,6 +85,7 @@ enum Commands {
/// Decrypt a secret and print the plaintext /// Decrypt a secret and print the plaintext
Get { Get {
/// Name of the secret to retrieve /// Name of the secret to retrieve
#[arg(add = ArgValueCompleter::new(secrets_completer))]
name: String, name: String,
}, },
@@ -89,12 +93,14 @@ enum Commands {
/// If a provider does not support updating secrets, this command will return an error. /// If a provider does not support updating secrets, this command will return an error.
Update { Update {
/// Name of the secret to update /// Name of the secret to update
#[arg(add = ArgValueCompleter::new(secrets_completer))]
name: String, name: String,
}, },
/// Delete a secret from the configured secret provider /// Delete a secret from the configured secret provider
Delete { Delete {
/// Name of the secret to delete /// Name of the secret to delete
#[arg(add = ArgValueCompleter::new(secrets_completer))]
name: String, name: String,
}, },
@@ -129,6 +135,7 @@ async fn main() -> Result<()> {
panic::set_hook(Box::new(|info| { panic::set_hook(Box::new(|info| {
panic_hook(info); panic_hook(info);
})); }));
CompleteEnv::with_factory(Cli::command).complete();
let cli = Cli::parse(); let cli = Cli::parse();
if cli.show_log_path { if cli.show_log_path {