Skip to content

Commit 4b49294

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 d833c51 commit 4b49294

File tree

4 files changed

+122
-2
lines changed

4 files changed

+122
-2
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: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class CompletionsCommand(Command):
3737
)
3838
]
3939

40-
SUPPORTED_SHELLS = ("bash", "zsh", "fish")
40+
SUPPORTED_SHELLS = ("bash", "zsh", "fish", "PowerShell")
4141

4242
hidden = True
4343

@@ -135,6 +135,8 @@ def render(self, shell: str) -> str:
135135
return self.render_zsh()
136136
if shell == "fish":
137137
return self.render_fish()
138+
if shell == "PowerShell":
139+
return self.render_power_shell()
138140

139141
raise RuntimeError(f"Unrecognized shell: {shell}")
140142

@@ -280,9 +282,49 @@ def sanitize(s: str) -> str:
280282
"cmds_names": " ".join(cmds_names),
281283
}
282284

285+
def render_power_shell(self) -> str:
286+
script_name, script_path = self._get_script_name_and_path()
287+
function = self._generate_function_name(script_name, script_path)
288+
289+
assert self.application
290+
# Global options
291+
opts = [
292+
f'"--{opt.name}"'
293+
for opt in sorted(self.application.definition.options, key=lambda o: o.name)
294+
]
295+
296+
# Command + options
297+
cmds = []
298+
cmds_opts = []
299+
for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""):
300+
if cmd.hidden or not cmd.enabled or not cmd.name:
301+
continue
302+
303+
command_name = f'"{cmd.name}"'
304+
cmds.append(command_name)
305+
if len(cmd.definition.options) == 0:
306+
continue
307+
options = ", ".join(
308+
f'"--{opt.name}"'
309+
for opt in sorted(cmd.definition.options, key=lambda o: o.name)
310+
)
311+
cmds_opts += [f" {command_name} {{ $options += {options}; Break; }}"]
312+
313+
return TEMPLATES["PowerShell"] % {
314+
"function": function,
315+
"script_name": script_name,
316+
"opts": ", ".join(opts),
317+
"cmds": ", ".join(cmds),
318+
"cmds_opts": "\n".join(cmds_opts),
319+
}
320+
283321
def get_shell_type(self) -> str:
284322
shell = os.getenv("SHELL")
323+
285324
if not shell:
325+
if len(os.getenv("PSModulePath", "").split(os.pathsep)) >= 3:
326+
return "PowerShell"
327+
286328
raise RuntimeError(
287329
"Could not read SHELL environment variable. "
288330
"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)