A few days ago I wrote about my experiences in designing a PowerShell function that reports on the size of the hidden .git folder. In that version of the function I decided to include a parameter that would permit the user to get the size pre-formatted as either KB, MB or GB. I thought long and hard about it as this really is not the best design pattern. Still, I'm not the only one who likes the ability to get values formatted into a more user-friendly form. So let me show you a revision that I think better adheres to PowerShell scripting best practices, yet also provides the flexibility of data formatting.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Extending a PowerShell Object
One of the things I appreciate about PowerShell is how easy it is to extend or modify an object type. This is because PowerShell has an extensible type system. Using the Update-TypeData cmdlet, it is very easy to add new properties to an object type. That is the approach I decided to take. I decided to keep my "base" object very simple and show the directory size in bytes. This means I removed all the code around the -As parameter.
#get the total size of all files in the .git folder $stat = Get-Childitem -path $git -Recurse -File | Measure-Object -Property length -sum #create the output $obj = [PSCustomObject]@{ Path = $full Files = $stat.count Size = $stat.sum } #customobject
In order to extend a type you need to know the typename. When creating a PSCustomobject as I'm doing here, this will require a few extra steps to define an additional typename for the custom object. This is typically done with code like this:
#insert a new typename $obj.psobject.typenames.insert(0, "gitSize") #write object to the pipeline $obj
Update
Thanks to a comment from Chris Warwick, there is an even better way to insert a type name that was introduced in PowerShell at some point and I missed it. Define a property called PSTypeName in the object itself.
$obj = [PSCustomObject]@{ PSTypeName = "gitSize" Path = $full Files = $stat.count Size = $stat.sum } #customobjectIt won't show in the object's output but it will be used for the typename. You can verify it by piping the object to Get-Member.
I could also have created a PowerShell class which would have baked in the typename from the beginning. But that is a bit more complicated than I what to get into here. Now for the fun stuff.
Exploiting Update-TypeData
My function will now write an object to the pipeline with a typename of gitSize. I decided that all of the other properties I might want to see were completely optional. To accomplish this I am going to use the Update-TypeData command outside of the function. Since I would need to dot source the script file with the function anyway, I added these lines after the function definition.
Update-TypeData -TypeName gitSize -MemberType ScriptProperty -MemberName SizeKB -Value {$this.size / 1kb} -force Update-TypeData -TypeName gitSize -MemberType ScriptProperty -MemberName SizeMB -Value {$this.size / 1mb} -force Update-TypeData -TypeName gitSize -MemberType ScriptProperty -MemberName SizeGB -Value {$this.size / 1gb} -force Update-TypeData -TypeName gitSize -MemberType NoteProperty -MemberName Computername -Value $env:COMPUTERNAME -force Update-TypeData -TypeName gitSize -MemberType ScriptProperty -MemberName Date -Value {Get-Date} -force Update-TypeData -TypeName gitSize -MemberType ScriptProperty -MemberName Name -Value {Split-Path -path $this.path -leaf} -force
I also added a few other properties I had been thinking about like Computername and Date. Yes, I could have defined many of these as part of the custom object in which case I would have used code like this:
$obj = [PSCustomObject]@{ Path = $full Files = $stat.count Size = $stat.sum SizeKB = $stat.sum/1kb Computername = $env:computername } #customobject
Regardless, I knew that by default I only wanted to see the path, number of files and size in bytes. As things currently stand, when I run my function I'll get an object that displays everything. The solution is to use Update-TypeData and define a default set of properties.
#set default properties Update-TypeData -TypeName gitSize -DefaultDisplayPropertySet "Path", "Files", "Size" -Force
When I run the function, which will write a gitSize object to the pipeline, I'll get the defaults.
But all of the properties are there if I want them.
The Extended Function
I've left the original function up on Github. This version can be found here.
As before, you will need to save the file locally and dot source it into your PowerShell session. Now, I have some flexibility. I can either get the default output as I showed earlier or I can run code like this to utilize the additional properties.
Using a PowerShell Format File
Another approach I thought about, but didn't pursue, would be the use of a format.ps1xml file. This too would require me to define a typename. With this approach I would have had to create a specially formatted xml file that define table or list (usually these two) views. The views could include code to dynamically display data formatted as KB or MB. This is the type of thing done with process objects that you see when you run Get-Process. When you run Get-Process alone, PowerShell uses a defined table view for process objects that formats values. But it doesn't change the underlying object. Creating the xml file can be a bit tedious and I didn't want to go down that rabbit hole today. There certainly may be situations where you want to take advantage of both type and format extensions and hopefully we can revisit this topic in a future article.
PowerShell Scripting is a Process
In the mean time, take a look at the code. Consider the design process I went through. I write these articles as much for teaching you concepts and techniques as I do showing you a finished product. Although if you find it helpful, that's a bonus. Embrace and enjoy.
Nice solution. Thank you.
Hi Jeff,
You can just add a ‘PsTypeName’ property to your [PsCustomObject] rather than using $obj.psobject.typenames.insert(0, “gitSize”)
So:
$obj = [PSCustomObject]@{
PsTypeName = ‘gitSize’
Path = $full
Files = $stat.count
Size = $stat.sum
SizeKB = $stat.sum/1kb
Computername = $env:computername
} #customobject
Cheers,
Chris
Well that is much easier! This is one of those things that snuck in some earlier version that I missed while I kept using my “old-school” ways. Thanks for the tip.