Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Solving the PowerShell Object Age Challenge – Part 1

Posted on June 9, 2020

A few weeks ago, the Iron Scripter site posted an interesting challenge about writing a generic function to get the age of objects. Many things that we deal with in PowerShell have an "age" such as files, processes or even AD groups and users. I think this is an especially useful exercise because it forces you to understand the PowerShell paradigm of "objects in the pipeline."

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

Methodology

I always find it helpful to try and visualize the pipeline process. If nothing else, you need to be able to verbally describe what your command is going to do. If you can't, you will have a very hard time writing code. I knew I would have to identify at least one property for the object to use when retrieving the creation time. Because part of the challenge was to get an average age of all processed objects, I knew I had to do the bulk of the work in the End script block. Here's my verbalization:

Initialize an array to hold incoming objects. As objects are piped in to the function add each function to the temporary array. Once everything has been processed, then calculate the average age. For each object in the temporary array create a custom result with calculated time spans.

That's a decent starting point.

Outlining the Code

Sketching this out in code I come up with this.

Begin {
  #initialize holding array
}
Process {
    foreach ($item in $input) {
    #add each item to the array
    }
}
End {
    #calculate average age for all items in holding array
    $Age = #code
    foreach ($object in $array) {
      #create custom object 
      [pscustomobject]@{
        Created  = $object.$CreationProperty
        Modified = $object.$ChangeProperty
        Age      = $Age
        Average  = If ($Age -gt $Avg) {"Above"} else {"Below"}
      }
    }
}

I expect this to change but it gives me a good place to start. For parameters, I can take input for any type of object. But I'll have to rely on the user to know what to use for the creation property and optionally a property that indicates when the object was last changed. It is possible an object may not have a "changed" property , so I'll use the creation property by default. I also want the user to be able to specify additional parameters they'd like in the output.

Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullorEmpty()]
        [object[]]$InputObject,

        [Parameter(Mandatory, Position = 0)]
        [string]$CreationProperty,

        [Parameter(Position = 1)]
        [string]$ChangeProperty,

        [object[]]$Properties = @("Name")
    )

This seems manageable.

Normally, I'd simply initalize an empty array. But to make things more interesting I'm going to use a generic list object.

$all = [System.Collections.Generic.List[object]]::new()

This type of object is supposed to be better performing. As a quick test, this code

measure-command {
 $all = [System.Collections.Generic.List[object]]::new()
 dir c:\scripts -file -Recurse | foreach-object { $all.Add($_) }
}

took 2.95 seconds to add almost 7900 files. Compared to the "traditional" approach.

measure-command {
 $all = @()
 dir c:\scripts -file -Recurse | foreach-object { $all+=$_ }
}

Which took 5.5 seconds. I don't want to get sidetracked into a discussion of the merits of either approach, but for this solution I'll use the generic list. I'm basically going to use the same pattern: for each piped in object, add it to the list.

In the End block I can now process all objects. First, I need to build an array of the creation age timespan for each object.

$allTimeSpans = $all | ForEach-Object {
            Try {
                $X = $_
                New-TimeSpan -start $_.$creationProperty -end $Now -ErrorAction Stop
            }
            Catch {
                Write-Warning "Failed to get $CreationProperty value. $($_.exception.message)"
                Write-Warning ($X | Out-String)
            }
        }  #foreach

Then I can measure that collection and generate an average timespan. I'll use TotalSeconds.

$avg = New-TimeSpan -seconds ($allTimeSpans | Measure-Object -Property TotalSeconds -average).Average

I'll use this value as a comparison for the creation age of each object so I can indicate if it is above or below average.

For each file in the list, I'll create an [ordered] hashtable. I did this because I wanted at least some structure to the output.

foreach ($item in $all) {
   $Age = New-TimeSpan -end $Now -Start ($item.$creationProperty)
   $tmpHash = [ordered]@{
       Created  = $item.$CreationProperty
       Modified = $item.$ChangeProperty
       Age      = $Age
       Average  = If ($Age -gt $Avg) {"Above"} else {"Below"}
    }

Next, I need to account for the extra properties. I also decided to support a hashtable like you would use with Select-Object.

foreach ($prop in $Properties) {
    if ($prop.gettype().name -eq 'hashtable') {
        Write-Verbose "[END    ] Expanding hashtable"
        $custom = $item | Select-object -property $prop
        $prop = $custom.psobject.properties.Name
        $val = $custom.psobject.properties.Value
    }
    else {
        $val = $item.$prop
    }
    #shorten the value for the verbose message
    if ($VerbosePreference -eq "Continue") {
        $valtest = ($val | Out-String).trim()
        if ($valtest.length -ge 19) {
            $valstring = "{0}.." -f $valtest.Substring(0, 19)
        }
        else {
            $valstring = $valtest
        }
    }
    Write-Verbose "[END    ] Adding Property: $prop Value: $valstring"
    $tmpHash.Add($prop, $val)
} #foreach property

The gist of this to get the property value for each object and add it as a new element in the temporary hashtable. I  alsolike using Verbose messages and in my testing I decided to shorten up the value text to keep the verbose messages from wrapping. Purely cosmetic.

The last step for each object is to create the custom object.

New-Object -TypeName PSObject -property $tmpHash

Extras

I decided to have a little extra fun with this function. First, after the results are displayed, I use Write-Host to display a colorized summary of the average overall creation age.

Write-Host "`n$([char]0x1b)[1;38;5;154mAverage Creation Age Overall : $avg"

I'm even using an ANSI escape sequence to color the output with a custom color. Why Write-Host? Functions should only write one type of object to the pipeline, and mine already is. I didn't want it to also write a string. I could have added this information as a property to each object, but instead decided to simply write information to the console and not the pipeline. This escape sequence will work in Windows PowerShell and PwoerShell 7.

The other thing I wanted to try was a console-style progress indicator. I wanted to have something to indicate items were being processed and added to the list. Again, a little ANSI magic to the rescue.

In the Begin block, I define an array of characters and a counter.

$ch = @("|", "/", "-", "\", "|", "/", "-")
$c = 0

As each item is processed, I use Write-Host with an ANSI sequence that makes sure each line is written at the same position.

if (-Not ($VerbosePreference -eq "Continue")) {
    if ($c -ge $ch.count) {
        $c = 0
    }
    Write-Host "$([char]0x1b)[1EProcessing $($ch[$c])$([char]0x1b)[0m" -ForegroundColor cyan -NoNewline
    $c++
    #adding an artificial sleep so the progress wheel looks like it is spinning
    Start-Sleep -Milliseconds 10
}

The end result is a little spinner. I added an artificial sleep to make it look nicer. And I only use the progress spinner when NOT using -Verbose since the list of Verbose messages would move the spinner off the screen pretty quickly.

The Complete Function

Here's the complete function. I will not display the ANSI sequences very well in the PowerShell ISE.

Function Get-ObjectAge {
    [cmdletbinding()]
    [alias('goa')]

    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullorEmpty()]
        [object[]]$InputObject,

        [Parameter(Mandatory, Position = 0)]
        [string]$CreationProperty,

        [Parameter(Position = 1)]
        [string]$ChangeProperty,

        [object[]]$Properties = @("Name")
    )

    Begin {
        Write-Verbose "[BEGIN  ] Starting: $($MyInvocation.Mycommand)"

        #create a list object to hold all incoming objects.
        #this should perform slightly better than a traditional array
        $all = [System.Collections.Generic.List[object]]::new()

        #use the same date time for all timespan calculations
        $Now = Get-Date
        Write-Verbose "[BEGIN  ] Using $NOW for timespan calculations"
        Write-Verbose "[BEGIN  ] Using $CreationProperty for CreationProperty"

        #use $CreationProperty parameter value for Change property if latter is not specified
        if (-Not $ChangeProperty) {
            Write-Verbose "[BEGIN  ] Using $CreationProperty for ChangeProperty"
            $changeProperty = $CreationProperty
        }
        else {
            Write-Verbose "[BEGIN  ] Using $ChangeProperty for ChangeProperty"
        }

        #initialize counters and an array of characters to be using in a progress wheel
        $i = 0
        $ch = @("|", "/", "-", "\", "|", "/", "-")
        $c = 0

    } #begin

    Process {
        foreach ($object in $InputObject) {
            $i++
            #display a progress wheel if NOT using -Verbose
            if (-Not ($VerbosePreference -eq "Continue")) {
                if ($c -ge $ch.count) {
                    $c = 0
                }
                Write-Host "$([char]0x1b)[1EProcessing $($ch[$c])$([char]0x1b)[0m" -ForegroundColor cyan -NoNewline
                $c++
                #adding an artificial sleep so the progress wheel looks like it is spinning
                Start-Sleep -Milliseconds 10
            }
            #add the incoming object to the list
            $all.Add($object)
        }
    } #process

    End {
        Write-Verbose "[END    ] Calculating average creation age for $($all.count) objects"
        $allTimeSpans = $all | ForEach-Object {
            Try {
                $X = $_
                New-TimeSpan -start $_.$creationProperty -end $Now -ErrorAction Stop
            }
            Catch {
                Write-Warning "Failed to get $CreationProperty value. $($_.exception.message)"
                Write-Warning ($X | Out-String)
            }
        }  #foreach

        #get the average creation age timespan
        $avg = New-TimeSpan -seconds ($allTimeSpans | Measure-Object -Property TotalSeconds -average).Average

        #create a result object for each processed object and save to an array
        $results = foreach ($item in $all) {
            $Age = New-TimeSpan -end $Now -Start ($item.$creationProperty)
            $tmpHash = [ordered]@{
                Created  = $item.$CreationProperty
                Modified = $item.$ChangeProperty
                Age      = $Age
                Average  = If ($Age -gt $Avg) {"Above"} else {"Below"}
            }

            #add user specified properties
            foreach ($prop in $Properties) {
               if ($prop.gettype().name -eq 'hashtable') {
                    Write-Verbose "[END    ] Expanding hashtable"
                    $custom = $item | Select-object -property $prop
                    $prop = $custom.psobject.properties.Name
                    $val = $custom.psobject.properties.Value
                }
                else {
                    $val = $item.$prop
                }
                #shorten the value for the verbose message
                if ($VerbosePreference -eq "Continue") {
                    $valtest = ($val | Out-String).trim()
                    if ($valtest.length -ge 19) {
                        $valstring = "{0}.." -f $valtest.Substring(0, 19)
                    }
                    else {
                        $valstring = $valtest
                    }
                }
                Write-Verbose "[END    ] Adding Property: $prop Value: $valstring"
                $tmpHash.Add($prop, $val)
            } #foreach property

            #create the object
            New-Object -TypeName PSObject -property $tmpHash

        } #foreach item

        #display all the results at once
        $results

        #display a message about the average creation age
        Write-Host "`n$([char]0x1b)[1;38;5;154mAverage Creation Age Overall : $avg"
        
        Write-Verbose "[END    ] Ending: $($MyInvocation.Mycommand)"
    } #end
} #end function

Want to see it work? The function works with files.

get-objectage-fileIt also works with processes. This example is using the alias I defined for the function.

get-objectage-process

Making It Better

I have a generic enough function that works with any object type, assuming it has a property that reflects when it was created. And as nice as this is I know it can be even better. But I think I've given you enough to play with for one day so I'll save my enhanced version for another day...soon.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

1 thought on “Solving the PowerShell Object Age Challenge – Part 1”

  1. Pingback: ICYMI: PowerShell Week of 12-June-2020 – 247 TECH

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d