@@ -14,6 +14,7 @@ use cap_std_ext::cap_std;
1414use cap_std_ext:: cap_std:: fs:: Dir ;
1515use clap:: Parser ;
1616use clap:: ValueEnum ;
17+ use clap:: CommandFactory ;
1718use composefs:: dumpfile;
1819use composefs_boot:: BootOps as _;
1920use 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" ) ]
411421pub ( 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<()> {
18411873mod 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