Sign in
Log inSign up
PowerShell Tab Completion For Your Command-Line Application

PowerShell Tab Completion For Your Command-Line Application

Fast and fully customizable PowerShell tab completions for any CLI.

Tejas Ravishankar's photo
Tejas Ravishankar
ยทAug 18, 2021ยท

6 min read

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.

Tab Completion

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

  1. Receiving Input Arguments
  2. Linking to PowerShell API
  3. Parsing Arguments
  4. Generating the completion
  5. 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!