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..26aea112d 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,6 @@ use std::fs; -use clap_complete::{generate_to, Shell}; +use clap_complete::generate_to; include!("src/cli.rs"); diff --git a/src/cli.rs b/src/cli.rs index b12f6d34c..01f1eb180 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,21 +1,41 @@ use std::ffi::OsString; use clap::{ - builder::NonEmptyStringValueParser, crate_version, Arg, ArgAction, ArgMatches, Command, - ValueHint, + builder::NonEmptyStringValueParser, crate_version, error::ErrorKind, value_parser, Arg, + ArgAction, ArgMatches, Command, ValueHint, }; +use clap_complete::shells::Shell; pub fn get_cli_arguments<'a, I, T>(args: I) -> ArgMatches where I: IntoIterator, T: Into + Clone + 'a, { - let command = build_command(); + let mut command = build_command(); + let args = args.into_iter().map(Into::into).collect::>(); + + if let Some(subcommand_index) = args + .iter() + .position(|arg| arg == "generate-shell-completion") + .filter(|index| *index > 1) + { + command + .error( + ErrorKind::ArgumentConflict, + format!( + "the argument '{}' cannot be used with the subcommand '{}'", + args[subcommand_index - 1].to_string_lossy(), + args[subcommand_index].to_string_lossy() + ), + ) + .exit(); + } + command.get_matches_from(args) } /// 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,19 @@ fn build_command() -> Command { .about("A command-line benchmarking tool.") .help_expected(true) .max_term_width(80) + .subcommand_negates_reqs(true) + .args_conflicts_with_subcommands(true) + .subcommand_precedence_over_arg(true) + .subcommand( + Command::new("generate-shell-completion") + .about("Generate shell completion scripts") + .arg( + Arg::new("shell") + .help("The shell to generate completions for.") + .required(true) + .value_parser(value_parser!(Shell)), + ), + ) .arg( Arg::new("command") .help("The command to benchmark. This can be the name of an executable, a command \ diff --git a/src/main.rs b/src/main.rs index 2a9750503..faf3efd68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,13 @@ use std::env; use benchmark::scheduler::Scheduler; -use cli::get_cli_arguments; +use cli::{build_command, get_cli_arguments}; use command::Commands; use export::ExportManager; use options::Options; use anyhow::Result; +use clap_complete::{generate, shells::Shell}; use colored::*; pub mod benchmark; @@ -32,6 +33,15 @@ fn run() -> Result<()> { colored::control::set_virtual_terminal(true).unwrap(); let cli_arguments = get_cli_arguments(env::args_os()); + if let Some(("generate-shell-completion", subcommand)) = cli_arguments.subcommand() { + let shell = subcommand + .get_one::("shell") + .expect("required by clap"); + let mut command = build_command(); + generate(*shell, &mut command, "hyperfine", &mut std::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..fae1c5691 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -24,6 +24,37 @@ fn runs_successfully() { .success(); } +#[test] +fn generates_shell_completion() { + hyperfine() + .arg("generate-shell-completion") + .arg("fish") + .assert() + .success() + .stdout(predicate::str::contains("complete").and(predicate::str::contains("hyperfine"))); +} + +#[test] +fn rejects_unknown_shell_completion() { + hyperfine() + .arg("generate-shell-completion") + .arg("unknown") + .assert() + .failure() + .stderr(predicate::str::contains("invalid value 'unknown'")); +} + +#[test] +fn rejects_benchmark_command_with_shell_completion_subcommand() { + hyperfine() + .arg("echo ok") + .arg("generate-shell-completion") + .arg("fish") + .assert() + .failure() + .stderr(predicate::str::contains("cannot be used with")); +} + #[test] fn one_run_is_supported() { hyperfine()