This article is something I've been meaning to write for sometime. As often as I tell people PowerShell is easy to use once you understand its core concepts, that isn't always the case. This is a problem my friend Gladys Kravitz brought to my attention some time ago.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Like her, you probably have written a PowerShell function like this that accepts pipelined input.
Function Get-Foo {
[cmdletbinding()]
param(
[Parameter(Mandatory, ValueFromPipelinebyPropertyName)]
[string]$Name
)
Begin {}
Process {
Write-Host "working on $Name" -ForegroundColor Green
}
End {}
}
This sample function looks at any incoming object, and assigns the Name property to the Name parameter. You can see the results with a simple test.
Get-Service bits, winrm | Get-Foo working on bits working on winrm
This function isn't doing anything other than displaying the value of the $Name parameter. Let's take a step forward.
Here's a simple function to get the BITS service that takes pipeline input for the Computername.
Function Get-Bits {
[cmdletbinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[alias("Name")]
[string]$Computername
)
Begin {}
Process {
Write-Host "$Computername" -ForegroundColor yellow
Get-service -Name bits -ComputerName $computername | Select-Object Machinename,Name,Status
}
End {}
}
This works mostly as expected. I can specify a parameter value. I can also take input from the pipeline by property name.
But not with an Active Directory object.
This is where Gladys started wondering what she was doing wrong. Clearly, the object has a Name property, because this variation works as expected.
It was time to see what was really happening. By the way, if you pipe the Get-ADComputer command to Get-Member, PowerShell will tell you the Name property is a String.
To dive deeper, we turned to the Trace-Command cmdlet. You definitely want to read help and examples. In our case, we needed to see what PowerShell was doing with parameter binding.
There's a lot to take in. As I scrolled down and got to the Get-Bits function, I noticed what PowerShell was doing with parameter binding for Computername.
PowerShell wasn't seeing a string. It was seeing an ADPropertyValueCollection, which it couldn't convert which results in an error.
With this tidbit of information, I revised my sample Get-Bits function.
Function Get-Bits {
#revised version
[cmdletbinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[alias("Name")]
[object]$Computername
)
Begin {}
Process {
write-Host "processing type $($computername.GetType().fullname)" -ForegroundColor yellow
[Microsoft.ActiveDirectory.Management.ADPropertyValueCollection]::new($Computername)
switch ($computername.GetType().fullname) {
"system.string" {$cn = $Computername}
"microsoft.activedirectory.management.adcomputer" {$cn = $computername.name}
"Microsoft.ActiveDirectory.Management.ADPropertyValueCollection" {$cn = [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection]::new($computername).name }
default { $cn = $Computername.computername }
}
write-Host "working on $cn" -fore Green
# Get-service -Name bits -ComputerName $cn | Select-Object Machinename,Name,Status
}
End {}
}
I need to pay attention the type of object I'm getting for Computername. I can't assume it will be a string. I want to verify my theory.
Gladys, and most people, was assuming that the AD Object property was a simple thing, like a String. But clearly it is not. I can also infer from testing, that Select-Object plucks the property from collection and uses it to created the Selected.Microsoft.ActiveDirectory.Management.ADComputer object.
Now that we know this, I can revise my sample function to better accommodate pipeline input from an Active Directory cmdlet. This will require the use of parameter sets.
Function Get-Bits {
#with parameter sets
[cmdletbinding(DefaultParameterSetName = "byString")]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, ParameterSetName = "byString")]
[alias("Name")]
[string]$Computername,
[Parameter(ValueFromPipeline, ParameterSetName = "byADComputer")]
[Microsoft.ActiveDirectory.Management.ADComputer]$Computer
)
Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
}
Process {
Write-Verbose "Using parameter set $($pscmdlet.ParameterSetName)"
if ($PSCmdlet.ParameterSetName -eq "byString") {
$cn = $Computername
}
else {
Write-Verbose "Getting the Name property"
$cn = $Computer.Name
}
Write-Verbose "Getting BITS service from $cn"
Try {
Get-Service -Name bits -ComputerName $cn -ErrorAction Stop |
Select-Object Machinename, Name, Status
}
Catch {
Write-Warning "Failed to get service status from $CN. $($_.exception.message)"
}
}
End {
Write-Verbose "Ending $($myinvocation.mycommand)"
}
}
This version of the function will take pipeline input by property name for Computername, or the alias Name. Or, it will process an ADComputer object. In the Process block, I can get the proper computername value to pass on to Get-Service. This version gives me the PowerShell experience I'm expecting.
And most importantly, using Get-ADComputer. My test domain has entries for non-existent computers.
Don't ask me why the AD cmdlets are written this way. They just are. If we want to incorporate them into our PowerShell work, we will need to make some accommodations. My sample Get-Bits function is just one example.
At least Gladys knows she isn't going crazy and that there are solutions to her Active Directory scripting needs. I encourage you to try out Trace-Command. It might come in handy the next time you encounter a problem and want to cut down on the number of bruises on your forehead.
As usual, comments and feedback are welcome.
Nice!. Much more elegant than get-adcomputer -filter ‘blah’ | select -expandproperty name | do-stuff.