The Power of Custom Properties

The other day fellow PowerShell MVP Adam Bertram published an article about using custom properties with Select-Object. It is a good article in that it gets you thinking about PowerShell in terms of objects and not simple text. But I want to take Adam’s article as a jumping off point and take his ideas a bit further. I’m going to use Adam’s same example as a learning tool. Don’t get distracted by other ways to get the same information. The process and techniques are what matter here.

Whenever I’m working with PowerShell, I’m always thinking about how I can use this at scale.  How can I get this same information for 10 or 100 servers? And of course,  at this point I need to make sure I include a computername in the results.  First, I’ll try something with a single computer.

image

Close but not quite. Get-CimInstance is writing multiple objects to the pipeline.  The server in question has 2  8GB sticks of memory which is what you see in the output. I need something more along Adam’s original idea to that this becomes 16GB.

What I really want is the Sum property from Measure-Object and to that I need to add a Computername property. I’ll turn things around a bit.

image

This works because I’m using the common PipelineVariable parameter introduced in PowerShell 4.  What happens is that the pipeline output from Get-CimInstance is stored in a variable, pv, which I can access later in the expression. In my case I’m defining a new property for the computername using $pv and adding it to the selected output from Measure-Object.

However, if I try this for multiple computer names, I don’t get the expected result.

image

That’s because I’m adding up the physical memory instances from all servers, which isn’t really what I want. Instead, this is a situation where I have to process each computer individually.

image

One thing to be careful of when using the ForEach enumerator is that you can’t pipe the output to another cmdlet like Export-CSV, unless you explicitly save the results to a variable.

Then you can pipe $data to other cmdlets. You can use ForEach-Object although it might be little harder to follow.

But this makes it easier if you need to pipe the output to something else.

image

To wrap this up let’s go all out and define a few more custom properties.

image

Even though I’m selecting a few properties from the output of Measure-Object, I’m defining several others which are calculated on the fly. There is so much you can do with this technique,  but if I lost you anywhere please let me know.

PowerShell for the People: Making the shell work for you

021913_2047_WordTest1.png Once you have some basic PowerShell experience I think you will begin looking for all sorts of ways to use PowerShell. Although one of the biggest obstacles for many IT Pros is the thought of having to type everything. Certainly, PowerShell has a number of features to mitigate this, often misperceived, burden such as tab completion and aliases. It is the latter that I want to discuss today.

An alias is simply an short command name alternative. Instead of typing Get-Process you can type ps. Even I can’t screw that up. Using aliases at the command prompt is a great way to speed up your work and cut down on typos. While I wouldn’t type this in a script:

At a prompt, it is quick and I get the desired results. However, you can’t create an alias for a command and its parameters. For example, I love the -First and -Last parameters with Select-Object. But I can’t create an alias equivalent to Select-Object -last 10. What you can do however, is create your own wrapper around a command and put an alias to that. Here is a function I wrote for Select-Object -Last X.

You’ll notice that the last part of my code snippet is defining an alias called Last that references this function. But now I have something quick and easy to use at a PowerShell prompt.

You’ll have to figure out the best way to wrap the cmdlet. In this case, I am taking each piped in object and adding it to an array, only keeping the specified number of items. As each item is added, the other items are moved “up” in the array.

Be aware that there may be a trade-off between convenience and performance. This command using my alias and custom function:

took 237ms. Whereas the Select-Object approach:

Only took 49ms. But I bet you might be willing to accept that tradeoff to save some typing. Of course, if there is a Last command there should be a First command.

Performance-wise this is easier because all I have to do is count piped in objects and bail out once I reach the limit.

In this example I’m not only taking advantage of aliases, but also positional parameters and only having to type enough the of the parameter name so PowerShell knows what I want.

So there are ways to make PowerShell more keyboard friendly, although it might take a little work on your part. Next time we’ll look at another alternative.

Friday Fun Get Content Words

Recently I was tracking down a bug in script for a client. The problem turned out to be a simple typo. I could have easily avoided that by using Set-StrictMode, which I do now, but that’s not what this is about. What I realized I wanted was a way to look at all the for “words” in a script. If I could look at them sorted, then typos would jump out. At least in theory.

My plan was to get the content of a text file or script, use a regular expression pattern to identify all the “words” and then get a sorted and unique list. Here’s what I came up with.


Function Get-ContentWords {

[cmdletbinding()]

Param (
[Parameter(Position=0,Mandatory=$True,
HelpMessage="Enter the filename for your text file",
ValueFromPipeline=$True)]
[string]$Path
)

Begin {
Set-StrictMode -Version 2.0

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

#define a regular expression pattern to detect "words"
[regex]$word="\b\S+\b"
}

Process {

if ($path.gettype().Name -eq "FileInfo") {
#$Path is a file object
Write-Verbose "Getting content from $($Path.Fullname)"
$content=Get-Content -Path $path.Fullname
}
else {
#$Path is a string
Write-Verbose "Getting content from $path"
$content=get-content -Path $Path
}

#add a little information
$stats=$content | Measure-Object -Word
Write-Verbose "Found approximately $($stats.words) words"

#write sorted unique values
$word.Matches($content) | select Value -unique | sort Value
}

End {
Write-Verbose "Ending $($myinvocation.mycommand)"
}

} #close function

The function uses Get-Content to retrieve the content (what else?!) of the specified file. At the beginning of the function I defined a regular expression object to find “words”.


#define a regular expression pattern to detect "words"
[regex]$word="\b\S+\b"

This is an intentionally broad pattern that searches for anything not a space. The \b element indicates a word boundary. Because this is a REGEX object, I can do a bit more than using a basic -match operator. Instead I’ll use the Matches() method which will return a collection of match objects. I can pipe these to Select-Object retrieving just the Value property. I also use the -Unique parameter to filter out duplicates. Finally the values are sorted.


$word.Matches($content) | select Value -unique | sort Value

The matches and filtering are NOT case-sensitive, which is fine for me. With the list I can see where I might have used write-host instead of Write-Host and go back to clean up my code. Let me show you how this works. Here’s a demo script.


#Requires -version 2.0

$comp = Read-Host "Enter a computer name"

write-host "Querying services on $comp" -fore Cyan
$svc = get-service -comp $comp

$msg = "I found {0} services on $comp" -f $svc.count
Write-Host "Results" -fore Green
Write-Host $mgs -fore Green

The script has some case inconsistencies as well as a typo. I’ve dot sourced the function in my PowerShell session. Here’s what I end up with.

For best results, you need to make sure there are spaces around commands that use the = sign. But now I can scan through the list and pick out potential problems. Sure, Set-StrictMode would help with variable typos but if I had errors in say comment based help, that wouldn’t help. Maybe you’ll find this useful in your scripting work, maybe not. But I hope you learned a few things about working with REGEX objects and unique properties.

Download Get-ContentWords and enjoy.

Select Object Properties with Values

Here’s another concept I know I’ve written about in the past but that needed an update. A common technique I use when exploring and discovering objects is to pipe the object to Select-Object specifying all properties, get-service spooler | select *. There’s nothing wrong with this approach but depending on the object I might get a lot of empty values. This is especially true with WMI objects or items from Active Directory like a user account. The other issue I have is that when using this technique with a WMI object, I also get the system properties like __PATH, which I’d often like to ignore. The solution I came up is a function called Select-PropertyValue. Pipe objects to the function and it will write a custom object to the pipeline only with properties that have a value.

[cc lang=”PowerShell”]
Function Select-PropertyValue {
[cmdletbinding()]

Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
[object]$InputObject,
[Switch]$NoSystem,
[Switch]$Inquire = $False
)

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

#define a variable we will be using to keep StrictMode happy.
$properties=@()
} #begin

Process {

<# set debug preference to continue when using -Debug unless user has also used -Inquire. Otherwise, PowerShell will prompt for each command. Due to scoping issues I found the best solution was to set the debug preference for each object in the pipeline #>

if (($PSCmdlet.MyInvocation.BoundParameters.ContainsKey(“debug”)) -AND (-NOT $Inquire)) {
$DebugPreference=”Continue”
}

Write-Debug “In process”
Write-Debug “Checking for `$properties”
<# because this only takes pipelined objects, we only need to get the properties from Get-Member once. When the second object is processed, this IF block will get skipped. The assumption that all pipelined objects are of the same type. #>

if (-Not $properties) {
Write-Verbose “Creating property list”
Write-Debug “Creating property list for pipelined object”

#get properties for the pipelined object sorted by property name
$properties=$_ | Get-Member -membertype Properties | sort Name
$typename=($_ |Get-Member)[0].TypeName
Write-Debug “Typename =$typename”

#filter out WMI System properties if -NoSystem
if ($NoSystem) {
Write-Verbose “Filtering out WMI System properties”
Write-Debug “Filtering out WMI System properties.”
$properties=$properties | where {$_.name -notlike “__*”}
}

Write-Verbose “Found $($properties.count) properties”
Write-Debug “Found $($properties.count) properties”
}

#create an empty custom object
Write-Debug “Creating empty object”
$obj=New-Object PSObject

#enumerate the list of properties
Write-Verbose “Checking properties for values”
foreach ($property in $properties) {
Write-Debug “Checking $($property.name)”
#if object has a value for the current property
if ($_.($property.name)) {
Write-Debug “found value for: $($_.($property.name))”

#assign properties to the custom object
Write-Debug “Adding property and value to custom object”
$obj | Add-Member -MemberType Noteproperty -name $property.Name -value ($_.($property.name))

} #end If
} #end ForEach

#Add the typename to the object
Write-Debug “Adding typename to custom object”
$obj | Add-Member -MemberType Noteproperty -Name “Type” -Value $typename

#write the custom object to the pipeline
Write-Debug “Writing custom object to the pipeline”
Write $obj
} #end process

End {
Write-Verbose “Ending $($myinvocation.mycommand)”
}

} #end function
[/cc]

The function is intended to be used in a pipelined expression and assumes that all the objects are of the same type. The essence of the function is to run each object through Get-Member and keep a list of property names.

[cc lang=”PowerShell”]
if (-Not $properties) {
Write-Verbose “Creating property list”
Write-Debug “Creating property list for pipelined object”
$properties=$_ | Get-Member -membertype Properties | sort Name
[/cc]

Because this happens in the process script block, which runs once for every piped in object, this line only runs if $properties doesn’t already exist. If the user includes the -NoSystem parameter, then any property that starts with __ is removed.

[cc lang=”PowerShell”]
if ($NoSystem) {
Write-Verbose “Filtering out WMI System properties”
Write-Debug “Filtering out WMI System properties.”
$properties=$properties | where {$_.name -notlike “__*”}
}
[/cc]

Armed with the array of property names, the pipelined object is checked for each property name.

[cc lang=”PowerShell”]
foreach ($property in $properties) {
Write-Debug “Checking $($property.name)”
#if object has a value for the current property
if ($_.($property.name)) {
Write-Debug “found value for: $($_.($property.name))”
[/cc]

If a value is found

[cc lang=”PowerShell”]
if ($_.($property.name))
[/cc]

Then the property and value are added to the blank custom object which is created for each piped object.

[cc lang=”PowerShell”]
$obj | Add-Member -MemberType Noteproperty -name $property.Name -value ($_.($property.name))
[/cc]

I include the object type name and write the custom object to the pipeline.

[cc lang=”PowerShell”]
#Add the typename to the object
Write-Debug “Adding typename to custom object”
$obj | Add-Member -MemberType Noteproperty -Name “Type” -Value $typename

#write the custom object to the pipeline
Write-Debug “Writing custom object to the pipeline”
Write $obj
[/cc]

Here’s the end result.

[cc lang=”DOS”]
BiosCharacteristics : {4, 7, 8, 9…}
BIOSVersion : {TOSQCI – 6040000, Ver 1.00PARTTBL}
Caption : Ver 1.00PARTTBL
Description : Ver 1.00PARTTBL
Manufacturer : TOSHIBA
Name : Ver 1.00PARTTBL
PrimaryBIOS : True
ReleaseDate : 20101210000000.000000+000
SerialNumber : Z9131790W
SMBIOSBIOSVersion : V2.90
SMBIOSMajorVersion : 2
SMBIOSMinorVersion : 6
SMBIOSPresent : True
SoftwareElementID : Ver 1.00PARTTBL
SoftwareElementState : 3
Status : OK
Version : TOSQCI – 6040000
Type : System.Management.ManagementObject#root\cimv2\Win32_BIOS
[/cc]

You probably also noticed the Write-Verbose and Write-Debug lines in the script. Continuing the discussion I started in my post on Write-Verbose vs Write-Debug, I’m including the suggestion on controlling the debug preference. By default if you the function with -debug you’ll get all the debug messages. But if you also use -Inquire, then you’ll get prompted for each command which is the normal process when using -Debug. I wanted to see how this would work in one of my own scripts.

As always, I hope you’ll let me know how this works out for you. Download Select-PropertyValue. The full script includes comment-based help as well as an optional alias. Uncomment the last line of the script if you want to use it or define your own.

ByValue, I Think He’s Got It

Recently I responded to an email from a student seeking clarification about the difference between ByValue and ByProperty when it comes to parameter binding. This is what makes pipelined expressions work in Windows PowerShell. When you look at cmdlet help, you’ll see that some parameters accept pipeline binding, which is what you are looking for. Often this means you don’t need to resort to a ForEach-Object construct. Here’s an example.

ByValue means if I see something I’ll use it. ByProperty means I’m looking for a specific object property. For example, Get-Service uses the Name property to find a service.

This parameter accepts both types of binding. This means you can do this:

This is an example of byValue. Get-Service sees something in the pipeline and assumes it is a service name. However, for Get-Service this would also work.

$all[0] is a service object with name property which when piped to Get-Service, finds the property and binds to it. Here’s another example that shows binding by property name can be from any object, not just a service object.

This is a one line expression that leverages the pipeline and uses some helpful (I hope) techniques. The first part using Get-Content retrieves the list of computer names from the text file. Because the text file might have blank lines and some computers might be offline, each name is piped to Where-Object which will only pass on names that exist (skipping blanks) and that can be pinged. To speed things up I’m only sending 2 pings. Now the fun part. I could use ForEach-Object and pass $_ as the value for -Computername. But according to help, this parameter accepts binding by property name.

So I’ll take the computername value coming from Where-Object and use a hash table with Select-Object to define a new “property” name, called Computername. I also take the liberty of trimming off any leading or trailing spaces, just in case. Now I have an object with a property called Computername that is piped to Get-Service which binds on the computername property. The rest is merely formatting.

Look for opportunities to bind by parameter, which means reading cmdlet help which is a good habit to have regardless.