PowerShell pipeline parameter binding order - powershell

I have an advanced function that can accept two kinds of pipeline data:
A custom object with a PSTypeName of "MyType"
Any object with an ID property
Here's my function:
function Test-PowerShell {
[CmdletBinding(DefaultParameterSetName = "ID")]
param (
[Parameter(
Mandatory = $true,
ParameterSetName = "InputObject",
ValueFromPipeline = $true
)]
[PSTypeName('MyType')]
$InputObject,
[Parameter(
Mandatory = $true,
ParameterSetName = 'ID',
ValueFromPipelineByPropertyName = $true
)]
[int]
$ID
)
process {
if ($InputObject) {
$objects = $InputObject
Write-Verbose 'InputObject binding'
}
else {
$objects = Get-MyType -ID $ID
Write-Verbose 'ID binding'
}
# Do something with $objects
}
}
I can use this function like this:
$obj = [PSCustomObject]#{
PSTypeName = 'MyType'
ID = 5
Name = 'Bob'
}
$obj | Test-PowerShell -Verbose
Note that this object satisfies both of the above conditions: It is a MyType, and it has an ID property. In this case, PowerShell always binds to the ID property. This isn't ideal performance-wise because the piped object is discarded and I have to re-query it using the ID. My question is this:
How do I force PowerShell to bind the pipeline to $InputObject if possible?
If I change the default parameter set to InputObject, PowerShell binds on $InputObject. I don't want this, however, because when run without parameters, I want PowerShell to prompt for an ID, not an InputObject.

Simple answer: remove the Mandatory argument to the Parameter attribute on $InputObject to get the functionality you want. I don't have enough knowledge on how parameter binding works to explain why this works.
function Test-PowerShell {
[CmdletBinding(DefaultParameterSetName = 'ID')]
param(
[Parameter(ParameterSetName = 'InputObject', ValueFromPipeline)]
[PSTypeName('MyType')]
$InputObject,
[Parameter(ParameterSetName = 'ID', Mandatory, ValueFromPipelineByPropertyName)]
[int]
$ID
)
process {
$PSBoundParameters
}
}
$o = [pscustomobject]#{
PSTypeName = 'MyType'
ID = 6
Name = 'Bob'
}
PS> $o | Test-PowerShell
Key Value
--- -----
InputObject MyType
PS> [pscustomobject]#{ID = 6} | Test-PowerShell
Key Value
--- -----
ID 6
Thoughts and experimentation below.
Here's a workaround to your problem (defining your own type):
Add-Type -TypeDefinition #'
public class MyType
{
public int ID { get; set; }
public string Name { get; set; }
}
'#
And then you would tag your parameter as [MyType], creating objects like you would from [pscustomobject]:
[MyType]#{ ID = 6; Name = 'Bob' }
In hindsight, this method does not work. What you're running into is the behavior of the DefaultParameterSet. I'd suggest changing what you take as pipeline input. Is there a use-case for taking the ID as pipeline input versus a user just using Test-PowerShell -ID 5 or Test-PowerShell and being prompted for the ID?
Here's a suggestion that may work as you intend from my testing:
function Test-PowerShell {
[CmdletBinding(DefaultParameterSetName = 'ID')]
param(
[Parameter(ParameterSetName = 'InputObject', Mandatory = $true, ValueFromPipeline = $true)]
[PSTypeName('MyType')]
$InputObject,
[Parameter(ParameterSetName = 'ID', Mandatory = $true, ValueFromPipeline = $true)]
[int]
$ID
)
process {
$PSBoundParameters
}
}
To take an example from an existing built-in cmdlet, they don't use the same name or properties on an object for multiple parameters. In Get-ChildItem, both the LiteralPath and Path take pipeline input, but LiteralPath only takes it by PropertyName LiteralPath or PSPath (aliased). Path is ByValue and PropertyName, but only as Path.

Related

Powershell passing mandatory dynamic type variable to function

I am implementing a function in Powershell which will perform REST calls. One of the parameters may differ in contents, depending on given scenarios. For instance, the body of the REST call may be a string or a hash table. How do you implement this within the CmdletBinding() declaration?
For instance
Function doRESTcall(){
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[Hashtable]$headers
[Parameter(Mandatory=$true)]
[???????]$body # what type here??
)
.
.
.
}
To declare parameters where any type is allowed you can either not type-constrain the parameter at all or use type constraint [object] (System.Object), by doing so, no type conversion will be needed, since all objects in PowerShell inherit from this type.
It's worth mentioning that unconstrained parameters will allow $null as argument, to avoid this, [ValidateNotNull()] and / or [parameter(Mandatory)] can be used.
function Test-Type {
param(
[parameter(ValueFromPipeline, Mandatory)]
[object]$Value
)
process
{
[pscustomobject]#{
Type = $Value.GetType().FullName
IsObject = $Value -is [object]
}
}
}
PS /> 1, 'foo', (Get-Date) | Test-Type
Type IsObject
---- --------
System.Int32 True
System.String True
System.DateTime True
The correct way to tackel this is to create a ParameterSet:
Function doRESTcall(){
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ParameterSetName = 'StringBody', Position = 0)]
[Parameter(Mandatory=$true, ParameterSetName = 'HashBody', Position = 0)]
[Hashtable]$headers,
[Parameter(Mandatory=$true, ParameterSetName = 'StringBody', Position = 1)]
[String]$Stringbody,
[Parameter(Mandatory=$true, ParameterSetName = 'HashBody', Position = 1)]
[Hashtable]$Hashbody
)
Write-Host 'Parameter set:' $PSCmdlet.ParameterSetName
Write-Host 'StringBody:' $StringBody
Write-Host 'HashBody:' $HashBody
}
doRESTcall -?
NAME
doRESTcall
SYNTAX
doRESTcall [-headers] <hashtable> [-Hashbody] <hashtable> [<CommonParameters>]
doRESTcall [-headers] <hashtable> [-Stringbody] <string> [<CommonParameters>]
ALIASES
None
REMARKS
None
doRESTcall #{a = 1} 'Test'
Parameter set: StringBody
StringBody: Test
HashBody:
Note: to accept a larger variety of dictionaries (like [Ordered]), I would use a [System.Collections.Specialized.OrderedDictionary] (rather than [Hashtable]) type for the concerned parameters.

Does a DynamicParameter Switch not read like a Static Parameter [switch]?

Hopefully the Title is clear enough but, I am having some trouble understanding on how to evaluate against a DynamicParameter Switch, compared to a Static (type casted) switch.
In the following code block, there are 2 switches that become available only if the other 2 parameters are not null, and/or, are not empty:
Add
Remove
Function Test-DynamParam {
Param (
# Input Parameters
[Parameter(Mandatory = $false,
HelpMessage='Enter. Workflow. Name.')]
[Alias('OMB','MailBox')]
[string]$Workflow,
[Parameter(Mandatory = $false)]
[Alias('EDIPI','DisplayName')]
[string[]]$UserName
)
DynamicParam {
if ($Workflow -ne $null -and $UserName -ne $null) {
$parameterAttribute = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = "AddingMembers"
Mandatory = $false
}
$attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection.Add($parameterAttribute)
$dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'Add', [switch], $attributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add('Add', $dynParam1)
$parameterAttribute1 = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = "RemovingMembers"
Mandatory = $false
}
$attributeCollection1 = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection1.Add($parameterAttribute1)
$dynParam11 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'Remove', [switch], $attributeCollection1
)
$paramDictionary.Add('Remove', $dynParam11)
return $paramDictionary
}
}
Process {
$Add.IsPresent
}
}
Running:
Test-DynamParam -Workflow 'd' -UserName 'a' -Add
returns empty.
Unfortunately, $Add.IsPresent is not evaluated to any boolean value regardless if the switch is present or not. Yet in this function it is (which makes sense):
Function Test-StaticParam {
Param (
[switch]$Add
)
$Add.IsPresent
}
Running:
Test-StaticParam -Add
returns True.
Question
How can I evaluate against dynamic parameter chosen?
Use the $PSBoundParameters automatic variable:
Process {
$PSBoundParameters['Add'].IsPresent
}

Get value from pipeline with ValueFromPipelineByPropertyName

I have some issues getting a value from the pipeline using ValueFromPipelineByPropertyName.
When I run Get-Input -ComputerName 'PC-01' | Get-Data the cmdlet Get-Input should simply return the computer name "PC-01", whereas the Get-Data function should return "Value passed from Get-Input: PC-01". Instead, I get this error:
Get-Data : The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input or the input and its
properties do not match any of the parameters that take pipeline input.
At line:1 char:33
+ Get-Input -ComputerName PC-01 | Get-Data
+ ~~~~~~~~
+ CategoryInfo : InvalidArgument: (PC-01:PSObject) [Get-Data], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,Get-Data
I have build these two small sample cmdlets just to get the hang of working with the pipeline.
function Get-Input {
[CmdletBinding()]
Param(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[string]$ComputerName
)
Process {
Write-Output -InputObject $ComputerName
}
}
function Get-Data {
[CmdletBinding()]
Param(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[string]$ComputerName
)
Process {
Write-Output -InputObject "Value passed from Get-Input: $($ComputerName)."
}
}
If I change $ComputerName to $Name and run the following, it works:
PS C:\Users\frede> Get-Service -Name AdobeARMservice | Get-Data
Value passed from Get-Input: AdobeARMservice.
If I have grasped the concept of the pipeline in PowerShell, I should be able to run the following command Get-Input -ComputerName 'PC-01' | Get-Data and have the ComputerName passed to Get-Data.
Is there something I need to declare somewhere?
As the name (ValueFromPipelineByPropertyName) indicates, you're telling the parser to bind a value based on a property name.
The Get-Input function will need to output an object that has a property named ComputerName for this to work:
function Get-Input
{
[CmdletBinding()]
param
(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[string]$ComputerName
)
process
{
Write-Output $(New-Object psobject -Propert #{ComputerName = $ComputerName})
}
}
Now you can do:
Get-Input -ComputerName 'PC-01' |Get-Data
If you want Get-Data to support computer name input from Get-Service, you'll have to add an alias that matches the appropriate property name on the object types output by Get-Service, ie. MachineName:
function Get-Data
{
[CmdletBinding()]
param
(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[Alias('MachineName')]
[string]$ComputerName
)
process
{
Write-Output -InputObject "Value passed from Get-Input: $($ComputerName)."
}
}
And now both of these will work:
Get-Service -Name AdobeARMService |Get-Data
Get-Input -ComputerName PC-01 |Get-Data
You will need this bud.
ValueFromPipelineByPropertyName is for boolean ($True / $False) and isn't looking for your string.
[CmdletBinding()]
param
(
[Parameter(
Mandatory = $true,
ValueFromPipeline = $true
)]
[string]$ComputerName
)
you have to write also ValueFromPipeline=$true:
Mandatory = $true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName = $true
Greetings
Jannik

Conditional Mandatory in PowerShell

I'm trying to make a parameter mandatory, but only if another parameter uses certain ValidateSet values. It seems that using a code block on Mandatory doesn't work as expected.
function Test-Me {
[CmdletBinding()]
Param (
[Parameter()]
[ValidateSet("NameRequired", "AlsoRequired")]
[string]
$Type = "NoNameRequired",
[Parameter(Mandatory = {-not ($Type -eq "NoNameRequired")})]
[string]
$Name
)
Process {
Write-Host "I ran the process block."
Write-Host "Type = '$Type'"
Write-Host "Name = '$Name'"
Write-Host "Name Parameter Mandatory? = '$(-not ($Type -eq "NoNameRequired"))'"
}
}
Set-StrictMode -Version Latest
function Test-Me {
[CmdletBinding(DefaultParameterSetName = "Gorgonzola")]
Param (
[Parameter(Mandatory)]
[int]
$Number,
[Parameter(Mandatory, ParameterSetName = "NameNeeded")]
[ValidateSet("NameRequired", "AlsoRequired")]
[string]
$Type = "NoNameRequired",
[Parameter(Mandatory, ParameterSetName = "NameNeeded")]
[string]
$Name
)
Process {
Write-Host "I ran the process block."
Write-Host "Number = '$Number'"
Write-Host "Type = '$Type'"
Write-Host "Name = '$Name'"
Write-Host "Name Parameter Mandatory = '$(-not ($Type -eq "NoNameRequired"))'"
}
}
Parameter sets seem to help simulate conditional mandatory parameters.
I can make it to where if either the Type or Name parameter is given, then they are both required. This can happen regardless of other parameters in the function, such as the sibling Number parameter above.
I set the default parameter set name to something random; I usually specify "None". That parameter set name doesn't need to actually exist, again indicated by the Number parameter.
All of this works regardless of your strict mode setting.

Validate parameter value using another parameter value

I'm trying to validate whether a path exists before running a function.
There is no default for the folder path, but the file name default should be template.csv. Is there a way, through the ValidateScript attribute, to validate a parameter value based on another parameter value?
The below code returns the error that the variable $TemplateDir has not been set. I'm also not entirely sure if it would test for the default file name value.
function get-Template {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateScript({Test-Path $_})]
[string]$TemplateDir,
[Parameter(Mandatory = $false)]
[ValidateScript({Test-Path ($TemplateDir + "\" + $_)})]
[string]$TemplateFile = "template.csv"
)
...
}
Any advice?
You can set up a dynamic parameter with the DynamicParam block, that depends on the value of another mandatory parameter:
function Get-FilePath
{
Param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateScript({Test-Path $_})]
[string]$TemplateDir
)
DynamicParam {
# Set up parameter attribute
$fileParamAttribute = New-Object System.Management.Automation.ParameterAttribute
$fileParamAttribute.Position = 3
$fileParamAttribute.Mandatory = $false
$fileParamAttribute.HelpMessage = "Please supply a file name"
# Set up ValidateSet param with actual file name values
$fileValidateParam = New-Object System.Management.Automation.ValidateSetAttribute #(Get-ChildItem $TemplateDir -File |Select-Object -ExpandProperty Name)
# Add the parameter attributes to an attribute collection
$attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($fileParamAttribute)
$attributeCollection.Add($fileValidateParam)
# Create the actual $TemplateFile parameter
$fileParam = New-Object System.Management.Automation.RuntimeDefinedParameter('TemplateFile', [string], $attributeCollection)
# Push the parameter(s) into a parameter dictionary
$paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add('TemplateFile', $fileParam)
# Return the dictionary
return $paramDictionary
}
begin{
# Check if a value was supplied, otherwise set it
if(-not $PSBoundParameters.ContainsKey('TemplateFile'))
{
$TemplateFile = 'template.csv'
}
$myPath = Join-Path $TemplateDir -ChildPath $TemplateFile
}
end {
return $myPath
}
}
This will also give you automatic tab completion for the arguments to -TemplateFile
You can read more about DynamicParam with Get-Help about_Functions_Advanced_Parameters