56

Consider the following Powershell script, which searches for folders in C:\ with a 'og' in their name:

PS C:\> (ls | %{$_.Name} | ?{$_.Contains("og")})
PerfLogs
Program Files
setup.log

Now I narrow down the search to get only one item:

PS C:\> (ls | %{$_.Name} | ?{$_.Contains("Prog")})
Program Files

The strange thing is that the first operation yields an array, whereas the second operation (which is IMHO semantically the same operation, so it should yield the same type of result) yields a string. This can be seen in the following result:

PS C:\> (ls | %{$_.Name} | ?{$_.Contains("og")}).Length
3
PS C:\> (ls | %{$_.Name} | ?{$_.Contains("Prog")}).Length
13

This can be very irritating, since apparently there are less folders which match 'og' than those who match 'Prog'.

Evidently, PowerShell implicitly 'unboxes' a single-item array to a single object, and we never get an array of length 1. It seems that every time I want to count the results coming over the pipeline, I have to check if I'm dealing with an array or not.

How can I prevent this from happening? How do you deal with this?

Jonas Sourlier
  • 1,493
  • 2
  • 17
  • 29
  • These from StackOverflow may help: http://stackoverflow.com/questions/1827862/what-determines-whether-the-powershell-pipeline-will-unroll-a-collection http://stackoverflow.com/questions/1390782/jagged-powershell-array-is-losing-a-dimension-when-only-one-element-exists If you were not piping to `$_.Contains`, then `%{,,$_.Name}` works... – Bob Apr 20 '12 at 08:59

6 Answers6

75

Evidently, PowerShell implicitly 'unboxes' a single-item array to a single object,

And zero item results to $null.

How can I prevent this from happening?

You can't.

How do you deal with this?

Use the array constructor (@(...)) to force a collection (possibly with zero or one elements) return:

$res = @(ls | %{$_.Name} | ?{$_.Contains("Prog")})
Richard
  • 8,952
  • 3
  • 26
  • 27
  • 3
    Not sure you can "force" it. `@(1) | ConvertTo-Json` still returns `1` instead of `[1]`. – Marc Dec 06 '16 at 12:19
  • @Marc: [`ConvertTo-Json`](https://msdn.microsoft.com/powershell/reference/4.0/microsoft.powershell.utility/ConvertTo-Json) never returns a collection: it reads the whole input and converts to a single string. If you want input objects individually converted you'll need to process each separately. – Richard Dec 06 '16 at 13:05
  • 1
    @Richard, I think you misunderstand: I, and many others, basically want the entire object (i.e. collection) serialized (e.g. for external persistance). We are not interested in processing each object in the collection separately. ConvertTo-Json should return a string which, if run through, ConvertFrom-Json returns the original object albeit an empty array/collection. – Marc Dec 07 '16 at 08:07
  • 1
    @Marc The point of this question is to avoid the treatment of a single element array as that element (which is less of an issue due to subsequent PSH changes: note the question's date). You are talking about a completely different case (forcing a collection to be a single object) hence me misunderstanding. – Richard Dec 07 '16 at 08:12
  • 5
    `ConvertTo-Json -InputObject @(1)` outputs `[1]`. You need to explicitly wrap arrays into `@(...)` – SliverNinja - MSFT Mar 30 '17 at 15:12
  • In PowerShell 6.0.2 Core in Linux, `ConvertTo-Json -InputObject @(1)` outputs `[1]`, but `@(1) | ConvertTo-Json` outputs `1`. It appears that you must use data as a parameter for `ConvertTo-Json` if you want the expected results. – Dave F May 20 '18 at 02:31
  • `@(,@(1)) | ConvertTo-JSON` is an alternative if you really want to pipe the input in. – Arnavion Dec 24 '18 at 11:04
15

Note the difference between these two results:

PS C:\> ConvertTo-Json -InputObject @(1)
[
    1
]
PS C:\> @(1)|ConvertTo-Json
1
PS C:\>

The point is that the 'unboxing' is being done by the pipe operation. ConvertTo-Json still sees the object as an array if we use InputObject rather than piping.

Larry Young
  • 151
  • 1
  • 2
2

An alternative to this problem is to explicitly set the variable type to be an array like this:

[array]$res = (ls | %{$_.Name} | ?{$_.Contains("og")})
Vomit IT - Chunky Mess Style
  • 40,038
  • 27
  • 84
  • 117
1

This worked for me, but I had to declare the results in a new variable, or PowerShell just kept breaking down the array into a single object...most frustrating.

Thank you Microsoft for not considering the use case of JSON...insert seething sarcasm

$result = ConvertTo-Json -InputObject @($body) -compress
Io-oI
  • 7,588
  • 3
  • 12
  • 41
John Davis
  • 11
  • 1
  • Doesn't help when your body object has child properties which are also arrays ... API's that accept JSON as input do not natively assume a string as same schema as an array of one object :( – felickz Jul 01 '21 at 21:31
1

In your case, you should use count rather then length.

> (ls | %{$_.Name} | ?{$_.Contains("og")}).count
3
> (ls | %{$_.Name} | ?{$_.Contains("Prog")}).count
1

count returns

  • number of element, for array like objects,
  • 1, for scalar objects,
  • 0, for $null.

Note: count does not work for pscustomobject in versions older than PowerShell 6.1, which was a bug: https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.1

C. Aknesil
  • 71
  • 2
1

This has been resolved in PowerShell v3:

http://blogs.microsoft.co.il/blogs/scriptfanatic/archive/2012/03/19/Counting-objects-in-PowerShell-3.0.aspx

On a side note, you can find if a name contains something using a wildcard:

PS> ls *og*
Shay Levy
  • 377
  • 1
  • 3
  • 6
    [Shay](http://superuser.com/users/12257/shay-levy), I cannot comment on answers yet, but your statement is not true. PowerShell still boxes elements, but they have, as you noted, given single items a "Count" value. Single item results are still unboxed, though. You can test the above example against PS 3 and see the results. – Tohuw Jul 17 '13 at 12:15
  • 1
    The behaviour is still the same in PS 5. – MEMark May 04 '16 at 07:46
  • Yep, def still present – James Wiseman Jul 14 '17 at 10:16
  • 1
    This behaviour is still the same in PS 6.0.1 – spuder Apr 05 '18 at 23:21