перенаправление объектов в powershell в другие функции - PullRequest
1 голос
/ 22 февраля 2020

У меня есть функция, которая работает для сравнения процессов с пользователями, но когда я пытаюсь направить вывод в остановочный процесс, я получаю следующие ошибки:

"Stop-Process: невозможно оценить параметр 'InputObject', потому что его аргумент указан как блок сценария, и вход отсутствует. Блок сценария не может быть оценен без ввода. "

 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}

                    }

            }
              }

Я прочитал несколько статей о командах конвейеризации в powershell и что он основан на объектах, но Я заблудился о том, как использовать возвращенный объект одной функции и передавать в другую, хотя я уверен, что это просто, когда вы знаете, как

Ответы [ 4 ]

2 голосов
/ 22 февраля 2020

Внутри вашей функции нет необходимости делать ForEach-Object l oop дважды. Если я правильно прочитал ваш вопрос, все, что вам нужно, это остановить процессы, в которых имя пользователя владельца совпадает с любым из тех, которые прочитаны из файла oldusers.txt.

Упрощенно, ваша функция может выглядеть следующим образом:

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
        }
    }
}

и вы называете это так:

Stop-OldUserProcess -SourceFile 'oldusers.txt' -Verbose

Другой подход может заключаться в создании функция, позволяющая просто собрать процессы, принадлежащие старым пользователям, и вернуть эту информацию в качестве объектов вызывающему сценарию:

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
            }
        }
    }
}

Я предпочитаю использовать 'Name' и 'Id' в качестве имен свойств, потому что Командлет Stop-Process может принимать объекты через конвейер, и оба свойства 'Name' и 'Id' принимаются в качестве входных данных конвейера ByPropertyName.

Затем вызывается функция для получения (массива) объектов и сделать то, что вам нужно, с этим:

Get-OldUserProcess -SourceFile 'oldusers.txt' | Stop-Process -Force

Я изменил имена функций в соответствии с PowerShells Verb-Noun соглашение об именах.


PS Если у вас PowerShell версии 3.0 или выше, вы можете изменить строки

Get-WMIobject -Class Win32_Process | ForEach-Object {
    $Owner = $_.GetOwner()

на

Get-CimInstance -ClassName Win32_Process | ForEach-Object {
    $Owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwner

для повышения производительности. См. Get-CIMInstance против Get-WMIObject

2 голосов
/ 22 февраля 2020

Мне нравится ответ Тео. Я просто хочу добавить некоторые дополнительные вещи, которые, конечно, не поместятся в комментарии ...

Изначально я думаю, что мы все отлаживаем ваш код, но придерживаемся шаблона, который вы изначально изложили. Строго говоря, в этом нет ничего плохого.

Я думаю, что здесь что-то потеряно, так это ваш актуальный вопрос; почему вы не можете просто передать вывод одной функции в другую? Ответ в том, как принимающий командлет или функция ожидает данные.

Если вы посмотрите на Get-Help Stop-Process -Parameter Id (ИЛИ Имя параметра), вы увидите, что оно получит свойство через конвейер, если свойство будет названо правильно. :

-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

Таким образом, вы могли бы передавать по конвейеру, если бы у вашего пользовательского объекта было свойство с именем "Id".

Stop-Process примет идентификатор процесса, но это выполняется поиск имени свойства «Id», а Win32_Process возвращает «ProcessID».

Но есть вторая проблема. Значение переданного свойства должно быть приемлемым для принимающей функции / командлета. К сожалению, в Win32_Process обычно возвращается имя с суффиксом «.exe», и Get-Process не примет это.

Ответ Тео очень хорош и работает с Stop-Process, потому что его новый объект имеет свойство с именем ID, которое принимается конвейером и является частью набора параметров по умолчанию, что означает, что оно предпочтительнее, чем свойство name.

Однако, если вы передадите эти объекты в Get-Process, он не будет работать. Get-Process предпочитает имя над ID и ожидает значение типа «блокнот», а не «Notepad.exe», который возвращает Win32_Process. В этом случае Get-Process не сможет найти процесс и выдаст ошибку.

Примечание. Выше исправлено на основе сотрудничества с Тео, вы можете посмотреть предыдущую редакцию и комментарии для справки.

Чтобы заставить объекты работать также с Get-Process, просто измените значение, перейдя в «Имя». "свойство удалить завершающий файл" .exe ". Я отредактировал ответ Тео только для того, чтобы добавить этот бит. Вы должны увидеть, что если он его одобрит.

Я понимаю, что это не является частью вашего исходного вопроса, но это иллюстрирует дополнительная оговорка о передаче между различными инструментами / командлетами / функциями и т. д. c ...

Примечание: может быть несколько исключений, например: Win32_Process возвращает «System Idle Process», но Get-Process возвращает «Idle». Для ваших целей это, вероятно, не проблема. Конечно, вы никогда не остановите этот процесс!

Примечание: Вероятная причина, по которой Get-Process предпочитает имя, в то время как Stop-Process предпочитает идентификатор, состоит в том, что имя не уникально, а идентификатор. Stop-Process Notepad уничтожит все экземпляры Notepad, что обычно (и в вашем случае) не соответствует цели.

Относительно подхода в целом. Я хотел бы отметить, что существует несколько способов расширения объектов и создания пользовательских объектов PS. Add-Member - это хороший подход, если вам нужно или хотите, чтобы тип экземпляра оставался прежним; Я бы посчитал это расширением объекта. Однако в вашем случае вы создаете пользовательский объект, а затем добавляете в него членов. В таком случае я обычно использую Select-Object, который по умолчанию преобразует объекты в пользовательские объекты PS.

Ваш код с исправленным свойством «Имя»: $ 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
}

Примечание: для краткости я удалил некоторый окружающий код.

Использование select выглядело бы примерно так:

$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} }

Затем его можно перенаправить прямо в предложение where для фильтрации процессов, которые вы просматриваете. for, а затем снова отправьте в командлет Stop-Process:

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

Примечание. Это исключает даже присвоение переменной $ Processes. Вам все еще нужно было заполнить переменную $ TargetUsers.

Также: в предыдущем комментарии указывалось, что, учитывая то, что вы делаете, вам не нужны все реквизиты, поэтому что-то вроде:

Get-WmiObject win32_process |
Select-Object @{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
    @{Name = 'User'; Expression = { $_.GetOwner().User} } |    
Where-Object{ $TargetUsers -contains $_.User } |
Stop-Process

Однако, если вы делаете в своем коде другие вещи, такие как регистрация завершенных процессов, установить / 1060 * и сохранить больше свойств относительно безопасно.

И просто для иллюстрации: с помощью ForEach-Object можно с легкостью упростить конвейерную обработку и не нужно отклоняться от исходных объектов:

Get-WmiObject win32_process | 
Where{$TargetUsers -contains $_.GetOwner().User } |
ForEach-Object{ Stop-Process -Id $_.ProcessID }

Одна из лучших особенностей PowerShell - это то, что много способов делать вещи. Насколько надежно вы хотите сделать данный проект. Последний пример, очевидно, очень лаконичен, но было бы неоптимально (хотя и выполнимо) добавить что-то вроде ведения журнала или вывода на консоль ...

Также Тео прав насчет Get-CimInstance. Если я не ошибаюсь, Get-WmiObject устарел. Старые привычки трудно сломать, поэтому во всех моих примерах использовался Get-Wmi ... Однако эти концепции должны применяться во всей PowerShell, включая Get-CimInstance ...

В любом случае, я надеюсь, что я кое-что добавил здесь. Есть несколько статей, в которых обсуждаются различные возможности создания и манипулирования объектами за и против и т. Д. c ... Если у меня будет время, я постараюсь их отследить.

1 голос
/ 24 февраля 2020

В существующих ответах есть отличная информация; позвольте мне дополнить его объяснением немедленной проблемы:

Я получаю следующие ошибки: "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} является блок сценария , который представляет собой очевидную попытку использовать этот блок сценария в качестве параметра привязки с задержкой с входом конвейера .

В вашем коде вы не , обеспечивающий конвейерный ввод для вашего Stop-Process вызова, о чем вам и сообщает сообщение об ошибке.

Вместо этого вы используете foreach l * От 1032 * до l oop сверх ввода, используя $Process в качестве переменной итерации, и поэтому вам необходимо указать целевой идентификатор как обычный прямой аргумент на основе этой переменной, используя $Process.ProcessID, как показано в Ответ Дезинтернауты .

1 голос
/ 22 февраля 2020

Изменить {$_.processid} на $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
        }
    }
}
...