I have the following simple PowerShell function:
function ValueFromPipelineTest
{
param
(
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string] $Param1,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string] $Param2
)
Process
{
Write-Output "Param1: $Param1"
Write-Output "Param2: $Param2"
}
}
and if I pipe values into it like so:
'Hello' | ValueFromPipelineTest
#{ Param1 = 'Hello'; Param2 = 'there' } | ValueFromPipelineTest
The 1st line works as expected, producing the output:
Param1: Hello
Param2:
However the 2nd line produces the output:
Param1: System.Collections.Hashtable
Param2:
I'm actually looking to have the 2nd line produce the output:
Param1: Hello
Param2: there
I assume this is possible, but I'm just missing something. If there's a way to do it without using a hashtable I'm open to that. I'm just looking for an easy way to define an object with some properties and pipe it into my function.
Any thoughts on how to accomplish what I'm after? Thanks in advance!
You can do this by passing a custom object (rather than a hashtable) to your function. Example:
function Test-Pipeline {
param(
[Parameter(ValueFromPipeline = $true,ValueFromPipelineByPropertyName = $true)]
[String] $Param1,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[String] $Param2
)
process {
"Param1: $Param1"
"Param2: $Param2"
}
}
$obj = [PSCustomObject] #{Param1 = "Hello";Param2 = "World"}
$obj | Test-Pipeline
# Output:
#
# Param1: Hello
# Param2: World
You could use Splatting but that isn't for pipeline.
$Params = #{
'Param1' = “Hello”
'Param2' = “There”
}
ValueFromPipelineTest #Params
Alternatively, try the below.
$Params = #{
Param1 = “Hello”
Param2 = “There”
}
function ValueFromPipelineTest
{
param
(
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[hashtable]$Param
)
Process
{
Write-Output "Param1: $($Param["Param1"])"
Write-Output "Param2: $($Param["Param2"])"
}
}
$Params | ValueFromPipelineTest
Related
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
}
I write own powershell func for debug like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName $OtherArg} catch {...} finally {...}
and use it everyway, but i need more arg after $FunctionName. is it realistic to pass many arguments in this case bec use from 0 to 10 arg. do I have to list all the arguments that can be in the parameters of the function? like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg,
[PARAMETER(Mandatory = $false)]
$OtherArg1,
[PARAMETER(Mandatory = $false)]
$OtherArg2,
[PARAMETER(Mandatory = $false)]
$OtherArg3
)
try {& $FunctionName $OtherArg OtherArg1 OtherArg2 OtherArg3 } catch {...} finally {...}
but i dont use positional parameters in code and too many named parameters in code (~100)
Interested in any ideas about this. tnx!
The magic word is Splatting. You can provide an array or a hashtable containing your arguments to a function. The splatter is written with an #VariableName instead of the $:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName #OtherArg # Watch out for the # in the OtherArg
} catch {$_} finally {}
}
$FunctionName = 'Get-ChildItem'
$Splatter = #{
Path = 'C:\'
Filter = 'Users'
Directory = $true
}
$Splatter2 = #('c:\')
StartDebug $FunctionName $Splatter
StartDebug $FunctionName $Splatter2
However if you want to use single items as $OtherArg you will have to provide them as single element array as can be seen with $Splatter2. Or extend your function to transform single arguments in arrays automatically, but thats up to you.
I think you better run it using scriptblock:
$result = Invoke-DeepDebug { Get-ChildItem -Path 'C:\' ; Get-Service -InformationAction Continue}
And in Invoke-DeepDebug you can work with $Command.AST as deep and detailed as you want.
Function Invoke-DeepDebug {
param(
[Parameter(Mandatory=$true, Position=0)]
[Scriptblock]$Command
)
Write-Host -f Cyan "Executing " -n
Write-Host -f Magenta $Command.Ast.Extent.Text -n
Write-Host -f Yellow " ... " -n
$result = $null
try {
$result = Invoke-Command $Command -ErrorAction Stop
Write-Host -f Green "OK!"
} catch {
Write-Host -f Red "Error"
Write-Host -f Red "`t$($_.Exception.Message)"
}
return $result
}
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.
I have a module which has the following two functions, which are almost identical:
<#
.SYNOPSIS
Retrieves a VApp from VCloud.
#>
Function Get-VApp
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $VAppName
)
Begin {
[System.Xml.XmlElement] $queryList = $Session.GetQueryList();
[System.Xml.XmlElement[]] $vAppRecords = $queryList.GetVAppsByRecords().VAppRecord;
}
Process {
ForEach ($VAN in $VAppName)
{
$vAppRecords |
Where-Object { $_.name -eq $VAN } |
ForEach-Object { $_.Get(); }
}
}
End
{
#
}
}
and
<#
.SYNOPSIS
Retrieves a VAppRecord from VCloud.
#>
Function Get-VAppRecord
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $VAppName
)
Begin {
[System.Xml.XmlElement] $queryList = $Session.GetQueryList();
[System.Xml.XmlElement[]] $vAppRecords = $queryList.GetVAppsByRecords().VAppRecord;
}
Process {
ForEach ($VAN in $VAppName)
{
$vAppRecords |
Where-Object { $_.name -eq $VAN } |
ForEach-Object { $_; }
}
}
End
{
#
}
}
Essentially, Get-VApp is like Get-VAppRecord, except that the former calls a Get() method on the returned object. This seems wasteful. If I wasn't bothering with pipelines, it would be easy:
Function Get-VApp
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true)]
[string[]] $VAppName
)
Get-VAppRecord $Session $VAppName |
ForEach-Object {
$_.Get();
}
}
But obviously the pipeline messes things up. I don't call the code in the Begin block multiple times for efficiency, and I would like to find a way to "play nice" with the pipeline without having to batch up records.
The SteppablePipeline class is designed for wrapping pipeline-enabled commands without messing up their pipeline support.
You don't even need to know how to set it up, ProxyCommand.Create() will generate the scaffolding for it!
So let's start out by creating a proxy function for Get-VAppRecord:
$GetVAppRecordCommand = Get-Command Get-VAppRecord
$GetVAppRecordCommandMetadata = [System.Management.Automation.CommandMetadata]::new($GetVAppRecordCommand)
# returns the body of the new proxy functions
[System.Management.Automation.ProxyCommand]::Create($GetVAppRecordCommandMetadata)
... and then we just need to add the Get() call in the process block of it:
function Get-VApp {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[System.Xml.XmlElement]
${Session},
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]]
${VAppName})
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-VAppRecord', [System.Management.Automation.CommandTypes]::Function)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($MyInvocation.ExpectingInput) # Many examples use $PSCmdlet; however setting this ensures that $steppablePipeline.Process() returns the output of the inner function.
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_) |ForEach-Object {
# call Get() on the record
$_.Get()
}
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
Say I have a Powershell script TestParameters.ps1 like this, with two mandatory, named parameters and two optional parameters:
[CmdletBinding()]
Param (
[Parameter(Mandatory=$True)]
[string] $AFile = "C:\A\Path",
[Parameter(Mandatory=$True)]
[ValidateSet("A","B","C", "D")]
[string] $ALetter = "A",
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[string] $Optional1 = "Foo",
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[string] $Optional2 = "Bar"
)
echo "Hello World!"
$psboundparameters.keys | ForEach {
Write-Output "($_)=($($PSBoundParameters.$_))"
}
Say I call the script like this:
.\TestParameters.ps1 `
-AFile "C:\Another\Path" `
-ALetter "B"
which produces output:
Hello World!
(AFile)=(C:\Another\Path)
(ALetter)=(B)
Powershell set the variables $Optional1 and $Optional2 ... but how do I easily display them to the screen, like the way I use $PSBoundParameters?
I do not want to simply write the following every time I have a script:
Write-Host $AFile
Write-Host $ALetter
Write-Host $Optional1
Write-Host $Optional2
Notes:
$args only seems to include unbound parameters, not default
parameters
$MyInvocation seems to only include commands passed on the command lineEDIT: MyInvocation has member variable MyCommand.Parameters, which seems to have all the parameters, not just those passed on the command line...see the accepted answer, below.
Get-Variable seems to include the optional variables in the result list, but I do not know how to differentiate them from the other variables
The following seems to work on my box... probably not the best way to do it, but it seems to work in this case, at least...
[cmdletbinding()]
param([Parameter(Mandatory=$True)]
[string] $AFile = "C:\A\Path",
[Parameter(Mandatory=$True)]
[ValidateSet("A","B","C", "D")]
[string] $ALetter = "A",
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[string] $Optional1 = "Foo",
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[string] $Optional2 = "Bar"
)
echo "Hello World!"
($MyInvocation.MyCommand.Parameters ).Keys | %{
$val = (Get-Variable -Name $_ -EA SilentlyContinue).Value
if( $val.length -gt 0 ) {
"($($_)) = ($($val))"
}
}
Saved as allparams.ps1, and run it looks like:
.\allparams.ps1 -ALetter A -AFile "C:\Another\Path"
Hello World!
(AFile) = (C:\Another\Path)
(ALetter) = (A)
(Optional1) = (Foo)
(Optional2) = (Bar)
Using AST:
[CmdletBinding()]
Param (
[Parameter(Mandatory=$True)]
[string] $AFile = "C:\A\Path",
[Parameter(Mandatory=$True)]
[ValidateSet("A","B","C", "D")]
[string] $ALetter = "A",
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[string] $Optional1 = "Foo",
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[string] $Optional2 = "Bar"
)
echo "Hello World!"
$psboundparameters.keys | ForEach {
Write-Output "($_)=($($PSBoundParameters.$_))"
}
$ast = [System.Management.Automation.Language.Parser]::
ParseFile($MyInvocation.InvocationName,[ref]$null,[ref]$Null)
$ast.ParamBlock.Parameters | select Name,DefaultValue