Skip to content

Commit 14db8e1

Browse files
committed
powershell completion is working #156
1 parent e58baff commit 14db8e1

File tree

7 files changed

+88
-14
lines changed

7 files changed

+88
-14
lines changed

django_typer/management/commands/shellcompletion.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import contextlib
2323
import io
2424
import os
25+
import platform
2526
import re
2627
import sys
2728
import typing as t
@@ -47,7 +48,7 @@
4748
from django_typer.completers import these_strings
4849
from django_typer.management import TyperCommand, command, get_command, initialize
4950
from django_typer.types import COMMON_PANEL
50-
from django_typer.utils import get_usage_script
51+
from django_typer.utils import get_usage_script, get_win_shell
5152

5253
DETECTED_SHELL = None
5354

@@ -392,6 +393,11 @@ def shell(self, shell: t.Optional[str]):
392393
"autocompletion for. Unable to detect shell."
393394
)
394395
)
396+
elif self._shell == "cmd" and platform.system() == "Windows":
397+
try:
398+
self._shell = get_win_shell()
399+
except ShellDetectionFailure:
400+
pass
395401

396402
@property
397403
def shell_class(self):

django_typer/management/commands/shells/powershell.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,21 @@
1010
class PowerShellComplete(DjangoTyperShellCompleter):
1111
name = "powershell"
1212
template = "shell_complete/powershell.ps1"
13+
supports_scripts = False
1314

1415
def format_completion(self, item: CompletionItem) -> str:
15-
return f"{item.value}:::{item.help or ' '}"
16+
return ":::".join(
17+
[
18+
item.type,
19+
self.process_rich_text(item.value),
20+
" ".join(
21+
[
22+
ln.strip()
23+
for ln in self.process_rich_text(item.help or " ").splitlines()
24+
]
25+
),
26+
]
27+
)
1628

1729
def set_execution_policy(self) -> None:
1830
subprocess.run(
@@ -50,7 +62,7 @@ def install(self) -> Path:
5062
f.writelines([self.source()])
5163
return profile
5264

53-
def uninstall(self):
65+
def uninstall(self) -> None:
5466
# annoyingly, powershell has one profile script for all completion commands
5567
# so we have to find our entry and remove it
5668
assert self.prog_name

django_typer/templates/shell_complete/bash.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
elif [[ $type == 'file' ]]; then
4040
COMPREPLY=()
4141
compopt -o default
42-
elif [[ $type == 'plain' ]]; then
42+
else
4343
COMPREPLY+=($value)
4444
fi
4545
done

django_typer/templates/shell_complete/powershell.ps1

+9-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ $scriptblock = {
2222

2323
{{ manage_script_name }} {{ django_command }} $settingsOption $pythonPathOption --shell {{ shell }} {{ color }} complete "$($commandText)" | ForEach-Object {
2424
$commandArray = $_ -Split ":::"
25-
$command = $commandArray[0]
26-
$helpString = $commandArray[1]
27-
[System.Management.Automation.CompletionResult]::new(
28-
$command, $command, 'ParameterValue', $helpString)
25+
$type = $commandArray[0]
26+
$value = $commandArray[1]
27+
$help = $commandArray[2]
28+
if ($help -eq "") {
29+
[System.Management.Automation.CompletionResult]::new($value)
30+
} else {
31+
[System.Management.Automation.CompletionResult]::new(
32+
$value, $value, 'ParameterValue', $help)
33+
}
2934
}
3035
}
3136
Register-ArgumentCompleter -Native -CommandName {{ manage_script_name }} -ScriptBlock $scriptblock

django_typer/templates/shell_complete/zsh.sh

+5-5
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@
3838
response=("${(@f)$("${manage}" {{ django_command }} --shell zsh ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete "${words[*]}")}")
3939

4040
for type key descr in ${response}; do
41-
if [[ "$type" == "plain" ]]; then
41+
if [[ "$type" == "dir" ]]; then
42+
_path_files -/
43+
elif [[ "$type" == "file" ]]; then
44+
_path_files -f
45+
else
4246
if [[ "$descr" == "_" ]]; then
4347
completions+=("$key")
4448
else
4549
completions_with_descriptions+=("$key":"$descr")
4650
fi
47-
elif [[ "$type" == "dir" ]]; then
48-
_path_files -/
49-
elif [[ "$type" == "file" ]]; then
50-
_path_files -f
5151
fi
5252
done
5353

django_typer/utils.py

+51
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,54 @@ def accepted_kwargs(
218218
return kwargs
219219
param_names = set(inspect.signature(func).parameters.keys())
220220
return {k: v for k, v in kwargs.items() if k in param_names}
221+
222+
223+
def get_win_shell() -> str:
224+
"""
225+
The way installed python scripts are wrapped on Windows means shellingham will detect
226+
cmd.exe as the shell. This function will attempt to detect the correct shell, usually
227+
either powershell (<=v5) or pwsh (>=v6).
228+
229+
:raises ShellDetectionFailure: If the shell cannot be detected
230+
:return: The name of the shell, either 'powershell' or 'pwsh'
231+
"""
232+
import json
233+
import platform
234+
import subprocess
235+
236+
from shellingham import ShellDetectionFailure
237+
238+
assert platform.system() == "Windows"
239+
pwsh = shutil.which("pwsh")
240+
powershell = shutil.which("powershell")
241+
if pwsh and not powershell:
242+
return "pwsh"
243+
elif powershell and not pwsh:
244+
return "powershell"
245+
try:
246+
ps_command = """
247+
$parent = Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessId = {pid}";
248+
$parentPid = $parent.ParentProcessId;
249+
$parentInfo = Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessId = $parentPid";
250+
$parentInfo | Select-Object Name, ProcessId | ConvertTo-Json -Depth 1
251+
"""
252+
pid = os.getpid()
253+
while True:
254+
result = subprocess.run(
255+
["pwsh", "-NoProfile", "-Command", ps_command.format(pid=pid)],
256+
capture_output=True,
257+
text=True,
258+
).stdout.strip()
259+
if not result:
260+
break
261+
process = json.loads(result)
262+
if "pwsh" in process.get("Name", ""):
263+
return "pwsh"
264+
elif "powershell" in process.get("Name", ""):
265+
return "powershell"
266+
pid = process["ProcessId"]
267+
268+
raise ShellDetectionFailure("Unable to detect windows shell")
269+
270+
except Exception as e:
271+
raise ShellDetectionFailure("Unable to detect windows shell") from e

tests/test_parser_completers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def get_values(completion):
3232
elif SHELL == "bash":
3333
return [line.split(",")[1] for line in completion.split("\n") if line]
3434
elif SHELL in ["pwsh", "powershell"]:
35-
return [line.split(":::")[0] for line in completion.splitlines() if line]
35+
return [line.split(":::")[1] for line in completion.splitlines() if line]
3636
elif SHELL == "fish":
3737
raise NotImplementedError("Fish completion not implemented")
3838
raise NotImplementedError(f"get_values for shell {SHELL} not implemented")

0 commit comments

Comments
 (0)