Skip to content

Commit 644d69b

Browse files
committed
Add PowerShell completion generation
I'm not an expert in PowerShell or CLI's, but this developments may help someone else complete it Should be called like (on Windows): `script completions PowerShell > $HOME\Documents\PowerShell\Profile.ps1` or (on Linux or macOS): `script completions PowerShell > ~/.config/powershell/profile.ps1` If I understand correctly, where PowerShell saves its configurations
1 parent 920d83e commit 644d69b

File tree

4 files changed

+123
-3
lines changed

4 files changed

+123
-3
lines changed

src/cleo/commands/completions/templates.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,34 @@
122122
%(cmds_opts)s"""
123123

124124

125-
TEMPLATES = {"bash": BASH_TEMPLATE, "zsh": ZSH_TEMPLATE, "fish": FISH_TEMPLATE}
125+
POWERSHELL_TEMPLATE = """\
126+
$%(function)s = {
127+
param(
128+
[string] $wordToComplete,
129+
[System.Management.Automation.Language.Ast] $commandAst,
130+
[int] $cursorPosition
131+
)
132+
133+
$options = %(opts)s
134+
$commands = %(cmds)s
135+
136+
if ($wordToComplete -notlike '--*' -and $wordToComplete -notlike "" -and ($commandAst.CommandElements.Count -eq "2")) {
137+
return $commands | Where-Object { $_ -like "$wordToComplete*" }
138+
}
139+
140+
$result = $commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_ -notlike '--*' }
141+
switch ($result -Join " " ) {
142+
%(cmds_opts)s
143+
}
144+
145+
return $options | Where-Object { $_ -like "$wordToComplete*" }
146+
}
147+
148+
Register-ArgumentCompleter -Native -CommandName %(script_name)s -ScriptBlock $%(function)s"""
149+
150+
TEMPLATES = {
151+
"bash": BASH_TEMPLATE,
152+
"zsh": ZSH_TEMPLATE,
153+
"fish": FISH_TEMPLATE,
154+
"PowerShell": POWERSHELL_TEMPLATE,
155+
}

src/cleo/commands/completions_command.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
class CompletionsCommand(Command):
17-
17+
1818
name = "completions"
1919
description = "Generate completion scripts for your shell."
2020

@@ -29,7 +29,7 @@ class CompletionsCommand(Command):
2929
)
3030
]
3131

32-
SUPPORTED_SHELLS = ("bash", "zsh", "fish")
32+
SUPPORTED_SHELLS = ("bash", "zsh", "fish", "PowerShell")
3333

3434
hidden = True
3535

@@ -127,6 +127,8 @@ def render(self, shell: str) -> str:
127127
return self.render_zsh()
128128
if shell == "fish":
129129
return self.render_fish()
130+
if shell == "PowerShell":
131+
return self.render_power_shell()
130132

131133
raise RuntimeError(f"Unrecognized shell: {shell}")
132134

@@ -272,9 +274,49 @@ def sanitize(s: str) -> str:
272274
"cmds_names": " ".join(cmds_names),
273275
}
274276

277+
def render_power_shell(self) -> str:
278+
script_name, script_path = self._get_script_name_and_path()
279+
function = self._generate_function_name(script_name, script_path)
280+
281+
assert self.application
282+
# Global options
283+
opts = [
284+
f'"--{opt.name}"'
285+
for opt in sorted(self.application.definition.options, key=lambda o: o.name)
286+
]
287+
288+
# Command + options
289+
cmds = []
290+
cmds_opts = []
291+
for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""):
292+
if cmd.hidden or not cmd.enabled or not cmd.name:
293+
continue
294+
295+
command_name = f'"{cmd.name}"'
296+
cmds.append(command_name)
297+
if len(cmd.definition.options) == 0:
298+
continue
299+
options = ", ".join(
300+
f'"--{opt.name}"'
301+
for opt in sorted(cmd.definition.options, key=lambda o: o.name)
302+
)
303+
cmds_opts += [f" {command_name} {{ $options += {options}; Break; }}"]
304+
305+
return TEMPLATES["PowerShell"] % {
306+
"function": function,
307+
"script_name": script_name,
308+
"opts": ", ".join(opts),
309+
"cmds": ", ".join(cmds),
310+
"cmds_opts": "\n".join(cmds_opts),
311+
}
312+
275313
def get_shell_type(self) -> str:
276314
shell = os.getenv("SHELL")
315+
277316
if not shell:
317+
if len(os.getenv("PSModulePath", "").split(os.pathsep)) >= 3:
318+
return "PowerShell"
319+
278320
raise RuntimeError(
279321
"Could not read SHELL environment variable. "
280322
"Please specify your shell type by passing it as the first argument."
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
$_my_function = {
2+
param(
3+
[string] $wordToComplete,
4+
[System.Management.Automation.Language.Ast] $commandAst,
5+
[int] $cursorPosition
6+
)
7+
8+
$options = "--ansi", "--help", "--no-ansi", "--no-interaction", "--quiet", "--verbose", "--version"
9+
$commands = "command:with:colons", "hello", "help", "list", "spaced command"
10+
11+
if ($wordToComplete -notlike '--*' -and $wordToComplete -notlike "" -and ($commandAst.CommandElements.Count -eq "2")) {
12+
return $commands | Where-Object { $_ -like "$wordToComplete*" }
13+
}
14+
15+
$result = $commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_ -notlike '--*' }
16+
switch ($result -Join " " ) {
17+
"command:with:colons" { $options += "--goodbye"; Break; }
18+
"hello" { $options += "--dangerous-option", "--option-without-description"; Break; }
19+
"spaced command" { $options += "--goodbye"; Break; }
20+
}
21+
22+
return $options | Where-Object { $_ -like "$wordToComplete*" }
23+
}
24+
25+
Register-ArgumentCompleter -Native -CommandName script -ScriptBlock $_my_function

tests/commands/completion/test_completions_command.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,26 @@ def test_fish(mocker: MockerFixture) -> None:
9696
expected = f.read()
9797

9898
assert expected == tester.io.fetch_output().replace("\r\n", "\n")
99+
100+
101+
def test_power_shell(mocker: MockerFixture) -> None:
102+
mocker.patch(
103+
"cleo.io.inputs.string_input.StringInput.script_name",
104+
new_callable=mocker.PropertyMock,
105+
return_value="/path/to/my/script",
106+
)
107+
mocker.patch(
108+
"cleo.commands.completions_command.CompletionsCommand._generate_function_name",
109+
return_value="_my_function",
110+
)
111+
112+
command = app.find("completions")
113+
tester = CommandTester(command)
114+
tester.execute("PowerShell")
115+
116+
with open(
117+
os.path.join(os.path.dirname(__file__), "fixtures", "PowerShell.txt")
118+
) as f:
119+
expected = f.read()
120+
121+
assert expected == tester.io.fetch_output().replace("\r\n", "\n")

0 commit comments

Comments
 (0)