Powershell splatting operator only for accepted parameters? - powershell

Is it possible, using PowerShell, to use splatting from hashtable when the hashtable contains more entries that the function accepts ?
My use case is to have config objects I pass from one function to another. However, all functions does not require same parameters.
Ex:
function Process-Something{
param(
[Parameter()]
[string]$Owner
)
}
function Process-SomethingElse{
param(
[Parameter()]
[string]$Owner,
[Parameter()]
[int]$x,
[Parameter()]
[int]$y
)
}
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
Process-Something #config
Process-SomethingElse #config
It fails with these error:
Process-Something : Cannot find a matching parameter « Y ».
The idea is to avoid specifying individual properties for each functions.

As #Ansgar is stating in the comments, the whole idea of having defined your parameters, is to get validation. When you are splatting parameters to your function, you are forcing them to the function. So if a given property of your hashtable doesn't exist as a parameter, you will get an error - just like it is intended.
What you can do, is going into a PSCustomObject and utilize the pipe. If you set all you parameters to accept value from the pipeline, using property name (ValueFromPipelineByPropertyName = $true), then you can actually get the desired behavior.
First I'm redefining your different functions, to have the ValueFromPipelineByPropertyName = $true parameter attribute configured.
function Process-Something{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner
)
$PSBoundParameters
}
function Process-SomethingElse{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$x,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$y
)
$PSBoundParameters
}
With that in place, I'm able to create a hashtable like your example, convert it to a PSCustomObject, and now I can pipe that new object to the different methods and have them pick up only the properties that they need.
I included the PSBoundParameters to showcase that they get what they expect.
Testing is done like this:
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
$psConfig = [PSCustomObject]$config
$psConfig | Process-Something
$psConfig | Process-SomethingElse

Related

Powershell: Improve class constructor for IntelliSense support

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.

Powershell cmdlet parameter definition contain parameter name starts with $ creating change in default be haviour

function Test1 {
[CmdletBinding()]
Param (
[Parameter( Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
[String]
${$count},
[Parameter( Position = 1,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
[String]
${$inlinecount}
)
Process {
Write-Host "TEST1 ${$count} : ${$inlinecount}"
}
}
Calling the function as below
PS> Test1 10 20
TEST1 10:20
PS> Test1 -$count 10
TEST1 : 10
PS> Test1 -$inlinecount 100
TEST1 : 100
PS> Test1 -$count 10 -$inlinecount 100
Error Test1 : A positional parameter cannot be found that accepts argument '-'.
I am not able to specify only one value, i.e only $count, it is taking 'Test1 $count 10' with $count as the value for $count, and 10 as the value for $inlinecount. I don't wanted to remove the $ prefixed with each parameter name. Also not able to specify both parameter values by name
How can i change the parameter definition, so that following will results
PS> Test1 -$count 10
TEST1 10 :
PS> Test1 -$inlinecount 100
TEST1 :100
PS> Test1 -$count 10 -$inlinecount 100
TEST1 10:100
I think this might clarify some things for you:
Function Start-Test1 {
[CmdletBinding()]
Param (
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[String[]]$Count,
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName )]
[String]$InlineCount
)
Process {
[PSCustomObject]#{
Count = $Count
InlineCount = $InlineCount
}
Write-Verbose "TEST1 ${$count} : ${$inlinecount}"
}
}
# No parameters, empty result
Start-Test1
# Only one parameter, output Count = 1
Start-Test1 -Count 1
# Only one parameter, output InlineCount = A
Start-Test1 -InlineCount A
# Two parameters, output Count = 1 InlineCount = A
Start-Test1 -Count 1 -InlineCount A
# Multiple input for Count
Start-Test1 -Count 1, 2, 3
The dollar sign is only used once in the Param clause for each parameter. It makes it also easier to read.
Defining positions, with Position = 1 and so on.. is only needed when the parameters are out of order. In this case, they can be omitted. The same goes for Mandatory = $false, when it is omitted, PowerShell assumes by default that the parameter is not mandatory. But when you write Mandatory it assumes you mean that the parameter is mandatory. So no need to use the boolean $true/$false.
In case you want to accept multiple arguments for one parameter, you can use []. As an example [String]$Name accepts only one name, but [String[]]$Name will accept multiple names.
Some tips:
Some remarks, always use a correct function name. You can find the verbs with Get-Verb. These are the allowed verbs you can use.
Try to output an object (array, PSCustomObject, HashTable, ...) whenever you can, not just Write-Host because that's only useful for visualizing output in the console. For this, it's better to use Write-Verbose. You can then run your function with Start-Test -Verbose only in case you want extra information to be displayed on the console.

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
}

Parameter binding by name through pipeline when some values are empty

In my module, I have two functions a 'Get-Data' and 'Add-Data' I'm having trouble passing information between them through the pipeline.
The following simplified code shows my issue.
function Get-Data{
param ($path)
$out = Import-Csv $path
Write-Output $out
}
The data is the following, notice there are 'gaps' in the data and not every object has all three properties, in fact they cant. You can only have Name and Color or Name and Fruit. (In the real code there are many more properties)
Name,Color,Fruit
Jim,red,
Kate,,Apple
Bob,green,
Abby,,Banana
In the Add-Data function I want to use parameter sets as there are lots of parameters, but only 4 parameter sets possible (two in the simplified code, 'A' and 'B'). This function is exported from the module and I don't want the user to be able to input invalid parameter combinations.
Here is the add-data function:
function Add-Data {
[CmdletBinding()]
Param (
[Parameter(
Position = 1,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Name,
[Parameter(
ParameterSetName = 'A',
Position = 2,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Color,
[Parameter(
ParameterSetName = 'B',
Position = 3,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Fruit
)
BEGIN{}
PROCESS{
Write-Output "$Name $Color $Fruit"
}
END{}
}
When I pipe one to the other like below:
Get-Data -Path c:\some.csv | Add-Data
I get the error:
Cannot bind argument to parameter 'Fruit' because it is an empty string
I know that I am passing an empty string to the fruit parameter and it's what is causing me the issue, but it's obviously not the behaviour I want.
I'd like the fact that the property is empty to help resolve the parameter set so it only needs Name and Color.
I can't use [AllowNull()] or [AllowEmptyString()] and remove the parameter sets as I expect users to use 'add-data' from the command line and I want
help add-data
to show them the correct params to enter.
Thanks in advance for any help.

Dynamic invoke command with different parameters

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