From 0211fe238e31474e6767503d7408a3bfda24fc79 Mon Sep 17 00:00:00 2001 From: Shion Tanaka Date: Fri, 9 Jan 2026 06:12:09 +0900 Subject: [PATCH] cli: Add shell completion generation command - Add completion subcommand supporting bash, zsh, and fish Assisted-by: Cursor (Auto) Signed-off-by: Shion Tanaka --- Cargo.lock | 10 ++++++++++ crates/lib/Cargo.toml | 1 + crates/lib/src/cli.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d0888849b..3e29efdfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "cfg-if", "chrono", "clap", + "clap_complete", "clap_mangen", "comfy-table", "composefs", @@ -506,6 +507,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.49" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 42f46b37d..4fb539974 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -33,6 +33,7 @@ cap-std-ext = { workspace = true, features = ["fs_utf8"] } cfg-if = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive","cargo"] } +clap_complete = "4" clap_mangen = { workspace = true, optional = true } composefs = { workspace = true } composefs-boot = { workspace = true } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 7a4036c42..c0d367ff6 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -12,6 +12,7 @@ use anyhow::{anyhow, ensure, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; +use clap::CommandFactory; use clap::Parser; use clap::ValueEnum; use composefs::dumpfile; @@ -733,6 +734,15 @@ pub(crate) enum Opt { /// Diff current /etc configuration versus default #[clap(hide = true)] ConfigDiff, + /// Generate shell completion script for supported shells. + /// + /// Example: `bootc completion bash` prints a bash completion script to stdout. + #[clap(hide = true)] + Completion { + /// Shell type to generate (bash, zsh, fish) + #[clap(value_enum)] + shell: clap_complete::aot::Shell, + }, #[clap(hide = true)] DeleteDeployment { depl_id: String, @@ -1573,6 +1583,15 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } }, + Opt::Completion { shell } => { + use clap_complete::aot::generate; + + let mut cmd = Opt::command(); + let mut stdout = std::io::stdout(); + let bin_name = "bootc"; + generate(shell, &mut cmd, bin_name, &mut stdout); + Ok(()) + } Opt::Image(opts) => match opts { ImageOpts::List { list_type, @@ -1978,4 +1997,29 @@ mod tests { ])); assert_eq!(args.as_slice(), ["container", "image", "pull"]); } + + #[test] + fn test_generate_completion_scripts_contain_commands() { + use clap_complete::aot::{generate, Shell}; + + // For each supported shell, generate the completion script and + // ensure obvious subcommands appear in the output. This mirrors + // the style of completion checks used in other projects (e.g. + // podman) where the generated script is examined for expected + // tokens. + + // `completion` is intentionally hidden from --help / suggestions; + // ensure other visible subcommands are present instead. + let want = ["install", "upgrade"]; + + for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] { + let mut cmd = Opt::command(); + let mut buf = Vec::new(); + generate(shell, &mut cmd, "bootc", &mut buf); + let s = String::from_utf8(buf).expect("completion should be utf8"); + for w in &want { + assert!(s.contains(w), "{shell:?} completion missing {w}"); + } + } + } }