Add PowerShell tab completion support - #212
Conversation
pmhahn
left a comment
There was a problem hiding this comment.
I just had a short look. Looks okay mostly, but someone with PowerShell knowledge should have a look at the embedded PS1 code.
Found some minor nits.
Maybe split the large __init__.py into individual files in the future? Or at least move the embedded shell-scripts parts into individual files? See #199 ?
|
|
||
|
|
||
| def _powershell_escape(string: str) -> str: | ||
| """Escape a string for use inside a PowerShell single-quoted string.""" |
There was a problem hiding this comment.
Nit: I would prefer that _ps1_escape adds the surrounding single-quotes itself. I'm no PS1 expert myself, but at least with bash there are several ways to escape strings
- in single-quotes only single-quotes must be handles special
- in double-quotes several characters still need backslash escaping
- without quote many characters must be backslash escaped
With bash shlex.quote() could switch between those choices or even depend on the string to escape and we don't care which is uses as it is an internal details.
With your _ps1_escape here you always must use single quotes and we later can't change it if we find something better.
There was a problem hiding this comment.
I rewrote this a bit to always wrap in single quotes. I did find an very weird edge case, but that should be dealt with now. I checked where I use it, and honestly I don't see where we would want anything but single quoted strings. What the tool passes in should probably never be interpolated. If there is something dynamic, we should do that in the python code to keep things deterministic. If you can think of case, let me know.
| return "@{\n" + "\n".join(entries) + "\n}" | ||
|
|
||
|
|
||
| def get_powershell_commands(root_parser, root_prefix, choice_functions=None): |
There was a problem hiding this comment.
Oh oh, I see lots of get_bash_commands() code here. There are many differences, but I don't have time to dig in right now to find out, why they differ.
Maybe someone™ should refactor this and to extract common code? 🤔
There was a problem hiding this comment.
Do you need this addressed in this PR?
There was a problem hiding this comment.
No, but a mental note to whoever to address this soon.
pmhahn
left a comment
There was a problem hiding this comment.
Sorry for the delay; only 1 nit you might want to fix.
Again: I'm a Linux-only user and have no experience with PS, so I cannot say if the completion works as expected. Nice work, thanks.
8fd5dfa to
ca739c5
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds a new PowerShell completion backend to shtab, generating native PowerShell argument completion scripts via Register-ArgumentCompleter -Native, and updates tests + documentation to include PowerShell as a supported shell.
Changes:
- Added a PowerShell completion generator (
complete_powershell) and supporting serializer/parser-traversal helpers inshtab/__init__.py. - Extended the existing pytest suite expectations to include PowerShell output markers.
- Updated docs/README/CONTRIBUTING to list PowerShell support and provide PowerShell installation/usage snippets.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
shtab/__init__.py |
Implements PowerShell script generation, adds PowerShell entries to choice/utility infrastructure. |
tests/test_shtab.py |
Extends parametrized expectations to cover the new powershell shell output. |
README.rst |
Documents PowerShell as supported and updates usage examples accordingly. |
docs/use.md |
Adds PowerShell setup instructions and updates shell lists in usage text. |
docs/index.md |
Lists PowerShell among supported shells. |
CONTRIBUTING.md |
Notes the new complete_powershell() entry point among core functions. |
Suppressed comments (4)
shtab/init.py:1038
_powershell_escape()currently doubles curly apostrophes (U+2018/U+2019) as well as the ASCII quote. PowerShell only needs escaping for the ASCII single-quote delimiter ('), so doubling curly quotes will change the literal content of strings and break completions for choices containing those characters.
s = str(string)
for ch in ("'", "\u2018", "\u2019"):
s = s.replace(ch, ch * 2)
return "'" + s + "'"
shtab/init.py:1126
- In
get_powershell_commands(), the optional-action suppression check isif optional == SUPPRESS:, butoptionalis anargparse.Actionobject. This condition will never be true, so suppressed options will be included in completions.
for optional in parser._get_optional_actions():
if optional == SUPPRESS:
continue
shtab/init.py:1095
get_powershell_commands()does not skip positional actions whose help isSUPPRESS. This will leak hidden/internal arguments into the generated completion tables (unlike the other shell implementations which skip suppressed actions).
for i, positional in enumerate(parser._get_positional_actions()):
action_key = f"{prefix}_pos_{i}"
if hasattr(positional, 'complete'):
comp_pattern = complete2pattern(positional.complete, 'powershell', choice_type2fn,
shtab/init.py:1117
get_powershell_commands()currently skips storingnargswhen it is'?'(and also compares against string'1'). The PowerShell completion state machine relies on correctnargsto know when to advance positional slots, so omitting'?'(and not handling int1) can lead to mis-tracking positional consumption.
if positional.nargs not in (None, "1", "?"):
nargs[action_key] = str(positional.nargs)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cd7cdc7 to
aaad68e
Compare
There was a problem hiding this comment.
Thanks for this start! Just rebased.
Locally, some things don't work for me:
customcomplete -<TAB>doesn't complete anythingcustomcomplete process foo csv <tab>shouldn't complete filescustomcomplete process <tab>needs changingshtab.cmd("head -c5 /dev/random ...")to something windows-compatible
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #212 +/- ##
==========================================
+ Coverage 84.71% 84.88% +0.16%
==========================================
Files 3 3
Lines 471 549 +78
Branches 93 114 +21
==========================================
+ Hits 399 466 +67
- Misses 38 44 +6
- Partials 34 39 +5 🚀 New features to boost your workflow:
|
--------- Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Philipp Hahn <pmhahn+github@pmhahn.de>
- Enhanced the `_powershell_escape` function to handle smart quotes. - Updated the function to return a properly quoted string for PowerShell. - Adjusted related functions to ensure consistent escaping behavior.
for more information, see https://pre-commit.ci
- complete_powershell now captures argparse help= text (with %(default)s/ %(prog)s expansion) into a $..._help table and surfaces it as each CompletionResult's tooltip via a new Get-ActionHelp helper. - _shtab_powershell_compgen_files/_dirs (and the glob() preamble) preserved only the basename, dropping the typed directory prefix (e.g. completing "dir/pre" returned "file.txt" instead of "dir/file.txt"); fixed via a shared _shtab_powershell_join_prefix helper, guarding Split-Path against an empty $WordToComplete (it throws on ""). - When a completer has nothing to offer, PowerShell falls back to native filesystem completion unless the script block explicitly returns $null (PowerShell/PowerShell#19628); the generated script now does so instead of silently producing zero output. - cmd()'s PowerShell preamble named its wrapper function _shtab_pattern_compgen_{abs(hash(command))} while the compgens table referenced _shtab_pattern_compgen_{sha(command)} -- always a mismatch, so the dynamic-command completer never worked. Now both use sha(). - examples/customcomplete.py: replaced the Unix-only `head -c5 /dev/random | base32` completer (invalid under PowerShell's Invoke-Expression) with `git rev-parse --short HEAD`, matching the cross-shell convention already used in pathcomplete.py. - tests: added a powershell_candidates() harness that drives the generated script through pwsh's TabExpansion2 and covers test_file_completion[powershell].
for more information, see https://pre-commit.ci
Summary
This PR adds PowerShell tab completion support to shtab, enabling native PowerShell argument completion for CLI applications built with argparse.
Key features:
Register-ArgumentCompleter -NativeChanges
shtab/init.py: Added PowerShell completer implementation with:
get_powershell_commands()for parser tree extractioncomplete_powershell()decorated completer functionCHOICE_FUNCTIONSinfrastructureDocumentation & Examples:
README.rstanddocs/index.mdto list PowerShell as supported shelldocs/use.mdexamples/customcomplete.pywith PowerShell preambleTests: Extended existing parametrized test suite to include PowerShell
Building & Testing Locally
Prerequisites
pip install -e .Run tests
Generate completion script manually
Test completion in PowerShell
Enable debug output
Set the
SHTAB_DEBUGenvironment variable to trace completion state: