ChatGPT解决这个技术问题 Extra ChatGPT

How can I force Powershell to return an array when a call only returns one object?

I'm using Powershell to set up IIS bindings on a web server, and having a problem with the following code:

$serverIps = gwmi Win32_NetworkAdapterConfiguration 
    | Where { $_.IPAddress } 
    | Select -Expand IPAddress 
    | Where { $_ -like '*.*.*.*' } 
    | Sort

if ($serverIps.length -le 1) {
    Write-Host "You need at least 2 IP addresses for this to work!"
    exit
}

$primaryIp = $serverIps[0]
$secondaryIp = $serverIps[1]

If there's 2+ IPs on the server, fine - Powershell returns an array, and I can query the array length and extract the first and second addresses just fine.

Problem is - if there's only one IP, Powershell doesn't return a one-element array, it returns the IP address (as a string, like "192.168.0.100") - the string has a .length property, it's greater than 1, so the test passes, and I end up with the first two characters in the string, instead of the first two IP addresses in the collection.

How can I either force Powershell to return a one-element collection, or alternatively determine whether the returned "thing" is an object rather than a collection?

Most single-handedly annoying / bug-ridden aspect of PowerShell..
I deem your example as overcomplicated. Simpler question: <<$x = echo Hello; $x -is [Array]>> yields False.
was this behaviour changed in powershell 5? i've got a similar issue that i can't reproduce on 5, but can on 4

J
JNK

Define the variable as an array in one of two ways...

Wrap your piped commands in parentheses with an @ at the beginning:

$serverIps = @(gwmi Win32_NetworkAdapterConfiguration 
    | Where { $_.IPAddress } 
    | Select -Expand IPAddress 
    | Where { $_ -like '*.*.*.*' } 
    | Sort)

Specify the data type of the variable as an array:

[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration 
    | Where { $_.IPAddress } 
    | Select -Expand IPAddress 
    | Where { $_ -like '*.*.*.*' } 
    | Sort

Or, check the data type of the variable...

IF ($ServerIps -isnot [array])
{ <error message> }
ELSE
{ <proceed> }

Wrapping a command in @(...) will return an array even if there are zero objects. Whereas assigning the result to an [Array]-typed variable will still return $null if there are zero objects.
Just a note that none of these solutions work if the object being returned is a PSObject (possibly others).
@Deadly-Bagel Can you show example of this? For me @(...) work properly (produce result I expect it should produce) for any types of objects.
Funny how you end up back on the same questions. I had (and have again) a slightly different problem, yes as in the question this works fine but when returning from a function it's a different story. If there's one element, the array is ignored and only the element is returned. If you put a comma before the variable it forces it to an array but a multi-element array will then return a two-dimensional array. Very tedious.
Gah, this is what happened last time too, now I can't replicate it. At any rate I solved my recent problem by using Return ,$out which seems to always work. If I run into the problem again I'll post an example.
S
Shay Levy

Force the result to an Array so you could have a Count property. Single objects (scalar) do not have a Count property. Strings have a length property so you might get false results, use the Count property:

if (@($serverIps).Count -le 1)...

By the way, instead of using a wildcard that can also match strings, use the -as operator:

[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration -filter "IPEnabled=TRUE" | Select-Object -ExpandProperty IPAddress | Where-Object {($_ -as [ipaddress]).AddressFamily -eq 'InterNetwork'}

For this couldn't he also just check the data type with -is?
Strings have a .length property - that's why it's working... :)
The @($serverIps).Count is unnecessary. Use the $serverIps.Count just works.
L
Luckybug

You can either add a comma(,) before return list like return ,$list or cast it [Array] or [YourType[]] at where you tend to use the list.


just cast to array. so simple. awesome! best answer! this effing comma haunted me.
In a case where I'm always trying to return an array from a function, even if there's only one element, I'm only having success using the comma , (unary array operator).
So, what's the difference between ,(...), @(...) and [array](...). In the following example - $strArray|%{ $_ -split ' ' } - the only option that returns a list of arrays is $strArray|%{ ,($_ -split ' ') }. I'm running Powershell 5
J
JNK

If you declare the variable as an array ahead of time, you can add elements to it - even if it is just one...

This should work...

$serverIps = @()

gwmi Win32_NetworkAdapterConfiguration 
    | Where { $_.IPAddress } 
    | Select -Expand IPAddress 
    | Where { $_ -like '*.*.*.*' } 
    | Sort | ForEach-Object{$serverIps += $_}

I actually feel like this is the most clear and safe option. You can reliably use ".Count - ge 1' on the collection or 'Foreach'
Building arrays incrementally is bad practice as a new array is created and filled each cycle. This is dreadfully inefficient with large arrays. The current preferred option is to use a typed list object and .Add() $List = [System.Collections.Generic.List[string]]::new() # OR $list = New-Object System.Collections.Generic.List[string] $List.Add("Test1")
P
Patrick

You can use Measure-Object to get the actual object count, without resorting to an object's Count property.

$serverIps = gwmi Win32_NetworkAdapterConfiguration 
    | Where { $_.IPAddress } 
    | Select -Expand IPAddress 
    | Where { $_ -like '*.*.*.*' } 
    | Sort

if (($serverIps | Measure).Count -le 1) {
    Write-Host "You need at least 2 IP addresses for this to work!"
    exit
}

The Measure-Object is unnecessary. Use the $serverIps.Count just works.
The Count property is not available if there's only a single object. Which was the entire point of the original question. Measure-Object will guarantee a valid Count property, with multiple, single, or even $null objects.
As I tested, every object has a .Count property. function Get1 { return @('Hello') }; (Get1).GetType(); (Get1).Count.
I realized only PowerShell Core support .Count on almost every objects.
m
masato

Return as a referenced object, so it never converted while passing.

return @{ Value = @("single data") }

C
Community

I had this problem passing an array to an Azure deployment template. If there was one object, PowerShell "converted" it to a string. In the example below, $a is returned from a function that gets VM objected according to the value of a tag. I pass the $a to the New-AzureRmResourceGroupDeployment cmdlet by wrapping it in @(). Like so:

$TemplateParameterObject=@{
     VMObject=@($a)
}

New-AzureRmResourceGroupDeployment -ResourceGroupName $RG -Name "TestVmByRole" -Mode Incremental -DeploymentDebugLogLevel All -TemplateFile $templatePath -TemplateParameterObject $TemplateParameterObject -verbose

VMObject is one of the template's parameters.

Might not be the most technical / robust way to do it, but it's enough for Azure.

Update

Well the above did work. I've tried all the above and some, but the only way I have managed to pass $vmObject as an array, compatible with the deployment template, with one element is as follows (I expect MS have been playing again (this was a report and fixed bug in 2015)):

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
    
    foreach($vmObject in $vmObjects)
    {
        #$vmTemplateObject = $vmObject 
        $asJson = (ConvertTo-Json -InputObject $vmObject -Depth 10 -Verbose) #-replace '\s',''
        $DeserializedJson = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property @{MaxJsonLength=67108864}).DeserializeObject($asJson)
    }

$vmObjects is the output of Get-AzureRmVM.

I pass $DeserializedJson to the deployment template' parameter (of type array).

For reference, the lovely error New-AzureRmResourceGroupDeployment throws is

"The template output '{output_name}' is not valid: The language expression property 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Expression.Expressions.JTokenExpression' 
can't be evaluated.."

W
Will Huang

There is a way to deal with your situation. Leave most of you code as-is, just change the way to deal with the $serverIps object. This code can deal with $null, only one item, and many items.

$serverIps = gwmi Win32_NetworkAdapterConfiguration 
    | Where { $_.IPAddress } 
    | Select -Expand IPAddress 
    | Where { $_ -like '*.*.*.*' } 
    | Sort

# Always use ".Count" instead of ".Length".
# This works on $null, only one item, or many items.
if ($serverIps.Count -le 1) {
    Write-Host "You need at least 2 IP addresses for this to work!"
    exit
}

# Always use foreach on a array-possible object, so that
# you don't have deal with this issue anymore.
$serverIps | foreach {
    # The $serverIps could be $null. Even $null can loop once.
    # So we need to skip the $null condition.
    if ($_ -ne $null) {
        # Get the index of the array.
        # The @($serverIps) make sure it must be an array.
        $idx = @($serverIps).IndexOf($item)

        if ($idx -eq 0) { $primaryIp = $_ }
        if ($idx -eq 1) { $secondaryIp = $_ }
    }
}

In PowerShell Core, there is a .Count property exists on every objects. In Windows PowerShell, there are "almost" every object has an .Count property.