clap_complete/aot/shells/
powershell.rs

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