While grading scripts in advanced category event 4 I noticed that most people assume that it’s necessary to change number into string to format it nicely. The problem with this approach: once you changed number into string you kind of kill PowerShell ability to sort, filter and group objects based on properties that are numbers. If you ever decide to use it for something else than showing to the user – you will probably have to parse results and reverse whole process. And because it’s PowerShell that you use – you do not really have to. You can get both. As advanced scripter – you should know that already.
Let’s take a look at some practical examples, to make it more clear when problems start to show up. If I will sort two objects:
2,10 | foreach { New-Object PSObject -Property @{ Name = "Foo $_" Size = "$_ Bars" } } | Sort-Object -Property Size
… PowerShell will try to convince me, that 2 Bars are greater than 10. In world of strings that’s true. In world of numbers – not so much. To be honest – for me that’s huge flaw of that approach. It looks nice, but that’s not usually what I’m after. I want PowerShell to give me information first, look nice later. So what could one do?
First approach: usability over visuals
I’ve seen several scripts, that would be better if authors would not focus so much on making output exactly the same as the one they’ve seen on screenshot attached to this task. Obviously, best approach was to get it done exactly the same. But if you had no idea how – it was better to keep important bits in output too. In other words: if you can’t make it look the same and give full functionality – focus on functionality rather than “look”. Back to our fictional objects:
2,10 | foreach { New-Object PSObject -Property @{ Name = "Foo $_" Size = $_ SizeFormatted = "$_ Bars" } } | Sort-Object -Property Size
We get formatted output and ability to process data with real values kept. It won’t look perfect, but we won’t loose important bits “in translation”.
Second approach: nice by default, optionally functional too.
Second approach is smart way to work around issue of loosing important information during process of making data look pretty. PowerShell has very strong parameter support. You can name parameters, you can have flags (parameters of type [switch]) to easily enable/ disable some options. That’s a perfect use case for that kind of parameters. If you want to display it nicely without “polluting” output with raw data – make your script aware of situations, were raw data is more important than nice view. If you name parameter wisely and for even better user experience – explain why would you want to use it in your comment based help and show use case in examples, than you have little of both worlds. Several contestants actually used it for their entries. It could be as simple as that:
function Get-Foo { param ( [switch]$IncludeRaw) 2, 10 | foreach { $PropertyHash = @{ Name = "Foo $_" SizeFormatted = "$_ Bars" } if ($IncludeRaw) { $PropertyHash.Size = $_ } New-Object PSObject -Property $PropertyHash } }
It will remove numbers by default, but if I ever decided to use them – all I need to do is use switch to get them back.
Third approach: customize ToString.
This is another pretty smart idea I picked up from one of contestants. If you understand how PowerShell formatting works, you may trick it pretty easily. Data will be kept intact but output will be more human-readable. How it works? Let’s take a look:
$AddMember = @{ MemberType = 'ScriptMethod' Name = 'ToString' Force = $true } 2, 10 | foreach { $Out = New-Object PSObject -Property @{ Size = $_ Name = "Foo $_" } $Out.Size | Add-Member @AddMember -Value { "$this Bars" } $Out } | Sort Size
ToString() method is what PowerShell will usually use to display data “for humans”. But value will remain untouched, so you won’t loose anything. IMO that’s the best way to solve this task. Not the one I would choose however before I’ve seen this approach. My solution would involve ps1xml file, probably because I like this concept so much.
Fourth way: temporary px1xml.
This is more complicated approach than the one we had used before. Maybe some people would say – too complicated for the task of that importance. But I would like to show you how to do that anyway: get ps1xml file created on-the-fly and use it to create decent output without loosing important information. It may be helpful in some other applications.
There are two parts of that solution: first you need to create and use ps1xml format file for pseudo-type that you create. Than – tattoo your objects using this pseudo-type name.
First one is pretty complex, mainly due to complexity of ps1xml files:
$FormatFile = [System.IO.Path]::GetTempFileName() |
Rename-Item -NewName { $_ -replace ‘$’, ‘.ps1xml’ } -PassThru
$FormatXml = @’
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<ViewDefinitions>
<View>
<Name>Foo</Name>
<ViewSelectedBy>
<TypeName>Foo.Bar</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>20</Width>
</TableColumnHeader>
<TableColumnHeader>
<Label>Formatted Size</Label>
<Width>20</Width>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
"$($_.Size) Bars"
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
‘@
Set-Content -Path $FormatFile.FullName -Value $FormatXml
Update-FormatData -PrependPath $FormatFile.FullName
Once we have our pseudo-type’s formatting set up – we can create object and add this types to $Object.PSTypeNames:
2, 10 | foreach { $Out = New-Object PSObject -Property @{ Size = $_ Name = "Foo $_" } $Out.PSTypeNames.Insert(0,'Foo.Bar') $Out } | sort Size
Output will look like that:
It gives us full control over object’s formatting, and obviously show it’s value mainly in more complex applications. Here – adding optional parameter to show numbers would be enough IMO. What is not enough – is leaving data nice, pretty and – frankly speaking – useless. So if you don’t want to dig into solutions that require some extra work – leave all information you have for future user. If he won’t need raw data, he can always cut it later. No need for you to do that for him.
I’d think you’d want to make the object’s property generic units, and leave the formatting decisions for output time, e.g.
2, 10 | foreach {
New-Object PSObject -Property @{
Size = $_
Name = “Foo $_”
}
}| sort size |
format-table Name,@{l=’Formatted Size’;e={‘{0} Bars’ -f $_.size}}
Yes and no. 😉 Obviously, most of time it should be done with Format-*.
On the other hand: if you see 12313123123123 -> it may be hard to interpret this value.
11.20 TB looks more promising. But if the choice is readability for usability, usability should be the answer. If you can have both – then why not? 🙂
The same reason the provider doesn’t return it as an object property in the first place?
But here – you *wanted* to have both. At least: that’s how I see requirements. When I see:
* You should return a custom object to permit further analysis and sorting.
* You should display folder size in the most appropriate unit of measurement. Therefore, do not report 1073741824 bytes when you could just as easily report 1 Gigabyte.
… I see ps1xml. As you can see 3 of 4 possible options was picked up from guys writing scripts.
If your script ends with ‘Format-Table’ or will not give me option (at least) to keep raw data in – it’s not perfect (5*) in my book. If you look at expert solution – even there you won’t see that happening. Does it means expert did it wrong? Obviously, no. It means it could be done (IMO) better. And I’ve seen few scripts that – in my opinion – did it better. None with ps1xml so far, but in advanced category it’s something I would expect to see. 🙂
My preference for using the format conversion function is more along the lines of practicality and utility. The ps1xml file is specific to the particular custom object type you have defined specifically for reporting on folder sizes. The format conversion function is easily applicable to any object that has an integer byte count property, and easily portable to other scripts, and even command line scripting.
On of the general design points specified in the judging criteria is to “Write reusable self-contained functions”. I thought that very good advice, and applied it to this problem.
Rob, you are right, obviously. But how about function that would give us both format and keep raw data in? Watch that space. 😉 I’ll blog about it as soon as I’m done with grading. 😉
Hello Bartek,
another approach, maybe an easy way out, might be to include a format width value.
In your example this would help
2,10 | foreach {
New-Object PSObject -Property @{
Name = (“Foo {0,2}” -f $_)
Size = (“{0,2} Bars” -f $_)
}
} | Sort-Object -Property Size
Or just use your 2nd approach and add an “invisible” property (RawSize)
2,10 | foreach {
New-Object PSObject -Property @{
Name = “Foo $_”
RawSize = $_
Size = “$_ Bars”
}
} | Sort-Object -Property RawSize | Select Name, Size
kind regards, Klaus
And thanks again, for being a judge … a hard job, I suppose …
Roll on PS 3, where you can easily change the default display properties on a custom object (I don’t think that messing around with ps1xml files is a good solution). http://blogs.microsoft.co.il/blogs/scriptfanatic/archive/2012/04/13/Custom-objects-default-display-in-PowerShell-3-0.aspx
I rolled to v3 a while ago and yes, I enjoy all goodies it offers. 😉 Together with new syntax around Update-TypeData, that works smoothly without ps1xml files.
Still: can you explain why you call it “messing” with ps1xml files? I define custom pseudo-type, so no reason to panic, I won’t destroy anything with it. v2/ v3 are both allowed, so I can’t assume v3 everywhere. And tools are there to use, not to avoid. Writing advanced scripts is IMO all about using what environment has to offer. If you ignore it and your script will give me text instead of number, or even worse – stream of formatting objects – I won’t see your solution _advanced_, sorry.
@Tim: You may wait for the general availabilty of PS v3, but there still isn’t a locallized version avaiable up to now, it’s still a beta version and the final release might not be out before the end of the year,
We will stay with v2 for at least one year maybe longer, so there is nothing wrong, if we won’t take v3 into account now. I don’t prefer the format files but they are there and should be used, if appropriate.
Klaus.
Pingback: More on output… | IT Pro PowerShell experience