Skip to content

Commit a1b9c0e

Browse files
author
wuyangfan
committed
feat: add --gen-completions for runtime shell completion generation
Allow generating bash/fish/zsh/powershell/elvish completions to stdout via --gen-completions (alias --generate-shell-completion). Fixes #881
1 parent f12f3d9 commit a1b9c0e

5 files changed

Lines changed: 65 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ rand = "0.8"
2828
shell-words = "1.0"
2929
thiserror = "2.0"
3030
anyhow = "1.0"
31+
clap_complete = "4.2.1"
3132

3233
[target.'cfg(not(windows))'.dependencies]
3334
libc = "0.2"

build.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::fs;
22

33
use clap_complete::{generate_to, Shell};
44

5-
include!("src/cli.rs");
5+
mod cli {
6+
include!("src/cli.rs");
7+
}
68

79
fn main() {
810
let var = std::env::var_os("SHELL_COMPLETIONS_DIR").or_else(|| std::env::var_os("OUT_DIR"));
@@ -12,7 +14,7 @@ fn main() {
1214
};
1315
fs::create_dir_all(&outdir).unwrap();
1416

15-
let mut command = build_command();
17+
let mut command = cli::build_command();
1618
for shell in [
1719
Shell::Bash,
1820
Shell::Fish,

src/cli.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::ffi::OsString;
2+
use std::io;
23

34
use clap::{
45
builder::NonEmptyStringValueParser, crate_version, Arg, ArgAction, ArgMatches, Command,
56
ValueHint,
67
};
8+
use clap_complete::{generate, Shell};
79

810
pub fn get_cli_arguments<'a, I, T>(args: I) -> ArgMatches
911
where
@@ -14,23 +16,53 @@ where
1416
command.get_matches_from(args)
1517
}
1618

19+
pub fn print_completions(shell: &str, dest: &mut dyn io::Write) -> io::Result<()> {
20+
let shell = match shell {
21+
"bash" => Shell::Bash,
22+
"fish" => Shell::Fish,
23+
"zsh" => Shell::Zsh,
24+
"powershell" => Shell::PowerShell,
25+
"elvish" => Shell::Elvish,
26+
other => {
27+
return Err(io::Error::new(
28+
io::ErrorKind::InvalidInput,
29+
format!("unknown shell for completions: {other}"),
30+
));
31+
}
32+
};
33+
generate(shell, &mut build_command(), "hyperfine", dest);
34+
Ok(())
35+
}
36+
1737
/// Build the clap command for parsing command line arguments
18-
fn build_command() -> Command {
38+
pub fn build_command() -> Command {
1939
Command::new("hyperfine")
2040
.version(crate_version!())
2141
.next_line_help(true)
2242
.hide_possible_values(true)
2343
.about("A command-line benchmarking tool.")
2444
.help_expected(true)
2545
.max_term_width(80)
46+
.arg(
47+
Arg::new("gen-completions")
48+
.long("gen-completions")
49+
.alias("generate-shell-completion")
50+
.value_name("SHELL")
51+
.action(ArgAction::Set)
52+
.help(
53+
"Generate shell completions for SHELL to stdout. \
54+
[possible values: bash, fish, zsh, powershell, elvish]",
55+
)
56+
.value_parser(["bash", "fish", "zsh", "powershell", "elvish"]),
57+
)
2658
.arg(
2759
Arg::new("command")
2860
.help("The command to benchmark. This can be the name of an executable, a command \
2961
line like \"grep -i todo\" or a shell command like \"sleep 0.5 && echo test\". \
3062
The latter is only available if the shell is not explicitly disabled via \
3163
'--shell=none'. If multiple commands are given, hyperfine will show a \
3264
comparison of the respective runtimes.")
33-
.required(true)
65+
.required_unless_present("gen-completions")
3466
.action(ArgAction::Append)
3567
.value_hint(ValueHint::CommandString)
3668
.value_parser(NonEmptyStringValueParser::new()),

src/main.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
)]
55

66
use std::env;
7+
use std::io;
78

89
use benchmark::scheduler::Scheduler;
9-
use cli::get_cli_arguments;
10+
use cli::{get_cli_arguments, print_completions};
1011
use command::Commands;
1112
use export::ExportManager;
1213
use options::Options;
@@ -32,6 +33,12 @@ fn run() -> Result<()> {
3233
colored::control::set_virtual_terminal(true).unwrap();
3334

3435
let cli_arguments = get_cli_arguments(env::args_os());
36+
37+
if let Some(shell) = cli_arguments.get_one::<String>("gen-completions") {
38+
print_completions(shell, &mut io::stdout())?;
39+
return Ok(());
40+
}
41+
3542
let mut options = Options::from_cli_arguments(&cli_arguments)?;
3643
let commands = Commands::from_cli_arguments(&cli_arguments)?;
3744
let export_manager = ExportManager::from_cli_arguments(

tests/integration_tests.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ fn runs_successfully() {
2424
.success();
2525
}
2626

27+
#[test]
28+
fn gen_completions_fish() {
29+
hyperfine()
30+
.arg("--gen-completions")
31+
.arg("fish")
32+
.assert()
33+
.success()
34+
.stdout(predicate::str::contains("complete -c hyperfine"));
35+
}
36+
37+
#[test]
38+
fn gen_completions_requires_shell() {
39+
hyperfine()
40+
.arg("--gen-completions")
41+
.assert()
42+
.failure();
43+
}
44+
2745
#[test]
2846
fn one_run_is_supported() {
2947
hyperfine()

0 commit comments

Comments
 (0)