Sometimes, the true information you are after is burried deep inside object models. For example, when you manage virtual machines with the VMWare cmdlets, Get-VM will get you only very sparse pieces of information. The real goodies are stored inside of subproperties (which was true for the MPEG3 module I created last week as well). Time to get a look at what is going on here (and how you can "move" hidden subproperties onto the stage and make them visible).
Just so you don't think today I was discussing a really geeky topic: what you learn today has tremendous real-life-value. For example, you are getting to see two pieces of code that seem to do the exact same thing, yet the one approach (which is used by a lot of folks out there) takes 40x more time to execute than the other (which is not so known).
[This kept spinning in my head: how come there can be such a huge performance difference? Turns out it was a caching effect. The true and real world speed benefit is about 2x, not 40x. Sorry if I got you excited
. Fortunately, the performance gain was just an added benefit, the main story is about auto-adding script properties.]
To get a feeling for the problem, did you ever try and create a list of system DLLs and their versions? A plain dir won't get you far:
PS > dir $env:windir\system32\*.dll
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 14.07.2009 03:39 158208 aaclient.dll
-a--- 14.07.2009 03:40 3745792 accessibilitycpl.dll
-a--- 14.07.2009 03:24 39424 ACCTRES.dll
(...)
This line does, though:
PS > dir $env:windir\system32\*.dll | Select-Object -ExpandProperty VersionInfo
ProductVersion FileVersion FileName
-------------- ----------- --------
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\aaclient.dll
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\accessibilitycpl.dll
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\ACCTRES.dll
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\acledit.dll
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\aclui.dll
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\acppage.dll
6.1.7600.16385 6.1.7600.1638... C:\Windows\system32\acproxy.dll
(...)
So what's going on here?
Either - Or: Select-Object can not always help
It turns out that file objects have a subproperty called VersionInfo. It contains the DLL versions we are after. To display that information, I used Select-Object with its -expandProperty parameter. This parameter tells PowerShell to replace the current object with the content of the property you specify. Thus I lost my original FileInfo object and in return got the VersionInfo object found in the property I specified.
Sounds confusing? Now, what it means is that Select-Object can only give you the content of one subproperty, and you pay for it by loosing all the properties in your original object. It is a simple trade-off.
Getting Dirty: Using Ad-Hoc Script Properties
In the previous example, the trade-off was ok. It is not always ok, though. Let's assume you need a report that includes filename, version, length and creationtime. How can you do that? After all, the information needed now comes partially from the FileInfo object and partially from the VersionInfo object stored inside of it.
Well, if your hair is on fire and you haven't got the time for a decent approach, use quick-and-dirty scripted properties. Here is an example:
PS > dir $env:windir\system32\*.dll |
Select-Object Name, { $_.VersionInfo.FileVersion }, Length, CreationTime
Name $_.VersionInfo.Fil Length CreationTime
eVersion
---- ------------------- ------ ------------
aaclient.dll 6.1.7600.16385 (... 158208 14.07.2009 02:17:30
accessibilitycpl... 6.1.7600.16385 (... 3745792 14.07.2009 02:34:00
ACCTRES.dll 6.1.7600.16385 (... 39424 14.07.2009 01:57:56
acledit.dll 6.1.7600.16385 (... 9216 14.07.2009 01:48:08
aclui.dll 6.1.7600.16385 (... 154112 14.07.2009 01:57:30
(...)
As you see, instead of specifying a column name (property name), you can also submit a script block and access the object subproperty you are after. That approach is ok, and you can even use a hash table if you want to get rid of the ugly column header:
PS > $version = @{name = 'Version'; expression={ $_.VersionInfo.FileVersion } }
PS > dir $env:windir\system32\*.dll | Select-Object Name, $version, Length, Crea
tionTime
Name Version Length CreationTime
---- ------- ------ ------------
aaclient.dll 6.1.7600.16385 (... 158208 14.07.2009 02:17:30
(...)
(Note that this only works when you define the hash table in one line. That's a PowerShell bug)
However, it's a lot of writing, especially with the hash tables. Even worse, Select-Object actually replaces your object and is creating a read-only copy. For reports, that is ok. If you want to keep the object methods, though, you may never use Select-Object. And if you want fast scripts, you should avoid Select-Object as well. Let's check out why and what to do about it next.
ScriptProperties to the Rescue
Rather than using Select-Object to add properties, you can also add script properties to the original object. In fact, that is something PowerShell uses by default for a lot of objects. Take a look at process objects returned by Get-Process:
PS > Get-Process | Get-Member -MemberType ScriptProperty
TypeName: System.Diagnostics.Process
Name MemberType Definition
---- ---------- ----------
Company ScriptProperty System.Object Company {get=$this.Mainmodule.Fi...
CPU ScriptProperty System.Object CPU {get=$this.TotalProcessorTim...
Description ScriptProperty System.Object Description {get=$this.Mainmodul...
FileVersion ScriptProperty System.Object FileVersion {get=$this.Mainmodul...
Path ScriptProperty System.Object Path {get=$this.Mainmodule.FileN...
Product ScriptProperty System.Object Product {get=$this.Mainmodule.Fi...
ProductVersion ScriptProperty System.Object ProductVersion {get=$this.Mainmo...
Here, PowerShell has made internal subproperties surface the top level by adding script properties. To see their actual code, try this:
PS > Get-Process | Get-Member -MemberType ScriptProperty | Select-Object -ExpandProperty Definition
System.Object Company {get=$this.Mainmodule.FileVersionInfo.CompanyName;}
System.Object CPU {get=$this.TotalProcessorTime.TotalSeconds;}
System.Object Description {get=$this.Mainmodule.FileVersionInfo.FileDescription;}
System.Object FileVersion {get=$this.Mainmodule.FileVersionInfo.FileVersion;}
System.Object Path {get=$this.Mainmodule.FileName;}
System.Object Product {get=$this.Mainmodule.FileVersionInfo.ProductName;}
System.Object ProductVersion {get=$this.Mainmodule.FileVersionInfo.ProductVersion;}
Note the use of -expandProperty again? Whenever PowerShell truncates your output, use it to pick the contents of the column you are interested in, and you get that information without truncation. Using Select-Object is not a bad thing, it is very useful in many scenarios. It is just not the best way to add new properties to objects as you'll see in a sec.
Creating New ScriptProperties With Add-Member
To add version information to file objects without changing the objects, you can add this property using Add-Member. Try this:
PS > dir $env:windir\system32\*.dll | Add-Member -MemberType ScriptProperty -Nam
e Version -Value { $this.VersionInfo.FileVersion } -PassThru | Select-Object Nam
e, Version, Length, CreationTime
Name Version Length CreationTime
---- ------- ------ ------------
aaclient.dll 6.1.7600.16385 (... 158208 14.07.2009 02:17:30
accessibilitycpl... 6.1.7600.16385 (... 3745792 14.07.2009 02:34:00
ACCTRES.dll 6.1.7600.16385 (... 39424 14.07.2009 01:57:56
(...)
Here I used Add-Member inside the pipeline to add the script property to the file objects returned by dir. I used the very same script block that I used with Select-Object except for one major difference: when you work with scriptproperties, replace $_ by $this.
The result seems to be just the same, so what is the difference compared to using Select-Object with hashtables and scripted properties?
- Huge Performance Benefit: It is much faster. My approach with Select-Object ran 41 seconds.[and the SECOND time I ran it, it ran 3 seconds, so there is a caching effect here]. This approach with Add-Member just takes 1 second. Thats because Select-Object always has to copy the entire object which is a very expensive and ressource-hungry task. Add-Member just leaves the object as-is, and just attaches the new property to it.
- Methods: For the same reasons, you keep the original objects and all the methods contained in them. Select-Object removes all methods and disconnects the copied object from the real object as well, so any changes to any properties will not do anything to the underlying object anymore.
Add-SubProps - Enjoy and Automate!
So you now know there can be tons of useful information in subproperties, and you should use Add-Member to make these properties visible. But... isn't that a bit too hard? Too much code, too much to remember? Right. That's why I created a function called Add-SubProps.
All you do is pipe your objects to it and specify the subproperty you would like to append. You can do that multiple times if you want and add the properties of a number of subobjects. It is all automated for you, and it is fast. It even creates read/write ScriptProperties for properties that are writeable. Here is what you can do with this function:
PS > dir $env:windir\system32\*.dll | Add-SubProps VersionInfo | Select-Object Name, ProductVersion, OriginalFileName, FileDescription
Name ProductVersion OriginalFilename FileDescription
---- -------------- ---------------- ---------------
aaclient.dll 6.1.7600.16385 aaclient.dll.mui Client für Zugri...
accessibilitycpl... 6.1.7600.16385 AccessibilityCpl... Systemsteuerung ...
ACCTRES.dll 6.1.7600.16385 ACCTRES.DLL.MUI Microsoft Intern...
acledit.dll 6.1.7600.16385 acledit.dll.mui Zugriffssteuerun...
aclui.dll 6.1.7600.16385 aclui.dll.mui Sicherheitsdeskr...
acppage.dll 6.1.7600.16385 acppage.dll.mui Shellerweiterung...
(...)
Now isn't that cool? Simply by piping file objects to Add-SubProps, I was able to access all the additional properties usually only found in the VersionInfo property.
Here is the Add-SubProps function (you can download it here):
function Add-SubProps {
param(
$prefix,
[Parameter(ValueFromPipeline=$true)]
$object
)
process {
$props = $object.$prefix | Get-Member -MemberType Property
$props | ForEach-Object {
$propname = $_.name
$writeable = $_.Definition -like '*{get;set;}*'
$getter = [ScriptBlock]::Create(('$this.{0}.{1}' -f $prefix,$propname))
if ($writeable) {
$setter = [ScriptBlock]::Create(('param(${1}) $this.{0}.{1} = ${1}' -f $prefix,$propname))
try {
$object = $object | Add-Member -MemberType ScriptProperty -Name $propname -Value $getter -SecondValue $setter -ea Stop -passthru
} catch {}
} else {
try {
$object = $object | Add-Member -MemberType ScriptProperty -Name $propname -Value $getter -ea Stop -passthru
} catch {}
}
}
$object
}´
}
If you read my post about reading and writing MPEG3 tags, you now know how the module magic was done. The module loaded the DLL that made the functionality available, and then Add-SubProps was used to compose the MPEG3 media objects with all their rich information. Since Add-SubProps leaves the original object untouched and adds read/write script properties, you were able to not just only read audio and video tags, but also change them and save them.
Add-SubProps is extremely valuable as you will see. For example, when you manage virtual machines with VMWare, Get-VM gets you objects that keep the interesting parts of your virtual machines in objects found in subproperties. You can now easily make them visible. Hope you had fun, see you next week around...
Tobias
Microsoft MVP PowerShell Germany
P.S.
If you live in Germany or other parts of Europe and your company would like to set up a truly great PowerShell training, just contact me! I regularly train mid- to large-size companies. Trainings are always a blast with tons of real-world-examples and solutions. Here's how to get in touch with me: tobias.weltner@scriptinternals.de
Posted
Feb 17 2011, 02:22 AM
by
Tobias