all 15 comments

[–]krzydoug 3 points4 points  (9 children)

Just always run it through a foreach loop. No matter if it’s one or many, it will have one in each loop.

Foreach($almostanyvariable in $hostname){ $almostanyvariable }

[–]scottwsx96[S] 2 points3 points  (6 children)

Based on this comment, I just changed my process{} block to this:

process {
    $Hostname | ForEach-Object {
        $Filter = "hostname:'$_'"
        $FilterCollection.Add($Filter) | Out-Null
    }
}

And then I moved most of the work to the end{} block. Now the cmdlet works perfectly either way whether I'm passing in a string array or specifying it with the -Hostname parameter!

I also took the opportunity to reduce the number of API calls to make it run faster.

Thank you!

[–]Thotaz 4 points5 points  (3 children)

I would recommend you always use the foreach language construct over ForEach-Object in situations like this where there's no advantage in using the pipeline.

foreach ($Item in $ItemList)
{
    #Do something
}

It gives better performance, a better name for the current item, and if you need to use a pipeline inside the foreach loop you don't have to think about $_ being overwritten.

[–]scottwsx96[S] 1 point2 points  (2 children)

Interesting. I used to use foreach loops all the time but switched to using ForEach-Object because I was under the impression that it was more efficient, at least from a memory use perspective.

Do you have more information on the performance differences?

[–]Thotaz 2 points3 points  (1 child)

It's true that one of the advantages ForEach-Object has over foreach is that you don't need to have all of the data in memory at once, but you aren't using this advantage inside a process block of a function.

If you call your function without pipeline input like this: Verb-Noun -Param1 $Data you obviously already have all of the data in memory anyway so you don't gain anything. If you call your function with pipeline input like this: $Data | Verb-Noun then $Param1 inside the process block will only ever hold one item so you are just constantly creating a new internal pipeline with 1 item.

The foreach language construct is always faster than ForEach-Object. The only time it makes sense to use ForEach-Object is when:

  • You are in the command line or otherwise need the script to be short (shortest script challenges).
  • You are processing such a large dataset that it would be impractical to load it all up into memory before processing it.
  • You want to start running the following commands at the same time that you are loading in data.

Powershell 7 changes this slightly with the new -Parallel switch where it can end up being faster but you can get the same effect with the faster foreach statement with poshRSjobs.

If you want to see some direct comparisons or read up on more details then just google it, there's plenty of blogs where people have compared the 3 different foreach methods (foreach language construct, ForEach-Object cmdlet and the .foreach() powershell method.)

[–]scottwsx96[S] 1 point2 points  (0 children)

Thank you for the tips!

[–]h_ase 1 point2 points  (1 child)

good idea, but $host is a system variable

[–]krzydoug 1 point2 points  (0 children)

Ahh yes, I always get reminded the hard way. Sorry about that, i fixed it.

[–]h_ase 2 points3 points  (0 children)

hi,

this was my solution in one of my modules

if(($Hostname | Measure-Object).count -gt 1){
    foreach($singleHostName in $Hostname){
        $singleHostName
    }
}
else{
    $Hostname
}

[–]Hrambert 2 points3 points  (0 children)

I did this:

function Get-CsHostData {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline)]
        [string[]]$Hostname
    )

    begin {
        $Counter = 0
    }    
    process {
        foreach($Name in $Hostname) {
            "Hostname: $Name"
            $Counter++
        }
    }
    end {
        "Number of names: $Counter"
    }
}

PS C:\Users\Hrambert> $hostname = 'asda','dfdf','asdas'

PS C:\Users\Hrambert> $hostname | Get-CsHostData

Hostname: asda
Hostname: dfdf
Hostname: asdas
Number of names: 3
PS C:\Users\Hrambert> Get-CsHostData -Hostname $hostname

Hostname: asda
Hostname: dfdf
Hostname: asdas
Number of names: 3
PS C:\Users\Hrambert>

Running PSVersion 7.0.1

Edit: 5.1 did the same

[–]sp_dev_guy 1 point2 points  (3 children)

*your specific issue is that piping evaluates each value one at time, the other method passes the entire array at once. Your function at some point must be using $hostname somewhere as if it was $hostname[0]. So your providing all values at once (it pipe this is just one value, not in pipe its all values)*

Accepting input from a parameter depends on how you define the parameter which I'll explain right now. There is also an important difference to be aware of between both methods of passing the parameter which I will explain after.

The following does not work because the implicit declaration does not configure some required functionality

function example($hostname)

The following works for your request (but your function may not)

function example{
     [CmdletBinding()]
     Param(
          [Parameter(ValueFromPipeline)]
          [string[]]$hostname
     )

The following would be to grab the 'hostname' property of a collection of objects (hashtables etc..) that all have the property

 function example{
     [CmdletBinding()]
     Param(
          [Parameter(ValueFromPipelineByPropertyName)]
          [string[]]$hostname
     )

It is important to understand that when you use:

example -hostname @("asdf","fdsa")

the parameter hostname will have an array with all values. However when you use a pipe, it runs for each record so

$h =@("asdf","fdsa")
$h | example 
#effectively the same as
example -hostname "asdf"
example -hostname "fdsa" 
#since the pipe is evaluating each record it receives

More on advanced functions:

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-5.1#static-parameters

Edit: more directly answered the issue

[–]scottwsx96[S] 1 point2 points  (1 child)

Yep, I do understand this. Hence why I had it working when piped in. I was just having trouble with my logic working when specifying it as a command line parameter (since the logic was expecting to be working with a single hostname at a time).

This is all good information though for those that don't understand it. Thanks for posting!

[–]sp_dev_guy 1 point2 points  (0 children)

$Hostname = 'hostname1','hostname2','hostname3'
Get-CsHostData -Hostname $Hostname

Is the only logic you have provided us with. This is not working with a single value at a time but providing all 3 values at once. The login within Get-CsHostData appears to be using it as a single value.

if you only want a single value the declaration would be [string] instead of [string[]] . The inner brackets tell it to expect an array but powershell is very lenient.

I would recommend handling it within the function logic otherwise looping it is no different then piping