forest/cli_shared/cli/
completion_cmd.rs

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