Select the values of one property on all objects of an array in PowerShell Select the values of one property on all objects of an array in PowerShell powershell powershell

Select the values of one property on all objects of an array in PowerShell


I think you might be able to use the ExpandProperty parameter of Select-Object.

For example, to get the list of the current directory and just have the Name property displayed, one would do the following:

ls | select -Property Name

This is still returning DirectoryInfo or FileInfo objects. You can always inspect the type coming through the pipeline by piping to Get-Member (alias gm).

ls | select -Property Name | gm

So, to expand the object to be that of the type of property you're looking at, you can do the following:

ls | select -ExpandProperty Name

In your case, you can just do the following to have a variable be an array of strings, where the strings are the Name property:

$objects = ls | select -ExpandProperty Name


As an even easier solution, you could just use:

$results = $objects.Name

Which should fill $results with an array of all the 'Name' property values of the elements in $objects.


To complement the preexisting, helpful answers with guidance of when to use which approach and a performance comparison.

  • Outside of a pipeline[1], use (PSv3+):

    $objects.Name
    as demonstrated in rageandqq's answer, which is both syntactically simpler and much faster.

    • Accessing a property at the collection level to get its members' values as an array is called member enumeration and is a PSv3+ feature.

    • Alternatively, in PSv2, use the foreach statement, whose output you can also assign directly to a variable:

      $results = foreach ($obj in $objects) { $obj.Name }

    • If collecting all output from a (pipeline) command in memory first is feasible, you can also combine pipelines with member enumeration; e.g.:

       (Get-ChildItem -File | Where-Object Length -lt 1gb).Name
    • Tradeoffs:

      • Both the input collection and output array must fit into memory as a whole.
      • If the input collection is itself the result of a command (pipeline) (e.g., (Get-ChildItem).Name), that command must first run to completion before the resulting array's elements can be accessed.
  • In a pipeline, in case you must pass the results to another command, notably if the original input doesn't fit into memory as a whole, use:

    $objects | Select-Object -ExpandProperty Name

    • The need for -ExpandProperty is explained in Scott Saad's answer (you need it to get only the property value).
    • You get the usual pipeline benefits of the pipeline's streaming behavior, i.e. one-by-one object processing, which typically produces output right away and keeps memory use constant (unless you ultimately collect the results in memory anyway).
    • Tradeoff:
      • Use of the pipeline is comparatively slow.

For small input collections (arrays), you probably won't notice the difference, and, especially on the command line, sometimes being able to type the command easily is more important.


Here is an easy-to-type alternative, which, however is the slowest approach; it uses simplified ForEach-Object syntax called an operation statement (again, PSv3+):; e.g., the following PSv3+ solution is easy to append to an existing command:

$objects | % Name      # short for: $objects | ForEach-Object -Process { $_.Name }

Note: Use of the pipeline is not the primary reason this approach is slow, it is the inefficient implementation of the ForEach-Object (and Where-Object) cmdlets, up to at least PowerShell 7.2. This excellent blog post explains the problem; it led to feature request GitHub issue #10982; the following workaround greatly speeds up the operation (only somewhat slower than a foreach statement, and still faster than .ForEach()):

# Speed-optimized version of the above.# (Use `&` instead of `.` to run in a child scope)$objects | . { process { $_.Name } }

The PSv4+ .ForEach() array method, more comprehensively discussed in this article, is yet another, well-performing alternative, but note that it requires collecting all input in memory first, just like member enumeration:

# By property name (string):$objects.ForEach('Name')# By script block (more flexibility; like ForEach-Object)$objects.ForEach({ $_.Name })
  • This approach is similar to member enumeration, with the same tradeoffs, except that pipeline logic is not applied; it is marginally slower than member enumeration, though still noticeably faster than the pipeline.

  • For extracting a single property value by name (string argument), this solution is on par with member enumeration (though the latter is syntactically simpler).

  • The script-block variant ({ ... }) allows arbitrary transformations; it is a faster - all-in-memory-at-once - alternative to the pipeline-based ForEach-Object cmdlet (%).

Note: The .ForEach() array method, like its .Where() sibling (the in-memory equivalent of Where-Object), always returns a collection (an instance of [System.Collections.ObjectModel.Collection[psobject]]), even if only one output object is produced.
By contrast, member enumeration, Select-Object, ForEach-Object and Where-Object return a single output object as-is, without wrapping it in a collection (array).


Comparing the performance of the various approaches

Here are sample timings for the various approaches, based on an input collection of 10,000 objects, averaged across 10 runs; the absolute numbers aren't important and vary based on many factors, but it should give you a sense of relative performance (the timings come from a single-core Windows 10 VM:

Important

  • The relative performance varies based on whether the input objects are instances of regular .NET Types (e.g., as output by Get-ChildItem) or [pscustomobject] instances (e.g., as output by Convert-FromCsv).
    The reason is that [pscustomobject] properties are dynamically managed by PowerShell, and it can access them more quickly than the regular properties of a (statically defined) regular .NET type. Both scenarios are covered below.

  • The tests use already-in-memory-in-full collections as input, so as to focus on the pure property extraction performance. With a streaming cmdlet / function call as the input, performance differences will generally be much less pronounced, as the time spent inside that call may account for the majority of the time spent.

  • For brevity, alias % is used for the ForEach-Object cmdlet.

General conclusions, applicable to both regular .NET type and [pscustomobject] input:

  • The member-enumeration ($collection.Name) and foreach ($obj in $collection) solutions are by far the fastest, by a factor of 10 or more faster than the fastest pipeline-based solution.

  • Surprisingly, % Name performs much worse than % { $_.Name } - see this GitHub issue.

  • PowerShell Core consistently outperforms Windows Powershell here.

Timings with regular .NET types:

  • PowerShell Core v7.0.0-preview.3
Factor Command                                       Secs (10-run avg.)------ -------                                       ------------------1.00   $objects.Name                                 0.0051.06   foreach($o in $objects) { $o.Name }           0.0056.25   $objects.ForEach('Name')                      0.02810.22  $objects.ForEach({ $_.Name })                 0.04617.52  $objects | % { $_.Name }                      0.07930.97  $objects | Select-Object -ExpandProperty Name 0.14032.76  $objects | % Name                             0.148
  • Windows PowerShell v5.1.18362.145
Factor Command                                       Secs (10-run avg.)------ -------                                       ------------------1.00   $objects.Name                                 0.0121.32   foreach($o in $objects) { $o.Name }           0.0159.07   $objects.ForEach({ $_.Name })                 0.10510.30  $objects.ForEach('Name')                      0.11912.70  $objects | % { $_.Name }                      0.14727.04  $objects | % Name                             0.31229.70  $objects | Select-Object -ExpandProperty Name 0.343

Conclusions:

  • In PowerShell Core, .ForEach('Name') clearly outperforms .ForEach({ $_.Name }). In Windows PowerShell, curiously, the latter is faster, albeit only marginally so.

Timings with [pscustomobject] instances:

  • PowerShell Core v7.0.0-preview.3
Factor Command                                       Secs (10-run avg.)------ -------                                       ------------------1.00   $objects.Name                                 0.0061.11   foreach($o in $objects) { $o.Name }           0.0071.52   $objects.ForEach('Name')                      0.0096.11   $objects.ForEach({ $_.Name })                 0.0389.47   $objects | Select-Object -ExpandProperty Name 0.05810.29  $objects | % { $_.Name }                      0.06329.77  $objects | % Name                             0.184
  • Windows PowerShell v5.1.18362.145
Factor Command                                       Secs (10-run avg.)------ -------                                       ------------------1.00   $objects.Name                                 0.0081.14   foreach($o in $objects) { $o.Name }           0.0091.76   $objects.ForEach('Name')                      0.01510.36  $objects | Select-Object -ExpandProperty Name 0.08511.18  $objects.ForEach({ $_.Name })                 0.09216.79  $objects | % { $_.Name }                      0.13861.14  $objects | % Name                             0.503

Conclusions:

  • Note how with [pscustomobject] input .ForEach('Name') by far outperforms the script-block based variant, .ForEach({ $_.Name }).

  • Similarly, [pscustomobject] input makes the pipeline-based Select-Object -ExpandProperty Name faster, in Windows PowerShell virtually on par with .ForEach({ $_.Name }), but in PowerShell Core still about 50% slower.

  • In short: With the odd exception of % Name, with [pscustomobject] the string-based methods of referencing the properties outperform the scriptblock-based ones.


Source code for the tests:

Note:

  • Download function Time-Command from this Gist to run these tests.

    • Assuming you have looked at the linked code to ensure that it is safe (which I can personally assure you of, but you should always check), you can install it directly as follows:

      irm https://gist.github.com/mklement0/9e1f13978620b09ab2d15da5535d1b27/raw/Time-Command.ps1 | iex
  • Set $useCustomObjectInput to $true to measure with [pscustomobject] instances instead.

$count = 1e4 # max. input object count == 10,000$runs  = 10  # number of runs to average # Note: Using [pscustomobject] instances rather than instances of #       regular .NET types changes the performance characteristics.# Set this to $true to test with [pscustomobject] instances below.$useCustomObjectInput = $false# Create sample input objects.if ($useCustomObjectInput) {  # Use [pscustomobject] instances.  $objects = 1..$count | % { [pscustomobject] @{ Name = "$foobar_$_"; Other1 = 1; Other2 = 2; Other3 = 3; Other4 = 4 } }} else {  # Use instances of a regular .NET type.  # Note: The actual count of files and folders in your file-system  #       may be less than $count  $objects = Get-ChildItem / -Recurse -ErrorAction Ignore | Select-Object -First $count}Write-Host "Comparing property-value extraction methods with $($objects.Count) input objects, averaged over $runs runs..."# An array of script blocks with the various approaches.$approaches = { $objects | Select-Object -ExpandProperty Name },              { $objects | % Name },              { $objects | % { $_.Name } },              { $objects.ForEach('Name') },              { $objects.ForEach({ $_.Name }) },              { $objects.Name },              { foreach($o in $objects) { $o.Name } }# Time the approaches and sort them by execution time (fastest first):Time-Command $approaches -Count $runs | Select Factor, Command, Secs*

[1] Technically, even a command without |, the pipeline operator, uses a pipeline behind the scenes, but for the purpose of this discussion using the pipeline refers only to commands that use |, the pipeline operator, and therefore by definition involve multiple commands.