Looping over a pipeline parameter - what is the point? - powershell

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.

Related

Pass multiple parameters to function by pipeline

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])"
}
}

how to use hashtable to pass parameters to a powershell cmd that uses double dashes before the parameters

i m writing a script that makes use of a cmdlet from a console app, let's call the cmdlet as cmdA. I have no control over implementation of cmdA. cmdA takes parameters with a double dash (cmdA --param1 value1 --param2 value2 --param3 value3 --param4 value4 --param5 value5)
Now param2, param3, param4 and param5 are optional. The user of my script may or may not provide values for these optional parameters. In the script, i formed a hashtable using the parameters for which the user provided values. Let's call it paramHashtable.
I am executing cmdA #paramHashtable but this is not working. I think it has to do something with double dashes because this approach works fine with a cmd that takes parameters with single dash
What would be an efficient way to pass parameters to the cmdA (an inefficient way would be to use many if blocks to check which values were provided and make calls accordingly)
edited*
This is how i m creating the hashtable :-
$optionalParameters = #{}
$optionalParameters = ParameterSelection ${OutputFolder} ${PerfQueryIntervalInSec} ${StaticQueryIntervalInSec} ${NumberOfIterations}
$requiredParameters = #{"sqlConnectionStrings " = "$SqlConnectionStrings"}
$parameters = $requiredParameters + $optionalParameters
function ParameterSelection ([string] $outputFolder, [string] $perfQueryIntervalInSec, [string] $staticQueryIntervalInSec, [string] $numberOfIterations)
{
$parametersList = #{}
if($outputFolder -ne "")
{
$parametersList["outputFolder "] = $outputFolder
}
if($perfQueryIntervalInSec -ne "")
{
$parametersList["perfQueryIntervalInSec "] = $perfQueryIntervalInSec
}
if($staticQueryIntervalInSec -ne "")
{
$parametersList["staticQueryIntervalInSec "] = $staticQueryIntervalInSec
}
if($numberOfIterations -ne "")
{
$parametersList["numberOfIterations "] = $numberOfIterations
}
return $parametersList
}
This is how i m calling it :-
& $ExePath actionName #parameters
The $ExePath has the path of the program to be executed
The actionName takes parameters like this:-
actionName --sqlConnectionStrings "Data Source=Server1" --outputFolder C:\Output
Can splatting with hash table work on cmdlets / functions where it's parameter have dashes?
It may work, but it is definitely not a good idea to have parameter names with dashes as this will result in a function / cmdlet where named parameters cannot be used, PowerShell binds the arguments positionally! (thanks mklement0 for pointing this out):
function Test-Splatting {
param(${-param1}, ${-param2})
"${-param1} ${-param2}"
}
$param = #{ '--param1' = 'hello'; '--param2' = 'world' }
Test-Splatting #param # => hello world
Example of what was mentioned before using the same function above:
# --param1 is bound positionally and not interpreted as a parameter:
Test-Splatting --param1 hello # => --param1 hello
As for an external programs, the linked answer in comments explains very well the approach you could take using a wrapper function and the use of the automatic variable $args:
function myfunc {
$binaryPath = 'path/to/file.exe'
& $binaryPath actionName $args
# or #args we can't be sure until testing
}
myfunc --sqlConnectionStrings "Data Source=Server1" --outputFolder C:\Output
As noted in the comments, because you are calling an external program, you should use array-based splatting or simply arrays directly to pass programmatically constructed arguments.
By contrast, hashtable-based splatting is usually only helpful when calling PowerShell commands (while it technically works with external programs too, the resulting parameter format (e.g. -foo:bar or -foo:"bar none") is unusual and understood by few external programs)
Note that the parameter names and values must be passed as separate array elements and the elements representing parameter names must include the - or -- prefix; e.g., consecutive array elements
'--sqlConnectionStrings' (the name) and
'Data Source=Server1' (the value).
PowerShell then constructs a command line for invocation of the external program behind the scenes, space-separating the array elements and double-quoting elements with embedded spaces on demand; the above example turns into the following:
--sqlConnectionStrings "Data Source=Server1"
Note that it is up to the target program to parse the single string that is its command line in a way that recognizes parameter name-value pairs.
# Get the array of *optional* parameter names and values from your helper function.
$optionalParameters = ParameterSelection ${OutputFolder} ${PerfQueryIntervalInSec} ${StaticQueryIntervalInSec} ${NumberOfIterations}
# Declare the *required* parameter(s) as an array.
$requiredParameters = '--sqlConnectionStrings', $SqlConnectionStrings
# Pass the arrays as-is to the external program.
# (Empty arrays are effectively ignored.)
& $ExePath actionName $requiredParameters $optionalParmeters
Your ParameterSelection function can be simplified as follows:
function ParameterSelection ([string] $outputFolder, [string] $perfQueryIntervalInSec, [string] $staticQueryIntervalInSec, [string] $numberOfIterations)
{
# Loop over all bound parameters.
$PSBoundParameters.GetEnumerator() | ForEach-Object {
# Emit the parameter name prefixed with '--' and the value.
# PowerShell automatically collects the output strings in an array
# when you assign a call to this function to a variable.
'--' + $_.Key
$_.Value
}
}

I am trying to convert the "Return First recurring character in a string problem" from python to powershell

I have completed coding this same problem in python and was trying to generate a similar logic or atleast achieve a similar result in powershell.
Python Code-
def FRC(str):
h = {}
for ch in str:
if ch in h:
return ch
else:
h[ch] = 0
return '\0'
print(FRC("abcdedcba"))
I have tried a few possible codes and was able to only enumerate the array of characters to count their occurrences. Thank you for any suggestions.
Update1 - The code I have worked on is as follows:
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$teststring
)
$hash = #()
$teststring = $teststring.ToCharArray()
foreach ($letter in $teststring)
{
if($letter -contains $hash){
return $letter
}else {
$hash = $hash + $letter
}
return "\0"
}
}
get-duplicatechar("saahsahh")
You could use the (.Net) HashSet class for this, which Add method (besides adding the value,) returns true if the element is added to the HashSet<T> object and false if the element is already present.
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$str
)
$h = [System.Collections.Generic.HashSet[char]]::new()
foreach ($ch in $str.ToCharArray()) {
if(!$h.add($ch)) { return $ch }
}
}
Here's a working version using your code as base:
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$teststring
)
$hash = #{}
$CharArray = $teststring.ToCharArray()
foreach ($letter in $CharArray) {
if($letter -in $hash.Keys) {
$letter
break
}
else {
$hash[$letter] = $null
}
}
}
One problem is that you are strongly typing $teststring to be a string, so when you add a character array later PowerShell just converts it into a string and thus $teststring remains a string (try $teststring.GetType() after $teststring = $teststring.ToCharArray() to see this for yourself).
One way to solve this is to do what I did and use a different variable for the character array. You could also solve it by changing the variable to a character array directly by replacing [string]$teststring with [char[]]$teststring, that way any strings input to the function will be automatically cast as a character array.
The next mistake is using -contains where you need -in. The letter doesn't contain the array, you're looking for the letter in the array, just like you did in Python.
You can drop the return keyword entirely, PowerShell does not need it. Any output in your function will be output from the function automatically.
You also call your collection variable "hash", but you made an array. I changed it to be an actual hashtable, just like in your Python code. I also changed the way we add to it to more closely reflect what you did in Python. There are many ways to do this, this is just one. Notice we'll need to add ".Keys" in our if-statement as well so we check for keys matching our letter.
I think that's it, ask if anything is unclear.

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 - Array Range from user input

What would be the easiest way to get user input for an array range.
For example:
function MyArrayOfMachines {
[CmdletBinding()]
param(
[parameter(Mandatory=$true)][string]$Machine
# What should I assign the $Range variable as?
)
# Hardcoded range. User should be able to enter the range
$Range = 2..5
for ($i=0; $i -lt $array.length; $i +=1)
{
$result = $array[$i]
$output = $machine+$result
$output
}
}
The above function should take the input as the name of the machine and the array range. For now I have the array range hardcoded. When I assign $Range as [Array]$Range in the user prompt, there is a prompt for $Range[0] etc etc. But I would like the user the enter the range.
Doesn't this work? Unless I misunderstood your question...
function test($range){
$range
}
test -range (1..5)
You can also accept the range as a string and parse it yourself:
function Test
{
param($range)
if($range -is [String])
{
[int]$start, [int]$end = $range.split('.', [StringSplitOptions]::RemoveEmptyEntries)
$start..$end
}
else
{
$range
}
}
The reason for the if / else is for cases where the user passes an actual range, as in manojlds answer, rather than a string to be parsed (like 1..5). This means you can't strongly type the param though.
Make it two parameters:
function test{
param ( [int]$st,
[int]$end)
$Range = $st..$end
$Range
}
test 1 5
If they input the start and end of the range you can use that to create it dynamically in the function.
EDIT:
To get the range from a string, try:
function test{
param ($Range)
$NewRange = $Range.substring(0,($Range.indexof('.')))..$Range.substring(($Range.lastindexof('.') + 1))
$NewRange
}
test 1..5
I agree with #manojlds, the range should be passed in as an array. Parsing a string limits the possibilities of what a user could enter. By using [int[]] you can force the user to specify an array of integers. This would also allow a user to specify a broken range such as ((2..4)+(6..12)) which is harder to allow for when parsing strings.
In your example I'm not sure where $array is coming from, and you only need one line to return a computed machine name.
function MyArrayOfMachines {
param(
[parameter(mandatory=$true)]
[string] $machine,
[parameter(mandatory=$true)]
[int[]] $range
)
foreach($n in $range) {
$machine+$n
}
}
You could create a single machine name,
MyArrayOfMachines Laptop 1
a range of machines,
MyArrayOfMachines Workstation (2..10)
or a non-consecutive array of machines
MyArrayOfMachines Server ((2..3)+(5..9))
You could just pass a string and evaluate it:
function Test([string]$range) {
if ($Range -match '^\d+\.\.\d+$') {
$RangeArray = Invoke-Expression $Range
} else {
$RangeArray = 1..5
}
}
Some minimal validation is done to ensure that the user cannot pass arbitrary code.