Dynamic invoke command with different parameters - powershell

In a PowerShell script, I want to read a CSV file that contains something like this:
Type Title Param1 Param2
---- ----- ------ ------
Type1 Foo type 1 ValueForType1
Type2 Foo type 2 ValueForType2
When type is Type1, I have to call a function named New-FooType1, when type is Type2, the funcation is named New-FooType2, and so on:
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
I'm trying to route the call to either of the functions, using a dynamic invocation:
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
However, I always get an error:
Parameter set cannot be resolved using the specified named parameters
As you can see, different type mean different parameters set.
How can I solve this? I would like to avoid manipulating properties manually, because in my real life script, I have a dozen of different types, with up to 6 parameters.
Here is a complete repro sample of the issue:
$csvData = "Type;Title;Param1;Param2`nType1;Foo type 1;ValueForType1;;`nType2;Foo type 2;;ValueForType2"
$csv = ConvertFrom-csv $csvData -Delimiter ';'
$csv | ft -AutoSize
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
The expected output of this script is:
New-FooType1 Foo type 1 with ValueForType1
New-FooType2 Foo type 2 with ValueForType2

Use the call operator &:
$CmdName = "New-FooType1"
$Arguments = "type1"
& $CmdName $Arguments
the call operator also supports splatting if you want the arguments bound to specific named parameters:
$Arguments = #{
"title" = "type1"
}
& $CmdName #Arguments

To invoke command by name you should use invoke operator &. Invoke-Command cmdlet support only ScriptBlock and file invocation, and file invocation only supported for remote calls.
For dynamic parameter binding you can use spatting, but in that case you have to convert PSCustomObjects, returned by ConvertFrom-Csv cmdlet, to Hashtable. You also have to strip any extra parameters from Hashtable because splatting will fail if you try to bind non-existing parameter.
Another approach for dynamic parameter binding would be to use binding from pipeline object. It looks like it is what you want to do, since you mark all your parameters with ValueFromPipelineByPropertyName option. And this approach will just ignore any extra property it can not bind to parameter. I recommend you to remove ValueFromPipeline option, because with this option in case of absence of property with parameter name PowerShell will just convert PSCustomObject to string (or to whatever type you use for parameter) and pass it as value for parameter.
So, all you need is to pass object by pipeline and use invoke operator for invocation of command with dynamic name:
$_ | & "New-Foo$($_.Type)"

dont know exactly what your trying to do, but
Invoke-Command (gcm $cmdName) ?
Try invoke-expression $cmdname

Related

How to call a script with unknown parameters

I have a script that calls other scripts that other people manage. It's essentially a CI/CD script that gives users the ability to tap into the pipeline.
The issue I'm running into now is that I would like this calling script to implement a couple new parameters. However, the old scripts don't always implement those parameters.
If I call their script that doesn't implement the parameters, I get an error "A parameter cannot be found that matches parameter name 'newparameter'".
Is there a way to dynamically pass in a parameter so that it doesn't fail if the parameter doesn't exist? I don't mind if they don't implement it. It's a bonus parameter that they don't need to use.
Alternately, can I do something like a Get-Command for a custom .ps1 script, to get a list of accepted parameters? With that, I could confirm that a parameter is implemented before I pass it.
This might help you get started, you could use the Parser Class
to get all functions and it's parameters from a script, this answer shows a minimal reproduction. I'll leave it to you to investigate further.
Given myScript.ps1 that has these 3 functions:
function ExampleFunc {
param([int] $param1 = 123, [string] $param2)
}
function ExampleFunc2 {
param([object] $param3, [switch] $param4)
}
function ExampleFunc3 ($param5, [hashtable] $param6 = #{foo = 'var'}) {
}
You can use the ParseFile Method to get the AST, then you can use the .FindAll method to filter for all FunctionDefinitionAst and subsequently find all parameters filtering for all ParameterAst.
using namespace System.Management.Automation.Language
$ast = [Parser]::ParseFile('path\to\myScript.ps1', [ref] $null, [ref] $null)
$ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true) | ForEach-Object {
$out = [ordered]#{ Function = $_.Name }
$_.FindAll({ $args[0] -is [ParameterAst] }, $true) | ForEach-Object {
$out['ParameterName'] = $_.Name.VariablePath
$out['Type'] = $_.StaticType
$out['DefaultValue'] = $_.DefaultValue
[pscustomobject] $out
}
} | Format-Table
Above code would result in the following for myScript.ps1:
Function ParameterName Type DefaultValue
-------- ------------- ---- ------------
ExampleFunc param1 System.Int32 123
ExampleFunc param2 System.String
ExampleFunc2 param3 System.Object
ExampleFunc2 param4 System.Management.Automation.SwitchParameter
ExampleFunc3 param5 System.Object
ExampleFunc3 param6 System.Collections.Hashtable #{foo = 'var'}
The same could be accomplished using Get-Command:
(Get-Command 'fullpath\to\myScript.ps1').ScriptBlock.Ast.FindAll({
... same syntax as before ... }, $true # or $false for non-recursive search
)

Better way of multiplayer IFs with PowerShell

Im wondering if I am tackling this in the best way or if there is a better way of achieving my task.
I've written a function in PowerShell which takes different parameters, but some of the parameters won't work together.
So for example if I'm running the function and specifying a computerName parameter then I can't also pass list of multiple computer names.
I know I can write multiple If statements as along the lines if If(($computerName) - and ($computerList)){Then write and error}
but there are several parameters not just two, so do I need to do an if for each set of parameters someone could type in, or is there a better way of me tackling this?
currently I have multiple Ifs like If $computerName -and !(log file) and $computerlist) then write an error etc.
The PowerShell-idiomatic solution here is to declare a parameter that accepts either:
function Get-Stuff
{
param(
[Parameter(ValueFromPipeline = $true)]
[string[]]$ComputerName
)
process {
foreach($Computer in $ComputerName){
# do stuff with each individual -ComputerName argument
}
}
}
Now the user can do both
Get-Stuff -ComputerName oneComputerName
... and
"many","computer","names" |Get-Stuff
or for that matter
$computers = Get-Content .\Computers.txt
Get-Stuff -ComputerName $computers
# or
$computers |Get-Stuff
I know I can write multiple If statements as along the lines of If(($computerName) -and ($computerList)){Then write and error}
You can, but this is generally a bad idea - to test whether an argument value was passed to a parameter is better done through the automatic variable $PSBoundParameters:
function Get-Stuff
{
param(
[string]$AParameter,
[string]$ADifferentOne
)
if($PSBoundParameters.ContainsKey('AParameter')){
# an argument was definitely provided to $AParameter
}
if($PSBoundParameters.ContainsKey('ADifferentOne')){
# an argument was definitely provided to $ADifferentOne
}
}
The answer to the implied question of "how do I declare and work with mutually exclusive parameters" is parameter sets:
function Verb-Noun
{
param(
[Parameter(Mandatory = $true, ParameterSetName = 'SingleComputer')]
[string]$ComputerName,
[Parameter(Mandatory = $true, ParameterSetName = 'MultipleComputers')]
[string[]]$ComputerList
)
if($PSCmdlet.ParameterSetName -eq 'SingleComputer'){
# just one, we can deal with $ComputerName
}
else {
# we got multiple names via $ComputerList
}
}
PowerShell now recognizes two distinct parameter sets, each of which only accepts one of our parameters:
PS ~> Get-Command Verb-Noun -Syntax
Verb-Noun -ComputerName <string> [<CommonParameters>]
Verb-Noun -ComputerList <string[]> [<CommonParameters>]

How do you call a PowerShell function with an Object of Arguments

In PowerShell, one can generally call a function with arguments as follows:
DoRoutineStuff -Action 'HouseKeeping' -Owner 'Adamma George' -Multiples 4 -SkipEmail
To trap these 4 supplied arguments at runtime, one might place this inside the function definition
""
"ARGUMENTS:"
$PSBoundParameters
And the resulting object displayed might look like so:
ARGUMENTS:
Key Value
--- -----
Action HouseKeeping
Owner Adamma George
Multiples 4
SkipEmail True
Now, my question is: If I were to manually build the $MyObject identical to $PSBoundParameters displayed above, is there a way to say:
RunFunction 'DoRoutineStuff' -oArgument $MyObject
Again, if it were to be a script file rather than the function DoRoutineStuff, does that make any difference?
Why might one need to do this?
Picture a situation where you need to catch the arguments supplied to first script or function, using $PSBoundParameters, like so:
DoRoutineStuff{
param(
[string]$Action,
[string]$Owner,
[Int]$Multiples,
[switch]$SkipEmail
)
$Data = $PSBoundParameters
#Update one object property
$Data.Multiples = 1
#Then, recursively call `DoRoutineStuff` using `$Data`
#Other tasks
exit;
}
It sounds like the language feature you're looking for is splatting.
You simply pack you're named parameter arguments into a hashtable, store that in a variable and then pass the variable using # in front of its name:
$myArguments = #{
Action = 'HouseKeeping'
Owner = 'Adamma George'
Multiples = 4
SkipEmail = $true
}
Do-Stuff #myArguments
You can also use this technique to only pass a partial set of parameter arguments (or none at all), great for passing along conditional arguments:
$myArguments = #{}
if($someCondition){
$myArguments['Multiples'] = 1
$myArguments['SkipEmail'] = $true
}
if($somethingElse){
$myArguments['Multiple'] = 4
}
Do-Stuff -Action 'HouseKeeping' -Owner 'Adamma George' #myArguments
You can also reuse $PSBoundParameters for splatting further - very useful for proxy functions:
function Measure-Files
{
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $false)]
[string]$Filter,
[Parameter(Mandatory = $false)]
[switch]$Recurse
)
return (Get-ChildItem #PSBoundParameters |Measure-Object -Property Length).Sum
}

Default value of parameter is not used in function

I have a very basic PowerShell script:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
This is how I execute it:
PS C:\> .\test.ps1 -MyWord 'hello'
hello
All fine. But I want to set a default value if -MyWord isn't specified.
I tried this:
Param(
[string]$MyWord='hi'
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output of my script was just empty. It was printing nothing when I did not describe my parameter. (it only showed 'hello' if I specified the parameter).
I also tried:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
[string]$MyWord='hi'
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output was of course always 'hi' and never 'hello'. Even when I executed the script with the parameter -MyWord 'hello'
Can someone explaining what I'm doing wrong?
When I'm not using the function it is working as expected:
Param(
[string]$MyWord='hi'
)
Write-Host $MyWord
Output:
PS C:\> .\test.ps1 -MyWord 'hallo'
hallo
PS C:\> .\test.ps1
hi
Automatic variable $PSBoundParameters, as the name suggests, contains only bound parameters, where bound means that an actual value was supplied by the caller.
Therefore, a parameter default value does not qualify as binding the associated parameter, so $MyWord with its default value of 'hi' does not become part of $PSBoundParameters.
Note: Arguably, a parameter with a default value should also be considered bound (it is bound by its default value, as opposed to by a caller-supplied value). Either way, it would be convenient to have an automatic variable that includes default values too, so as to enable simple and comprehensive passing through of arguments. A suggestion has been submitted to the PowerShell repository as GitHub issue #3285.
Workarounds
The following solutions assume that you want to pass the default value through, and don't want to simply duplicate the default value in function myfunc (as demonstrated in Ansgar Wiecher's helpful answer), because that creates a maintenance burden.
Regarding function syntax: The following two forms are equivalent (in this case), though you may prefer the latter for consistency and readability.[1]
function myfunc([string] $MyWord = 'hi') { ... }
parameter declaration inside (...) after the function name.
function myfunc { param([string] $MyWord = 'hi') ... }
parameter declaration inside a param(...) block inside the function body.
A simple fix would be to add the default value explicitly to $PSBoundParameters:
Param(
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord){
Write-Host "$MyWord"
}
# Add the $MyWord default value to PSBoundParameters.
# If $MyWord was actually bound, this is effectively a no-op.
$PSBoundParameters.MyWord = $MyWord
myfunc #PSBoundParameters
To achieve what you want generically, you must use reflection (introspection):
param(
[alias('foop')]
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord) {
Write-Host "$MyWord"
}
# Add all unbound parameters that have default values.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultVal = Get-Variable -Scope Local $paramName -ValueOnly
# A default value is identified by either being non-$null or
# by being a [switch] parameter that defaults to $true (which is bad practice).
if (-not ($null -eq $defaultVal -or ($defaultVal -is [switch] -and -not $defaultVal))) {
$PSBoundParameters[$paramName] = $defaultVal
}
}
}
myfunc #PSBoundParameters
[1] The param(...) form is required if you need to use the [CmdletBinding()] attribute with non-default values, as well as in scripts (.ps1). See this answer.
A parameter is bound only if you actually pass it a value, meaning that a parameter's default value does not show up in $PSBoundParameters. If you want to pass script parameters into a function, you must replicate the script parameter set in the function parameter set:
Param(
[string]$MyWord = 'hi'
)
function myfunc([string]$MyWord = 'hi') {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
Maintaining something like this is easier if you define both parameter sets the same way, though, so I'd put the function parameter definition in a Param() block as well:
Param(
[string]$MyWord = 'hi'
)
function myfunc {
Param(
[string]$MyWord = 'hi'
)
Write-Host "$MyWord"
}
If you want to use "Param" enclose it in the function like this:
function myfunc {
Param(
[string]$MyWord='hi'
)
Write-Host "$MyWord"
}
Very simple way is,
function myfunc([string]$MyWord = "hi") {
Write-Output $MyWord
}

Pass an unspecified set of parameters into a function and thru to a cmdlet

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