Skip to main content

forest/cli_shared/cli/
completion_cmd.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3use crate::cli::subcommands::Cli as ForestCli;
4use crate::daemon::main::Cli as ForestDaemonCli;
5use crate::tool::subcommands::Cli as ForestToolCli;
6use crate::wallet::subcommands::Cli as ForestWalletCli;
7use ahash::HashMap;
8use clap::{Command, CommandFactory};
9use clap_complete::aot::{Shell, generate};
10use itertools::Itertools as _;
11
12/// Completion Command for generating shell completions for the CLI
13#[derive(Debug, clap::Args)]
14pub struct CompletionCommand {
15    /// The binaries for which to generate completions (e.g., 'forest-cli,forest-tool,forest-wallet').
16    /// If omitted, completions for all known binaries will be generated.
17    #[arg(value_delimiter = ',')]
18    binaries: Option<Vec<String>>,
19    /// The Shell type to generate completions for
20    #[arg(long, default_value = "bash")]
21    shell: Shell,
22}
23
24impl CompletionCommand {
25    pub fn run<W: std::io::Write>(self, writer: &mut W) -> anyhow::Result<()> {
26        let mut bin_cmd_map: HashMap<String, Command> = HashMap::from_iter([
27            ("forest".to_string(), ForestDaemonCli::command()),
28            ("forest-cli".to_string(), ForestCli::command()),
29            ("forest-wallet".to_string(), ForestWalletCli::command()),
30            ("forest-tool".to_string(), ForestToolCli::command()),
31        ]);
32
33        let valid_binaries = bin_cmd_map.keys().cloned().collect_vec();
34        let binaries = self.binaries.unwrap_or_else(|| valid_binaries.clone());
35
36        for b in binaries {
37            let cmd = bin_cmd_map.get_mut(&b).ok_or_else(|| {
38                anyhow::anyhow!(
39                    "Unknown binary: '{}'. Valid binaries are: {:?}",
40                    b,
41                    valid_binaries.join(",")
42                )
43            })?;
44
45            generate(
46                self.shell,
47                cmd,
48                cmd.get_bin_name().unwrap().to_string(),
49                writer,
50            );
51        }
52        Ok(())
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_completion_no_binaries_succeeds() {
62        let cmd = CompletionCommand {
63            binaries: None,
64            shell: Shell::Bash,
65        };
66
67        // Execution should succeed
68        let result = cmd.run(&mut std::io::sink());
69        assert!(
70            result.is_ok(),
71            "Expected command to succeed, got: {result:?}"
72        );
73    }
74
75    #[test]
76    fn test_completion_binaries_succeeds() {
77        let cmd = CompletionCommand {
78            binaries: Some(vec!["forest-cli".to_string(), "forest-tool".to_string()]),
79            shell: Shell::Bash,
80        };
81
82        let result = cmd.run(&mut std::io::sink());
83        assert!(
84            result.is_ok(),
85            "Expected command to succeed, got {result:?}"
86        );
87    }
88
89    #[test]
90    fn test_completion_binaries_fails() {
91        let cmd = CompletionCommand {
92            binaries: Some(vec!["non-existent-binary".to_string()]),
93            shell: Shell::Bash,
94        };
95
96        let result = cmd.run(&mut std::io::sink());
97        assert!(
98            result.is_err(),
99            "Expected command to fail, but it succeeded"
100        );
101
102        let err = result.unwrap_err().to_string();
103        assert!(
104            err.contains("Unknown binary") && err.contains("non-existent-binary"),
105            "Error message '{err}' did not contain expected text"
106        );
107    }
108
109    #[test]
110    fn test_completion_mixed_valid_invalid_fails() {
111        // Create a completion command with mix of valid and invalid binaries
112        let cmd = CompletionCommand {
113            binaries: Some(vec![
114                "forest-cli".to_string(),
115                "non-existent-binary".to_string(),
116            ]),
117            shell: Shell::Bash,
118        };
119
120        let result = cmd.run(&mut std::io::sink());
121        assert!(
122            result.is_err(),
123            "Expected command to fail, but it succeeded"
124        );
125
126        let err = result.unwrap_err().to_string();
127        assert!(
128            err.contains("Unknown binary") && err.contains("non-existent-binary"),
129            "Error message '{err}' did not contain expected text"
130        );
131    }
132}