@@ -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,83 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15731592 Ok ( ( ) )
15741593 }
15751594 } ,
1595+ Opt :: Completion { shell } => {
1596+ // Build the clap Command from our derived Opt
1597+ let cmd = Opt :: command ( ) ;
1598+
1599+ // Collect visible top-level subcommands and their about text
1600+ fn visible_subcommands ( cmd : & clap:: Command ) -> Vec < ( String , String ) > {
1601+ let mut subs: Vec < ( String , String ) > = cmd
1602+ . get_subcommands ( )
1603+ . filter ( |c| {
1604+ // skip hidden subcommands and the help pseudo-command
1605+ if c. is_hide_set ( ) {
1606+ return false ;
1607+ }
1608+ if c. get_name ( ) == "help" {
1609+ return false ;
1610+ }
1611+ true
1612+ } )
1613+ . map ( |c| {
1614+ let name = c. get_name ( ) . to_string ( ) ;
1615+ let about = c. get_about ( ) . map ( |s| s. to_string ( ) ) . unwrap_or_default ( ) ;
1616+ ( name, about)
1617+ } )
1618+ . collect ( ) ;
1619+ subs. sort_by_key ( |( n, _) | n. clone ( ) ) ;
1620+ subs
1621+ }
1622+
1623+ let subs = visible_subcommands ( & cmd) ;
1624+
1625+ match shell {
1626+ CompletionShell :: Zsh => {
1627+ // zsh: produce a simple _describe-based completion with descriptions
1628+ println ! ( "#compdef bootc" ) ;
1629+ println ! ( "# Generated by bootc" ) ;
1630+ println ! ( "_bootc() {{" ) ;
1631+ println ! ( " local -a commands" ) ;
1632+ print ! ( " commands=(" ) ;
1633+ for ( name, about) in & subs {
1634+ // escape single quotes
1635+ let about_esc = about. replace ( '\'' , "'\\ ''" ) ;
1636+ print ! ( " '{}:{}'" , name, about_esc) ;
1637+ }
1638+ println ! ( " )" ) ;
1639+ println ! ( " _describe 'bootc commands' commands" ) ;
1640+ println ! ( "}}" ) ;
1641+ println ! ( "compdef _bootc bootc" ) ;
1642+ }
1643+ CompletionShell :: Fish => {
1644+ // fish: emit a complete line per command with description
1645+ println ! ( "# Generated by bootc" ) ;
1646+ for ( name, about) in & subs {
1647+ let about_esc = about. replace ( '"' , "\\ \" " ) ;
1648+ println ! ( "complete -c bootc -n '__fish_use_subcommand' -a '{}' -d \" {}\" " , name, about_esc) ;
1649+ }
1650+ }
1651+ CompletionShell :: Bash => {
1652+ // bash: generate a simple completer that only lists top-level subcommands
1653+ println ! ( "# Generated by bootc" ) ;
1654+ println ! ( "_bootc() {{" ) ;
1655+ println ! ( " local cur prev words cword" ) ;
1656+ println ! ( " _init_completion || return" ) ;
1657+ print ! ( " local -a cmds=(" ) ;
1658+ for ( name, _about) in & subs {
1659+ print ! ( " {}" , name) ;
1660+ }
1661+ println ! ( " )" ) ;
1662+ println ! ( " if [ $COMP_CWORD -eq 1 ]; then" ) ;
1663+ println ! ( " COMPREPLY=( $(compgen -W \" ${{cmds[*]}}\" -- \" $cur\" ) )" ) ;
1664+ println ! ( " return 0" ) ;
1665+ println ! ( " fi" ) ;
1666+ println ! ( "}}" ) ;
1667+ println ! ( "complete -F _bootc bootc" ) ;
1668+ }
1669+ } ;
1670+ Ok ( ( ) )
1671+ }
15761672 Opt :: Image ( opts) => match opts {
15771673 ImageOpts :: List {
15781674 list_type,
@@ -1841,6 +1937,41 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
18411937mod tests {
18421938 use super :: * ;
18431939
1940+ #[ test]
1941+ fn visible_subcommands_filter_and_sort ( ) {
1942+ let cmd = Opt :: command ( ) ;
1943+ // use the same helper as completion
1944+ let subs = {
1945+ fn visible_subcommands_for_test ( cmd : & clap:: Command ) -> Vec < String > {
1946+ let mut names: Vec < String > = cmd
1947+ . get_subcommands ( )
1948+ . filter ( |c| {
1949+ if c. is_hide_set ( ) {
1950+ return false ;
1951+ }
1952+ if c. get_name ( ) == "help" {
1953+ return false ;
1954+ }
1955+ true
1956+ } )
1957+ . map ( |c| c. get_name ( ) . to_string ( ) )
1958+ . collect ( ) ;
1959+ names. sort ( ) ;
1960+ names
1961+ }
1962+ visible_subcommands_for_test ( & cmd)
1963+ } ;
1964+
1965+ // basic expectations: completion subcommand is hidden and must not appear
1966+ assert ! ( !subs. iter( ) . any( |s| s == "completion" ) ) ;
1967+ // help must not be present
1968+ assert ! ( !subs. iter( ) . any( |s| s == "help" ) ) ;
1969+ // ensure sorted order
1970+ let mut sorted = subs. clone ( ) ;
1971+ sorted. sort ( ) ;
1972+ assert_eq ! ( subs, sorted) ;
1973+ }
1974+
18441975 #[ test]
18451976 fn test_callname ( ) {
18461977 use std:: os:: unix:: ffi:: OsStrExt ;
@@ -1978,4 +2109,52 @@ mod tests {
19782109 ] ) ) ;
19792110 assert_eq ! ( args. as_slice( ) , [ "container" , "image" , "pull" ] ) ;
19802111 }
2112+
2113+ #[ test]
2114+ fn test_generate_completion_scripts_contain_commands ( ) {
2115+ use clap_complete:: { generate, shells:: { Bash , Zsh , Fish } } ;
2116+
2117+ // For each supported shell, generate the completion script and
2118+ // ensure obvious subcommands appear in the output. This mirrors
2119+ // the style of completion checks used in other projects (e.g.
2120+ // podman) where the generated script is examined for expected
2121+ // tokens.
2122+
2123+ // `completion` is intentionally hidden from --help / suggestions;
2124+ // ensure other visible subcommands are present instead.
2125+ let want = [ "install" , "upgrade" ] ;
2126+
2127+ // Bash
2128+ {
2129+ let mut cmd = Opt :: command ( ) ;
2130+ let mut buf = Vec :: new ( ) ;
2131+ generate ( Bash , & mut cmd, "bootc" , & mut buf) ;
2132+ let s = String :: from_utf8 ( buf) . expect ( "bash completion should be utf8" ) ;
2133+ for w in & want {
2134+ assert ! ( s. contains( w) , "bash completion missing {w}" ) ;
2135+ }
2136+ }
2137+
2138+ // Zsh
2139+ {
2140+ let mut cmd = Opt :: command ( ) ;
2141+ let mut buf = Vec :: new ( ) ;
2142+ generate ( Zsh , & mut cmd, "bootc" , & mut buf) ;
2143+ let s = String :: from_utf8 ( buf) . expect ( "zsh completion should be utf8" ) ;
2144+ for w in & want {
2145+ assert ! ( s. contains( w) , "zsh completion missing {w}" ) ;
2146+ }
2147+ }
2148+
2149+ // Fish
2150+ {
2151+ let mut cmd = Opt :: command ( ) ;
2152+ let mut buf = Vec :: new ( ) ;
2153+ generate ( Fish , & mut cmd, "bootc" , & mut buf) ;
2154+ let s = String :: from_utf8 ( buf) . expect ( "fish completion should be utf8" ) ;
2155+ for w in & want {
2156+ assert ! ( s. contains( w) , "fish completion missing {w}" ) ;
2157+ }
2158+ }
2159+ }
19812160}
0 commit comments