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