A Tridion tree-walk in Powershell
Now that I've got some reasonably terse syntax working for Tridion scripting, it's time to start building out some tooling to make the whole thing useful. It's quite often useful to be able to enumerate everything in your Tridion system, so walking the tree is a basic operation. You don't want to write the tree walk every time you have a different operation to perform, so it's handy to abstract the mechanics of the recursion out into a function. Somewhere in the nether regions of this blog, you'll find a JavaScript implementation of such a function. The basic technique I used in JavaScript was to have my tree-walking function accept a "process" function as an argument. For each item in your system, this is invoked, and is able to perform whatever processing is necessary on your item. (In the JavaScript version, I actually had two functions: process and filter. The filter function was responsible for deciding whether the item was interesting to process. In practice, this is probably too much abstraction. You can just as easily code an if-block in your process function, so on this occasion I'm restricting myself to just the one.)
To anyone who has written any JavaScript, it's pretty much impossible to miss the fact that functions are first-class objects. It may not be immediately apparent that this is true in Powershell, but it is. A Script Block in Powershell, is simply an anonymous function, and you can pass them around in variables or as parameters to other functions. (These days, the concept isn't even weird to C# hackers, what with lambda expressions and all.)
So - here goes: if you start with the function "recurseTridionItems" shown below....
import-module Reflection import-namespace Tridion.ContentManager.CoreService.Client function recurseTridionItems{ Param( [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [SessionAwareCoreServiceClient]$core, [IdentifiableObjectData]$parent, [ScriptBlock]$scriptblock, [int]$level = 0 ) $ro = new-object ReadOptions if ($parent -eq $null){ [PublicationData[]]$items = @($core.GetSystemWideList((new-object PublicationsFilterData))) foreach ($item in $items) { $fullItem = $core.Read($item.Id, $ro) &$Scriptblock $fullItem $level recurseTridionItems $core $fullItem $scriptblock ($level + 1) } } else { if ($parent -is [OrganizationalItemData]){ $items = $core.GetList($parent.Id, (new-object OrganizationalItemItemsFilterData)) } else { $items = $core.GetList($parent.Id, (new-object RepositoryItemsFilterData)) } foreach($item in $items) { $fullItem = $core.Read($item.Id, $ro) &$Scriptblock $fullItem $level if ($fullItem -is [PublicationData]) { recurseTridionItems $core $fullItem $scriptblock ($level + 1) } elseif ($item -is [OrganizationalItemData]) { recurseTridionItems $core $fullItem $scriptblock ($level + 1) } } } }
... this will take care of all the tree walking. For an example to show how you might use this, I've written a script block that outputs the Title of the item, indented based on the recursion level.
EDIT: my first version of this function didn't re-read the items that come from GetList. It worked fine for the trivial case of listing the titles but as soon as I tried anything more interesting, I discovered that GetList returns objects that are only partially loaded. This is apparently by design, as the documentation mentions it.
recurseTridionItems $core $null {param($item,$level)"`t" * $level + $item.Title}
On my system, this produces output like this:
_Empty Master Building Blocks Default Templates Outbound E-mail Generate Plain Text E-mail Outbound E-mail Post-processing Outbound E-mail Pre-processing Generate Plain Text E-mail Outbound E-mail Post-processing Outbound E-mail Pre-processing Set Output Item By Email Mode Tridion.OutboundEmail.Templating.Templates SDL External Content Library Adjust SiteEdit 2009 markup for External Content Library i Adjust SiteEdit 2012 markup for External Content Library i Resolve External Content Library items Search External Content Library items Tridion.ExternalContentLibrary.Templating Component Query Convert Html to Xml Convert Xml to Html Default Finish Actions Dreamweaver Region Selection Enable inline editing for content Enable inline editing for Page Extract Binaries from Html Image Resizer Link Resolver Publish Binaries in Package Default Component Template Default Component Template for UGC Default Page Template Default Page Template for UGC Activate Tracking Cleanup Template Component Query Convert Html to Xml Convert Xml to Html Default Dreamweaver Component Design Default Dreamweaver Page Design Default Finish Actions Default UGC Dreamweaver Template design Enable inline editing for content Enable inline editing for Page Enable User Generated Content Processing Extract Binaries from Html Extract Components from Page Image Resizer Link Resolver Publish Binaries in Package Sample XSLT Component Design Target Group Personalization Tridion.SiteEdit.Templating Tridion.Ugc.Templating.DefaultTemplates Default Multimedia Schema root 01 Definitions Building Blocks Default Templates Outbound E-mail Generate Plain Text E-mail
I think I'll truncate it there: you get the picture. Obviously, this is a trivial use-case that probably isn't terribly useful on an industrial scale installation. Fortunately, your script-block doesn't have to be a one-liner, and you can easily expand on this technique to meet your own needs. I should think I'll find quite a few uses for it myself. Just one word of caution: this was just a quick hack, and I haven't tested it exhaustively.
Sweet, we're one step closer to bringing back the authorization tool. Right after you finish Component Synchronizer. ;-)