Tag Archives: Function

Get CIMInstance from PowerShell 2.0

I love the new CIM cmdlets in PowerShell 3.0. Querying WMI is a little faster because the CIM cmdlets query WMI using the WSMAN protocol instead of DCOM. The catch is that remote computers must be running PowerShell 3 which includes the latest version of the WSMAN protocol and the WinRM service. But if your computers are running 3.0 then you can simply run a command like this:

However, if one of the computers is running PowerShell 2.0, you’ll get an error.

get-ciminstance-error

In this example, CHI-DC02 is not running PowerShell 3.0. The solution is to create a CIMSession using a CIMSessionOption for DCOM.

get-ciminstance-dcom

So there is a workaround, but you have to know ahead of time which computers are not running PowerShell 3.0. When you use Get-CimInstance and specify a computername, the cmdlet setups up a temporary CIMSession. So why not create the temporary CIMSession with the DCOM option if it is needed? So I wrote a “wrapper” function called Get-MyCimInstance to do just that.

The heart of the function is a nested function to test if a remote computer is running WSMAN 3.0.

The function uses Test-WSMan and a regular expression to get the remoting version. If it is 3.0 the function returns True. In Get-MyCIMInstance I test each computer and if not running 3.0, create the CIMSession option and include it when creating the temporary CIMSession.

I’m using a Try/Catch block because if the computer is offline, my test function will throw an exception which I can catch.

Otherwise, all is good and  I can pass the rest of the parameters to Get-CimInstance.

At the end of the process, I remove the temporary CIMSession. With this, now I can query both v2 and v3 computers.
get-myciminstance01
Notice for CHI-DC02 I’m creating the DCOM option. Here’s the command without all the verboseness.
get-myciminstance02
I could have created a proxy function for Get-CimInstance, but not only are they more complicated, I didn’t want that much transparency. I wanted to know that I’m querying using my function and not Get-CimInstance. Here’s the complete script.

I hope you’ll let me know what you think and if you find this useful.

UPDATE: I’ve revised this script and article since it’s original posting to better handle errors if you can’t test WSMAN. I also added support for alternate credentials, which is something you can’t do with Get-CimInstance.

Find Files with WMI and PowerShell Revisited

computereyeLast week I posted a PowerShell function to find files using WMI. One of the comments I got was about finding files with wildcards. In WMI, we’ve been able to search via wildcards and the LIKE operator since the days of XP.

In a WMI query we use the % as the wildcard character. Here’s an example:


PS C:\scripts> get-wmiobject win32_service -filter "displayname LIKE 'Micro%'" | Select Name,Displayname,State,Startmode

Name Displayname State Startmode
---- ----------- ----- ---------
MSiSCSI Microsoft iSCSI Initi... Stopped Manual
swprv Microsoft Software Sh... Stopped Manual
wlidsvc Microsoft Account Sig... Stopped Manual

So it wasn’t especially difficult to revise my original function to accept wildcards as part of the file name. Since we are used to using * as the wildcard character, I assumed it would be used here as well. So all I had to do was replace the * with % and change the operator.


#if there is * in the filename or extension, replace it with %
#and change the comparison operator for the WMI query
if ($filename -match "\*" ) {
Write-Verbose "Wildcard search on filename"
$filename = $filename.Replace("*","%")
$fileOp="LIKE"
}
else {
$fileOp="="
}
if ($extension -match "\*") {
Write-Verbose "Wildcard search on extension"
$extension = $extension.Replace("*","%")
$extOp="LIKE"
}
else {
$extOp="="
}
$filter = "Filename $fileOp '$filename' AND extension $extOp '$extension' AND Drive='$drive'"
Write-Verbose $filter

The reason I didn’t simply make the expression use LIKE all the time is performance. When you use the LIKE operator, there is a significant performance price to pay. So I only wanted to use LIKE if I really had to.

The other change I made was to accept an alternate credential (using a technique Boe Prox turned me on to).


...
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,
...

If you pass a string like mydomain\admin, you’ll get prompted for the password. Or you can pass a previously created credential object. Here’s the revised code, less the comment based help.


#requires -version 2.0

Function Get-CIMFile {

[cmdletbinding(DefaultParameterSetName="Default")]
Param(
[Parameter(Position=0,Mandatory=$True,HelpMessage="What is the name of the file?")]
[ValidateNotNullorEmpty()]
[alias("file")]
[string]$Name,
[ValidateNotNullorEmpty()]
[string]$Drive="C:",
[ValidateNotNullorEmpty()]
[string[]]$Computername=$env:computername,
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,
[Parameter(ParameterSetName="Job")]
[switch]$AsJob,
[Parameter(ParameterSetName="Job")]
[int32]$ThrottleLimit=32
)

<#
strip off any trailing characters to drive parameter
that might have been passed.
#>
If ($Drive.Length -gt 2) {
$Drive=$Drive.Substring(0,2)
}

Write-Verbose "Searching for $Name on Drive $Drive on computer $Computername."

<#
Normally you might think to simply split the name on the . character. But
you might have a filename like myfile.v2.dll so that won't work. In a case
like this the extension would be everything after the last . and the filename
everything before.

So instead I'll use the substring method to "split" the filename string.
#>

#get the index of the last .
$index = $name.LastIndexOf(".")
#get the first part of the name
$filename=$Name.Substring(0,$index)
#get the last part of the name
$extension=$name.Substring($index+1)

#if there is * in the filename or extension, replace it with %
#and change the comparison operator for the WMI query
if ($filename -match "\*" ) {
Write-Verbose "Wildcard search on filename"
$filename = $filename.Replace("*","%")
$fileOp="LIKE"
}
else {
$fileOp="="
}
if ($extension -match "\*") {
Write-Verbose "Wildcard search on extension"
$extension = $extension.Replace("*","%")
$extOp="LIKE"
}
else {
$extOp="="
}
$filter = "Filename $fileOp '$filename' AND extension $extOp '$extension' AND Drive='$drive'"
Write-Verbose $filter

#build the core command
$cmd="Get-WmiObject -Class CIM_Datafile -Filter ""$filter"" -ComputerName $Computername"

if ($credential) {
write-Verbose "Adding credential for $($credential.username)"
$cmd+=" -credential `$credential"

} #if credential

#get all instances of the file and write the WMI object to the pipeline
if ($AsJob) {
Write-Verbose "Running query as a job"
$cmd+=" -Asjob -ThrottleLimit $ThrottleLimit"
}
else {
#record the start time
$start=Get-Date
}

Write-Verbose $cmd
Invoke-Expression -Command $cmd

#display how long this took if not running as a job
if ($start) {
#get the end time and report how long the search took
$end=Get-Date
Write-Verbose "Search completed in $($end-$start)"
}

} #end Get-CIMFile

The last major change is that I construct a Get-WmiObject command string based on the parameter values. I start with a core command.


#build the core command
$cmd="Get-WmiObject -Class CIM_Datafile -Filter ""$filter"" -ComputerName $Computername"

And then add other parameters based on the user’s input. I use Invoke-Expression to run the final command.

You can download my revised version of Get-CIMFile

Finally, while I was working on this revision PowerShell madman Boe Prox was working up his own version which has even more bells and whistles. Many of the articles I write here are intended as tutorials and not necessarily production ready tools. Boe’s version on the other hand is on PEDs (PowerShell Enhancing Drug). Check it out.

Find Files with WMI and PowerShell

magnifying-glass-text-label-searchFinding files is one of those necessary evils for IT Pros. Sometimes we’re searching for a needle in a haystack. And it gets even more complicated when the haystacks are on 10 or 100 or 1000 remote computers. You might think using Get-ChildItem is your only option. Certainly it works, but if you are searching an entire hard drive it is pretty resource intensive.

Another option is to use WMI and CIM_Datafile class. Don’t let the name fool you. You can use Get-WmiObject in PowerShell 2.0 or 3.0. Every file, as far as I know, is also registered with WMI so all you need to do is query for all instances of the CIM_Datafile class. However, you must be clever. Just like searching an entire drive, searching via WMI can be time consuming. So you need to make your WMI query as specific as possible. To do that you need to know the properties. Here’s what a CIM_Datafile object looks like.

Status : OK
Name : c:\program files (x86)\windows defender\mpclient.dll
__GENUS : 2
__CLASS : CIM_DataFile
__SUPERCLASS : CIM_LogicalFile
__DYNASTY : CIM_ManagedSystemElement
__RELPATH : CIM_DataFile.Name="c:\\program files (x86)\\windows defender\\mpclient.dll"
__PROPERTY_COUNT : 33
__DERIVATION : {CIM_LogicalFile, CIM_LogicalElement, CIM_ManagedSystemElement}
__SERVER : SERENITY
__NAMESPACE : root\cimv2
__PATH : \\SERENITY\root\cimv2:CIM_DataFile.Name="c:\\program files (x86)\\windows d
efender\\mpclient.dll"
AccessMask : 17957033
Archive : True
Caption : c:\program files (x86)\windows defender\mpclient.dll
Compressed : False
CompressionMethod :
CreationClassName : CIM_LogicalFile
CreationDate : 20120725214205.814611-240
CSCreationClassName : Win32_ComputerSystem
CSName : SERENITY
Description : c:\program files (x86)\windows defender\mpclient.dll
Drive : c:
EightDotThreeFileName : c:\program files (x86)\windows defender\mpclient.dll
Encrypted : False
EncryptionMethod :
Extension : dll
FileName : mpclient
FileSize : 662016
FileType : Application Extension
FSCreationClassName : Win32_FileSystem
FSName : NTFS
Hidden : False
InstallDate : 20120725214205.814611-240
InUseCount :
LastAccessed : 20120725214205.814611-240
LastModified : 20120725231905.556000-240
Manufacturer : Microsoft Corporation
Path : \program files (x86)\windows defender\
Readable : True
System : False
Version : 4.0.9200.16384
Writeable : True
Scope : System.Management.ManagementScope
Options : System.Management.ObjectGetOptions
ClassPath : \\SERENITY\root\cimv2:CIM_DataFile
Properties : {AccessMask, Archive, Caption, Compressed...}
SystemProperties : {__GENUS, __CLASS, __SUPERCLASS, __DYNASTY...}
Qualifiers : {dynamic, Locale, provider, UUID}
Site :
Container :

At a minimum you should limit your query to the drive. Otherwise the WMI query will search ALL drives. If you are searching by path, description or caption, don’t forget that the \ character needs to be escaped, e.g. \\program files (x86)\\windows defender\\. Of course if you know that much already you might as well use Get-Childitem.

For me, the real benefit in using WMI is when I know the file name but don’t know for sure where it might be on a given drive. So I put together an advanced function called Get-CIMFile.


Function Get-CIMFile {
#comment based help is here

[cmdletbinding()]
Param(
[Parameter(Position=0,Mandatory=$True,HelpMessage="What is the name of the file?")]
[ValidateNotNullorEmpty()]
[alias("file")]
[string]$Name,
[ValidateNotNullorEmpty()]
[string]$Drive="C:",
[ValidateNotNullorEmpty()]
[string[]]$Computername=$env:computername,
[switch]$AsJob
)

<#
strip off any trailing characters to drive parameter
that might have been passed.
#>
If ($Drive.Length -gt 2) {
$Drive=$Drive.Substring(0,2)
}

Write-Verbose "Searching for $Name on Drive $Drive on computer $Computername."

<#
Normally you might think to simply split the name on the . character. But
you might have a filename like myfile.v2.dll so that won't work. In a case
like this the extension would be everything after the last . and the filename
everything before.

So instead I'll use the substring method to "split" the filename string.
#>

#get the index of the last .
$index = $name.LastIndexOf(".")
#get the first part of the name
$filename=$Name.Substring(0,$index)
#get the last part of the name
$extension=$name.Substring($index+1)

$filter = "Filename='$filename' AND extension='$extension' AND Drive='$drive'"
Write-Verbose $filter

#get all instances of the file and write the WMI object to the pipeline
Get-WmiObject -Class CIM_Datafile -Filter $filter -ComputerName $Computername -Asjob:$AsJob

} #end Get-CIMFile

The code is documented to explain what is going on so I won’t repeat it here. The function will work anywhere you have WMI access. This version doesn’t handle alternate credentials or other features of Get-WmiObject, which you could add if you want. But with this I can check files on multiple computers. Suppose I’m concerned about a vulnerability like the recent Java problem. Or I need to see if any computers are out of date on a given file. I can run a command like this.


PS Scripts:\> $files = get-cimfile mpclient.dll -comp serenity,novo8
PS Scripts:\> $files | Sort name,CSName | Select Name,Version,CSName

Name Version CSName
---- ------- ------
c:\program files (x86)\windows... 4.0.9200.16384 SERENITY
c:\program files\windows defen... 4.0.9200.16384 NOVO8
c:\program files\windows defen... 4.0.9200.16384 SERENITY
c:\windows\winsxs\amd64_window... 4.0.9200.16384 SERENITY
c:\windows\winsxs\wow64_window... 4.0.9200.16384 SERENITY
c:\windows\winsxs\x86_windows-... 4.0.9200.16384 NOVO8

The command writes the full WMI object to the pipeline so I could filter or format $files however I need.

Download Get-CIMFile and let me know what you think.

Create PowerShell Scripts with a Single Command

One of the drawbacks to using a PowerShell script or function is that you have to write it. For many IT Pros, especially those new to PowerShell, it can be difficult to know where to start. I think more people would write their own tools if there was an easy way to automatically write them. So here’s my solution. I wrote a function called New-PSCommand that is intended as a rapid development tool for a PowerShell advanced function. It should work in either PowerShell v2 or v3.

Here’s the function, although I’m not going to spend much time explaining how it works but rather how to use it.


Function New-PSCommand {
#comment base help goes here

[cmdletbinding()]

Param(
[Parameter(Mandatory=$True,HelpMessage="Enter the name of your new command")]
[ValidateNotNullorEmpty()]
[string]$Name,
[ValidateScript({
#test if using a hashtable or a v3 [ordered] hash table
($_ -is [hashtable]) -OR ($_ -is [System.Collections.Specialized.OrderedDictionary])
})]

[Alias("Parameters")]
[object]$NewParameters,
[switch]$ShouldProcess,
[string]$Synopsis,
[string]$Description,
[string]$BeginCode,
[string]$ProcessCode,
[string]$EndCode,
[switch]$UseISE
)

Write-Verbose "Starting $($myinvocation.mycommand)"
#add parameters
$myparams=""
$helpparams=""

Write-Verbose "Processing parameter names"

foreach ($k in $newparameters.keys) {
Write-Verbose " $k"
$paramsettings = $NewParameters.item($k)

#process any remaining elements from the hashtable value
#@{ParamName="type[]",Mandatory,ValuefromPipeline,ValuefromPipelinebyPropertyName,Position}

if ($paramsettings.count -gt 1) {
$paramtype=$paramsettings[0]
if ($paramsettings[1] -is [object]) {
$Mandatory = "Mandatory=`${0}," -f $paramsettings[1]
Write-Verbose $Mandatory
}
if ($paramsettings[2] -is [object]) {
$PipelineValue = "ValueFromPipeline=`${0}," -f $paramsettings[2]
Write-Verbose $PipelineValue
}
if ($paramsettings[3] -is [object]) {
$PipelineName = "ValueFromPipelineByPropertyName=`${0}" -f $paramsettings[3]
Write-Verbose $PipelineName
}
if ($paramsettings[4] -is [object]) {
$Position = "Position={0}," -f $paramsettings[4]
Write-Verbose $Position
}
}
else {
#the only hash key is the parameter type
$paramtype=$paramsettings
}

$item = "[Parameter({0}{1}{2}{3})]`n" -f $Position,$Mandatory,$PipelineValue,$PipelineName
$item +="[{0}]`${1}" -f $paramtype,$k
Write-Verbose "Adding $item to myparams"
$myparams+="$item, `n"
$helpparams+=".PARAMETER {0} `n`n" -f $k
#clear variables but ignore errors for those that don't exist
Clear-Variable "Position","Mandatory","PipelineValue","PipelineName","ParamSettings" -ErrorAction SilentlyContinue

} #foreach hash key

#get trailing comma and remove it
$myparams=$myparams.Remove($myparams.lastIndexOf(","))

Write-Verbose "Building text"
$text=@"
Function $name {
<#
.SYNOPSIS
$Synopsis

.DESCRIPTION
$Description

$HelpParams
.EXAMPLE
PS C:\> $Name

.NOTES
Version: 0.1
Author : $env:username

.INPUTS

.OUTPUTS

.LINK
#>

[cmdletbinding(SupportsShouldProcess=`$$ShouldProcess)]

Param (
$MyParams
)

Begin {
Write-Verbose "Starting `$(`$myinvocation.mycommand)"
$BeginCode
} #begin

Process {
$ProcessCode
} #process

End {
$EndCode
Write-Verbose "Ending `$(`$myinvocation.mycommand)"
} #end

} #end $name function

"@

if ($UseISE -and $psise) {
$newfile=$psise.CurrentPowerShellTab.Files.Add()
$newfile.Editor.InsertText($Text)
}
else {
Write $Text
}

Write-Verbose "Ending $($myinvocation.mycommand)"

} #end New-PSCommand function

The premise of this function is to take a hash table of parameter definitions and create a new PowerShell advanced function.

The hash table key is the name of your parameter and the key is its type. The only other value you need is the name for your new function. The New-PSCommand function creates a new advanced function, complete with comment-based help, and writes the text to the pipeline. That way you can either review it, pipe it to Out-File or copy it to the clipboard.


PS C:\>$paramhash=@{Name="string[]";Test="switch";Path="string"}
PS C:\> New-PSCommand -name "Set-MyScript" -Newparameters $paramhash | out-file "c:\scripts\set-myscript.ps1"

The hash table of parameters for my Set-MyScript function includes an array of strings for $Name, a string for $Path, and a switch for $Test. Here’s the output:


Function Set-MyScript {
<#
.SYNOPSIS

.DESCRIPTION

.PARAMETER Path

.PARAMETER Name

.PARAMETER Test

.EXAMPLE
PS C:\> Set-MyScript

.NOTES
Version: 0.1
Author : Jeff

.INPUTS

.OUTPUTS

.LINK
#>

[cmdletbinding(SupportsShouldProcess=$False)]

Param (
[Parameter()]
[string]$Path,
[Parameter()]
[string[]]$Name,
[Parameter()]
[switch]$Test
)

Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"

} #begin

Process {

} #process

End {

Write-Verbose "Ending $($myinvocation.mycommand)"
} #end

} #end Set-MyScript function

All you need to do is fill in the script with the code you want to run. New-PSCommand does all of the grunt work for you leaving you just the fun part. I also support an alternate hashtable setup if you want to specify some parameter attributes. Create a hash table using this format:


@{ParamName="type[]",Mandatory,ValuefromPipeline,ValuefromPipelinebyPropertyName,Position}

Here’s an example:

$h = @{Name="string[]",$True,$True,$False,0;
Path="string",$false,$false,$false,1;
Size="int",$false,$false,$true;
Recurse="switch"
}

I also let you specify commands to use in the Begin, Process and End scriptblocks. You can also define values for the help synopsis and description.

The last little bell on this tool is that if you run it in the PowerShell ISE, you can use the -UseISE switch which will open your new script in a new file in the ISE. This means you could open a new PowerShell tab in the ISE and run commands like this:


$hash = [ordered]@{
Name="string[]",$True,$True,$False,0
Path="string",$false,$false,$false,1
PixieDust="int",$false,$false,$true
NoUnicorn="switch"
}

$begin={
#initialize some variables
$arr=@()
$Love=$True
$ring=1
}

$end="write-host 'Finished' -foreground Green"
$synopsis = "Get magical user data"
$desc = @"
This command will do something really amazing and magical. All you need to do is provide
the right amount of pixie dust and shavings from a unicorn horn.

This requires PowerShell v4 and a full moon.
"@

. C:\scripts\New-PSCommand.ps1
New-PSCommand -Name Get-UserData -NewParameters $hash -BeginCode $begin -EndCode $end -Synopsis $synopsis -Description $desc -UseISE

By the way, I’m running PowerShell v3 so I can use a [ordered] hash table which I encourage you to use if you can. When executed, I get a new script in the ISE ready for me to finish.


Function Get-UserData {
<#
.SYNOPSIS
Get magical user data

.DESCRIPTION
This command will do something really amazing and magical. All you need to do is provide
the right amount of pixie dust and shavings from a unicorn horn.

This requires PowerShell v4 and a full moon.

.PARAMETER Name

.PARAMETER Path

.PARAMETER PixieDust

.PARAMETER NoUnicorn

.EXAMPLE
PS C:\> Get-UserData

.NOTES
Version: 0.1
Author : Jeff

.INPUTS

.OUTPUTS

.LINK
#>

[cmdletbinding(SupportsShouldProcess=$False)]

Param (
[Parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$False)]
[string[]]$Name,
[Parameter(Position=1,Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelineByPropertyName=$False)]
[string]$Path,
[Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelineByPropertyName=$True)]
[int]$PixieDust,
[Parameter()]
[switch]$NoUnicorn
)

Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"

#initialize some variables
$arr=@()
$Love=$True
$ring=1

} #begin

Process {

} #process

End {
write-host 'Finished' -foreground Green
Write-Verbose "Ending $($myinvocation.mycommand)"
} #end

} #end Get-UserData function

I hope that a tool like this helps cut down on your development time. Please download New-PSCommand and let me know what you think.

PowerShell Pivot Tables

I was working on a question in the Ask Don and Jeff forum about multidimensional arrays. The ultimate goal of sorts was to create an HTML report that contained in essence a pivot table. This device allows you to slice and dice data so that you can identify trends or patterns. In PowerShell, there aren’t too many tools that allow you to “re-organize” an object. For example, say you have a variable that contains the results of querying a few computers for some critical services. But you want a table that shows you the computername as the Y-Axis and each service name as the header on the X axis. For each computer, the intersection should be the status of said property. In other words, a result like this:


Computername Lanmanserver Wuauserv Bits Spooler Audiosrv
------------ ------------ -------- ---- ------- --------
serenity Running Running Running Running Running
quark Stopped Running Running Running
jdhit-dc01 Running Running Running Stopped

At a glance I can see where there might be problems. Blanks indicate the service doesn’t exist on that computer. In this case, you might be able to get similar results with some grouping and Format-Table tricks. But more than likely you wouldn’t have something you could export to XML or convert to HTML.
Or say you have a directory listing for a handful of file extensions and you want to see a count of each extension for each folder, perhaps something like this:


Directory .ZIP .BAT .PS1 .TXT
--------- ---- ---- ---- ----
C:\scripts\AD-Old\New 0 0 1 1
C:\scripts\AD-Old 1 0 82 1
C:\scripts\ADTFM-Scripts\LocalUsersGroups 0 0 8 0
C:\scripts\ADTFM-Scripts 0 0 55 3
C:\scripts\en-US 0 0 1 0
C:\scripts\GPAE 0 0 8 3
C:\scripts\modhelp 1 0 0 0
C:\scripts\PowerShellBingo 0 0 4 0
C:\scripts\PS-TFM 1 0 69 2
C:\scripts\PSVirtualBox 0 0 0 1
C:\scripts\quark 0 0 0 1
C:\scripts\TechEd2012 1 0 11 3
C:\scripts\Toolmaking\old 0 0 10 0
C:\scripts\Toolmaking 0 0 48 0
C:\scripts 55 13 1133 305

That last row is not a sum, but rather the count of each of those file types in the root of C:\Scripts. I did all of this with a function I call New-PSPivotTable.


Function New-PSPivotTable {
<#
comment based help omitted here
#>

[cmdletbinding(DefaultParameterSetName="Property")]

Param(
[Parameter(Position=0,Mandatory=$True)]
[object]$Inputobject,
[Parameter()]
[String]$yLabel,
[Parameter(Mandatory=$True)]
[String]$yProperty,
[Parameter(Mandatory=$True)]
[string]$xLabel,
[Parameter(ParameterSetName="Property")]
[string]$xProperty,
[Parameter(ParameterSetName="Count")]
[switch]$Count,
[Parameter(ParameterSetName="Sum")]
[string]$Sum,
[Parameter(ParameterSetName="Sum")]
[ValidateSet("None","KB","MB","GB","TB")]
[string]$Format="None",
[Parameter(ParameterSetName="Sum")]
[ValidateScript({$_ -gt 0})]
[int]$Round
)

Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
$Activity="PS Pivot Table"
$status="Creating new table"
Write-Progress -Activity $Activity -Status $Status
#initialize an array to hold results
$result=@()
#if no yLabel then use yProperty name
if (-Not $yLabel) {
$yLabel=$yProperty
}
Write-Verbose "Vertical axis label is $ylabel"
}
Process {
Write-Progress -Activity $Activity -status "Pre-Processing"
if ($Count -or $Sum) {
#create an array of all unique property names so that if one isn't
#found we can set a value of 0
Write-Verbose "Creating a unique list based on $xLabel"
<#
Filter out blanks. Uniqueness is case sensitive so we first do a
quick filtering with Select-Object, then turn each of them to upper
case and finally get unique uppercase items.
#>
$unique=$inputobject | Where {$_.$xlabel} |
Select-Object -ExpandProperty $xLabel -unique | foreach {
$_.ToUpper()} | Select-Object -unique

Write-Verbose ($unique -join ',' | out-String).Trim()

}
else {
Write-Verbose "Processing $xLabel for $xProperty"
}

Write-Verbose "Grouping objects on $yProperty"
Write-Progress -Activity $Activity -status "Pre-Processing" -CurrentOperation "Grouping by $yProperty"
$grouped=$Inputobject | Group -Property $yProperty
$status="Analyzing data"
$i=0
$groupcount=($grouped | measure).count
foreach ($item in $grouped ) {
Write-Verbose "Item $($item.name)"
$i++
#calculate what percentage is complete for Write-Progress
$percent=($i/$groupcount)*100
Write-Progress -Activity $Activity -Status $Status -CurrentOperation $($item.Name) -PercentComplete $percent
$obj=new-object psobject -property @{$yLabel=$item.name}
#process each group
#Calculate value depending on parameter set
Switch ($pscmdlet.parametersetname) {

"Property" {
<#
take each property name from the horizontal axis and make
it a property name. Use the grouped property value as the
new value
#>
$item.group | foreach {
$obj | Add-member Noteproperty -name "$($_.$xLabel)" -value $_.$xProperty
} #foreach
}
"Count" {
Write-Verbose "Calculating count based on $xLabel"
$labelGroup=$item.group | Group-Object -Property $xLabel
#find non-matching labels and set count to 0
Write-Verbose "Finding 0 count entries"
#make each name upper case
$diff=$labelGroup | Select-Object -ExpandProperty Name -unique |
Foreach { $_.ToUpper()} |Select-Object -unique

#compare the master list of unique labels with what is in this group
Compare-Object -ReferenceObject $Unique -DifferenceObject $diff |
Select-Object -ExpandProperty inputobject | foreach {
#add each item and set the value to 0
Write-Verbose "Setting $_ to 0"
$obj | Add-member Noteproperty -name $_ -value 0
}

Write-Verbose "Counting entries"
$labelGroup | foreach {
$n=($_.name).ToUpper()
write-verbose $n
$obj | Add-member Noteproperty -name $n -value $_.Count -force
} #foreach
}
"Sum" {
Write-Verbose "Calculating sum based on $xLabel using $sum"
$labelGroup=$item.group | Group-Object -Property $xLabel

#find non-matching labels and set count to 0
Write-Verbose "Finding 0 count entries"
#make each name upper case
$diff=$labelGroup | Select-Object -ExpandProperty Name -unique |
Foreach { $_.ToUpper()} |Select-Object -unique

#compare the master list of unique labels with what is in this group
Compare-Object -ReferenceObject $Unique -DifferenceObject $diff |
Select-Object -ExpandProperty inputobject | foreach {
#add each item and set the value to 0
Write-Verbose "Setting $_ sum to 0"
$obj | Add-member Noteproperty -name $_ -value 0
}

Write-Verbose "Measuring entries"
$labelGroup | foreach {
$n=($_.name).ToUpper()
write-verbose "Measuring $n"

$measure= $_.Group | Measure-Object -Property $Sum -sum
if ($Format -eq "None") {
$value=$measure.sum
}
else {
Write-Verbose "Formatting to $Format"
$value=$measure.sum/"1$Format"
}
if ($Round) {
Write-Verbose "Rounding to $Round places"
$Value=[math]::Round($value,$round)
}
$obj | Add-member Noteproperty -name $n -value $value -force
} #foreach
}
} #switch

#add each object to the results array
$result+=$obj
} #foreach item
} #process
End {
Write-Verbose "Writing results to the pipeline"
$result
Write-Verbose "Ending $($myinvocation.mycommand)"
Write-Progress -Completed -Activity $Activity -Status "Ending"
}
} #end function

There is a lot going on here but I’ve tried to use comments and Write-Verbose statements to make it easier to track. In short, the function takes a collection of data and builds a table based on the property names and values you specify. You can build a table on a property like I did with the services. You can create a table based on count or on size. For the latter you need to also specify what property name to measure to calculate the size.

The function is writing an object to the pipeline which means you can transform the output however you need it.

PS C:\> $files=dir c:\scripts -include *.ps1,*.txt,*.zip,*.bat -recurse
PS C:\> $pivot=New-PSPivotTable $files -yProperty Directory -xlabel Extension -sum length -round 2 -format kb
PS C:\> $pivot[0] | format-table -auto

Directory .ZIP .BAT .PS1 .TXT
--------- ---- ---- ---- ----
C:\scripts\AD-Old\New 0 0 2.32 1.49
PS C:\> $pivot | convertto-html -Title "Extension Pivot" | out-file extpivot.htm

Honestly, I think the best thing is for you to download New-PSPivotTable and try it out. The download has full help and examples which you should read. I really hope you find it useful and if you run into some scenarios where this doesn’t work the way you think it should, please let me know.

Create an HTML PowerShell Help Page

Yesterday I posted an article about getting the online url for a cmdlet help topic. Today I want to demonstrate how we might take advantage of this piece of information.

Since the link is already in the form of a URL, wouldn’t it make sense to put this in an HTML document? At first glance, you might take the command from yesterday and pipe it to ConvertTo-HTML.


Get-Command -CommandType cmdlet | Get-Help |
Select Name,Synopsis,@{Name="URI";Expression={
($_.RelatedLinks | select -ExpandProperty NavigationLink | where {$_.uri}).uri}} |
Where {$_.URI} | ConvertTo-HTML -Title "Help Links" | Out-File c:\work\pshelp.htm

This code will work just fine. But if you look at the resulting file, we don’t have a link. I suppose it would be nice if ConvertTo-HTML could auto-detect URLs and automatically add a link, but it looks like we’ll have to do it. We could probably use a custom hash table to insert the HTML Anchor tags so here’s the first attempt:


Get-Command -CommandType cmdlet | Get-Help | Where {$_.RelatedLinks} |
Select Name,Synopsis,@{Name="URI";Expression={
#add the link tags as part of the output!
$link=$_.RelatedLinks | select -ExpandProperty NavigationLink | where {$_.uri}
if ($link.uri) {
$uri=$link.uri
Write "$uri"
}
else {
#no link so write a null
write $Null
}
}} | Where {$_.URI} | ConvertTo-HTML -Title "Help Links" |
Out-File c:\work\pshelp.htm

One additional change I made was to filter out cmdlets with no related links. Then in the expression scriptblock I can create a new value based on the URI value if it exists. But there is a problem with this, which you’ll see immediately if you run this code. ConvertTo-HTML sees the value of my new URI property and escapes the < and > characters.

That doesn’t help. But this is where the fact that ConvertTo-HTML only creates HTML code because we can parse the code and do a simple replace. Let me jump ahead and pull part of the finished dessert from the oven.


Function Convert-HTMLEscape {

<#
convert < and > to < and >
It is assumed that these will be in pairs
#>

[cmdletbinding()]

Param (
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Text
)

Process {
foreach ($item in $text) {
if ($item -match "<") {
<#
replace codes with actual symbols
This line is a shortcut to do two replacements
with one line of code. The code in the first
set of parentheses revised text with "<". This
normally gets written to the pipeline. By wrapping
it in parentheses it tells PowerShell to treat it
as an object so I can then call the Replace()
method again and add the >.
#>
($item.Replace("<","<")).Replace(">",">")
}
else {
#otherwise just write the line to the pipeline
$item
}
}
} #close process

} #close function

This function takes string input, presumably from ConvertTo-HTML and replaces the HTML escapes with the “real” characters. This I can use to write to the file.


...| Where {$_.URI} | ConvertTo-HTML -Title "Help Links" | Convert-HTMLEscape | Out-File c:\work\pshelp.htm

One of the reasons I put this in a function is to make it re-usable for future projects where I might need to escape these characters again. We’re getting closer. One last thing before we ice the final dessert: I have been using an expression to get all cmdlet help. But I like flexibility. What if tomorrow I only want a page with New* cmdlets from PowerCLI? So once again, I took my code that works just fine from a prompt into a more flexible and re-usable function.

Function Get-HelpUri {

[cmdletbinding()]

Param(
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a cmdlet name",
ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True)]
[ValidateNotNullorEmpty()]
[string]$Name
)

Process {
Write-Verbose "Processing $name"
Get-Help $name | Where {$_.RelatedLinks} |
Select Name,Synopsis,@{Name="URI";Expression={
#add the link tags as part of the output!
$link=$_.RelatedLinks | select -ExpandProperty NavigationLink | where {$_.uri}
if ($link.uri) {
$uri=$link.uri
Write "$uri"
}
else {
#no link so write a null
write $Null
}
}}
} #close process

} #close function

This will write a custom object with the cmdlet name, synopsis and URI property with the HTML code. I still need to convert to HTML and then fix the tags but I can verify it works.


PS C:\> get-command new-object | get-helpuri | ConvertTo-Html |Convert-HTMLEscape
">

Name Synopsis URI
New-Object Creates an instance of a Microsoft .NET Framework or COM object. 'http://go.microsoft.com/fwlink/?LinkID=113355' target='_blank'>http://go.microsoft.com/fwlink/?LinkID=11335
5


The icing is to include some style via a CSS file. Here’s a short script on how I might build a file for all of the cmdlets in my current session.


Write-Host "Building cmdlet help report" -ForegroundColor Green

#the file to create
$file="c:\work\cmdletonline.htm"

#be sure to change the path to the CSS file if you want to use it
$cssPath="c:\scripts\blue.css"

#optional image
$imagePath="c:\work\talkbubble.gif"

#some pre content
$preContent=@"



Cmdlet Online Help




"@

#some post content
$postContent=@"

Help for cmdlets found on $env:computername on $(Get-Date)
"@

#a title for the report
$Title="Cmdlet Help"

<#
Get all cmdlets in the current session, send them to the Get-HelpURI
function to parse out help URLS, filter out those without a link, pass
the remaining to the Convertto-HTML to generate HTML code which is piped
to my function to replace < with > and the final HTML code is piped to
a file.
#>
Get-Command -CommandType cmdlet | Get-HelpURI | Where {$_.URI} |
ConvertTo-Html -PreContent $PreContent -PostContent $postContent -Title $Title -cssUri $cssPath |
Convert-HTMLEscape | Out-File -FilePath $file -Encoding ASCII

Write-Host "Finished. See $file for the results" -ForegroundColor Green

And here’s the final result in Internet Explorer.

Ok, maybe you don’t have a compelling need for this exact script, but I hope you picked up on the importance of writing code for re-use and taking advantage of the pipeline.

If you’d like to try my code out for yourself, including the graphic and CSS file, download this zip file.

Export and Import Hash Tables

I use hash tables quite a bit and with the impending arrival of PowerShell 3.0 I expect even more so. PowerShell v3 allows you to define a hash table of default parameter values. I’m not going to to cover that feature specifically, but it made me realize I needed a better way to export a hash table, say to a CSV file. So I put together a few functions to do just that.

To walk you through them, here’s a simple hash table.


$hash=@{Name="jeff";pi=3.14;date=Get-Date;size=3 }
$hash
Name Value
---- -----
Name jeff
pi 3.14
date 2/2/2012 10:04:54 AM
size 3

I want to export this to a CSV file, but because PowerShell is all about the objects, I want to be sure to get the type information as well. Otherwise when I go to importing, everything will be a string. Here’s what I can expect to export:


$hash.GetEnumerator() | Select Key,Value,@{Name="Type";Expression={$_.value.gettype().name}}

Key Value Type
--- ----- ----
Name jeff String
pi 3.14 Double
date 2/2/2012 10:05:57 AM DateTime
size 3 Int32

That looks good. I can take this command and run it through Export-CSV which gives me this file:

#TYPE Selected.System.Collections.DictionaryEntry
"Key","Value","Type"
"Name","jeff","String"
"pi","3.14","Double"
"date","2/2/2012 10:05:57 AM","DateTime"
"size","3","Int32"

Perfect. Later, I will need to import this file and recreate my hash table. I can use Import-CSV as a starting point.


PS C:\> import-csv hash.csv

Key Value Type
--- ----- ----
Name jeff String
pi 3.14 Double
date 2/2/2012 10:05:57 AM DateTime
size 3 Int32

Good so far. All I need to do is create a hash table and add each entry to it. I could do something like this:


Import-csv hash.csv | foreach -begin {$hash=@{}} -process {$hash.Add($_.Key,$_.Value)} -end {$hash}

But if I do this, everything will be a string. Since I have Type information, let’s use it.


Import-Csv -Path $path | ForEach-Object -begin {
#define an empty hash table
$hash=@{}
} -process {
<#
if there is a type column, then add the entry as that type
otherwise we'll treat it as a string
#>
if ($_.Type) {

$type=[type]"$($_.type)"
}
else {
$type=[type]"string"
}
Write-Verbose "Adding $($_.key)"
Write-Verbose "Setting type to $type"

$hash.Add($_.Key,($($_.Value) -as $type))

} -end {
#write hash to the pipeline
Write-Output $hash
}

Here I’m taking the Type value from the import and turning it into a System.Type object which I can then use to cast each value to the correct type. I’m checking for the Type property because I might have a CSV file without it. But as long as I have column headings for Key and Value this will work.

I turned all of this into a pair of advanced functions, Export-HashtoCSV and Import-CSVtoHash.


PS C:\> $hash | Export-HashtoCSV myhash.csv
PS C:\> $newhash=Import-CSVtoHash .\myhash.csv -verbose
VERBOSE: Importing data from .\myhash.csv
VERBOSE: Adding Name
VERBOSE: Setting type to string
VERBOSE: Adding pi
VERBOSE: Setting type to double
VERBOSE: Adding date
VERBOSE: Setting type to System.DateTime
VERBOSE: Adding size
VERBOSE: Setting type to int
VERBOSE: Import complete
PS C:\> $newhash

Name Value
---- -----
Name jeff
pi 3.14
date 2/2/2012 10:05:57 AM
size 3
PS C:\> $newhash.date

Thursday, February 02, 2012 10:05:57 AM
PS C:\> $newhash.pi.gettype().name
Double

This certainly fulfills my needs. You can download a script file with both functions, including help. As always, enjoy and I hope you’ll let me know how these work out for you.