I'm looking for a way to make a cmdlet which receives parameter and while typing, it prompts suggestions for completion from a predefined array of options.
I was trying something like this:
$vf = #('Veg', 'Fruit')
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet($vf)]
$Arg
)
}
The expected result should be:
When writing 'Test-ArgumentCompleter F', after clicking the tub button, the F autocompleted to Fruit.
To complement the answers from #mklement0 and #Mathias, using dynamic parameters:
$vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param ()
DynamicParam {
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute)
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($vf)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Arg', [string], $AttributeCollection)
$RuntimeParameterDictionary.Add('Arg', $RuntimeParameter)
return $RuntimeParameterDictionary
}
}
Depending on how you want to predefine you argument values, you might also use dynamic validateSet values:
Class vfValues : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() { return 'Veg', 'Fruit' }
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet([vfValues])]$Arg
)
}
note: The IValidateSetValuesGenerator class [read: interface] was introduced in PowerShell 6.0
In addition to mklement0's excellent answer, I feel obligated to point out that in version 5 and up you have a slightly simpler alternative available: enum's
An enum, or an "enumeration type", is a static list of labels (strings) associated with an underlying integral value (a number) - and by constraining a parameter to an enum type, PowerShell will automatically validate the input value against it AND provide argument completion:
enum MyParameterType
{
Veg
Fruit
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[MyParameterType]$Arg
)
}
Trying to tab complete the argument for -Arg will now cycle throw matching valid enum labels of MyParameterType:
PS ~> Test-ArgumentCompleter -Arg v[<TAB>]
# gives you
PS ~> Test-ArgumentCompleter -Arg Veg
PowerShell generally requires that attribute properties be literals (e.g., 'Veg') or constants (e.g., $true).
Dynamic functionality requires use of a script block (itself specified as a literal, { ... }) or, in specific cases, a type literal.
However, the [ValidateSet()] attribute only accepts an array of string(ified-on-demand) literals or - in PowerShell (Core) v6 and above - a type literal (see below).
Update:
If you're using PowerShell (Core) v6+, there's a simpler solution based on defining a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface - see the 2nd solution in iRon's helpful answer.
Even in Windows PowerShell a simpler solution is possible if your validation values can be defined as an enum type - see Mathias R. Jessen's helpful answer.
To get the desired functionality based on a non-literal array of values, you need to combine two other attributes:
[ArgumentCompleter()] for dynamic tab-completion.
[ValidateScript()] for ensuring on command submission that the argument is indeed a value from the array, using a script block.
# The array to use for tab-completion and validation.
[string[]] $vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
# Tab-complete based on array $vf
[ArgumentCompleter({
param($cmd, $param, $wordToComplete) $vf -like "$wordToComplete*"
})]
# Validate based on array $vf.
# NOTE: If validation fails, the (default) error message is unhelpful.
# You can work around that in *Windows PowerShell* with `throw`, and in
# PowerShell (Core) 7+, you can add an `ErrorMessage` property:
# [ValidateScript({ $_ -in $vf }, ErrorMessage = 'Unknown value: {0}')]
[ValidateScript({
if ($_ -in $vf) { return $true }
throw "'$_' is not in the set of the supported values: $($vf -join ', ')"
})]
$Arg
)
"Arg passed: $Arg"
}
To add to the other helpful answers, I use something similiar for a script I made for work:
$vf = #('Veg', 'Fruit','Apple','orange')
$ScriptBlock = {
Foreach($v in $vf){
New-Object -Type System.Management.Automation.CompletionResult -ArgumentList $v,
$v,
"ParameterValue",
"This is the description for $v"
}
}
Register-ArgumentCompleter -CommandName Test-ArgumentCompleter -ParameterName Arg -ScriptBlock $ScriptBlock
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String]$Arg )
}
Documentation for Register-ArgumentCompleter is well explained on Microsoft Docs. I personally don't like to use the enum statement as it didnt allow me to uses spaces in my Intellisense; same for the Validate parameter along with nice features to add a description.
Output:
EDIT:
#Mklement made a good point in validating the argument supplied to the parameter. This alone doesnt allow you to do so without using a little more powershell logic to do the validating for you (unfortunately, it would be done in the body of the function).
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$Arg )
if($PSBoundParameters.ContainsKey('Arg')){
if($VF -contains $PSBoundParameters.Values){ "It work:)" }
else { "It no work:("}
}
}
I think it's worth sharing another alternative that complements the helpful answers from mklement0, Mathias, iRon and Abraham. This answer attempts to show the possibilities that PowerShell can offer when it comes to customization of a Class.
The Class used for this example offers:
Compatibility with Windows PowerShell 5.1 and PowerShell Core.
Validation, Completion and custom Error Message.
For the example below I'll be using completion and validation on values from a current directory, the values are fed dynamically at runtime with Get-ChildItem -Name.
Class
When referring to the custom validation set I've decided to use the variable $this, however that can be easily change for a variable name of one's choice:
[psvariable]::new('this', (& $this.CompletionSet))
The completion set could be also a hardcoded set, i.e.:
[string[]] $CompletionSet = 'foo', 'bar', 'baz'
However that would also require some modifications in the class logic itself.
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class CustomValidationCompletion : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
[scriptblock] $CompletionSet = { Get-ChildItem -Name }
[scriptblock] $Validation
[scriptblock] $ErrorMessage
CustomValidationCompletion() { }
CustomValidationCompletion([scriptblock] $Validation, [scriptblock] $ErrorMessage) {
$this.Validation = $Validation
$this.ErrorMessage = $ErrorMessage
}
[void] ValidateElement([object] $Element) {
$context = #(
[psvariable]::new('_', $Element)
[psvariable]::new('this', (& $this.CompletionSet))
)
if(-not $this.Validation.InvokeWithContext($null, $context)) {
throw [MetadataException]::new(
[string] $this.ErrorMessage.InvokeWithContext($null, $context)
)
}
}
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[List[CompletionResult]] $result = foreach($item in & $this.CompletionSet) {
if(-not $item.StartsWith($wordToComplete)) {
continue
}
[CompletionResult]::new("'$item'", $item, [CompletionResultType]::ParameterValue, $item)
}
return $result
}
}
Implementation
function Test-CompletionValidation {
[alias('tcv')]
[CmdletBinding()]
param(
[CustomValidationCompletion(
Validation = { $_ -in $this },
ErrorMessage = { "Not in set! Must be one of these: $($this -join ', ')" }
)]
[ArgumentCompleter([CustomValidationCompletion])]
[string] $Argument
)
$Argument
}
Demo
Related
I'm trying to do something like this.
The Get-SomeOtherParameter returns a system.Array type list from a database.
I don't want to hardcode my ValidateSet in case the list changes overTime in the database
function Get-SomeItems {
param (
[Parameter(Mandatory = $true)]
[ValidateSet(Get-SomeOtherParameter)]
[string]$filter,
[Parameter(Mandatory = $true)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
}
To complement Start-Automating's helpful answer by spelling out the [ValidateScript({ ... }] and [ArgumentCompleter({ ... }) approaches:
# Function that returns the valid values for the -filter parameter below.
function Get-ValidFilterValues {
# Sample, hard-coded values. This is where your database lookup would happen.
'foo', 'bar'
}
function Get-SomeItems {
param (
[Parameter(Mandatory)]
[ValidateScript({
$validValues = Get-ValidFilterValues
if ($_ -in $validValues) { return $true } # OK
throw "'$_' is not a valid value. Use one of the following: '$($validValues -join ', ')'"
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-ValidFilterValues) -like "$wordToComplete*"
})]
[string]$filter,
[Parameter(Mandatory)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
$filter, $filter2 # sample output.
}
A simpler PowerShell (Core) 7+ alternative is to implement validation via a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface, which automatically also provides tab-completion:
# Custom class that implements the IValidateSetValuesGenerator interface
# in order to return the valid values for the -filter parameter below.
class ValidFilterValues : System.Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() {
# Sample, hard-coded values. This is where your database lookup would happen.
return 'foo', 'bar'
}
}
function Get-SomeItems {
param (
[Parameter(Mandatory)]
[ValidateSet([ValidFilterValues])] # Pass the custom class defined above.
[string]$filter,
[Parameter(Mandatory)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
$filter, $filter2 # sample output.
}
There's two aspects to what you're trying to do:
Making sure the parameter validation is correct
Making the PowerShell experience around it "good" (aka supporting tab completion).
Parameter Validation :
As you might have already noticed [ValidateSet] is a hard-coded list. It's not really possible to soft code this (it is possible to dynamically build your script every time using some other modules, lemme know if you want more of an explainer for this).
To make the Validation work without [ValidateSet], I'd suggest [ValidateScript({})]. [ValidateScript] will run whatever script is in ValidateScript to ensure the script is valid. If the [ValidateScript()] throws, the user will see that message when they pass an invalid value in.
Tab-Completion :
To make it feel easy, you'll also want to add support for tab completion.
This is fairly straightforward using the [ArgumentCompleter] attribute.
Here's an example copied / pasted from a module called LightScript
[ArgumentCompleter({
param ( $commandName,
$parameterName,
$wordToComplete,
$commandAst,
$fakeBoundParameters )
$effectNames = #(Get-NanoLeaf -ListEffectName |
Select-Object -Unique)
if ($wordToComplete) {
$toComplete = $wordToComplete -replace "^'" -replace "'$"
return #($effectNames -like "$toComplete*" -replace '^', "'" -replace '$',"'")
} else {
return #($effectNames -replace '^', "'" -replace '$',"'")
}
})]
This ArgumentCompleter does a few things:
Calls some other command to get a list of effects
If $wordToComplete was passed, finds all potential completions (while stripping off whitespace and enclosing in quotes)
If $WordToComplete was not passed, puts each potential completion in quotes
Basically, all you should need to change are the command names / variables to make this work.
Hope this Helps
I worte this class with two custom constructors:
class ExtensionApp {
[String] $name
[String] $version
[String] $path
[switch] $isContainerPath
[switch] $useNugetDownloader
[switch] $force
[switch] $skipVerification
ExtensionApp() {}
ExtensionApp(
[String] $name,
[String] $version,
[switch] $useNugetDownloader,
[switch] $force,
[switch] $skipVerification
) {
$this.name = $name
$this.version = $version
$this.useNugetDownloader = $useNugetDownloader
$this.force = $force
$this.skipVerification = $skipVerification
if (($this.version -eq '') -or ($null -eq $this.version)) {
#do something
}
}
ExtensionApp(
[String] $name,
[switch] $isContainerPath,
[String] $path,
[switch] $force,
[switch] $skipVerification
) {
$this.name = $name
$this.path = $path
$this.isContainerPath = $isContainerPath
$this.force = $force
$this.skipVerification = $skipVerification
}
}
I'm using those Objects to fill a list i want to process later which looks something like this atm:
$CustApps = New-Object Collections.Generic.List[ExtensionApp]
$CustApps.Add([ExtensionApp]::new('Extension A', '10.0.2554.0', $true , $false, $false)) #first constructor
$CustApps.Add([ExtensionApp]::new('Extension b', $false, '\\server\folder\file.app' , $false, $false)) #second constructor
Thing is: I don't like the way i have to use the constuctors because you dont' get any Intellisense support, and if you have to mix up the constructors while filling the same List, it gets messy.
I could just define a function like this:
function CreateExtensionApp {
param(
[String] $name,
[String] $version,
[String] $path,
[switch] $isContainerPath,
[switch] $useNugetDownloader,
[switch] $force,
[switch] $skipVerification
)
$new = [ExtensionApp]::new()
$new.name = $name
$new.version = $version
$new.path = $path
$new.isContainerPath = $isContainerPath
$new.useNugetDownloader = $useNugetDownloader
$new.force = $force
$new.skipVerification = $skipVerification
if (($true -eq $new.useNugetDownloader) -and (($new.version -eq '') -or ($null -eq $new.version))) {
Get-LogiVersion -PackageId $this.name
}
return $new
}
and use that one like this:
$CustApps = New-Object Collections.Generic.List[ExtensionApp]
$CustApps.Add((New-ExtensionAppObj -name 'Extension A' -version '10.0.2554.0' -useNugetDownloader))
$CustApps.Add((New-ExtensionAppObj -name 'Extension B' -path '\\server\folder\file.app' -force -skipVerification))
but i'm kind of a perfectionist and in my opinion, this function should be a static method belonging to the class itself. - I also found a way to do that but it would deprive me of the parameter-names and intellisense support again.
is there a solution that would allow me to use the parameter-names AND have all the code in the class definition?
I can't offer a solution, but I'll try to shed some light on the challenges involved:
IntelliSense:
As of v2022.6.3 of the PowerShell extension for Visual Studio Code, IntelliSense support for calling methods is limited to read-only display of all overloads, and is only shown when the method name has been typed, before typing (
In other words, there is currently no support for:
(a) showing an overload signature (parameter names) as arguments are being typed.
(b) allowing selecting one of the overloads so as to auto-complete a call to that overload with argument placeholders named for the parameters.
GitHub issue #1356 discusses potentially improving method-call IntelliSense in the future:
(a) is problematic, given PowerShell's lack of static typing, though it's conceivable to let the user manually cycle through available overloads in the same way that the C# extension does, albeit without type awareness.
(b) may be feasible, if the IntelliSense feature allows calling a Visual Studio Code snippet in response to a user selection.
Self-documenting code:
Since all (declared) parameters in PowerShell cmdlets / functions / scripts have names, you always have the option to spell out the target parameter for each argument (though there's often also support for positional arguments for a subset of parameters).
Unlike C#, PowerShell, as of PowerShell 7.2.x, does not support named arguments in method calls:
That is, the following C# sample call currently has no PowerShell equivalent:
C#: string.Compare(strA: "foo", strB: "FOO", ignoreCase: true)
PowerShell: [string]::Compare(strA: "foo", strB: "FOO", ignoreCase: $true)
GitHub issue #13307 proposes adding support for named arguments.
I am using Pester testing library (with version 5.0.2) to test my PowerShell scripts (with version 5.1) and mock its dependencies.
Pester has a Mock method which can be used to mock dependencies. More info here.
I am trying to create a helper method wrapping this Mock method, to make my code more readable:
Function MockVstsInput {
Param(
[Parameter(Mandatory=$true, Position=1)]
[string]$inputName,
[Parameter(Mandatory=$true, Position=2)]
[string]$returnValue
)
Mock Get-VstsInput {return $returnValue} -ParameterFilter { $Name -eq $inputName}
}
In this helper method I am mocking the dependency Get-VstsInput which has a parameter $Name
Then I use this code in my test script:
MockVstsInput "targetapi" "api-name"
Meaning that if the Get-VstsInput is called with $Name param "targetapi" then it should return "api-name". Usually such parameterizing works in other languages (f.e.: C# or Java), but here the $inputName string is not resolved in the MockVstsInput method.
When I call the Get-VstsInput method in my production code:
$newapi=Get-VstsInput -Name targetapi
Then in the log I have the following Mock information:
Mock: Running mock filter { $Name -eq $inputName } with context: Name = targetapi.
Mock: Mock filter did not pass.
Where we can see that the $inputName string is not resolved in my scriptblock, so the mocking does not happen.
What I have tried so far, with no success:
swapping the members of the equal comparison in the predicate scriptblock { $inputName -eq $Name}
using $ExecutionContext.InvokeCommand.ExpandString($inputName) in the script block to resolve $inputName string
creating a script block with [ScriptBlock]::create($Name -eq $inputName) then using this in the -ParameterFilter
last but not least I tried to call GetNewClosure of my script block, but it did not help either: { $Name -eq $inputName}.GetNewClosure()
What do you think what the root cause of my problem is? Thanks in advance for all the help!
You are close. It's all in how you build the needed scriptblocks.
For the ParameterFilter scriptblock you need to escape $Name with a backtick so that it gets created as a variable. $inputName will be replaced with our variable value so you need to surround in quotes.
# $inputName = 'Daniel'
$sb_param = [scriptblock]::Create("`$Name -eq '$inputName'")
This way the final statement inside the scriptblock looks like
{ $Name -eq 'Daniel' }
Similarly, in our MockWith block, we need to also surround with quotes or the scriptblock will not include a string, but an invalid command
# $returnValue = 'Hi Daniel'
$sb_return = [scriptblock]::Create("'$returnValue'")
becomes
{ 'Hi Daniel' }
if we don't include the quotes when we create the scriptblock, the scriptblock will contain the statement Hi Daniel which will fail as command not found
Here is everything in a working example
Describe 'Setting up a Mock from a Function' {
BeforeAll {
Function Get-VstsInput {
[CmdletBinding()]
Param(
$Name
)
'Not Mocked'
}
Function MockVstsInput {
Param(
[Parameter(Mandatory = $true, Position = 1)]
[string]$inputName,
[Parameter(Mandatory = $true, Position = 2)]
[string]$returnValue
)
$sb_param = [scriptblock]::Create("`$Name -eq '$inputName'")
$sb_return = [scriptblock]::Create("'$returnValue'")
Mock Get-VstsInput -MockWith $sb_return -ParameterFilter $sb_param
}
MockVstsInput -inputname 'Daniel' -returnValue 'Hi Daniel'
}
It 'Should mock' {
$test = Get-VstsInput -Name 'Daniel'
$test | Should -Be 'Hi Daniel'
}
}
To clarify, this is a scoping issue. What is happening is that you are providing these scriptblocks to the mock command to run at a later time in a different scope. When you run the mocked command the variables $inputName and $returnValue are not defined in those scriptblocks when they are invoked. These variables were only available in the MockVstsInput function and were cleaned up once the function completed.
To illustrate this, the following code will work, but I do not recommend doing this way because if you run the function more than once to define different Mocks you will be overriding the global variables each time affecting any previously defined Mocks
Function MockVstsInput {
Param(
[Parameter(Mandatory = $true, Position = 1)]
[string]$inputName,
[Parameter(Mandatory = $true, Position = 2)]
[string]$returnValue
)
$global:mockInputName = $inputName
$global:mockReturnValue = $returnValue
Mock Get-VstsInput -ParameterFilter {$Name -eq $global:mockInputName} -MockWith {$global:mockReturnValue}
}
So instead of giving the 2 parameters scriptblocks with variables to later be resolved which will no longer be in scope the solution is to create the scriptblocks with the values of our variables hardcoded.
Also, see this answer for a way to do it using BeforeEach { }
How to pass value along with parameter? Something like ./test.ps1 -controllers 01. I want the script to use hyphen and also a value is passed along for the parameter.
Here is the part of the script I wrote. But if I call the script with hyphen (.\test.ps1 -Controllers) it says A parameter cannot be found that matches parameter name 'Controllers'.
param(
# [Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
Also I need to pass a value to it which is then used for a property.
if ($options -eq "controllers")
{
$callsomething.$arg1 | where {$_ -eq "$arg2" }
}
Lets talk about why it does not work
function Test()
param(
[Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
}
Parameters are Variables that are created and filled out at the start of the script
ValidateSet will only allow the script to run if $Options equals one of the three choices 'Controllers','test2','test3'
Lets talk about what exactly all the [] are doing
Mandatory=$false means that $options doesnt have to be anything in order for the script to run.
Position=0 means that if you entered the script without using the -options then the very first thing you put would still be options
Example
#If Position=0 then this would work
Test "Controllers"
#Also this would work
Test -options Controllers
[ValidateSet('Controllers','test2','test3')] means that if Option is used or is Mandatory then it has to equal 'Controllers','test2','test3'
It sounds like you are trying to create parameters at runtime. Well that is possible using DynamicParam.
function Test{
[CmdletBinding()]
param()
DynamicParam {
$Parameters = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
'Controllers','test2','test3' | Foreach-object{
$Param = New-Object System.Management.Automation.ParameterAttribute
$Param.Mandatory = $false
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$AttribColl.Add($Param)
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("$_", [string], $AttribColl)
$Parameters.Add("$_", $RuntimeParam)
}
return $Parameters
}
begin{
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
}
process {
"$Controllers $Test2 $Test3"
}
}
DynamicParam allows you to create parameters in code.
The example above turns the array 'Controllers','test2','test3' into 3 separate parameters.
Test -Controllers "Hello" -test2 "Hey" -test3 "Awesome"
returns
Hello Hey Awesome
But you said you wanted to keep the hypen and the parameter
So the line
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
allows you to define each parameter value. a slight change like :
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value "-$($_.Key) $($_.Value)"
}
Would return
-Controllers Hello -test2 Hey -test3 Awesome
Let's say I want to write a helper function that wraps Read-Host. This function will enhance Read-Host by changing the prompt color, calling Read-Host, then changing the color back (simple example for illustrative purposes - not actually trying to solve for this).
Since this is a wrapper around Read-Host, I don't want to repeat the all of the parameters of Read-Host (i.e. Prompt and AsSecureString) in the function header. Is there a way for a function to take an unspecified set of parameters and then pass those parameters directly into a cmdlet call within the function? I'm not sure if Powershell has such a facility.
for example...
function MyFunc( [string] $MyFuncParam1, [int] $MyFuncParam2 , Some Thing Here For Cmdlet Params that I want to pass to Cmdlet )
{
# ...Do some work...
Read-Host Passthru Parameters Here
# ...Do some work...
}
It sounds like you're interested in the 'ValueFromRemainingArguments' parameter attribute. To use it, you'll need to create an advanced function. See the about_Functions_Advanced and about_Functions_Advanced_Parameters help topics for more info.
When you use that attribute, any extra unbound parameters will be assigned to that parameter. I don't think they're usable as-is, though, so I made a little function that will parse them (see below). After parsing them, two variables are returned: one for any unnamed, positional parameters, and one for named parameters. Those two variables can then be splatted to the command you want to run. Here's the helper function that can parse the parameters:
function ParseExtraParameters {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$ParamHashTable = #{}
$UnnamedParams = #()
$CurrentParamName = $null
$ExtraParameters | ForEach-Object -Process {
if ($_ -match "^-") {
# Parameter names start with '-'
if ($CurrentParamName) {
# Have a param name w/o a value; assume it's a switch
# If a value had been found, $CurrentParamName would have
# been nulled out again
$ParamHashTable.$CurrentParamName = $true
}
$CurrentParamName = $_ -replace "^-|:$"
}
else {
# Parameter value
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName += $_
$CurrentParamName = $null
}
else {
$UnnamedParams += $_
}
}
} -End {
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName = $true
}
}
,$UnnamedParams
$ParamHashTable
}
You could use it like this:
PS C:\> ParseExtraParameters -NamedParam1 1,2,3 -switchparam -switchparam2:$false UnnamedParam1
UnnamedParam1
Name Value
---- -----
switchparam True
switchparam2 False
NamedParam1 {1, 2, 3}
Here are two functions that can use the helper function (one is your example):
function MyFunc {
[CmdletBinding()]
param(
[string] $MyFuncParam1,
[int] $MyFuncParam2,
[Parameter(Position=0, ValueFromRemainingArguments=$true)]
$ExtraParameters
)
# ...Do some work...
$UnnamedParams, $NamedParams = ParseExtraParameters #ExtraParameters
Read-Host #UnnamedParams #NamedParams
# ...Do some work...
}
function Invoke-Something {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string] $CommandName,
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$UnnamedParameters, $NamedParameters = ParseExtraParameters #ExtraParameters
&$CommandName #UnnamedParameters #NamedParameters
}
After importing all three functions, try these commands:
MyFunc -MyFuncParam1 Param1Here "PromptText" -assecure
Invoke-Something -CommandName Write-Host -Fore Green "Some text" -Back Red
One word: splatting.
Few more words: you can use combination of $PSBoundParameters and splatting to pass parameters from external command, to internal command (assuming names match). You would need to remove any parameter that you don't want to use though from $PSBoundParameters first:
$PSBoundParameters.Remove('MyFuncParam1')
$PSBoundParameters.Remove('MyFuncParam2')
Read-Host #PSBoundParameters
EDIT
Sample function body:
function Read-Data {
param (
[string]$First,
[string]$Second,
[string]$Prompt,
[switch]$AsSecureString
)
$PSBoundParameters.Remove('First') | Out-Null
$PSBoundParameters.Remove('Second') | Out-Null
$Result = Read-Host #PSBoundParameters
"First: $First Second: $Second Result: $Result"
}
Read-Data -First Test -Prompt This-is-my-prompt-for-read-host