Pass multiple parameters to function by pipeline - powershell

I'm having trouble passing two parameters via pipeline to a function.
function Test{
[cmdletbinding()]
param(
[parameter(ValueFromPipeline=$true, Mandatory=$true,Position=0)]
[string]$jeden,
[parameter(ValueFromPipeline=$true, Mandatory=$true,Position=1)]
[string]$dwa
)
Process{write-host "$jeden PLUS $dwa"}
}
"one", "two"|Test
What I expected as an outcome was
one PLUS two
but what I got was
one PLUS one
two PLUS two
I'm obviously doing something wrong, since both parameters get used twice. Please advise.

I got it to work by creating pscustomobject and piping it to function, where ValueFromPipelineByPropertyName property is set to true for both parameters.
function Test{
[cmdletbinding()]
param(
[parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true,Position=0)]
[string]$jeden,
[parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true,Position=1)]
[string]$dwa
)
Process{write-host "$jeden PLUS $dwa"}
}
$Params = [pscustomobject]#{
jeden = “Hello”
dwa = “There”
}
$Params |Test
OUTPUT:
Hello PLUS There
Variable assigning can be skipped and [pscustomobject] can be piped directly.

Have u tried to pass both strings as single object? Seems its ur pipeline is treating dem as 2 obj...
#("one", "two") | Test
EDIT. Try to define test in order to accept array:
function Test {
[cmdletbinding()]
param(
[parameter(ValueFromPipeline=$true, Mandatory=$true,Position=0)]
[string[]]$strings
)
Process {
write-host "$($strings[0]) PLUS $($strings[1])"
}
}

Related

Looping over a pipeline parameter - what is the point?

In a tutorial made by Microsoft there is a code snippet similar to the following (edited the original to reduce distraction):
function Test {
param (
[Parameter(ValueFromPipeline)]
[string[]]$Params
)
process {
foreach ($Param in $Params) {
Write-Output $Param
}
}
}
In all previous examples however, the process block itself was already used as a loop body. To my understanding, the following simplified code should be equivalent:
function Test {
param (
[Parameter(ValueFromPipeline)]
[string[]]$Params
)
process {
Write-Output $Params
}
}
Indeed, no matter what I pipe to it, the results are the same. However, the fact that it appeared in a first party tutorial makes me believe that there might be some actual reason for using the loop.
Is there any difference in using one pattern over the other? If yes, what is it? If no, which one is the preferred one?
Just in case my simplification is off, here is the original example:
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
PROCESS {
foreach ($Computer in $ComputerName) {
Test-WSMan -ComputerName $Computer
}
}
}
The Point
There is quiet a difference in what you passing on to the next cmdlet in the pipeline (in your case Test-WSMan):
function Test1 {
param (
[Parameter(ValueFromPipeline)]
[string[]]$Params
)
process {
foreach ($Param in $Params) {
Write-Output ('Param: {0}, ToString: {1}, Count: {2}' -f $Param, "$Param", $Param.Count)
}
}
}
1,2,#(3,4) |Test1
Param: 1, ToString: 1, Count: 1
Param: 2, ToString: 2, Count: 1
Param: 3, ToString: 3, Count: 1
Param: 4, ToString: 4, Count: 1
function Test2 {
param (
[Parameter(ValueFromPipeline)]
[string[]]$Params
)
process {
Write-Output ('Param: {0}, ToString: {1}, Count: {2}' -f $Params, "$Params", $Params.Count)
}
}
1,2,#(3,4) |Test2
Param: System.String[], ToString: 1, Count: 1
Param: System.String[], ToString: 2, Count: 1
Param: System.String[], ToString: 3 4, Count: 2
In other words, in the second example, you actually pass a string array ([String[]]) to Test-WSMan. As Test-WSMan actually requires a single string ([[-ComputerName] <String>]), PowerShell will conveniently Type Cast the string array (String[]) to a single string type (String).
Note that in some instances this might go wrong as in the second example if you e.g. (accidently) force an array (#(3,4)) in the pipeline. In that case multiple items in the array will get joined and passed to the next cmdlet.
Use Singular Parameter Names
In general, it is (strongly) recommended to Use Singular Parameter Names which usually also implies that you expect a single String for each pipeline item (e.g. a $ComputerName at the time):
Avoid using plural names for parameters whose value is a single element. This includes parameters that take arrays or lists because the user might supply an array or list with only one element.
Plural parameter names should be used only in those cases where the value of the parameter is always a multiple-element value. In these cases, the cmdlet should verify that multiple elements are supplied, and the cmdlet should display a warning to the user if multiple elements are not supplied.
This would mean for your second example:
function Test2 {
param (
[Parameter(ValueFromPipeline)]
[string]$Param
)
process {
Write-Output $Param
}
}
Where your own (rather than the invoked Test-WSMan) cmdlet will than produce the error:
1,2,#(3,4) |Test2
Test2: 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.

How can you mock an ErrorRecord with Pester in Powershell?

I have a function that I would like to include in my unit tests but I'm unable to figure out how. The method takes in an ErrorRecord object (from an Invoke-RestMethod call), grabs the status code and generates a custom error message. Here is what the first portion of the call looks like:
function Resolve-RestError {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
$RequestError
)
$statusCode = $requestError.Exception.Response.StatusCode.value__
switch ($statusCode) {
The problem that I am running into is in the recreation of the ErrorRecord object. I have spent many hours looking for a way to recreate the error object, but I am not able to recreate it exactly.
In the actual ErrorRecord object during execution, accessing the Response.StatusCode field returns a string with a description of the code. To access the actual value you need to call Response.StatusCode.value__. It seems that it's not possible to reconstruct an internal .value__ variable as this is done by the JIT compiler (from my understanding). Is there a way to either add a .value__ property to a custom object, or simply mock that variable during my tests?
You're right it doesn't seem overly simple to recreate an ErrorRecord object. My goto for this sort of thing is to simulate the output I want to Mock in my test and then store that result via Export-CliXml but when trying this with an ErrorRecord you only get as far as Exception.Response and the rest is lost in the conversion (even when setting a -Depth value).
There might be a smarter way, but one workaround would be to just fake the object as follows (note you didn't provide your full Function so I just created a simplified version of it to prove it worked for this specific case):
function Resolve-RestError {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
$RequestError
)
$statusCode = $RequestError.Exception.Response.StatusCode.value__
Return $statusCode
}
Describe 'Resolve-RestError' {
It 'Should handle a 404' {
$ErrorRecord = [pscustomobject]#{
Exception = #{
Response = #{
StatusCode = #{
value__ = 404
}
}
}
}
Resolve-RestError -RequestError $ErrorRecord | Should -Be 404
}
}

Pass an argument from a module

I need help understanding how to pass on an argument from an imported module.
The module contains some custom arguments such as -one, -two, -three
I am trying to make a GUI using the commands from the module.
eg. If "One" is selected from the drop down menu, pass through the -one command.
However when I do so (using the example below), I get the error: "A positional parameter cannot be found that accepts argument '-one'."
I can see that using the code below, it adds single quotations around the command which probably breaks it.
I know I can run an IF statement (eg if combobox.text = "one", do this), however I would prefer to use a variable instead of having to make multiple if statements or a loop. The use of a variable seems like a simpler option.
I'm learning this language as I go so I'm not quite there yet with the knowledge :)
Thanks for any help. Hope this made sense.
$variable = $comboboxNumbers.Text
#example One is selected from the dropdown
Custom-ADCommand -identity "username" $variable
Below is simple example method:
function Set-SwitchParams {
[CmdletBinding()]
param (
# Parameter help description
[Parameter(Mandatory = $false)]
[switch]
$SwitchA,
[Parameter(Mandatory = $false)]
[switch]
$SwitchB
)
begin {
}
process {
}
end {
if ($SwitchA){
Write-Host "SwitchA is activated"
}
if ($SwitchB){
Write-Host "SwitchB is activated"
}
}
}
Put the method in a PS1 file, e.g. SwitchPlayground.ps1. Then source the file in PowerShell via:
. .\SwitchPlayground.ps1
Afterward, you can play around with the command, e.g.:
Set-SwitchParmas -SwitchA
I'd suggest studying the following links:
about functions basic
about functions advanced
about function parameters
Hope that helps.
An If statement if probably much nicer, but its possible to create a string and then execute the string in powershell.
As a simple example take this string
$string = '#("test","hello","whats up")'
I can then execute it and use it to create an array
$array = invoke-expression $string
Which will create an array with "test", "hello" and "whats up" and store it in $array
PS C:\temp> $string = '#("test","hi","what")'
PS C:\temp> $array = Invoke-Expression $string
PS C:\temp> $array
test
hi
what

How to output the referenced value?

I have the following function.
function Params {
param (
[Parameter(Mandatory = $true)]
[Alias('Param1')]
[AllowNull()]
${Param1[default value]}
)
[ref] $Param1 =
if (${Param1[default value]}) {
${Param1[default value]}
} else {
'default'
}
}
Params
$input1 = $null
"the param input is $([ref]$input1)"
If i input something for parameter on prompt or if i leave it to default value, i get this as output for $([ref]$input)
the param input is System.Management.Automation.PSReference`1[System.Management.Automation.LanguagePrimitives+Null]
Why am i not getting a value instead?
I want this output for example:
the param input is default
I ended up resorting to a different method to achieve what i want:
Defining this at the top of script:
[CmdletBinding()]
Param(
$Param1 = (Read-Host -prompt "Param1")
)
if (!$Param1) { "default" }
"the param input is $Param1"
The [ref] type accelerator (it's not a type accelerator in the usual sense, but it does create PSReference objects, so... it sort of is) gets you, as it tells you, a PSReference object.
In order to retrieve the value from it, you'd need to ask for it specifically. In your code, you can access it by pulling the Value property from the created reference object.
"the param input is $(([ref]$input1).Value)"
However, given that $input1 isn't assigned to, you might have to refactor a bit to get what you're after.

powershell mandatory parameter with default value shown

I am looking for a way to have an PowerShell script ask for an parameter which needs to be mandatory, but shown with an default value, e.g.:
.\psscript
Supply values for the following parameters:
parameter1[default value]:
parameter2[1234]:
I want to ask for input but provide some default values.
If I use the mandatory option it asks for the values nicely but doesn't show the default value or process the given value. If I don't make it mandatory then PowerShell doesn't ask for the value at all.
Here's some script examples I tried:
[CmdletBinding()]
Param(
[parameter(Mandatory=$true)] $SqlServiceAccount = $env:computername + "_sa",
[parameter(Mandatory=$true)] $SqlServiceAccountPwd
)
This script asks for parameters but does not show or process the default value if I just press enter on the first parameter.
[CmdletBinding()]
Param(
[parameter(Mandatory=$false)] $SqlServiceAccount = $env:computername + "_sa",
[parameter(Mandatory=$true)] $SqlServiceAccountPwd
)
This script doesn't ask for the first parameter, but processes the default value.
Here's a short example that might help:
[CmdletBinding()]
Param(
$SqlServiceAccount = (Read-Host -prompt "SqlServiceAccount ($($env:computername + "_sa"))"),
$SqlServiceAccountPwd = (Read-Host -prompt "SqlServiceAccountPwd")
)
if (!$SqlServiceAccount) { $SqlServiceAccount = $env:Computername + "_sa" }
...
By definition: mandatory parameters don't have default values. Even if you provide one, PowerShell will prompt for value unless specified when the command is called. There is however a 'hacky' way to get what you ask for. As variables (and as consequence - parameters) can have any name you wish, it's enough to define command with parameters that match the prompt you would like to see:
function foo {
param (
[Parameter(Mandatory = $true)]
[Alias('Parameter1')]
[AllowNull()]
${Parameter1[default value]},
[Parameter(Mandatory = $true)]
[Alias('Parameter2')]
[AllowNull()]
${Parameter2[1234]}
)
$Parameter1 =
if (${Parameter1[default value]}) {
${Parameter1[default value]}
} else {
'default value'
}
$Parameter2 =
if (${Parameter2[1234]}) {
${Parameter2[1234]}
} else {
1234
}
[PSCustomObject]#{
Parameter1 = $Parameter1
Parameter2 = $Parameter2
}
}
When called w/o parameters, function will present user with prompt that match parameter names. When called with -Parameter1 notDefaultValue and/or with -Parameter2 7, aliases will kick in and assign passed value to the selected parameter. As variables named like that are no fun to work with - it makes sense to assign value (default or passed by the user) to variable that matches our alias/ fake parameter name.
I'd do it this way
param(
[Parameter(Mandatory)][string]$aString
)
if([string]::IsNullOrWhiteSpace($aString))
{
$aString = "A Default Value"
}
In my opinion, if you're using Read-Host in a param() block, then you're doing something wrong. At that point, what's the point of using param() at all?
There isn't a way to do what you want with a mandatory parameter and powershell prompting for you.
You would instead have to make it optional (remove mandatory), then implement the prompting code yourself (Read-Host, but take blank response as a default; something like that).