I found some code that allows you to output text one character at a time and I am trying to make it fit my use case better by allowing you to select the foreground color as well. I have already added the foreground color to my parameters and it responds correctly, but the tab complete does not cycle trough the colors as it does for Write-Host.
function get-easyview{
param(
[int]$Milliseconds= 20,
[string]$Foregroundcolor="RED")
$text = $input | Out-String
[char[]]$text | ForEach-Object{
Write-Host -nonewline -Foregroundcolor $Foregroundcolor $_
# Only break for a non-whitespace character.
if($_ -notmatch "\s"){Sleep -Milliseconds $Milliseconds}
}
}
$words="my salsa... my salsa" | get-easyview
This outputs the text in red and $words="my salsa... my salsa" | get-easyview -foregroundcolor green
would output it in green, but I want tab to cycle through the colors once I type -foregroundcolor.
Any ideas?
As PowerShell Core evolves, more options become available for Argument Completion in your functions, below you can find some examples of their implementation.
All of these options allows you to cycle through the set with the Tab key.
about Functions Advanced Parameters has listed these alternatives with some fine examples, I would also personally recommend you this excellent article from vexx32.
ValidateSet
attributeValidates and throw an exception if the argument used is not in the set.
[ValidateSet('Red','White','Blue','Yellow')]
[string] $ForegroundColor = 'Red'
ArgumentCompletions
attributeThis attribute was introduced in PowerShell 6, it's not compatible with Windows PowerShell and as the docs states, the main difference is:
However, unlike
ValidateSet
, the values are not validated and more like suggestions. Therefore the user can supply any value, not just the values in the list.
[ArgumentCompletions('Red','White','Blue','Yellow')]
[string] $ForegroundColor = 'Red'
PowerShell 6 also introduced the IValidateSetValuesGenerator
Interface and with it the ability to create custom classes that, when implementing this interface, can offer completion and validation at the same time:
class ColorCompleter : System.Management.Automation.IValidateSetValuesGenerator {
static [string[]] $Colors
static ColorCompleter() {
[ColorCompleter]::Colors = [System.Drawing.Color].GetProperties(
[System.Reflection.BindingFlags] 'Static, Public').Name
}
[string[]] GetValidValues() {
return [ColorCompleter]::Colors
}
}
function Test-IValidateSetValuesGenerator {
[CmdletBinding()]
param([ValidateSet([ColorCompleter])] $Color)
}
Test-IValidateSetValuesGenerator <TAB>
For other alternatives compatible with Windows PowerShell 5.1 see below.
Register-ArgumentCompleter
This cmdlet allows us to define a custom argument completer for a function, cmdlet or command.
Steps for registering a custom argument completer
function Test-ArgumentCompleter {
[cmdletbinding()]
param([string] $ForegroundColor)
$ForegroundColor
}
ScriptBlock
which will dynamically generate the Tab auto completion:The script block you provide should return the values that complete the input. The script block must unroll the values using the pipeline (
ForEach-Object
,Where-Object
, etc.), or another suitable method. Returning an array of values causes PowerShell to treat the entire array as one tab completion value.
$scriptBlock = {
param(
$commandName,
$parameterName,
$wordToComplete,
$commandAst,
$fakeBoundParameters
)
'Red', 'White', 'Blue', 'Yellow' |
Where-Object { $_ -like "*$wordToComplete*" }
}
$params = @{
CommandName = 'Test-ArgumentCompleter'
ParameterName = 'ForegroundColor'
ScriptBlock = $scriptBlock
}
Register-ArgumentCompleter @params
Since PowerShell 5.1, we can define our own PowerShell Class that implements the IArgumentCompleter
Interface.
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class Completer : IArgumentCompleter {
static [string[]] $Set
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[CompletionResult[]] $result = foreach($item in $this.Set) {
if($item -like "*$wordToComplete*") {
[CompletionResult]::new("'$item'")
}
}
return $result
}
}
ArgumentCompleterAttribute
passing the custom class type as argument, essentially, the ArgumentCompleterAttribute(Type)
overload:# define the new set here (can be also hardcoded in the class)
[Completer]::Set = 'foo', 'bar', 'baz'
function Test-ArgumentCompleter {
[cmdletbinding()]
param(
[ArgumentCompleter([Completer])]
[string] $Argument
)
$Argument
}
It's also worth noting that ArgumentCompleterAttribute
has another overload for Script Blocks, thus, something like this is also perfectly valid:
function Test-ArgumentCompleter {
[cmdletbinding()]
param(
[ArgumentCompleter({
param(
$commandName,
$parameterName,
$wordToComplete,
$commandAst,
$fakeBoundParameters
)
'Red', 'White', 'Blue', 'Yellow' |
Where-Object { $_ -like "*$wordToComplete*" }
})]
[string] $Argument
)
$Argument
}
The main advantage of using a custom Class is that it gives us more room for customizations, for example we could have a Class that can handle Completion and Validation by inheriting from ValidateEnumeratedArgumentsAttribute
and IArgumentCompleter
.
For example, the class definition could look like this:
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class ValidateCustomSet : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
static [string[]] $Set
static [string] $ErrorMessage
static ValidateCustomSet() {
if (-not [ValidateCustomSet]::ErrorMessage) {
[ValidateCustomSet]::ErrorMessage = "'{0}' is not in set! Valid Values '{1}'"
}
}
ValidateCustomSet() { }
ValidateCustomSet([string] $ErrorMessage) {
[ValidateCustomSet]::ErrorMessage = $ErrorMessage
}
ValidateCustomSet([scriptblock] $Set, [string] $ErrorMessage) {
[ValidateCustomSet]::Set = $Set.InvokeReturnAsIs()
[ValidateCustomSet]::ErrorMessage = $ErrorMessage
}
ValidateCustomSet([scriptblock] $Set) {
[ValidateCustomSet]::Set = $Set.InvokeReturnAsIs()
}
[void] ValidateElement([object] $Element) {
if ($Element -notin [ValidateCustomSet]::Set) {
throw [MetadataException]::new(
[string]::Format([ValidateCustomSet]::ErrorMessage,
$Element, [string]::Join(',', [ValidateCustomSet]::Set)))
}
}
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[List[CompletionResult]] $result = foreach ($i in [ValidateCustomSet]::Set) {
if ($i.StartsWith($wordToComplete, [System.StringComparison]::InvariantCultureIgnoreCase)) {
[CompletionResult]::new(
"'" + [CodeGeneration]::EscapeSingleQuotedStringContent($i) + "'",
$i,
[CompletionResultType]::ParameterValue,
$i)
}
}
return $result
}
}
Then the implementation would require to decorate the same parameter with ArgumentCompleter
passing our custom class and also to decorate it with the custom class (for the validation part):
function Test-Set {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateCustomSet(
# Positional binding here!
# If using the (scriptblock, string) overload must be in that order!
{ 'foo', 'bar', 'baz' },
'Invalid Value: {0}. Must be one of the following: {1}.'
)]
[ArgumentCompleter([ValidateCustomSet])]
[string] $Item
)
$Item
}