Skip to content

Commit 1c81141

Browse files
committed
cli: Add shell completion generation command
Add a hidden 'bootc completion <shell>' subcommand that generates shell completion scripts for bash, zsh, and fish. This enables distributions to generate and package shell completions during RPM/deb build by calling 'bootc completion bash' etc. The completion scripts include descriptions for all visible subcommands and support prefix filtering for a better user experience. Signed-off-by: Shion Tanaka <shtanaka@redhat.com>
1 parent 0aae35a commit 1c81141

File tree

3 files changed

+126
-0
lines changed

3 files changed

+126
-0
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ cap-std-ext = { workspace = true, features = ["fs_utf8"] }
3333
cfg-if = { workspace = true }
3434
chrono = { workspace = true, features = ["serde"] }
3535
clap = { workspace = true, features = ["derive","cargo"] }
36+
clap_complete = "4"
3637
clap_mangen = { workspace = true, optional = true }
3738
composefs = { workspace = true }
3839
composefs-boot = { workspace = true }

crates/lib/src/cli.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use cap_std_ext::cap_std;
1414
use cap_std_ext::cap_std::fs::Dir;
1515
use clap::Parser;
1616
use clap::ValueEnum;
17+
use clap::CommandFactory;
1718
use composefs::dumpfile;
1819
use composefs_boot::BootOps as _;
1920
use etc_merge::{compute_diff, print_diff};
@@ -406,6 +407,15 @@ pub(crate) enum ImageCmdOpts {
406407
},
407408
}
408409

410+
/// Supported completion shells
411+
#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
412+
#[clap(rename_all = "lowercase")]
413+
pub(crate) enum CompletionShell {
414+
Bash,
415+
Zsh,
416+
Fish,
417+
}
418+
409419
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
410420
#[serde(rename_all = "kebab-case")]
411421
pub(crate) enum ImageListType {
@@ -733,6 +743,15 @@ pub(crate) enum Opt {
733743
/// Diff current /etc configuration versus default
734744
#[clap(hide = true)]
735745
ConfigDiff,
746+
/// Generate shell completion script for supported shells.
747+
///
748+
/// Example: `bootc completion bash` prints a bash completion script to stdout.
749+
#[clap(hide = true)]
750+
Completion {
751+
/// Shell type to generate (bash, zsh, fish)
752+
#[clap(value_enum)]
753+
shell: CompletionShell,
754+
},
736755
#[clap(hide = true)]
737756
DeleteDeployment {
738757
depl_id: String,
@@ -1573,6 +1592,19 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15731592
Ok(())
15741593
}
15751594
},
1595+
Opt::Completion { shell } => {
1596+
use clap_complete::{generate, shells};
1597+
1598+
let mut cmd = Opt::command();
1599+
let mut stdout = std::io::stdout();
1600+
let bin_name = "bootc";
1601+
match shell {
1602+
CompletionShell::Bash => generate(shells::Bash, &mut cmd, bin_name, &mut stdout),
1603+
CompletionShell::Zsh => generate(shells::Zsh, &mut cmd, bin_name, &mut stdout),
1604+
CompletionShell::Fish => generate(shells::Fish, &mut cmd, bin_name, &mut stdout),
1605+
};
1606+
Ok(())
1607+
}
15761608
Opt::Image(opts) => match opts {
15771609
ImageOpts::List {
15781610
list_type,
@@ -1841,6 +1873,41 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
18411873
mod tests {
18421874
use super::*;
18431875

1876+
#[test]
1877+
fn visible_subcommands_filter_and_sort() {
1878+
let cmd = Opt::command();
1879+
// use the same helper as completion
1880+
let subs = {
1881+
fn visible_subcommands_for_test(cmd: &clap::Command) -> Vec<String> {
1882+
let mut names: Vec<String> = cmd
1883+
.get_subcommands()
1884+
.filter(|c| {
1885+
if c.is_hide_set() {
1886+
return false;
1887+
}
1888+
if c.get_name() == "help" {
1889+
return false;
1890+
}
1891+
true
1892+
})
1893+
.map(|c| c.get_name().to_string())
1894+
.collect();
1895+
names.sort();
1896+
names
1897+
}
1898+
visible_subcommands_for_test(&cmd)
1899+
};
1900+
1901+
// basic expectations: completion subcommand is hidden and must not appear
1902+
assert!(!subs.iter().any(|s| s == "completion"));
1903+
// help must not be present
1904+
assert!(!subs.iter().any(|s| s == "help"));
1905+
// ensure sorted order
1906+
let mut sorted = subs.clone();
1907+
sorted.sort();
1908+
assert_eq!(subs, sorted);
1909+
}
1910+
18441911
#[test]
18451912
fn test_callname() {
18461913
use std::os::unix::ffi::OsStrExt;
@@ -1978,4 +2045,52 @@ mod tests {
19782045
]));
19792046
assert_eq!(args.as_slice(), ["container", "image", "pull"]);
19802047
}
2048+
2049+
#[test]
2050+
fn test_generate_completion_scripts_contain_commands() {
2051+
use clap_complete::{generate, shells::{Bash, Zsh, Fish}};
2052+
2053+
// For each supported shell, generate the completion script and
2054+
// ensure obvious subcommands appear in the output. This mirrors
2055+
// the style of completion checks used in other projects (e.g.
2056+
// podman) where the generated script is examined for expected
2057+
// tokens.
2058+
2059+
// `completion` is intentionally hidden from --help / suggestions;
2060+
// ensure other visible subcommands are present instead.
2061+
let want = ["install", "upgrade"];
2062+
2063+
// Bash
2064+
{
2065+
let mut cmd = Opt::command();
2066+
let mut buf = Vec::new();
2067+
generate(Bash, &mut cmd, "bootc", &mut buf);
2068+
let s = String::from_utf8(buf).expect("bash completion should be utf8");
2069+
for w in &want {
2070+
assert!(s.contains(w), "bash completion missing {w}");
2071+
}
2072+
}
2073+
2074+
// Zsh
2075+
{
2076+
let mut cmd = Opt::command();
2077+
let mut buf = Vec::new();
2078+
generate(Zsh, &mut cmd, "bootc", &mut buf);
2079+
let s = String::from_utf8(buf).expect("zsh completion should be utf8");
2080+
for w in &want {
2081+
assert!(s.contains(w), "zsh completion missing {w}");
2082+
}
2083+
}
2084+
2085+
// Fish
2086+
{
2087+
let mut cmd = Opt::command();
2088+
let mut buf = Vec::new();
2089+
generate(Fish, &mut cmd, "bootc", &mut buf);
2090+
let s = String::from_utf8(buf).expect("fish completion should be utf8");
2091+
for w in &want {
2092+
assert!(s.contains(w), "fish completion missing {w}");
2093+
}
2094+
}
2095+
}
19812096
}

0 commit comments

Comments
 (0)