Here's what's going on in PowerShell when you are using the pipeline with arrays.
Take, for example, this case:
@(
@('single-element-array') |
ForEach-Object { "$_".Trim() } |
Sort-Object -Unique
) | ConvertTo-Json
The result will simply be:
"single-element-array"
First, that code above looks silly, but, assume that you had queried for some data and the result was a single element array, whose element may consist of whitespace or is $null
, and you wanted to ensure that the value collapses to ''
, hence the "$_".Trim()
. OK, with that out of the way, the resulting JSON is probably not what one would expect coming from other programming languages such as C#.
Much as described by the other answers, since this expression flows through a pipeline, the same rules as described by others apply, and the resulting object is "unwrapped".
Typically, a function implements a pipeline as follows:
function Some-Function {
[CmdletBinding()]
Param (
[Parameter(Mandatory, ValueFromPipeline)]
[object[]] $InputObject
)
Begin {
# This block is optional
}
Process {
# This block is optional, too, but is required
# to process pipelined objects
foreach ($o in $Object) {
# do something
}
}
End {
# This block is optional IF a Process block
# is defined.
# If no Begin, Process, or End blocks are defined,
# the code after the parameters is assumed to be
# an End block, which effectively turns the function
# into a PowerShell 2.0 filter function.
# When data is piped into this function, and a Process
# block exists, the value of $InputObject in this block
# is the very last item that was piped in.
# In cases where the pipeline is not being used, $InputObject
# is the value of the parameter as passed to the function.
}
}
To use the above:
$Stuff | Some-Function
In a function like this, using the pipeline, values are processed serially. Each value in $Stuff
is piped into the function. If the function has a Begin
block, that runs once, before any value is processed from parameters passed in via the pipeline, i.e. other non-pipelined parameter values are available in the Begin
block. The Process
block is then executed once for each value pipelined in. Finally, if the function has an End
block, that is run at the very end after all values piped in have been processed.
So, why use a foreach
statement in the Process
block? To handle when the function is called thusly:
Some-Function -InputObject $Stuff
In this invocation, the entire array of $Stuff
is passed by parameter to the function. So, in order to process the data correctly, a foreach
loop in the Process
block is used. And this should now give you the information you need to know how to circumvent the problem in my first example. In order for that example to run correctly, it needs to be done as follows:
ConvertTo-JSON -InputObject @(
@('single-element-array') |
ForEach-Object { "$_".Trim() } |
Sort-Object -Unique
)
And this results in the expected JSON:
[
"single-element-array"
]
Armed with this knowledge, to fix your code so that it works as you expect, you should do the following:
# Get-WmiInstance is deprecated in favor of GetCimInstance
# It's generally a good idea to avoid aliases--they can be redefined!
# That could result in unexpected results.
# But the big change here is forcing the output into an array via @()
$serverIps = @(
Get-CimInstance Win32_NetworkAdapterConfiguration
| Where-Object { $_.IPAddress }
| Select-Object -ExpandProperty IPAddress
| Where-Object { $_ -like '*.*.*.*' }
| Sort-Object
)
# Don't use length. In certain circumstances (esp. strings), Length
# can give unexpected results. PowerShell adds a "synthetic property"
# to all "collection objects" called Count. Use that instead, as
# strings (oddly enough, since they are IEnumerbale<Char>) don't get a
# Count synthetic property. :/
if ($serverIps.Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
Hope that helps to clarify the issue.