Dynamic parameter value depending on another dynamic parameter value Dynamic parameter value depending on another dynamic parameter value powershell powershell

Dynamic parameter value depending on another dynamic parameter value


I would recommend using argument completers, which are semi-exposed in PowerShell 3 and 4, and fully exposed in version 5.0 and higher. For v3 and v4, the underlying functionality is there, but you have to override the TabExpansion2 built-in function to use them. That's OK for your own session, but it's generally frowned upon to distribute tools that do that to other people's sessions (imagine if everyone tried to override that function). A PowerShell team member has a module that does this for you called TabExpansionPlusPlus. I know I said overriding TabExpansion2 was bad, but it's OK if this module does it :)

When I needed to support versions 3 and 4, I would distribute my commands in modules, and have the modules check for the existence of the 'Register-ArgumentCompleter' command, which is a cmdlet in v5+ and is a function if you have the TE++ module. If the module found it, it would register any completer(s), and if it didn't, it would notify the user that argument completion wouldn't work unless they got the TabExpansionPlusPlus module.

Assuming you have the TE++ module or PSv5+, I think this should get you on the right track:

function launcher {    [CmdletBinding()]    param(        [string] $Environment1,        [string] $Environment2,        [string] $Environment3    )    $PSBoundParameters}1..3 | ForEach-Object {    Register-ArgumentCompleter -CommandName launcher -ParameterName "Environment${_}" -ScriptBlock {        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)        $PathParts = $fakeBoundParameter.Keys | where { $_ -like 'Environment*' } | sort | ForEach-Object {            $fakeBoundParameter[$_]        }        Get-ChildItem -Path ".\configurations\$($PathParts -join '\')" -Directory -ErrorAction SilentlyContinue | select -ExpandProperty Name | where { $_ -like "${wordToComplete}*" } | ForEach-Object {            New-Object System.Management.Automation.CompletionResult (                $_,                $_,                'ParameterValue',                $_            )        }    }}

For this to work, your current working directory will need a 'configurations' directory contained in it, and you'll need at least three levels of subdirectories (reading through your example, it looked like you were going to enumerate a directory, and you would go deeper into that structure as parameters were added). The enumerating of the directory isn't very smart right now, and you can fool it pretty easy if you just skip a parameter, e.g., launcher -Environment3 <TAB> would try to give you completions for the first sub directory.

This works if you will always have three parameters available. If you need a variable # of parameters, you could still use completers, but it might get a little trickier.

The biggest downside would be that you'd still have to validate the users' input since completers are basically just suggestions, and users don't have to use those suggestions.

If you want to use dynamic parameters, it gets pretty crazy. There may be a better way, but I've never been able to see the value of dynamic parameters at the commandline without using reflection, and at that point you're using functionality that could change at the next release (the members usually aren't public for a reason). It's tempting to try to use $MyInvocation inside the DynamicParam {} block, but it's not populated at the time the user is typing the command into the commandline, and it only shows one line of the command anyway without using reflection.

The below was tested on PowerShell 5.1, so I can't guarantee that any other version has these exact same class members (it's based off of something I first saw Garrett Serack do). Like the previous example, it depends on a .\configurations folder in the current working directory (if there isn't one, you won't see any -Environment parameters).

function badlauncher {    [CmdletBinding()]    param()    DynamicParam {        #region Get the arguments         # In it's current form, this will ignore parameter names, e.g., '-ParameterName ParameterValue' would ignore '-ParameterName',        # and only 'ParameterValue' would be in $UnboundArgs        $BindingFlags = [System.Reflection.BindingFlags] 'Instance, NonPublic, Public'        $Context = $PSCmdlet.GetType().GetProperty('Context', $BindingFlags).GetValue($PSCmdlet)        $CurrentCommandProcessor = $Context.GetType().GetProperty('CurrentCommandProcessor', $BindingFlags).GetValue($Context)        $ParameterBinder = $CurrentCommandProcessor.GetType().GetProperty('CmdletParameterBinderController', $BindingFlags).GetValue($CurrentCommandProcessor)        $UnboundArgs = @($ParameterBinder.GetType().GetProperty('UnboundArguments', $BindingFlags).GetValue($ParameterBinder) | where { $_ } | ForEach-Object {            try {                if (-not $_.GetType().GetProperty('ParameterNameSpecified', $BindingFlags).GetValue($_)) {                    $_.GetType().GetProperty('ArgumentValue', $BindingFlags).GetValue($_)                }            }            catch {                # Don't do anything??            }        })        #endregion        $ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary        # Create an Environment parameter for each argument specified, plus one extra as long as there        # are valid subfolders under .\configurations        for ($i = 0; $i -le $UnboundArgs.Count; $i++) {            $ParameterName = "Environment$($i + 1)"            $ParamAttributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]            $ParamAttributes.Add((New-Object Parameter))            $ParamAttributes[0].Position = $i            # Build the path that will be enumerated based on previous arguments            $PathSb = New-Object System.Text.StringBuilder            $PathSb.Append('.\configurations\') | Out-Null            for ($j = 0; $j -lt $i; $j++) {                $PathSb.AppendFormat('{0}\', $UnboundArgs[$j]) | Out-Null            }            $ValidParameterValues = Get-ChildItem -Path $PathSb.ToString() -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name            if ($ValidParameterValues) {                $ParamAttributes.Add((New-Object ValidateSet $ValidParameterValues))                $ParamDictionary[$ParameterName] = New-Object System.Management.Automation.RuntimeDefinedParameter (                    $ParameterName,                    [string[]],                    $ParamAttributes                )            }        }        return $ParamDictionary    }    process {        $PSBoundParameters    }}

The cool thing about this one is that it can keep going as long as there are folders, and it automatically does parameter validation. Of course, you're breaking the laws of .NET by using reflection to get at all those private members, so I would consider this a terrible and fragile solution, no matter how fun it was to come up with.