PowerShell Tab Completion For Your Command-Line Application
Fast and fully customizable PowerShell tab completions for any CLI.
Introduction
Tab completion is a must-have for any good command-line application.
I'll be showing you how to build your very own Tab Completer, and how to connect it to Windows Powershell for fast tab completions.
We're going to be implementing the above tab-completion ๐
What is Tab Completion?
Command-line completion (also known as tab-completion) is a common feature in command line applications in which the terminal automatically fills in partially typed commands.
Building a Tab Completer
So let's get into writing our tab completer. We need to make sure it's really fast, so I highly suggest it be written in a language like Rust, C, C++, Go, Zig, or any other language with minimal startup time.
However, I believe it would be easiest to explain the logic with the simple syntax of Python.
Steps to Building a Tab Completer
- Receiving Input Arguments
- Linking to PowerShell API
- Parsing Arguments
- Generating the completion
- Outputting the completion result
๐ฆ Receiving Input Arguments
# complete.py
import sys
args = sys.argv # get input arguments
with open('data.txt', 'w+') as f:
f.write(str(args))
We'll need to import sys
to get access to its argv
property, which gives you access to the arguments the script's command line input arguments.
It is important to note that any statements that output text will be the final completion result used.
Hence, we're doing this in a slightly hacky way so that we can see the value of args
without actually sending it as a completion result.
๐ Linking to PowerShell API
We're not done yet! We'll need to link this to the Powershell API.
Let's start off by adding this code to your powershell profile.
Keep in mind that tab completion will not work without this step, so it must be done during the setup
or installation
stage of your command line application for any user who chooses to install it.
Register-ArgumentCompleter -Native -CommandName <yourcommandname> -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
[Console]::InputEncoding = [Console]::OutputEncoding = $OutputEncoding = [System.Text.Utf8Encoding]::new()
$Local:word = $wordToComplete.Replace('"', '""')
$Local:ast = $commandAst.ToString().Replace('"', '""')
py <path-to-python-script> --word="$Local:word" --commandline "$Local:ast" --position $cursorPosition | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
Whoa, that's a lotta complicated code. Let's break this down.
Register-ArgumentCompleter -Native -CommandName <commandname> -ScriptBlock
It's important that we replace <commandname>
with the name of the command line application you want to setup tab completion for.
In the case of the GIF I showed you earlier in this article, the command line application's name is electric
. So I'll be using that.
We use Register-ArgumentCompleter
to register a new tab completer with Powershell.
py <path-to-python-script> --word="$Local:word" --commandline "$Local:ast" --position $cursorPosition
Here's we replace <path-to-python-script>
with the path to complete.py
we just created.
The py
before <path-to-python-script>
is used to invoke Python and it should be replaced with python3
on Unix-based systems.
We're almost done! Notice the --word
, --commandline
and --position
? Those are input parameters that you're going to be receiving through sys.argv
!
Here's my final code for my command line application which can be invoked through electric
. The location of my complete.py
is C:\Users\xtremedevx\dev\complete.py
.
Register-ArgumentCompleter -Native -CommandName electric -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
[Console]::InputEncoding = [Console]::OutputEncoding = $OutputEncoding = [System.Text.Utf8Encoding]::new()
$Local:word = $wordToComplete.Replace('"', '""')
$Local:ast = $commandAst.ToString().Replace('"', '""')
py C:\Users\xtremedevx\dev\complete.py --word="$Local:word" --commandline "$Local:ast" --position $cursorPosition | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
๐งช Testing Functionality
To make sure all of this works and you're receiving arguments from the PowerShell API, type the name of your command followed by any text and hit tab. For example:
electric in<tab>
You should then see a result.txt
in the same directory as your complete.py
containing all the arguments passed in. If so, everything's working as expected and you can move on to the next step!
If this is not working, make sure the <commandname>
and <path-to-python-script>
mentioned previously are correct.
This will also not work if you don't provide a piece of text after the name of your command (in my case electric
) and remember to hit tab.
๐ฅ๏ธ Parsing Input Arguments
Phew, we're done with the hardest part. Now it's all going to be Python, no more Powershell Scripting ๐.
Your result.txt
should have something similar to this:
['C:\\Users\\xtremedevx\\dev\\complete.py', '--word=in', '--commandline', 'electric in', '--position', '11']
Let's start parsing our input arguments.
# complete.py
import sys
args = sys.argv
with open('result.txt', 'w+') as f:
f.write(str(sys.argv))
current_word = args[1].replace('--word=', '') # in
line = args[3] # electric in
position = eval(args[5])
Perfect! We're ready to move on to the next step!
๐งณ Generating and Outputting the Completion
Onto the most exciting steps!
We're going to use a really simple method for generating completions.
# complete.py
import sys
args = sys.argv
current_word = args[1].replace('--word=', '')
line = args[3]
completions = [
{
'command': 'install',
'flags': [
'--version',
'--debug',
'--no-color'
]
},
{
'command': '--version'
}
]
We're going to be using a list of dictionaries to store the flags and commands that we need to complete.
Let's generate the completion.
# complete.py
import sys
args = sys.argv
current_word = args[1].replace('--word=', '')
line = args[3]
position = eval(args[5])
completions = [
{
'command': 'install',
'flags': [
'--version',
'--debug',
'--no-color'
],
},
{
'command': '--version',
}
]
if len(line) < position:
line += ' '
with open('result.txt', 'w+') as f:
f.write(str(args))
if line.count(' ') == 1:
# complete commands
command = current_word
if current_word == ' ':
for completion in completions:
print(completion['command'])
exit()
for completion in completions:
if completion['command'].startswith(command):
print(completion['command'])
Perfect, now we have command completions.
To test if this works, type in
yourcommandname ins<tab>
and it should complete yourcommandname install
for you!
Now, onto the flags.
# complete.py
import sys
args = sys.argv
current_word = args[1].replace('--word=', '')
position = eval(args[5])
line = args[3]
completions = [
{
'command': 'install',
'flags': [
'--verbose',
'--debug',
'--no-color'
],
},
{
'command': '--version',
}
]
if len(line) < position:
line += ' '
current_word += ' '
with open('result.txt', 'w+') as f:
f.write(str(args))
if line.count(' ') == 1:
# complete commands
command = current_word
if current_word == ' ':
for completion in completions:
print(completion['command'])
exit()
for completion in completions:
if completion['command'].startswith(command):
print(completion['command'])
elif line.count(' ') > 1:
if current_word.startswith('-') or current_word == ' ':
# complete flags
command = line.split(' ')[1]
completion_data = {}
for completion in completions:
if completion['command'] == command:
completion_data = completion
if 'flags' in list(completion_data.keys()):
if current_word == ' ':
for flag in completion_data['flags']:
print(flag)
exit()
for flag in completion_data['flags']:
if flag.startswith(current_word):
print(flag)
else:
# complete package names
pass
๐ฅณ Conclusion
And that's it! We're all done. This kind of tab completer can be used for creating fast tab completions for any command-line application!
Happy Tabbing!