One reason why Windows PowerShell is so flexible and maybe even a little harder to learn is that it doesn't have monolithic commands that do six different things. Instead, there are simple cmdlets that you can string together in a pipelined expression. Each cmdlet is designed for a single purpose. One cmdlet that you'll use often is Group-Object, which has the commonly used alias of Group.

As the name suggests, the Group-Object cmdlet puts objects into groups based on a property. This can be an existing property or a custom property. I'll show you how to group objects using both types of properties. I'll also show you some special techniques such as viewing only the total count of grouped objects and creating a grouped hash table.

Using Existing Object Properties

Typically, an existing property is used to group objects. When you use Group-Object, it writes a new object to the pipeline. Take, for example, the command:

Get-Service | Group Status

Even though this command starts with services, at the end of the pipeline is a Microsoft.PowerShell.Commands.GroupInfo object. You can see most of its properties in the sample output in Figure 1. The Group property is a collection of the underlying objects that share the same property value—that is, all running or stopped services. The Name property reflects the name of each group. The Count property reflects the number of objects in each group.

Figure 1: Grouping Services by the Status Property

Because the output from Group-Object is another object, you can use it like anything else in PowerShell. For example, consider the command:

Get-Command | Group Verb | Sort Count -Descending |
  Select -First 5 Name,Count | Format-Table -Auto

This command gets information about the PowerShell cmdlets, groups this information by the Verb property, sorts the grouped information by the Count property in descending order, and selects the first five. Figure 2 shows sample results. Perhaps this example isn't the most compelling, but I think it adequately demonstrates using Group-Object in a pipelined expression.

Figure 2: Grouping Information by the Verb Property

Most of the time, administrators group on a single property, but it's possible to group on multiple properties. The assumption is that you have objects that share a set of properties. Here's one example:

Get-Eventlog System -Newest 250 | Sort Source |
  Group EntryType,Source | Out-GridView -OutputMode Single |
  Select -ExpandProperty Group |
  Format-Table -GroupBy Source -Property TimeGenerated,
  Message -Wrap

This command starts by retrieving the 250 latest entries from the System event log. The entries are sorted by the Source property. The sorted results are then grouped first by the EntryType property, then by the Source property. The results are piped to Out-GridView, as shown in Figure 3.

Figure 3: Grouping System Event Log Entries by the EntryType and Source Properties

The rest of the command executes after I select an entry and click OK. The command then expands the Group property, which is the collection of event log entries, and formats the results, as Figure 4 shows. This is possible because Out-GridView writes objects to the pipeline in PowerShell 3.0.

Figure 4: Expanding the Group Property

Using Custom Properties

You aren't limited to using existing object properties with Group-Object. You can group objects based on a value derived from a script block. Take, for example, the command:

Dir C:\Work | Group { ((Get-date) - $_.CreationTime).Days}

The $_ represents each object in the pipeline. In this example, I'm retrieving all the files in the C:\Work directory and grouping them based on a calculated value that's the total number of days since the file was created. Figure 5 shows sample results.

Figure 5: Grouping Objects Based on a Value Derived from a Script Block

Viewing Only the Group Totals

Sometimes you might not care about the grouped results and only want to see the group totals. In this situation, you can tell PowerShell to skip the individual objects. For example, when all I want to see is a distribution of file types in my Scripts folder, I run the command:

Dir C:\Scripts -File -Recurse | Group Extension -NoElement |
  Sort Count -Descending

As you can see in Figure 6, I write a lot of PowerShell scripts. Using this technique is a terrific way to slice and dice data.

Figure 6: Viewing Only the Group Totals

Creating a Grouped Hash Table

There might be situations in which you want to work with grouped data in a more interactive fashion. The easy way to accomplish this task is to turn the GroupInfo object into a hash table. A hash table, also known as an associative array (or a Dictionary object in the VBScript days), consists of a key/value pair. When you turn the GroupInfo object into a hash table, the Name property becomes the hash table key and the value is the collection of grouped objects. If you use this technique, I recommend that you filter out any objects that might result in a blank value.

To turn the GroupInfo object into a hash table and view its contents, you use commands like this:

$svc = Get-Service | Group Status -AsHashTable -AsString
$svc

Note the use of the -AsString parameter in the first command. Many object properties look like strings but are actually numeric values or enumerations under the hood. If you don't use the -AsString parameter, it will be very difficult to work with the hash table. It's difficult to know which properties you need to treat as a string, so I recommend always using it when creating a hash table. Figure 7 shows sample results from these commands.

Figure 7: Creating the $svc Hash Table and Viewing Its Contents

After you've created the hash table, you can use it for a wide variety of tasks. For example, if you want to see all the stopped services, you run the command:

$svc.Stopped

Figure 8 shows sample results.

Figure 8: Using the $svc Hash Table to See All the Stopped Services

Here's another example of creating a grouped hash table and viewing its contents:

$events = Get-Eventlog System -Newest 500 |
  Group Source -AsHashTable -AsString
$events

This command obtains the last 500 entries written to the System event log and creates a hash table based on the event log entry's Source property. You can see sample results from these commands in Figure 9.

Figure 9: Creating the $events Hash Table and Viewing Its Contents

You now have an object, $events, that you can work with interactively to easily explore events from different sources. The handy thing about a hash table is that you can reference the value by treating the key as a property. (This is where tab completion is very useful.) Consider the following example:

$events.'Microsoft-Windows-Ntfs'

This command accesses one of the event source keys (Microsoft-Windows-Ntfs) and displays all the corresponding event log entries, as shown in Figure 10. Be aware that even though the properties were turned into strings, some names have irregular characters and need to be quoted, as shown here.

Figure 10: Treating a Hash Table Key as a Property

Providing a Practical Example

To finish the exploration of Group-Object, let's look at a practical example. Listing 1 shows a script, FileExtensionAgeHTML, that analyzes a folder and creates an HTML report that groups files by their extension and age.

Here are the key parts of this script:

  • The code in callout A is grouping on a custom property that essentially drops the leading period from the extension name.
  • The code in callout B analyzes all the files and builds new groups based on the file age. Each file age group is converted to an HTML fragment.
  • The code in callout C assembles all the fragments into a single HTML report.

You can download FileExtensionAgeHTML by clicking the Download the Code button. This script works with PowerShell 2.0 and later. Because FileExtensionAgeHTML is a demonstration script, it has hard-coded values for the path to search and the name of the HTML file. You'll need to revise these values accordingly. I included instructions on how to do so as well as other helpful comments in the download file.

Gain Insights Without Extensive Scripting

Being able to group objects based on some criteria offers insights on an environment that might not have been possible before without extensive scripting. Group-Object doesn't care what type of object it uses. I've been demonstrating with files, but you could just as easily group Active Directory (AD) user objects or Microsoft IIS websites. Once you learn how to use Group-Object, you can apply it just about anywhere.

Listing 1: The FileExtensionAgeHTML Script
$head = @'

'@

# BEGIN CALLOUT A
$path = "C:\scripts"
$files = DIR $path -Recurse -File
$groupExt = $files | where {$_.extension} |
Group-Object {$_.Extension.Substring(1)}
# END CALLOUT A

# BEGIN CALLOUT B
# Create aging fragments.
$30days = $files |
where { $_.LastWritetime -ge (Get-Date).AddDays(-30) } |
Group-Object {if ($_.extension) {
  $_.Extension.Substring(1)}} | Select Name,Count,
@{Name="Size";Expression={
  ($_.Group | measure-object Length -sum).sum}} |
Sort Count -Descending |
ConvertTo-HTML -Fragment -PreContent "

30 Days

"

$90days = $files |
where { $_.LastWritetime -le (Get-Date).AddDays(-30) -and
  $_.LastWritetime -ge (Get-Date).AddDays(-90) } |
Group-object {if ($_.extension) {
  $_.Extension.Substring(1)}} | Select Name,Count,
@{Name="Size";Expression={
  ($_.Group | measure-object Length -sum).sum}} |
Sort Count -Descending |
ConvertTo-HTML -Fragment -PreContent "

30-90 Days

"

$180days = $files |
where { ($_.LastWritetime -le (Get-Date).AddDays(-90)) -and
  ($_.LastWritetime -ge (Get-Date).AddDays(-180)) } |
Group-object {if ($_.extension) {
  $_.Extension.Substring(1)}} | Select Name,Count,
@{Name="Size";Expression={
  ($_.Group | measure-object Length -sum).sum}} |
Sort Count -Descending |
ConvertTo-HTML -Fragment -PreContent "

90-180 Days

"

$1yr = $files |
where { ($_.LastWritetime -ge (Get-Date).AddDays(-356)) } |
Group-object {if ($_.extension) {
  $_.Extension.Substring(1)}} | Select Name,Count,
@{Name="Size";Expression={
  ($_.Group | measure-object Length -sum).sum}} |
Sort Count -Descending |
ConvertTo-HTML -Fragment -PreContent "

365 Days

"
# END CALLOUT B

$summary = $groupExt | Select Name,Count,
@{Name="Size";Expression={
  ($_.Group | measure-object Length -sum).sum}} |
Sort Size -descending |
ConvertTo-HTML -Fragment -PreContent `
"

Report by File Extension $Path

"

# BEGIN CALLOUT C
# Create the HTML report.
ConvertTo-Html -head $Head `
-title "Extension Report for $Path" `
-PostContent `
($summary + $30days + $90days + $180days + $1yr) |
Out-file c:\work\extrpt.htm
# END CALLOUT C