diff --git a/Cargo.toml b/Cargo.toml index b3ef3dee6..e15277317 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ rand = "0.8" shell-words = "1.0" thiserror = "2.0" anyhow = "1.0" +clap_complete = "4.2.1" [target.'cfg(not(windows))'.dependencies] libc = "0.2" diff --git a/build.rs b/build.rs index 0c3fe354d..d5b8633c2 100644 --- a/build.rs +++ b/build.rs @@ -2,7 +2,9 @@ use std::fs; use clap_complete::{generate_to, Shell}; -include!("src/cli.rs"); +mod cli { + include!("src/cli.rs"); +} fn main() { let var = std::env::var_os("SHELL_COMPLETIONS_DIR").or_else(|| std::env::var_os("OUT_DIR")); @@ -12,7 +14,7 @@ fn main() { }; fs::create_dir_all(&outdir).unwrap(); - let mut command = build_command(); + let mut command = cli::build_command(); for shell in [ Shell::Bash, Shell::Fish, diff --git a/src/cli.rs b/src/cli.rs index b12f6d34c..1d0233f78 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,11 @@ use std::ffi::OsString; +use std::io; use clap::{ builder::NonEmptyStringValueParser, crate_version, Arg, ArgAction, ArgMatches, Command, ValueHint, }; +use clap_complete::{generate, Shell}; pub fn get_cli_arguments<'a, I, T>(args: I) -> ArgMatches where @@ -14,8 +16,26 @@ where command.get_matches_from(args) } +pub fn print_completions(shell: &str, dest: &mut dyn io::Write) -> io::Result<()> { + let shell = match shell { + "bash" => Shell::Bash, + "fish" => Shell::Fish, + "zsh" => Shell::Zsh, + "powershell" => Shell::PowerShell, + "elvish" => Shell::Elvish, + other => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("unknown shell for completions: {other}"), + )); + } + }; + generate(shell, &mut build_command(), "hyperfine", dest); + Ok(()) +} + /// Build the clap command for parsing command line arguments -fn build_command() -> Command { +pub fn build_command() -> Command { Command::new("hyperfine") .version(crate_version!()) .next_line_help(true) @@ -23,6 +43,18 @@ fn build_command() -> Command { .about("A command-line benchmarking tool.") .help_expected(true) .max_term_width(80) + .arg( + Arg::new("gen-completions") + .long("gen-completions") + .alias("generate-shell-completion") + .value_name("SHELL") + .action(ArgAction::Set) + .help( + "Generate shell completions for SHELL to stdout. \ + [possible values: bash, fish, zsh, powershell, elvish]", + ) + .value_parser(["bash", "fish", "zsh", "powershell", "elvish"]), + ) .arg( Arg::new("command") .help("The command to benchmark. This can be the name of an executable, a command \ @@ -30,7 +62,7 @@ fn build_command() -> Command { The latter is only available if the shell is not explicitly disabled via \ '--shell=none'. If multiple commands are given, hyperfine will show a \ comparison of the respective runtimes.") - .required(true) + .required_unless_present("gen-completions") .action(ArgAction::Append) .value_hint(ValueHint::CommandString) .value_parser(NonEmptyStringValueParser::new()), diff --git a/src/main.rs b/src/main.rs index 2a9750503..615916f66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,10 @@ )] use std::env; +use std::io; use benchmark::scheduler::Scheduler; -use cli::get_cli_arguments; +use cli::{get_cli_arguments, print_completions}; use command::Commands; use export::ExportManager; use options::Options; @@ -32,6 +33,12 @@ fn run() -> Result<()> { colored::control::set_virtual_terminal(true).unwrap(); let cli_arguments = get_cli_arguments(env::args_os()); + + if let Some(shell) = cli_arguments.get_one::("gen-completions") { + print_completions(shell, &mut io::stdout())?; + return Ok(()); + } + let mut options = Options::from_cli_arguments(&cli_arguments)?; let commands = Commands::from_cli_arguments(&cli_arguments)?; let export_manager = ExportManager::from_cli_arguments( diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2de3a5049..437b1a20f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -24,6 +24,24 @@ fn runs_successfully() { .success(); } +#[test] +fn gen_completions_fish() { + hyperfine() + .arg("--gen-completions") + .arg("fish") + .assert() + .success() + .stdout(predicate::str::contains("complete -c hyperfine")); +} + +#[test] +fn gen_completions_requires_shell() { + hyperfine() + .arg("--gen-completions") + .assert() + .failure(); +} + #[test] fn one_run_is_supported() { hyperfine()