Summary: Guest blogger, James O'Neill, discusses customizing Windows PowerShell output from his function to search Windows Index.

Microsoft Scripting Guy, Ed Wilson, is here. Today James O’Neill provides his conclusion to this three part series on

Note   This is Part Three of a three part series. In Part One, James talked about about building a query string to search the Windows Index. In Part Two, he talked about modifying the user input to coerce it into the form required by Windows Index.

Here’s James…

In Part One, I introduced a function that queries the Windows Index by using filter parameters like these:

  • "Contains(*,'Stingray')"
  • "System.Keywords = 'Portfolio' "
  • "System.Photo.CameraManufacturer LIKE 'CAN%' "
  • "System.image.horizontalSize > 1024"

In Part Two, I showed how these parameters could be simplified to do the following:

  • Stingray:
    A word on its own becomes a Contains term
  • Keyword=Portfolio:
    Keyword, without the S is an alias for System.Keywords and quotation marks will be added automatically
  • CameraManufacturer=CAN*:
    * will become %, and = will become LIKE, quotation marks will be added, and CameraManufacturer will be prefixed with System.Photo
  • Width > 1024:
    Width is an alias or System.image.horizontalsize, and quotation marks are not added around numbers.

There is one remaining issue. Windows PowerShell is designed so that one command's output becomes another's input. This function is not going to do much with piped input. I cannot see another command spitting out search terms for this one, nor can I see multiple paths being piped in. But the majority of items found by a search will be files. So it should be possible to treat them like files, piping them into Copy-Item or whatever.

The following was my first attempt at transforming the data rows into something more helpful:

$Provider="Provider=Search.CollatorDSO; Extended Properties=’Application=Windows’;"

$adapter = new-object system.data.oledb.oleDBDataadapter -argument $SQL, $Provider

$ds      = new-object system.data.dataset

if ($adapter.Fill($ds)) { foreach ($row in $ds.Tables[0])  {

    if ($row."System.ItemUrl" -match "^file:")
      {

          $obj = New-Object psobject -Property @{
          Path = (($row."System.ItemUrl" -replace "^file:","") -replace "\/","\")}

      }

    Else {$obj = New-Object psobject -Property @{Path = $row."System.ItemUrl"}}

    Add-Member -force -Input $obj -Name "ToString" -MemberType "scriptmethod" `

           -Value {$this.path}

    foreach ($prop in (Get-Member -InputObject $row -MemberType property |

                       where-object {$row."$($_.name)" -isnot [system.dbnull] }))

      {

          Add-Member -ErrorAction "SilentlyContinue" -InputObject $obj `

             -MemberType NoteProperty  -Name (($prop.name -split "\." )[-1]) `

             -Value  $row."$($prop.name)"

      }

    foreach ($prop in ($PropertyAliases.Keys |

          Where-Object {$row."$($propertyAliases.$_)" -isnot [system.dbnull] }))

      {

          Add-Member -ErrorAction "SilentlyContinue" -InputObject $obj `

             -MemberType AliasProperty -Name $prop `

             -Value ($propertyAliases.$prop  -split "\." )[-1]

      }

    $obj

}}

This is where the function spends most of its time:

  • Looping through the data and creating a custom object for each row.
  • Giving non-file items a Path property that holds the System.ItemURL property.
  • Processing the ItemUrl for files into normal format (rather than the format file:c/users/james).

In many cases, the item can be piped to another command successfully if it has a Path property. Then, for each property (database column) in the row, a member is added to the custom object with a shortened version of the property name and the value (assuming the column isn't empty).

Next, Alias properties are added by using the definitions in $PropertyAliases. Finally, some standard members get added. In this version I've pared it down to a single method, because several things expect to be able to get the path for a file by calling its tostring() method.

When I had all of this working, I tried to get clever. I added aliases for all the properties that normally appear on a System.IO.FileInfo object. I even tried fooling the formatting system in Windows PowerShell into treating my file items as a file object—something that only needs one extra line of code:

$Obj.psobject.typenames.insert(0, "SYSTEM.IO.FILEINFO")

Pretending that a custom object is actually another type seems dangerous; but everything I tried seemed happy, provided the right properties were present. The formatting worked except for the "Mode" column. I found the method that it calculates: .Mode for FILEINFO objects. But it needs a real FILEINFO object. It was easy enough to get one—I had the path, and it only needs a call to Get‑Item.

But I realized that if I was getting a FILEINFO object anywhere in the process, it made more sense to add extra properties to that object and dispense with the custom object. I added an extra -NoFiles switch to supress this behavior. So the code then transformed into the following:

$Provider="Provider=Search.CollatorDSO; Extended Properties=’Application=Windows’;"

$adapter = new-object system.data.oledb.oleDBDataadapter -argument $SQL, $Provider

$ds      = new-object system.data.dataset

if ($adapter.Fill($ds)) { foreach ($row in $ds.Tables[0])  {

    if (($row."System.ItemUrl" -match "^file:") -and (-not $NoFiles)) {
      {

        $obj = Get-item -Path (($row."System.ItemUrl" -replace "^file:","") `
                                 -replace "\/","\")

      }

    Else {$obj = New-Object psobject -Property @{Path = $row."System.ItemUrl"}

          Add-Member -force -Input $obj -Name "ToString" `

                     -MemberType "scriptmethod" -Value {$this.path}

         }

   ForEach ...

The initial code was 36 lines. Making the user input more friendly took it to 60 lines. The output added about another 35 lines—bringing the total to 95 lines.

There were four other kinds of output that I wanted to produce:

  • Help. I added comment-based Help with plenty of examples. It runs 75 lines, making it the biggest constituent in the finished product. In addition, I have 50 lines that are comments or blank for readability as insurance against trying to understand what those regular expressions do after a few months' time. But there are only 100 lines of actual code.
  • A –list switch which lists the long and short names for the fields (including aliases).
  • Support for the –Debug switch. Because so many things might go wrong, I have Write‑Debug $SQL immediately before I carry out the query. And to enable that, I have [CmdletBinding()] before I declare the parameters.
  • A –Value switch which uses the GROUP ON… OVER…  search syntax so I can see what the possible values are in a column.

GROUP ON queries are unusual because they fill the dataset with two tables. GROUP ON System.kind OVER ( SELECT STATEMENT) will produce a something like this as the first table:

SYSTEM.KIND                     Chapter

-----------                     -------

communication                         0

document                              1

email                                 2

folder                                3

link                                  4

music                                 5

picture                               6

program                               7

recordedtv                            8

The second table is the normal data suitably sorted. In this case, it has all the requested fields grouped by kind plus one named Chapter, which ties into the first table. I'm not really interested in the second table, but the first table helps me know if I should enter "Kind=Image", "Kind=Photo", or "Kind=Picture".

I have a Select-List function that I use in my configurator and Hyper-V library on CodePlex. With this, I can choose which recorded TV program to watch, first selecting by title, and then if there is more than one episode, by episode.

$t=(Get-IndexedItem -Value "title" -filter "kind=recordedtv" -recurse |
    Select-List -Property title).title

    start (Get-IndexedItem -filter "kind=recordedtv","title='$t'" -path |
    Select-List -Property ORIGINALBROADCASTDATE,PROGRAMDESCRIPTION)

The full script can be found on the Repository here.

~James

Thank you, James. This is a great series of blogs. Thank you for your hard work on this project and for taking the time to share it with us.

Join me tomorrow for more cool Windows PowerShell stuff.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy