Piping computers to Get-Service

Hi! There’s a bit of behavior I’ve never quite understood, and I’m hoping someone can shed some light on this part of the pipelining process. Here’s my stream-of-consciousness thought process.

First idea:
Get-ADComputer -filter * | Get-Service
This doesn’t work, of course, because Get-Service is expecting computers to be described by a ComputerName property, and Get-ADComputer produces objects that store that data in a Name property.

Second idea:
Get-ADComputer -filter * | Select-Object @{Name=“ComputerName”; Expression={$_.Name} | Get-Service
This also fails, but I’m not clear on why. From the error message, it looks as though the result of the Select-Object is being picked up by the -Name parameter. I’m theorizing that the pipelined input looks like an array of strings to Get-Service, and -Name picks them up ByValue.

Am I in the neighborhood? I have seen that adding a manual -Name * to the Get-Service call solves the issue, allowing the incoming data to be consumed ByPropertyName, but I’m more interested in understanding what PowerShell is thinking when that has NOT been done.

Thanks in advance for the insights.

This was an interesting puzzle and I’ll be curious to see if anyone else adds some insight. We look at the parameters:

PS C:\> Get-Help Get-Service -Parameter Name

-Name 
    Specifies the service names of services to be retrieved. Wildcards are permitted. By default, Get-Service gets all of the services on the computer.
    
    Required?                    false
    Position?                    1
    Default value                All services
    Accept pipeline input?       true (ByPropertyName, ByValue)
    Accept wildcard characters?  true
    




PS C:\> Get-Help Get-Service -Parameter ComputerName

-ComputerName 
    Gets the services running on the specified computers. The default is the local computer.
    
    Type the NetBIOS name, an IP address, or a fully qualified domain name of a remote computer. To specify the local computer, type the computer name, a dot (.), or "localhost".
    
    This parameter does not rely on Windows PowerShell remoting. You can use the ComputerName parameter of Get-Service even if your computer is not configured to run remote commands.
    
    Required?                    false
    Position?                    named
    Default value                Local computer
    Accept pipeline input?       true (ByPropertyName)
    Accept wildcard characters?  false

Observations are Name is ByPropertyName or ByValue and ComputerName accepting ByPropertyName. Both parameters have Default values. The error message shows:

PS C:\> Get-ADComputer MyPuter| Select @{Label="ComputerName";Expression={$_.NAME}} | Get-Service

Get-Service : Cannot find any service with service name '@{ComputerName=MyPuter}'.
At line:1 char:86
+ ... n={$_.NAME}} | Get-Service
+                    ~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (@{ComputerName=MyPuter}:String) [Get-Service], ServiceCommandException
    + FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand
 

I test this with Get-Process and it works fine. I built a function with I would assume is the same logic in Get-Service:

function Test-This{
    param(
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName = "Local Computer",
        [Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name = "All Services"
    )
    begin{}
    process{
        foreach ($computer in $computername) {
            "Processing {0}" -f $computer
            foreach ($service in $name) {
                "Processing {0} in {1}" -f $service, $computer
            }
        }
    }
    end{}
}

So, now we try the same logic and pass it to this function:

PS C:\> Get-ADComputer -Filter * | Select @{Label="ComputerName";Expression={$_.NAME}}  -First 3 | Test-This
Processing MyServer1
Processing @{ComputerName=MyServer1} in MyServer1
Processing MyServer2
Processing @{ComputerName=MyServer2} in MyServer2
Processing MyServer3
Processing @{ComputerName=MyServer3} in MyServer3

It appears that because Name is stipulated as ByValue that since data is being piped to it that it overrides what is set as the default parameter and attempts to use the piped data. See how it’s searching for a service name of “@{ComputerName=MyServer1}” just like the error states above. If we add the Name=“*”, it works because now the pipe contains a value for Name and doesn’t use the data from the pipe line:

PS C:\> Get-ADComputer -Filter * | Select @{Label="ComputerName";Expression={$_.NAME}}, @{Label="Name";Expression={"*"}}  -First 3 | Test-This
Processing MyServer1
Processing * in MyServer1
Processing MyServer2
Processing * in MyServer2
Processing MyServer3
Processing * in MyServer3

I think that explains the behavior, but I would like to know the proper way to actually fix it. What is different in Get-Process versus Get-Service? I tried putting this:

if (!($PSBoundParameters.ContainsKey('Name'))) {$Name="*"}

In the begin and process blocks but it still uses the piped data. Hopefully someone can tell us the proper ways to handle and override this behavior. I’ll research more later, but gotta get some work done.

The default pipeline variable in Get-Service is -Name, meaning you can’t pipe a ComputerName into Get-Service. You pipe a service name, like

'wuauserv' | Get-Service

Status   Name               DisplayName
------   ----               -----------
Stopped  wuauserv           Windows Update

What you can do is flip it on its head, and do

Get-Service -ComputerName (Get-ADComputer -Filter { Name -like 'HYPERV*' } | Select-Object -ExpandProperty Name)

But it’s not very pretty since Get-Service doesn’t return the ComputerName, and Get-Service with -ComputerName uses the oldschool RPC protocol to query for data thanks to it being one of the very first commands introduced to PowerShell and never having been updated since. Instead I’d do this

Invoke-Command -ComputerName (Get-ADComputer -Filter { Name -like 'HYPERV*' } | Select-Object -ExpandProperty Name) -ScriptBlock { Get-Service }

Just for giggles, what happens if you do this?

Get-ADComputer -filter * | Select-Object @{Name="ComputerName"; Expression={ [string]$_.Name } | Get-Service

Parameter binding tries to do things without coercion first (where the piped object’s value or property exactly matches the type of the parameters), and only then falls through to trying type coercion. From your error message, it looks like you’re making it all the way to the ByValue (With Coercion) binding, so maybe forcing your ComputerName property to be a String object will help.

Also, the AD cmdlets are a pain. Their output objects behave very strangely, which sometimes leads to these types of problems in the pipeline. Whoever wrote that module was way too clever for their own good, and it leads to all sorts of gotchas. (Did you know that every single parameter in the AD cmdlets is dynamic? Why? Beats the heck out of me, but try creating a proxy function for one sometime, and marvel at the empty param() block.)

Just for giggles, what happens if you do this?

Two things happened for me

PS C:\Users\mni> Get-ADComputer -Filter { Name -like 'HYPERV*' } | Select-Object @{Name="ComputerName"; Expression={ [string]$_.Name } } | Get-Service
Get-Service : Cannot find any service with service name '@{ComputerName=HYPERV2}'.
At line:1 char:123
+ ... ]$_.Name } } | Get-Service
+                    ~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (@{ComputerName=HYPERV2}:String) [Get-Service], ServiceCommandException
    + FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand

Secondly, the command made my SCVMM console commit suicide. Not entirely sure why.

OK, one more attempt to make the AD cmdlet behave:

Get-ADComputer -filter * | Select-Object * | Select-Object @{Name="ComputerName"; Expression={ [string]$_.Name } | Get-Service

Piping to Select-Object * looks ridiculous, but it’s one of the common workarounds to make the AD cmdlets work well in a pipeline.

Still no dice, at least in my case. I’m pretty sure Get-Service won’t take anything but a service name or a service object. Heck, even an explicit [pscustomobject]@{ ComputerName = ‘HYPERV01’ } doesn’t work.

This does however:

PS C:\Users\mni> $service = Get-Service -ComputerName 'HYPERV01' -Name 'wuauserv'
PS C:\Users\mni> $service

Status   Name               DisplayName
------   ----               -----------
Stopped  wuauserv           Windows Update


PS C:\Users\mni> $service | Get-Service

Status   Name               DisplayName
------   ----               -----------
Stopped  wuauserv           Windows Update

@Dave - Tried the cast to string, still doesn’t work. I can do the same thing to Get-Process and it works.

@Martin - Your just creating a string array in your example, but don’t understand why you would not be able to pipe to the object. When I pipe to Get-Process, it works. If I look at it’s properties, it doesn’t have Name as ByValue, but there is InputObject setup for ByValue. Also, your examples are missing a end paren in the ComputerName examples.

I think it’s an interesting question. I reproduced the behavior in the function above, so I would be curious on how it would be handled. If it’s not an accurate portrayal of how the cmdlet would function, I’d be curious to know what is wrong with the implementation in the function above.

PS C:\> Get-ADComputer MyComputer | Select @{Label="ComputerName";Expression={[string]$_.NAME}} | Get-Service

Get-Service : Cannot find any service with service name '@{ComputerName=MyComputer}'.
At line:1 char:94
+ ... ng]$_.NAME}} | Get-Service
+                    ~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (@{ComputerName=MyComputer}:String) [Get-Service], ServiceCommandException
    + FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand
 

PS C:\> Get-ADComputer MyComputer | Select @{Label="ComputerName";Expression={[string]$_.NAME}} | Get-Process

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName                                                                                                                                       
-------  ------    -----      ----- -----   ------     -- -----------                                                                                                                                       
    259      35     5160       6352   100            2620 agent                                                                                                                                             
     81       7     1164       4204    44            1164 armsvc                                                                                                                                            
    523      16    12448      16304    81   636.47   7452 audiodg  ....

Okay, just played around with this a bit on my system. This fails:

[pscustomobject]@{ComputerName = 'localhost'} | Get-Service

It fails because the input object winds up binding to both ComputerName AND Name, which is annoying. This, on the other hand, works fine:

[pscustomobject]@{ComputerName = 'localhost'} | Get-Service -Name *

By binding the -Name parameter to * on the command line, we prevent the pipeline object from giving that parameter junk data. I’m not sure how much of the previous attempts you’ll need to keep with regards to the objects coming out of the AD module, but in combination with adding -Name *, you should be able to get it working.

Well as I said, Get-Service was one of, if not the first command in PowerShell, and it hasn’t been touched since it was introduced. It’s very likely that it’s just poorly written by todays standards.

Edit: Oh interesting. Maybe I’m wrong and the command is just funky?

@Dave - Is there anything wrong in the function above? Should ByValue be removed? I see the same behavior and am just curious if it’s Powershell behavior or bad parameter setup. Where would you check and override if the “Name” parameter is NULL and reset the global value?