Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make argumentcompleter work in a PowerShell dynamic parameter?

There are many examples on the internet showing how to write a basic dynamic parameter, I've seen only one example of how to have multiple dynamic parameters in one function, but I haven't seen any examples of how to implement argument completer in a dynamic parameter.

Here's an example of a basic dynamic parameter I have:

Param(
    [Parameter( Position = 0, Mandatory = $True, HelpMessage = 'Choose a VM host for the VM(s).' )]
    [ValidateScript( {
        If ($null -eq $global:DefaultVIServer) {
            Write-Host -BackgroundColor Yellow -ForegroundColor Red -Object 'Connect to vCenter!'
            Break
        }
        Else {
            If ($PSItem -in ((Get-VMHost).Name)) {
                $True
            }
            Else {
                Throw "Accepted values: $(((Get-VMHost).Name) -join ', ')"
            }
        }
    } )]
    [ArgumentCompleter( {
        Param ($Cmd, $Param, $ParamComplete)
        [Array]$ValidValues = (Get-VMHost | Sort-Object CpuUsageMhz).Name
        $ValidValues -like "$ParamComplete*"
    } )]
    [String]$VMHost
)

DynamicParam {
    ## Dynamic DataStore.
    $DynamicDataStore = 'DataStore'

    $Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
    $Attributes.Position = 1
    $Attributes.Mandatory = $True
    $AttributeCollection = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Attribute]'
    $AttributeCollection.Add($Attributes)

    $HostDataStores = (Get-VMHost -Name $VMHost | Get-Datastore | Sort-Object FreeSpaceGB -Descending).Name

    $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($HostDataStores)
    $AttributeCollection.Add($ValidateSetAttribute)

    $DynamicParamater = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($DynamicDataStore, [String], $AttributeCollection)

    $DynamicParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
    $DynamicParameterDictionary.Add($DynamicDataStore, $DynamicParamater)

    Return $DynamicParameterDictionary
}

Begin {
    # Make the dynamic parameter available for the auto-complete.
    $DataStore = $PSBoundParameters[$DynamicDataStore]
}

The problem with this implementation is that as you can see, I'm trying to list datastores with the most amount of space first, but the validateset array doesn't care about my ordering and orders the values alphabetically (argument tab completion works here).

So I thought of using argumentcompleter, which does work in a way I want in a regular parameter (see VMHost parameter in the first example), but looks like I can't get it to work in a dynamic parameter and I'm hoping someone else has solved this or can guide me in the right direction.

What I've tried so far:

DynamicParam {
    ## Dynamic DataStore.
    $DynamicDataStore = 'DataStore'

    $Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
    $Attributes.Position = 1
    $Attributes.Mandatory = $True
    $AttributeCollection = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Attribute]'
    $AttributeCollection.Add($Attributes)

    $ValidateScriptAttribute = New-Object -TypeName System.Management.Automation.ValidateScriptAttribute({
        If ($PSItem -in (Get-VMHost -Name $VMHost | Get-Datastore).Name) {
            $True
        }
        Else {
            Throw "Accepted values: $(((Get-VMHost -Name $VMHost | Get-Datastore).Name) -join ', ')"
        }
    })

    $DynamicArgumentCompleter = New-Object -TypeName System.Management.Automation.ArgumentCompleterAttribute({
        Param ($Cmd, $Param, $ParamComplete)
        [Array]$ValidValues = (Get-VMHost -Name $VMHost | Get-Datastore | Sort-Object FreeSpaceGB -Descending).Name
        $ValidValues -like "$ParamComplete*"
    })

    $AttributeCollection.Add($ValidateScriptAttribute)
    $AttributeCollection.Add($DyamicArgumentCompleter)

    $DynamicParamater = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($DynamicDataStore, [String], $AttributeCollection)

    $DynamicParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
    $DynamicParameterDictionary.Add($DynamicDataStore, $DynamicParamater)

    Return $DynamicParameterDictionary
}

This seemed like a logical way to do it.

The ValidateScriptBlock part works. I enter an incorrect value and get an error saying: "Accepted values: list of comma-delimited values", but the argumentcompleter doesn't do anything.

What I'm expecting it to do is show me accepted values when I press tab after -DataStore parameter, but there's no tab completion at all, not alphabetically ordered, or anything.

like image 216
Ramil Avatar asked Sep 05 '25 21:09

Ramil


1 Answers

Easiest and robust way to do it is with Register-ArgumentCompleter, I don't have access to your completion set but using Get-Process as the completion source and a dynamic param here is how the implementation looks like:

function Test-DynamicParam {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ParameterSetName = 'foo')]
        $foo
    )

    dynamicparam {
        if ($PSCmdlet.ParameterSetName -ne 'foo') {
            return
        }

        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        $paramDictionary['bar'] = [System.Management.Automation.RuntimeDefinedParameter]::new(
            'bar', [string], @([Parameter]@{ Position  = 1; Mandatory = $true }))
        $paramDictionary
    }
}

$params = @{
    CommandName   = 'Test-DynamicParam'
    ParameterName = 'bar'
    ScriptBlock   = {
        param(
            $commandName,
            $parameterName,
            $wordToComplete,
            $commandAst,
            $fakeBoundParameters
        )

        (Get-Process).ProcessName | Sort-Object -Unique |
            Where-Object { $_ -like "*$wordToComplete*" }
    }
}
Register-ArgumentCompleter @params

Demo:

demo


Based on comment, you're looking to know what the argument of -foo is at runtime in your completion logic and that's where you can use the $fakeBoundParameters dictionary, for example, if -foo equals to process you can give completions for processes, if equals to service completions for services else, give completions to the files in your current directory:

$params = @{
    CommandName   = 'Test-DynamicParam'
    ParameterName = 'bar'
    ScriptBlock   = {
        param(
            $commandName,
            $parameterName,
            $wordToComplete,
            $commandAst,
            $fakeBoundParameters
        )

        switch ($fakeBoundParameters['foo']) {
            process {
                (Get-Process).ProcessName | Sort-Object -Unique |
                    Where-Object { $_ -like "*$wordToComplete*" }
            }
            service {
                (Get-Service -EA 0).ServiceName | Sort-Object -Unique |
                    Where-Object { $_ -like "*$wordToComplete*" }
            }
            default {
                Get-ChildItem -File |
                    Where-Object { $_ -like "*$wordToComplete*" }
            }
        }
    }
}
Register-ArgumentCompleter @params
like image 130
Santiago Squarzon Avatar answered Sep 09 '25 04:09

Santiago Squarzon