Why is my parameter only processing one value - powershell

I played around with parameters and I ran into a problem.
function sign-script {
param(
[Parameter(
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
Position = 0,
Mandatory = $true
)]
[ValidateNotNullOrEmpty()]
[Alias('FullName')]
[string[]]$scripts,
[string]$certname = "codesigning",
[string]$certstore = "Cert:\LocalMachine\My"
)
Write-Host $scripts.count
}
If I run this function like this:
"Z:\test\test-sign.ps1","Z:\test\test-sign - Kopie (7).ps1" | sign-script
$scripts.count returns 1, instead of 2, why? Is the function not processing all values, or is it only accepting one value?

This is because you didn't split function body into blocks: begin, process and end.
begin block is executed before first item is arrived from pipeline
process block is executed for each item in pipeline
end block is executed after last item is recieved from pipeline.
If no blocks are defined, function body is implicitly set to end bblock. As the result, you will see only last item in pipeline. To fix that, I would suggest to rewrite the script as follows:
function sign-script {
param(
[Parameter(
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
Position = 0,
Mandatory = $true
)]
[ValidateNotNullOrEmpty()]
[Alias('FullName')]
[string[]]$scripts,
[string]$certname = "codesigning",
[string]$certstore = "Cert:\LocalMachine\My"
)
process {
Write-Host $scripts.count
}
}
you just put the code that is supposed to process each item in pipeline into process block. When you run the script, you will see 1 twice, because process block is restarted for each item.

Because you need a Process {} block to act on each item.
By default, with no Begin, End, or Process block, you only have an End, so you're only operating with the last item passed in.
function sign-script {
param(
[Parameter(
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
Position = 0,
Mandatory = $true
)]
[ValidateNotNullOrEmpty()]
[Alias('FullName')]
[string[]]$scripts,
[string]$certname = "codesigning",
[string]$certstore = "Cert:\LocalMachine\My"
)
Process {
Write-Host $scripts.count
}
}
If you do it this way, you're going to see 1 returned twice. This is expected, because the process block gets called once per item.
Whereas if you call it like this:
sign-script "Z:\test\test-sign.ps1","Z:\test\test-sign - Kopie (7).ps1"
Then it will return 2.
The way I typically handle this is to use foreach in the process block:
Process {
foreach ($script in $scripts) {
Write-Host $script
}
}
This ensures that you're always dealing with a single script in the innermost loop whether scripts were specified via pipeline or parameter.

Related

PowerShell Parameter Set Requires Named Parameter

I have the following snippet of a functions parameters and their sets
function Test {
[CmdletBinding(DefaultParameterSetName='StringConsole')]
param (
[Parameter(Mandatory,
ValueFromPipelineByPropertyName,
ParameterSetName = 'ObjectFile')]
[Parameter(Mandatory,
ValueFromPipelineByPropertyName,
ParameterSetName = 'StringFile')]
[Alias("PSPath")]
[ValidateNotNullOrEmpty()]
[string]
$Path,
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='StringFile',
Position = 0)]
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='StringConsole',
Position = 0)]
[ValidateNotNullOrEmpty()]
[string]
$Message,
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='ObjectFile',
Position = 0)]
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='ObjectConsole',
Position = 0)]
[ValidateNotNullOrEmpty()]
[object]
$Object,
[Parameter(ParameterSetName='StringFile')]
[Parameter(ParameterSetName='StringConsole')]
[ValidateSet('Information', 'Verbose', 'Warning', 'Error', 'Object')]
[string]
$Severity = 'Information',
[Parameter(ParameterSetName='StringFile')]
[Parameter(ParameterSetName='StringConsole')]
[switch]
$NoPreamble,
[Parameter(ParameterSetName = 'StringConsole')]
[Parameter(ParameterSetName = 'ObjectConsole')]
[switch]
$Console
)
}
If I call the function using
Test 'Hello, World'
it properly uses the StringConsole default parameter set from CmdletBinding
If I call the function using
Test -Message 'Hello, World' -Path C:\SomeFile.txt
It properly uses the StringFile parameter set
But if I call the function using
Test 'Hello, World' -Path C:\SomeFile.txt
I get this error and the function doesn't execute:
Parameter set cannot be resolved using the specified named parameters
The error specifically states it couldn't resolve the parameter set using the NAMED parameters. If a parameter gets bound by position does it not also satisfy the "named" parameter? Or do you have to specifically bind the parameter using the name?
Is there anyway I could design the parameter sets to make my last example work and not throw an error?
The logic used for your parameter sets looks perfectly fine but the issue is that you have 2 parameters with Position = 0 (-Message and -Object), normally this wouldn't be a problem but one of them is of the type System.Object and since all objects inherit from this class, no matter what you pass as argument in position 0 it will match this parameter. Since the other parameter on Position = 0 is of type System.String then 'Hello, World' (a string but also an object) matches both parameter sets and the binder has no idea which one did you mean to use.
A very easy way of seeing this, without changing your current code and just adding $PSCmdlet.ParameterSetName to the function's body, would be to pass an integer as positional parameter and everything works as expected:
function Test {
[CmdletBinding(DefaultParameterSetName='StringConsole')]
param(
# same param block here
)
'Using: ' + $PSCmdlet.ParameterSetName
}
Test 0 -Path C:\SomeFile.txt # => Using: ObjectFile

How to fix a Powersell bug

I have this powershell example that does not throw any error or exception.
However, NOTHING is printed on the output.
I am not able to find the reason
Maybe I don't call the Add-Values function correctly?
<#
.Synopsis
To add two integer values
.DESCRIPTION
Windows PowerShell Script Demo to add two values
This accepts pipeline values
.EXAMPLE
Add-Values -Param1 20 -Param2 30
.EXAMPLE
12,23 | Add-Values
#>
function Add-Values
{
[CmdletBinding()]
[Alias()]
[OutputType([int])]
Param
(
# Param1 help description
[Parameter(Mandatory=$true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
#Accepts Only Integer
[int]$Param1,
#Accepts only interger
[Parameter(Mandatory=$true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[int]$Param2
)
Begin
{
"Script Begins"
}
Process
{
$result = $Param1 + $Param2
}
End
{
$result
}
}
pause
Remove the pause? Then paste your whole function into powershell. After that, try your function.
It kinda works for me.
PS ~>Add-Values -Param1 1 -Param2 2
Script Begins
3
parameter binding by datatype will give you the wrong result (it will use the last value for both parameters.
PS ~>1, 2 | Add-Values
Script Begins
4

Reusable SteppablePipeline code breaks randomly

I have been writing a Powershell library which uses a lot of wrappers around one or two functions. Thanks to a previous question, I discovered that I could use the GetSteppablePipeline() method to do the wrapping. However, I would like to be able to generalize this code. Googling the command, I discovered that there are only one or two samples which have been copied to death, but no single function. So I tried it. It seemed to work at first, and the sample below is roughly what I created first. But when I started to use the code in anger, it just seemed to break all over the place.
If you run the below straight through, it works ok. But if you put in any breakpoints in the ISE, it will break with errors like "Cannot find drive. A drive with the name 'function' does not exist.". If you incorporate the code into any other script, it will also break with seemingly incomprehensible errors (for instance, Where-Object not being recognised as a Cmdlet).
Function CreateSteppablePipeline
{
Param(
[Parameter(Mandatory = $true, Position = 0)]
[string]
$InnerFunctionName,
[Parameter(Mandatory = $true, Position = 1)]
[System.Collections.Generic.Dictionary`2[System.String,System.Object]]
$OuterPSBoundParameters,
[Parameter(Mandatory = $true, Position = 2)]
[System.Management.Automation.EngineIntrinsics]
$OuterExecutionContext,
[Parameter(Mandatory = $true, Position = 3)]
[System.Management.Automation.InvocationInfo]
$OuterMyInvocation
)
$outBuffer = $null
if ($OuterPSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$OuterPSBoundParameters['OutBuffer'] = 1
}
[System.Management.Automation.FunctionInfo] $wrappedCmd = $OuterExecutionContext.InvokeCommand.GetCommand($InnerFunctionName, [System.Management.Automation.CommandTypes]::Function);
[ScriptBlock] $scriptCmd = { & $wrappedCmd #OuterPSBoundParameters };
[System.Management.Automation.SteppablePipeline] $steppablePipeline = $scriptCmd.GetSteppablePipeline($OuterMyInvocation.CommandOrigin);
$steppablePipeline.Begin($OuterMyInvocation.ExpectingInput);
$steppablePipeline;
}
Function Inner
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[string] $ExParam,
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]] $Value
)
Begin {
[int] $i = 0;
}
Process {
$i++;
ForEach ($myValue In $Value)
{
$myValue;
}
}
End {
$i;
$ExParam;
}
}
Function Outer
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[string] $ExParam,
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]] $Value
)
Begin {
[System.Management.Automation.SteppablePipeline] $steppablePipeline = CreateSteppablePipeline "Inner" $PSBoundParameters $ExecutionContext $MyInvocation;
}
Process {
$steppablePipeline.Process($_) |
ForEach-Object {
$_ + " : Pipelined";
}
}
End {
$steppablePipeline.End()
}
}
Outer -ExParam 22 "Name1","Name2","Name3","Name4","Name5","Name6","Name7"
"Name1","Name2","Name3","Name4","Name5","Name6","Name7" | Outer -ExParam 22
It has probably to do with scoping (a problem I have come across a number of times in scripts). I tried moving parts of the CreateSteppablePipeline() function between the calling and function scope, but with no luck.

I don't understand param binding on functions with begin/process/end blocks

function Format-File {
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $key,
[Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $value
)
}
I'm calling it like so, assume I've added values to the dictionary (removed for brevity here)
$dict = New-Object 'System.Collections.Generic.Dictionary[string,string]'
$dict.GetEnumerator() | Format-File -Path $updatePath
Here's my conundrum.
The above works perfectly. However, the following does not, note the difference in the key/value parameter
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $key=$(throw "key is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $value=$(throw "value is mandatory ($($MyInvocation.MyCommand))")
)
}
The above throws an exception. It appears to be getting the default value when the function is first called, but when processing, the key/value parameters are set properly.
It makes a bit of sense as to why the key/value wouldn't be set at the time of the function call, but this also means my mental model is off.
So my question is two-fold.
What is the parameter binding process for functions of this nature, and
How does one verify the input of values that have come in from the pipeline? Manually check in the begin block, or is there another method?
If you have links to describe this all in greater detail, I'm happy to read up on it. It just made me realize my mental model of the process is flawed and I'm hoping to fix that.
What is the parameter binding process for functions of this nature?
In the Begin block, pipeline bound parameters will be $null or use their default value if there is one. This makes some sense, considering that the pipelining of values hasn't started yet.
In the Process block, the parameter will be the current item in the pipeline.
In the End block, the parameter will be the last value from the Process block, unless there was an exception in validating the parameter, in which case it will use the default (or $null).
How does one verify the input of values that have come in from the pipeline?
You can't check in the Begin block.
The best way is to use [Validate attributes, as you have with [ValidateNotNullOrEmpty()].
Your examples with using throw as a default value are useful in some situations but they are a clever workaround. The thing is, you don't need them since you already declared the parameter as Mandatory.
Instead of using a default value, you can use [ValidateScript( { $value -eq 'MyString' } )] for example.
Since the error message from [ValidateScript()] sucks, you can combine the techniques:
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[ValidateScript( {
($_.Length -le 10) -or $(throw "My custom exception message")
} )]
[string] $key
)
}
Using [ValidateScript()] works whether it's a pipeline parameter or not.

Powershell pipelining only returns last member of collection

I have an issue running the following script in a pipeline:
Get-Process | Get-MoreInfo.ps1
The issue is that only the last process of the collection is being displayed. How do I work with all members of the collection in the following script:
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
function Get-Stats($Process)
{
New-Object PSObject -Property #{
Name = $Process.Processname
}
}
Get-Stats($Process)
try this:
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
process{
New-Object PSObject -Property #{
Name = $Process.Processname}
}
Edit:
if you need a function:
function Get-MoreInfo {
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
process{
New-Object PSObject -Property #{
Name = $Process.Processname}
}
}
then you can use:
. .\get-moreinfo.ps1 #
Get-Process | Get-MoreInfo
Edit after Comment:
Read about dot sourcing a script
I you simply create Get-MoreInfo as a Filter instead of Function, you will get the desired effect.
Filter Get-MoreInfo
{
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
...
Actually, both Christian's answer and tbergstedt's answer are both valid--and they are essentially equivalent. You can learn more about how and why in my recent article on Simple-Talk.com: Down the Rabbit Hole- A Study in PowerShell Pipelines, Functions, and Parameters.
In a nutshell, here are the salient points:
A function body includes begin, process, and end blocks.
A function not explicitly specifying any of the above 3 blocks operates as if all code is in the end block; hence the result you initially observed.
A filter is just another way to write a function without any of the above 3 blocks but all the code is in the process block. That is why the above two answers are equivalent.