It could probably be just yet another comment under “What you see is NOT what you get” post. But I’ve decided I would give it more space and make code more readable than it’s possible in comments.
As I said before: for me nice output is less important than data itself. If I can keep data and make it look nice – I’m more than OK with that. I spent most of my time in PowerShell window so nice output make me happy camper. That’s why whenever I write a module I spent some extra time on it to add ps1xml files to it and decide how both Format-List and Format-Table should behave with objects I create. Obviously, objects also have some custom names, usually they have some pseudo-type tattoo rather than type wrote in C# and added to PowerShell with Add-Type.
Problem with more ad-hoc scripting (or scripting in general if module is not an option) in version 2 of PowerShell is that you can’t easily create this custom formatting on the fly. Building ps1xml file is overkill in such scenario, and sending strings to the pipe is something I would rather avoid if original data is something of a different nature (numeric, DateTime, etc.). So, what I would do?
Function to polish object data.
When you or PowerShell formats data using any of custom formatters (both Format-Table, and Format- List) it will, this way or the other, convert your data to strings.
This solution is based fully on the feedback and observations during Scripting Games. I decided to go with ToString() method replacement because it’s both advanced and pretty easy to port on any object that exists. There are two parts of this solution. First – good enough ScriptBlock that will be used as methods value. Than – logic to either replace ToString method, or whole property (I decided to do that for “normal” objects, to avoid issues with read-only properties, like Length on files). This logic will also try to “fix” properties that look like numbers, but are strings instead (e.g. stuff from Import-Csv).
ScriptBlock on the fly
Because our function will in the end format data we may want to give user ability to tweak formatting. With numeric it means moving decimal point. For that to work smoothly it’s bests to create ScriptBlock once we know –DecimalPoint value with [ScriptBlock]::Create() method:
begin { $MethodOptions = @{ Name = 'ToString' MemberType = 'ScriptMethod' PassThru = $true Force = $true Value = [ScriptBlock]::Create(@" "{0:N$DecimalPoint} {1}" -f @( switch -Regex ([math]::Log(`$this,1024)) { ^0 { (`$this / 1), ' B' } ^1 { (`$this / 1KB), 'KB' } ^2 { (`$this / 1MB), 'MB' } ^3 { (`$this / 1GB), 'GB' } ^4 { (`$this / 1TB), 'TB' } default { (`$this / 1PB), 'PB' } } ) "@ ) } }
To add or not to add
Now that we have code prepared all we need to do is to inject it. But we will try to be smart about it. Few things I’ve tried to cover:
- property may not exist on the input object
if (!$SelectedProperty) { Write-Verbose "No such property: $Prop" continue }
- property can be of type System.String, so adding ToString() won’t effect way it displays
if ($SelectedProperty -is [System.String]) { [Int64]$InputObject.$Prop = $SelectedProperty }
- property can’t be converted into number
if ( ! ([double]$InputObject.$Prop) ) { Write-Verbose "Can't be casted into double: $Prop" continue }
- type is not PSCustomObject so we risk issues with ReadOnly properties
if (!$InputObject.PSTypeNames.Contains('System.Management.Automation.PSCustomObject')) { try { $Member = @{ MemberType = 'NoteProperty' Name = $Prop Force = $true Value = $SelectedProperty } $InputObject | Add-Member @Member } catch { Write-Verbose "Not able to replace property on this type: $($InputObject.GetType().FullName)" continue } }
If nothing is preventing as from adding ToString – we eventually do it for each property selected by user:
process { foreach ($Prop in $Property) { $SelectedProperty = $InputObject.$Prop # Tests using $SelectedProperty... $InputObject.$Prop = $InputObject.$Prop | Add-Member @MethodOptions } $InputObject }
I uploaded this function both to poshcode and TechNet library (once I finished with help and stuff). Hope it will be somewhat useful for people like me: who like it useful and pretty.
Credits
This solution would not come about if not two people who’s ideas I’ve used.
First – Rob Campbell (@mjolinor), with his brilliant use of [math]::log to decide which unit fits best for given numeric.
Next – Robert Eder who’s trick with custom ToString method from Scripting Games entry I’ve used in this solution.
Thanks you guys for inspiration! That’s the beauty of Scripting Games: you never stop learning new things!
Interesting solution. They seem to have done much the same thing in the .tostring() method of [Microsoft.Exchange.Data.ByteQuantifiedSize], but also output a formatted byte count along with it. Makes me wonder why that chose that particular format, and why that type is only available in the Exchange namespace.