4

Observe this example:

PS C:\> $list=("A","B")
PS C:\> $list.GetType().Name
Object[]
PS C:\> ($list | ? {$_ -ne "C"}).GetType().Name
Object[]
PS C:\> ($list | ? {$_ -ne "B"}).GetType().Name
String

Why on earth is Powershell converting my array to a String when there's only one element left? It breaks further processing of the variable in my script where I assume the output is an Array. How can I prevent it?

3
  • Force it to be an array with @(..) Commented Apr 26, 2021 at 7:50
  • I changed line 1 to be $list=@("A","B") but that didn't help. Commented Apr 26, 2021 at 7:55
  • Ah, @($list | ? {$_ -ne "B"}).GetType().Name works. Write that as an answer and I'll accept it Commented Apr 26, 2021 at 7:56

3 Answers 3

3

To explain why Powershell seemingly converts an array to a string is caused by the result of Where-Object / %'s filtering and data it returns.

When the first version filters elements not equal to "C", the result is a collection. There were more than one matching member: "A" and "B". Thus, a collection is needed to contain both values.

The second version returns just a single object, "A", and thus it's a string instead of a collection. Since there is just a single object, there is no need to return an array with one element - just the object itself.

Theo's answer works by making Where-Object's output to be an array. The array sub-expression operator @() can be used to create arrays. Like so,

# Convert where-object's output into an array,
# even if there is only a single object.
@($list | ? {$_ -ne "B"}) 
Sign up to request clarification or add additional context in comments.

1 Comment

Could you please include Theo's answer as your own answer so that when people visit this question they don't need to look up references?
3

To complement my comment and vonPryz's answer

PowerShell 'unravels' one-element arrays when returning from a function or scriptblock into a single scalar, in this case a string.

To prevent that from interfering with the rest of the code which expects an array, no matter if it is empty or has only one element, you can force the output to become an array by wrapping it inside @().

In your case,

($list | ? {$_ -ne "B"})

would return the single element A, turned into a scalar String, while

@($list | ? {$_ -ne "B"}).GetType().Name

returns an array (Object[]) with just one element in it.

Comments

1

While what the other two answers say usually works, it won't work in all cases.

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

Hope that helps to clarify the issue.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.