Argument Completion Attributes

Enable argument completion for your own function parameters and make them so much easier to use! Some lesser-known attributes can help you.

Auto completion is an important usability feature of PowerShell and makes writing code so much easier. In fact, PowerShell implements five distinct areas of auto completion:

  • Command completion: when you start typing a command name, PowerShell completes the rest of the name and shows all commands that contain the typed text anywhere in its name
  • Parameter completion: after you have typed a command, entering a hyphen enables parameter completion, and PowerShell lists all parameters that match the typed text anywhere in its name
  • Argument completion: after typing a parameter, PowerShell often can suggest and complete available argument values, depending on the command and parameter.
  • Variable completion: after typing $, PowerShell completes variable names and providers (drives)
  • Member completion: after a dot (“.”), PowerShell completes object members such as methods and properties.

Quick Overview

Most of these code completions “just” work transparently:

  • When you add more modules, command completion includes additional commands. PowerShell uses the results of Get-Command to feed command completion.
  • When you enter a command name, PowerShell uses Get-Command -Name [CommandName] -Parameter * to feed parameter completion. That’s why parameter completion automatically works for all cmdlets, functions, and aliases that point to these, but won’t work for parameters exposed by applications (such as ping.exe).
  • When you enter $, PowerShell uses Get-Variable to feed variable completion, plus dedicated PowerShell editors such as ISE and VSCode read all variables used in any opened scripts and adds these to variable completion results. This way, variable completion picks up variables you are using in scripts even before the script is run and the variables are found in memory.
  • When you enter . at the end of an object member (property or method), PowerShell takes the result of Get-Member to feed member completion.

Areas For Improvement

So most code completers work beautifully out of the box. There are just two areas that potentially need fine tuning:

  • Argument completion: PowerShell simply does not know what arguments a given parameter might expect. So by default, it looks at the parameter data type and invokes default completers. These default completers do not necessarily cover the need of your own function parameters, so by adding more precise argument completers to your functions, you can improve usability tremendously.
  • Parameter completion: For PowerShell commands, parameter completion works beautifully because cmdlets and functions always expose their parameters. This is not true for applications (i.e. ping.exe). If you find yourself working frequently with command line applications, it may make sense to add parameter completion for these manually.

In this article, you’ll learn how PowerShell code completion really works, and how you can fine-tune it and optimize usability tremendously. Some lesser-known attributes help you here.

This article is structured like this (when it is done):

  • Introduction: Let’s start with a quick walkthrough from a user perspective: how is code completion triggered, and what are the prerequisites and reasons for different user experiences.
  • Architecture: I’ll then dive deep into the technical implementation details of PowerShell code completion. You may not have to read this in sequence and might find yourself coming back to this part for reference while reading the others.
  • PowerShell Developer: Next, I’ll look at everything that is important from a PowerShell developers point of view: so when you write scripts and functions, here you find the beef to spice up your code and make it much more user-friendly.
  • Optimizing Environment: The final part covers areas where every PowerShell user can optimize his or her *PowerShell work environment to make it easier to work with code and get the best completion experience possible.

This article is covering a lot of ground which is why I designed it as a work-in-progress. I am publishing it step by step, so the agenda above isn’t fully implemented at first.

Voilá: Argument Completion in Action

Code completion is an exciting but huge topic, and by looking at the length of this article, you may be wondering if you really should indulge into this. So here is a quick appetizer that hopefully makes you hungry for more.

To discover the various types of argument completers that you can use to enhance the usability of your function parameters, below is a test script. Please copy, paste and run it to follow the examples:

#region type-based argument completion
function Get-Computer
{
  param
  (
    # default completer falls back to path completion:
    [String]
    $ComputerName
  )
  
  # output submitted arguments
  $PSBoundParameters

}

function Get-Color
{
  param
  (
    # enum types provide completion:
    [ConsoleColor]
    $Color
  )
  
  # output submitted arguments
  $PSBoundParameters

}

enum Cities
{
  Hannover
  Redmond
  NewYork
}

function Get-City
{
  param
  (
    # PS-defined enum types provide completion (with a flaw):
    [Cities]
    $City
  )
  
  # output submitted arguments
  $PSBoundParameters

}

Add-Type -TypeDefinition @"
   public enum Shifts
   {
      Morning,
      Afternoon,
      Night,
      Off
   }
"@

function Get-Shift
{
  param
  (
    # C#-defined enum types provide completion (flawless):
    [Shifts]
    $Type
  )
  
  # output submitted arguments
  $PSBoundParameters

}
#endregion type-based argument completers


#region hint-based argument completers
function Get-Customer
{
  param
  (
    # ValidateSets restrict strings and provide IntelliSense
    [ValidateSet('Microsoft','Amazon','Google')]
    [string]
    $Customer
  )
  
  # output submitted arguments
  $PSBoundParameters

}

function Get-RemoteSystem
{
  param
  (
    # completion hints provide Intellisense (with a flaw)
    [ArgumentCompleter({'Server01','Server02','DC1'})]
    [string]
    $ComputerName
  )
  
  # output submitted arguments
  $PSBoundParameters

}

function Get-Server
{
  param
  (
    # completion hints provide Intellisense (PowerShell 7 and better only)
    [ArgumentCompletions('Server01','Server02','DC1')]
    [string]
    $ComputerName
  )
  
  # output submitted arguments
  $PSBoundParameters

}
function Get-Mood
{
  param
  (
    # hints are supplied later via Register-ArgumentCompleter
    $Current
  )
  
  # output submitted arguments
  $PSBoundParameters

}

# register the hints separately with PowerShell:
Register-ArgumentCompleter -CommandName Get-Mood -ParameterName Current -ScriptBlock {'Great','Soso','Depressed', 'WontTell'}

#endregion hint-based argument completers

Type-Based Argument Completion

When you assign a type to your parameter, PowerShell automatically provides argument completion.

Path-Completion Fall-Back

In the worst case, the type won’t limit input to defined choices. So when your parameter uses generic types such as [Object] or [String], PowerShell falls back to automatic path completion:

Get-Computer -ComputerName   # press TAB or CTRL+SPACE

Once you press Tab or Ctrl+Space, PowerShell completes paths which is nice if your parameter coincidentally asked for a path but counter-productive if no path was requested.

At the time of writing, Ctrl+Space was buggy in VSCode when used in the console pane (for any type of argument completion). I am assuming this is a bug with the module PSReadLine.

Enum-Based Completion

If your type is a so-called enum type, it defines a list of values. In this case, PowerShell takes the values from the enum and composes the completion list:

Get-Color -Color   # in ISE, IntelliSense automatically opens. In VSCode, press CTRL+SPACE or TAB

Essentially, this is what completion does to fill the completion list:

[Enum]::GetValues([ConsoleColor])

Self-Defined PowerShell Enums

It would be pure coincidence if you found an existing enum that matches the choice you are after, so in most cases you will want to define your own custom enums. Starting with PowerShell 5, this can be done using pure PowerShell code:

Get-City -City # in ISE, IntelliSense automatically opens. In VSCode, press CTRL+SPACE or TAB

Due to a flaw we are looking at later, this will not work inside the editor pane where the function was defined. It only works in the console and any other open script pane, and it will work only once you ran the code and defined the function.

Self-Defined C# Enums

You can also use Add-Type to define a custom enum type. This works in all versions of PowerShell and works in every editor pane:

Get-Shift -Type # in ISE, IntelliSense automatically opens. In VSCode, press CTRL+SPACE or TAB

Since Add-Type needs to create the enum type first, this works only after you ran the code at least once. Until then, referencing an undefined type breaks parameter and argument completion for your function, and you won’t even see completion for parameters.

Hint-Based Argument Completers

Hint-based argument completers work completely different: you provide completion hints to PowerShell which then are completely independent of the actual type your parameter uses. In other words: type-based completers always restrict the values to exactly the ones defined by the type, whereas hint-based completers do not necessarily restrict the values to the hinted values (but they can).

Attribute ValidateSet

The most convenient hinting uses the attribute [ValidateSet()]:

Get-Customer -Customer # in ISE, IntelliSense automatically opens. In VSCode, press CTRL+SPACE or TAB

Since a validation attributes purpose is to validate input and make sure it meets your needs, this is the only hint-based validator that restricts the values to the ones listed.

Attribute [ArgumentCompleter()]

A lesser-known attribute is [ArgumentCompleter()]. It is a pure hint to the argument completer, so it does not restrict the possible arguments:

Get-RemoteSystem -ComputerName   # press TAB or CTRL+SPACE

PowerShell suggests the values Server01, Server02, and DC1, but you are free to enter whatever value you want since the parameter type is [string]. Which illustrates why this attribute can be extremely useful for scenarios where you just want to suggest some common choices but enable the user to submit alternate names when needed.

However, the attribute [ArgumentCompleter()] has some flaws (at least the way it is used in above simple example):

  • Like PowerShell-defined enums, it will not work in the editor pane where the function was defined that uses the attribute. It only works in the console and other open editor panes. This is not limiting in production scenarios where functions are imported from modules but can be annoying during development or when functions are part of one production script.
  • The suggestions do not show an icon in IntelliSense lists.
  • The completion won’t respect any typed text, so when you open IntelliSense, you always see the same list of values, regardless of whether you have already typed part of the argument or not.
  • ISE won’t open IntelliSense automatically, and you need to press Ctrl+Space to see the choices (VSCode always requires the key-stroke for argument completion)

We’ll work around these issues in a moment.

Attribute [ArgumentCompletion()]

Because of some of these limitations, the PowerShell team added a new attribute in PowerShell 7: [ArgumentCompletion()]. It works basically like [ArgumentCompleters()] but has a number of convenient built-in cleverness:

Get-Server -ComputerName # press TAB or CTRL+SPACE

This attribute was introduced in PowerShell 7, so when you use it, your code will no longer work in Windows PowerShell.

The attribute [ArgumentCompletion()] has these advantages over [ArgumentCompleters()]:

  • The hints can be submitted as string array and no longer needs to be a scriptblock
  • The IntelliSense list shows icons
  • The code completion respects any text that was entered already

It still has the flaws and a new limitation:

  • Won’t work in the editor pane where the function was defined
  • Won’t work in Windows PowerShell and is incompatible.

Register-ArgumentCompleter

Finally, hints can also be sent directly to PowerShell via Register-ArgumentCompleter. This cmdlet essentially works identical to the attribute [ArgumentCompleters()]:

Get-Mood -Current # press TAB or CTRL+SPACE

And it has the very same limitations:

  • It will not work in the editor pane where the function was defined that uses the attribute. It only works in the console and other open editor panes.
  • The suggestions do not show an icon in IntelliSense lists.
  • The completion won’t respect any typed text, so when you open IntelliSense, you always see the same list of values, regardless of whether you have already typed part of the argument or not.
  • ISE won’t open IntelliSense automatically, and you need to press Ctrl+Space to see the choices (VSCode always requires the key-stroke for argument completion)

When To Use Which Argument Completer?

There is a full plethora of ways how you can add argument completions to your code! And a lot of flaws and limitations to consider. Which raises the question: when should you use which?

  1. The first and most important action is to assign the most appropriate type to your parameter. Next, try and see whether PowerShells automatic argument completion may already be sufficient. Choosing the correct type has a number of benefits that go way beyond argument completion.
  2. If there is no type that exactly describes your parameter argument, and if you have developer background, use a custom enum, preferrably using C# and Add-Type. PowerShell-based enums won’t work when you later decide to ship your code with a PowerShell module. If you have no developer background, use the attribute [ValidateSet()]. It is very simple to use, and even though it won’t offer the same type-safety as enums, it does take care that only values defined in the set can be assigned.
  3. If you just want to provide the user with suggestions but don’t want to necessarily restrict the values, use the attribute [ArgumentCompleter()]. Most of its limitations can be eliminated with some cleverness (see below). Avoid the attribute [ArgumentCompletion()] at this time because it breaks compatibility with Windows PowerShell which still is an important target platform.
  4. If you are planning to provide dynamic argument completion beyond simple and static completion lists, also look into the attribute [ArgumentCompleter()] and the tricks exposed below.
  5. If you want to add sophisticated argument completion to commands owned and created by someone else, use Register-ArgumentCompleter. While you could use this cmdlet on your own functions as well, it is better to use [ArgumentCompleter()] inside your code to encapsulate argument completion and not make it dependent from external commands.

Using [ArgumentCompleter()]

The attribute [ArgumentCompleter()] is extremely versatile, and most of its initial flaws aren’t really flaws but rather wrong usage. So let’s take a look at how this attribute is used correctly. It may well become your catch-all for best argument completion.

Invoking ScriptBlock

The attribute takes a scriptblock rather than a list of completion strings which indicates that there is much more to it. In fact, PowerShell executes this scriptblock every time a completion is requested. In its simplest form, the scriptblock returns a string array of completion texts. They can be static, but they can also be calculated freshly each time:

function Get-ErrorEvent
{
	param
	(
		# suggest today, yesterday, and last week:
		[ArgumentCompleter({ (Get-Date -Format 'yyyy-MM-dd'),((Get-Date).AddDays(-1).ToString('yyyy-MM-dd')),((Get-Date).AddDays(-7).ToString('yyyy-MM-dd'))})]
		[DateTime]
		$After
	)

	# forward the parameter -After to Get-EventLog
	# if the user does not specify the parameter, all errors are returned:
	Get-EventLog -LogName System -EntryType Error @PSBoundParameters
}

Aside from the fact that the attribute becomes a bit ugly due to the complex scriptblock, the argument completion works fine:

Get-ErrorEvent -After  # press CTRL+SPACE

Remember the limitation of this attribute: it won’t work on the same editor pane where the function was defined. Run the code, then try the command in the console or another open editor pane.

PowerShell suggests three dates: today, yesterday, and last week. When you accept one, you get back all errors found in the System event log, but you can also use any other date you like, or omit the parameter -After altogether and see all errors (if any).

Returning Completion Results

There are two limitations: the completion list opens only when you press Ctrl+Space (ISE users are used to automatically opening argument completions), and the items in the Intellisense menu have no icons. Let’s fix that.

The truth is that argument completers are supposed to return rich completion result objects, and when the scriptblock returns simple texts, PowerShell converts them into completion result objects. So let’s create these objects directly:

[System.Management.Automation.CompletionResult]::new
System.Management.Automation.CompletionResult new(string completionText, string listItemText, System.Management.Automation.CompletionResultType resultType, string toolTip)                 
System.Management.Automation.CompletionResult new(string completionText)  

Instead of just supplying a completion text (second overload), we’ll now supply icon, list item text, and tooltip, and turn argument completion into a much nicer experience.

function Get-ErrorEvent
{
	param
	(
		# suggest today, yesterday, and last week:
		[ArgumentCompleter({ 
            $today = Get-Date -Format 'yyyy-MM-dd'
            $yesterday = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd')
            $lastWeek = (Get-Date).AddDays(-7).ToString('yyyy-MM-dd')
            
            # create the completions:
            [System.Management.Automation.CompletionResult]::new($today, "Today", "ParameterValue", "all errors after midnight")
            [System.Management.Automation.CompletionResult]::new($yesterday, "Yesterday", "ParameterValue", "all errors after yesterday")
            [System.Management.Automation.CompletionResult]::new($lastWeek, "Last Week", "ParameterValue", "all errors after last week")

            })]
		[DateTime]
		$After
	)

	# forward the parameter -After to Get-EventLog
	# if the user does not specify the parameter, all errors are returned:
	Get-EventLog -LogName System -EntryType Error @PSBoundParameters
}

Don’t worry about the attribute growing more complex and ugly. While you can’t replace the scriptblock with a variable, there are other ways to make it small and beautiful again. We’ll look at that later.

Get-ErrorEvent -After  # in ISE, IntelliSense automatically opens. In VSCode, press CTRL+SPACE or TAB

Try the command in the console or another open script pane. In ISE, IntelliSense opens automatically and shows Today, Yesterday, and Last Week. The tooltip explains each item, the icon is in place, and when you choose one, the value is inserted. In VSCode, press Ctrl + Space to request the IntelliSense menu. In the console, press Tab.

Keep in mind (again! I am not reminding you any longer) that this argument completer won’t work in the script pane that defines the function. Use it in the console or any other open script pane.

This limitation becomes less painful later when you export your functions to PowerShell modules because then the function definition will never be present in the same script pane.

Choosing IntelliSense Menu Icon

There is a number of pre-defined IntelliSense icons that you can use with your completion items:

[Enum]::GetNames([System.Management.Automation.CompletionResultType])
Command
DynamicKeyword
History
Keyword
Method
Namespace
ParameterName
ParameterValue
Property
ProviderContainer
ProviderItem
Text
Type
Variable

In above example, I used the icon ParameterValue, and that’s a good idea - at least when you are using the ISE editor. This is the only icon that opens IntelliSense menus automatically. When you use a different icon, you must request IntelliSense manually via Ctrl+Space (which is the default behavior in VSCode)

This phenomenon explains why knowing about Ctrl+Space is a good idea, at least when using the ISE editor. VSCode always requires this keyboard shortcut.

Not so ISE: most of the time, it opens IntelliSense menus automatically which is quite convenient. However, it doesn’t always do. For example, path completion always needs to be triggered manually by the keyboard shortcut:

Get-ChildItem -Path  # press CTRL+SPACE

You now know why (by looking at the icons in the IntelliSense menu icons): they are using ProviderContainer (for directories) and ProviderItem (for files).

The same “icon issue” is true for many other cases, like in this one:

Get-CimInstance -ClassName # press CTRL+SPACE (you may need to press multiple times)

Get-CimInstance sports IntelliSense for WMI classes, but the IntelliSense menu items use the icon NameSpace. And since it takes some time to gather all WMI classes, when you invoke argument completion for the first time, IntelliSense may time out. That’s why you may have to press the keyboard shortcut multiple times until at one point miraculously the IntelliSense menu finally pops up.

Respecting User Input

One huge flaw of the current implementation is: it neglects user input. Try this:

function Get-EventLogEntry
{
  param
  (
    # suggest today, yesterday, and last week:
    [ArgumentCompleter({
          # list all eventlog names...
          Get-WinEvent -ListLog * -ErrorAction Ignore | 
          # ...that have records...
          Where-Object RecordCount -gt 0 | 
          Sort-Object -Property LogName |
          Foreach-Object { 
            # create completionresult items:
            $logname = $_.LogName
            $records = $_.RecordCount
            [System.Management.Automation.CompletionResult]::new($logname, $logname, "ParameterValue", "$logname`r`n$records records available")
          }
            })]
    [string]
    $LogName,
    
    [int]
    $MaxEvents
  )

  # forward (splatting) the user parameters to this command (must be same name):
  Get-WinEvent @PSBoundParameters
}

Get-EventLogEntry is an awesome function because it internally uses Get-WinEvent to access all available Windows event. Unlike Get-WinEvent, you don’t need to guess log names. Instead, the parameter -LogName provides rich Intellisense argument completion:

Get-WinEvent -LogName  # press CTRL+SPACE
Get-EventLogEntry -LogName # ISE opens IntelliSense automatically, press CTRL+SPACE in VSCode

Get-WinEvent falls back to the useless default path completer whereas Get-EventLogEntry opens a menu with the names of all event logs that contain records.

However, the completer does not respect user input:

Get-EventLogEntry -LogName Sy  # press ESC to close IntelliSense, then press CTRL+SPACE to re-open

Even though the user typed “Sy”, when the argument completer is re-opened, it shows all entries. It should have shown only the ones with “Sy” anywhere in its name.

Accessing User Input

To be able to do so, the argument completer would need to know whether the user has typed text. That’s why the scriptblock receives arguments from PowerShell that we just did not take advantage of yet.

Here is a revised function that does the filtering correctly:

function Get-EventLogEntry
{
  param
  (
    # suggest today, yesterday, and last week:
    [ArgumentCompleter({
          # receive information about current state:
          param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    
          # list all eventlog names...
          Get-WinEvent -ListLog * -ErrorAction Ignore | 
          # ...that have records...
          Where-Object RecordCount -gt 0 | 
          Sort-Object -Property LogName |
          # filter results by word to complete
          Where-Object { $_.LogName -like "$wordToComplete*" } | 
          Foreach-Object { 
            # create completionresult items:
            $logname = $_.LogName
            $records = $_.RecordCount
            [System.Management.Automation.CompletionResult]::new($logname, $logname, "ParameterValue", "$logname`r`n$records records available")
          }
            })]
    [string]
    $LogName,
    
    [int]
    $MaxEvents
  )

  # forward (splatting) the user parameters to this command (must be same name):
  Get-WinEvent @PSBoundParameters
}

When you try again, this time the argument completer honors existing user input thanks to the argument submitted in $WordToComplete.

Most of the other arguments are useless in this scenario because when you apply an attribute to a parameter, both $Command and $Parameter are already known. $commandAst on the other hand is extremely powerful but also way too complex in most scenarios as it provides you with access to the Abstract Syntax Tree (AST) of the code that requested the completion.

But there’s one other information that may come handy: $FakeBoundParameters. Let’s check it out next.

Responding To Other Arguments

Maybe the choices you want to suggest are context-sensitive and depend on what other arguments the user already submitted. $FakeBoundParameters is a hashtable with all of the arguments the command received already.

Take a look at this example:

function Get-LogFile
{
  param
  (
    # suggest today, yesterday, and last week:
    [ArgumentCompleter({
          # receive information about current state:
          param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
          # check to see whether the user already specified a parent directory...
          if ($fakeBoundParameters.ContainsKey('ParentPath'))
          {
            # if so, take that user input...
            $ParentPath = $fakeBoundParameters['ParentPath']
          }
          else
          {
            # ...else fall back to a default path, i.e. the windows folder
            $ParentPath = $env:windir
          }
    
          # list all log files in the path
          Get-ChildItem -Path $ParentPath -Filter *.log -ErrorAction Ignore |
          Sort-Object -Property Name |
          # filter results by word to complete
          Where-Object { $_.Name -like "$wordToComplete*" } | 
          Foreach-Object { 
            # create completionresult items:
            $logname = $_.Name
            $fullpath = $_.FullName
            # convert size in bytes to MB:
            $size = '{0:n1} MB' -f ($_.Length/1MB)
            [System.Management.Automation.CompletionResult]::new($fullpath, $logname, "ParameterValue", "$logname`r`n$size")
          }
            })]
    [string]
    $FilePath,
    
    [string]
    $ParentPath = $env:windir
  )

  # return the selected file (or do with it whatever else you'd like, i.e. read it)
  Get-Item -Path $FilePath
}

Next, try and run this:

Get-LogFile -FilePath # ISE opens Intellisense automatically, press CTRL+SPACE in VSCode

You get a list of all files with extension .log in your Windows folder. The tooltip shows the log file size, and when you select an entry, the full (absolute) path is inserted.

As you can see, completion results can define the text in the IntelliSense menu and the actual value separately. This way, the IntelliSense menu can be clean and short even if the value is much longer.

Now try this:

Get-LogFile -ParentPath c:\windows\system32 -FilePath  # ISE opens Intellisense automatically, press CTRL+SPACE in VSCode

This time, the IntelliSense menu lists all files with extension .log located in the path defined in -ParentPath. The completion code checked $FakeBoundParameters to see whether the user had submitted this argument, and if so, used its value to calculate the completion results.

PowerShell 7 Shortcut

Our completion attribute has become fairly huge thanks to the code in the scriptblock. That’s not too bad though since function code doesn’t need to win a beauty price. Still, there is a lot of redundant code in the completer scriptblock:

  • Every completer needs to check $WordToComplete and perform the filtering based on existing user input
  • Every completer needs to create completion result objects to define icon and tooltip

Plus, in a majority of instances, there is no need for sophisticated dynamic arguments, and a static list of completion items is all that’s needed.

New Attribute: [ArgumentCompletions()]

That’s why the PowerShell team silently introduced a new attribute [ArgumentCompletions()] that takes care of this. It is also easier to use because it accepts a simple list of completion strings. So these two implementations provide the same argument completion experience.

Here is the classic approach that works in Windows PowerShell and PowerShell 7:

function Get-Country
{
  param
  (
    # suggest today, yesterday, and last week:
    [ArgumentCompleter({
          # receive information about current state:
          param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
          'USA','Germany','Norway','Sweden','Austria','YouNameIt' |
          Where-Object { $_.Name -like "$wordToComplete*" } | 
          Foreach-Object { 
            [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)
          }
            })]
    [string]
    $Name
  )

  # return parameter
  $PSBoundParameters
}

Try it:

Get-Country -Name # ISE opens IntelliSense automatically, in VSCode press CTRL+SPACE

And this is the shortcut that was introduced in PowerShell 7:

function Get-Country
{
  param
  (
    # suggest country names:
    [ArgumentCompletions('USA','Germany','Norway','Sweden','Austria','YouNameIt')]
    [string]
    $Name
  )

  # return parameter
  $PSBoundParameters
}

A lot easier, and so much cleaner! But unfortunately, when you use the attribute [ArgumentCompletions()] in your code, you break compatibility, and your function is no longer usable in Windows PowerShell. Too bad. Or not?

PowerShell 7 Shortcut for Windows PowerShell

Actually, you can safely use the new attribute provided you add this new and missing attribute to Windows PowerShell:

# are we running in Windows PowerShell?
if ($PSVersionTable.PSEdition -ne 'Core')
{
  # add the attribute [ArgumentCompletions()]:
  $code = @'
using System;
using System.Collections.Generic;
using System.Management.Automation;

    public class ArgumentCompletionsAttribute : ArgumentCompleterAttribute
    {
        
        private static ScriptBlock _createScriptBlock(params string[] completions)
        {
            string text = "\"" + string.Join("\",\"", completions) + "\"";
            string code = "param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams);@(" + text + ") -like \"*$WordToComplete*\" | Foreach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }";
            return ScriptBlock.Create(code);
        }
        
        public ArgumentCompletionsAttribute(params string[] completions) : base(_createScriptBlock(completions))
        {
        }
    }
'@

  $null = Add-Type -TypeDefinition $code *>&1
}

The type compiled by Add-Type exposes no public members which is why the cmdlet gets nervous and emits a warning. This warning isn’t needed so it should be suppressed. Unfortunately, the parameter -IgnoreWarnings does not ignore this warning. That’s why the code simply forwards all streams (*>&1) to the output stream, and discards this to $null.

This piece of code detects whether it runs in Windows PowerShell or not. If it runs in Windows PowerShell, it adds the missing attribute. Now you are safe to use the attribute in your code.

Simply make sure you add above chunk of code to your module(s) or scripts. It needs to run only once. Test for yourself: run above code, then run this in Windows PowerShell:

function Get-Country
{
  param
  (
    # suggest today, yesterday, and last week:
    [ArgumentCompletions('USA','Germany','Norway','Sweden','Austria','YouNameIt')]
    [string]
    $Name
  )

  # return parameter
  $PSBoundParameters
}

Should I remind once more? Argument completion will not work in the script pane where the function was defined. It works only in the console and other open script panes.

How Does This Work?

You can either just use above code and be happy, or try and wrap your head around it. Which would be a good idea because I am going to use the approach in a number of similar cases when I continue this article.

The code uses C# code to define a new class that is derived from the attribute [ArgumentCompleter()]. The name of the new class is the name of the missing attribute I want to add: ArgumentCompletionsAttribute.

public class ArgumentCompletionsAttribute : ArgumentCompleterAttribute
{}

This class gets a constructor that defines the mandatory parameters for the new attribute:

public ArgumentCompletionsAttribute(params string[] completions) : base(_createScriptBlock(completions))
{
}

So the constructor takes a Param Array (a list of strings) just like the original attribute in PowerShell 7. It then forwards these arguments to the base constructor (the one from the existing attribute [ArgumentCompleter()]. Except, we need someone who translates this list of string into the scriptblock that the existing attribute wants.

That’s where a static helper function comes into play: _createScriptBlock basically creates the redundant code that is always the same for any argument completer:

private static ScriptBlock _createScriptBlock(params string[] completions)
{
    string text = "\"" + string.Join("\",\"", completions) + "\"";
    string code = "param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams);@(" + text + ") -like \"*$WordToComplete*\" | Foreach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }";
    return ScriptBlock.Create(code);
}

It takes the string array with completion texts, converts it into a comma-separated list, and then creates PowerShell code that constructs the completion entries, just in the same way as the many examples did it above.

This PowerShell code is then turned into a ScriptBlock and returned to the base constructor. That’s it.

This is just a simple but effective implementation focusing on the important parts. You may find a lot of bits and pieces that can be improved. If you do, don’t forget to leave a comment at the bottom!

What’s Next

This part took a deep look at the argument completion attributes. This article isn’t finished yet. I’ll continue it over the next days and weeks and have a lot of additional surprises for you, to please return back often!

And make sure you have PowerShell Conference EU 2020 on your radar. That’s the place to be in June if you enjoy stuff like this. See you there!