Reputation: 13
I have a function that works to compare processes against users but when I try to pipe the output into stop-process I get the following errors:
"Stop-Process : Cannot evaluate parameter 'InputObject' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input. "
Function Proc {
$TargetUsers = get-content oldusers.txt
$WmiArguments = @{
'Class' = 'Win32_process'
}
$processes = Get-WMIobject @WmiArguments | ForEach-Object {
$Owner = $_.getowner();
$Process = New-Object PSObject
$Process | Add-Member Noteproperty 'ComputerName' $Computer
$Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName
$Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID
$Process | Add-Member Noteproperty 'Domain' $Owner.Domain
$Process | Add-Member Noteproperty 'User' $Owner.User
$Process
}
ForEach ($Process in $Processes) {
if ($TargetUsers -Contains $Process.User) {
stop-process -id {$_.processid}
}
}
}
I have read serveral articles on piping commands in powershell and that it is object based but I am lost on how to use the returned object of one function and piping into another even though I am sure it is simple when you know how
Upvotes: 1
Views: 681
Reputation: 7087
I like Theo's answer. I just want to add some additional stuff that certainly wouldn't fit in a comment...
Initially I think we're all debugging your code, but keeping to the pattern you originally laid out. Strictly speaking there's nothing wrong with that.
I think what's somewhat lost here is your actual question; why can't you simply pipe the output of one function to another?. The answer is in how the receiving cmdlet or function is expecting the data.
If you look at Get-Help Stop-Process -Parameter Id
(OR Parameter Name) you'll see it will take the property via pipeline if the property is named correctly:
-Id <Int32[]>
Specifies the process IDs of the processes to stop. To specify multiple IDs, use commas to separate the IDs. To find the PID of a process, type `Get-Process`.
Required? true
Position? 0
Default value None
Accept pipeline input? True (ByPropertyName)
Accept wildcard characters? false
So you would've been able to pipe if you're custom object had a property named "Id".
Stop-Process
will accept the process id, but it's looking for the property name to be "Id" and Win32_Process returns "ProcessID".
But there is a second issue. The value of the property passed in must be acceptable to the receiving function/cmdlet. Unfortunately, Win32_Process usually returns the Name with a ".exe" suffix, and Get-Process
won't accept that.
Theo's answer is very good and works with Stop-Process
because his new object has a property named "ID" which is accepted by the pipeline and part of the default parameter set, meaning it's preferred over the name property.
However, if you were to pipe those objects to Get-Process
it wouldn't work. Get-Process
prefers name over ID and is expecting a value like "notepad" not "Notepad.exe, which Win32_Process returns. In that case Get-Process
wouldn't be able to find the process and would error.
Note: The above was corrected based on collaboration with Theo, you can look at the previous revision & comments for reference.
To make the objects also work with Get-Process
simply modify the value going into the "Name" property to remove the trailing '.exe' . I edited Theo's answer just to add that bit. You should see that if he approves it.
I realize that's not part of your original question, but it's to illustrate the additional caveat of piping between different tools/cmdlets/functions etc...
Note: There may still be a couple of exceptions. For example: Win32_Process returns "System Idle Process" But Get-Process
returns "Idle". For your purposes that's probably not an issue. Of course you'd never stop that process!
Note: The likely reason Get-Process
prefers the name while Stop-Process
prefers the ID is that name is not unique but ID is. Stop-Process
Notepad will kill all instances of Notepad, which is usually (and in your case) not what's intended.
Regarding the approach in general. I'd point out there are several ways to both extend objects and create PS Custom objects. Add-Member
is a good approach if you need or want the instance type to remain the same; I'd consider that extending an object. However, in your case you are creating a custom object then adding members to it. In such a case I usually use Select-Object which already converts to PSCustomObjects.
Your code with corrected "Name" property: $Processes = Get-WmiObject win32_process
$Processes |
ForEach-Object{
$Owner = $_.getowner()
$Process = New-Object PSObject
$Process | Add-Member NoteProperty 'ComputerName' $_.CSName
$Process | Add-Member NoteProperty 'ProcessName' $_.ProcessName
$Process | Add-Member NoteProperty 'ProcessID' $_.ProcessID
$Process | Add-Member NoteProperty 'Domain' $Owner.Domain
$Process | Add-Member NoteProperty 'User' $Owner.User
$Process | Add-Member NoteProperty 'Name' -Value ( $_.ProcessName -Replace '\.exe$' )
$Process
}
Note: For brevity I removed some surrounding code.
Using select would look something like:
$Processes = Get-WmiObject win32_process |
Select-Object ProcessID,
@{Name = 'ComputerName'; Expression = { $_.CSName }},
@{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
@{Name = 'Id'; Expression = { $_.ProcessID } },
@{Name = 'Domain'; Expression = { $_.GetOwner().Domain} },
@{Name = 'User'; Expression = { $_.GetOwner().User} }
This can then be piped directly to a where clause to filter the processes you are looking for, then piped again to the Stop-Process
cmdlet:
Get-WmiObject win32_process |
Select-Object ProcessID,
@{Name = 'ComputerName'; Expression = { $_.CSName }},
@{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
@{Name = 'Id'; Expression = { $_.ProcessID } },
@{Name = 'Domain'; Expression = { $_.GetOwner().Domain} },
@{Name = 'User'; Expression = { $_.GetOwner().User} } |
Where-Object{ $TargetUsers -contains $_.User } |
Stop-Process
Note: This drops even the assignment to $Processes
. You'd still needed to populate the $TargetUsers
variable.
Also: An earlier comment pointed out that given what you are doing you don't need all the props so something like:
Get-WmiObject win32_process |
Select-Object @{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
@{Name = 'User'; Expression = { $_.GetOwner().User} } |
Where-Object{ $TargetUsers -contains $_.User } |
Stop-Process
However, if you are doing other things in your code like logging the terminated processes it's relatively harmless to establish & maintain more properties.
And just for illustration, piping could be facilitated through ForEach-Object
with relative ease as well and no need to stray from the original objects:
Get-WmiObject win32_process |
Where{$TargetUsers -contains $_.GetOwner().User } |
ForEach-Object{ Stop-Process -Id $_.ProcessID }
One of the best things about PowerShell is there are a lot of ways to do stuff. That last example is very concise, but it would be sub-optimal (albeit doable) to add something like logging or console output...
Also Theo is right about Get-CimInstance. If I'm not mistaken Get-WmiObject
is deprecated. Old habits are hard to break so all my examples used Get-WmiObject
However, these concepts should applicable throughout PowerShell including Get-CimInstance
...
At any rate, I hope I've added something here. There are a few articles out there discussing the different object creation and manipulation capabilities pros & cons etc... If I have time I'll try to track them down.
Upvotes: 3
Reputation: 439597
There's great information in the existing answers; let me complement it by explaining the immediate issue:
I get the following errors:
"Stop-Process : Cannot evaluate parameter 'InputObject' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input. "
{$_.processid}
is a script block, which is an apparent attempt to use that script block as a delay-bind parameter with pipeline input.
In your code you are not providing pipeline input to your Stop-Process
call, which is what the error message is trying to tell you.
Instead, you're using a foreach
loop to loop over input using $Process
as the iteration variable, and you therefore need to specify the target ID as regular, direct argument based on that variable, using $Process.ProcessID
, as shown in Desinternauta's answer.
Upvotes: 1
Reputation: 74
Change {$_.processid}
to $Process.ProcessID
Function Proc {
$TargetUsers = get-content oldusers.txt
$WmiArguments = @{
'Class' = 'Win32_process'
}
$processes = Get-WMIobject @WmiArguments | ForEach-Object {
$Owner = $_.getowner();
$Process = New-Object PSObject
$Process | Add-Member Noteproperty 'ComputerName' $Computer
$Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName
$Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID
$Process | Add-Member Noteproperty 'Domain' $Owner.Domain
$Process | Add-Member Noteproperty 'User' $Owner.User
$Process
}
ForEach ($Process in $Processes) {
if ($TargetUsers -Contains $Process.User) {
stop-process -id $Process.ProcessID
}
}
}
Upvotes: 1
Reputation: 61178
Inside your function, there is no need to do a ForEach-Object loop twice. If I read your question properly, all you want it to do is to stop processes where the owner username matches any of those read from an 'oldusers.txt' file.
Simplified, your function could look like:
function Stop-OldUserProcess {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
[Alias('FullName', 'FilePath')]
[string]$SourceFile
)
$TargetUsers = Get-Content $SourceFile
Get-WMIobject -Class Win32_Process | ForEach-Object {
$Owner = $_.GetOwner()
if ($TargetUsers -contains $Owner.User) {
Write-Verbose "Stopping process $($_.ProcessName)"
Stop-Process -Id $_.ProcessID -Force
}
}
}
and you call it like this:
Stop-OldUserProcess -SourceFile 'oldusers.txt' -Verbose
Another approach could be that you create a function to just gather the processes owned by old users and return that info as objects to the calling script:
function Get-OldUserProcess {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
[Alias('FullName', 'FilePath')]
[string]$SourceFile
)
$TargetUsers = Get-Content $SourceFile
Get-WMIobject -Class Win32_Process | ForEach-Object {
$Owner = $_.GetOwner()
if ($TargetUsers -contains $Owner.User) {
# output a PSObject
[PsCustomObject]@{
'Name' = $_.ProcessName
'Id' = $_.ProcessID
'Domain' = $Owner.Domain
'User' = $Owner.User
}
}
}
}
I opt for using 'Name' and 'Id' as property names, because the Stop-Process cmdlet can take objects through the pipeline and both the 'Name' and 'Id' property are accepted as pipeline input ByPropertyName.
Then call the function to receive (an array of) objects and do what you need to with that:
Get-OldUserProcess -SourceFile 'oldusers.txt' | Stop-Process -Force
I have changed the function names to comply with PowerShells Verb-Noun naming convention.
P.S. If you have PowerShell version 3.0 or better, you can change the lines
Get-WMIobject -Class Win32_Process | ForEach-Object {
$Owner = $_.GetOwner()
into
Get-CimInstance -ClassName Win32_Process | ForEach-Object {
$Owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwner
for better performance. See Get-CIMInstance Vs Get-WMIObject
Upvotes: 2