clap_complete/aot/shells/
powershell.rs

1use std::io::{Error, Write};
2
3use clap::builder::StyledStr;
4use clap::{Arg, Command};
5
6use crate::generator::{utils, Generator};
7use crate::INTERNAL_ERROR_MSG;
8
9/// Generate powershell completion file
10#[derive(Copy, Clone, PartialEq, Eq, Debug)]
11pub struct PowerShell;
12
13impl Generator for PowerShell {
14    fn file_name(&self, name: &str) -> String {
15        format!("_{name}.ps1")
16    }
17
18    fn generate(&self, cmd: &Command, buf: &mut dyn Write) {
19        self.try_generate(cmd, buf)
20            .expect("failed to write completion file");
21    }
22
23    fn try_generate(&self, cmd: &Command, buf: &mut dyn Write) -> Result<(), Error> {
24        let bin_name = cmd
25            .get_bin_name()
26            .expect("crate::generate should have set the bin_name");
27
28        let subcommands_cases = generate_inner(cmd, "");
29
30        write!(
31            buf,
32            r#"
33using namespace System.Management.Automation
34using namespace System.Management.Automation.Language
35
36Register-ArgumentCompleter -Native -CommandName '{bin_name}' -ScriptBlock {{
37    param($wordToComplete, $commandAst, $cursorPosition)
38
39    $commandElements = $commandAst.CommandElements
40    $command = @(
41        '{bin_name}'
42        for ($i = 1; $i -lt $commandElements.Count; $i++) {{
43            $element = $commandElements[$i]
44            if ($element -isnot [StringConstantExpressionAst] -or
45                $element.StringConstantType -ne [StringConstantType]::BareWord -or
46                $element.Value.StartsWith('-') -or
47                $element.Value -eq $wordToComplete) {{
48                break
49        }}
50        $element.Value
51    }}) -join ';'
52
53    $completions = @(switch ($command) {{{subcommands_cases}
54    }})
55
56    $completions.Where{{ $_.CompletionText -like "$wordToComplete*" }} |
57        Sort-Object -Property ListItemText
58}}
59"#
60        )
61    }
62}
63
64// Escape string inside single quotes
65fn escape_string(string: &str) -> String {
66    string.replace('\'', "''").replace('’', "'’")
67}
68
69fn escape_help<T: ToString>(help: Option<&StyledStr>, data: T) -> String {
70    if let Some(help) = help {
71        let help_str = help.to_string();
72        if !help_str.is_empty() {
73            return escape_string(&help_str.replace('\n', " "));
74        }
75    }
76    data.to_string()
77}
78
79fn generate_inner(p: &Command, previous_command_name: &str) -> String {
80    debug!("generate_inner");
81
82    let command_names = if previous_command_name.is_empty() {
83        vec![p.get_bin_name().expect(INTERNAL_ERROR_MSG).to_string()]
84    } else {
85        p.get_name_and_visible_aliases()
86            .into_iter()
87            .map(|name| format!("{previous_command_name};{name}"))
88            .collect()
89    };
90
91    let mut completions = String::new();
92    let preamble = String::from("\n            [CompletionResult]::new(");
93
94    for option in p.get_opts() {
95        generate_aliases(&mut completions, &preamble, option);
96    }
97
98    for flag in utils::flags(p) {
99        generate_aliases(&mut completions, &preamble, &flag);
100    }
101
102    for subcommand in p.get_subcommands() {
103        for name in subcommand.get_name_and_visible_aliases() {
104            let tooltip = escape_help(subcommand.get_about(), name);
105            completions.push_str(&preamble);
106            completions.push_str(&format!(
107                "'{name}', '{name}', [CompletionResultType]::ParameterValue, '{tooltip}')"
108            ));
109        }
110    }
111
112    let mut subcommands_cases = String::new();
113    for command_name in &command_names {
114        subcommands_cases.push_str(&format!(
115            r"
116        '{command_name}' {{{completions}
117            break
118        }}"
119        ));
120    }
121
122    for subcommand in p.get_subcommands() {
123        for command_name in &command_names {
124            let subcommand_subcommands_cases = generate_inner(subcommand, command_name);
125            subcommands_cases.push_str(&subcommand_subcommands_cases);
126        }
127    }
128
129    subcommands_cases
130}
131
132fn generate_aliases(completions: &mut String, preamble: &String, arg: &Arg) {
133    use std::fmt::Write as _;
134
135    if let Some(aliases) = arg.get_short_and_visible_aliases() {
136        let tooltip = escape_help(arg.get_help(), aliases[0]);
137        for alias in aliases {
138            let _ = write!(
139                completions,
140                "{preamble}'-{alias}', '-{alias}{}', [CompletionResultType]::ParameterName, '{tooltip}')",
141                // make PowerShell realize there is a difference between `-s` and `-S`
142                if alias.is_uppercase() { " " } else { "" },
143            );
144        }
145    }
146    if let Some(aliases) = arg.get_long_and_visible_aliases() {
147        let tooltip = escape_help(arg.get_help(), aliases[0]);
148        for alias in aliases {
149            let _ = write!(
150                completions,
151                "{preamble}'--{alias}', '--{alias}', [CompletionResultType]::ParameterName, '{tooltip}')"
152            );
153        }
154    }
155}