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
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.
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
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
}
}
}
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.
Related
I'm currently making a Powershell script that will analyze multiple log files from a mail server to gather various statistics that will be stored in a number of different arrays.
I have the following code snippet as an example of updating one of the arrays.
#Update arrays
#Overall array
$objNewValue = New-Object -TypeName PSObject
$objNewValue = $PSOSSLOverall | Where-Object {($_.Version -contains $strVersion -and $_.Cipher -contains $strCipher -and $_.Bits -contains $strBits)}
If ($objNewValue -ne $null) {
try {
Write-Verbose "$strVersion $strCipher $strBits is already in the array, so we'll update TimeSeen value"
$objNewValue.TimesSeen++
$objNewValue.LastSeen = $dtTimestamp
} #try
catch {
Write-Host "Something whent wrong while attempting to update an existing object in the overall array" -BackgroundColor DarkRed
Write-Host "Current line: $strtemp[$i]"
Write-Host "Current values: $dtTimestamp <-> $strVersion <-> $strCipher <-> $strBits"
Write-Host "Current array:"
$PSOSSLOverall | Sort-Object -Property Version, Cipher -Descending | Format-Table -AutoSize
Write-Host "Exception object:"
$_
} #catch
} #If Check for existence in Overall array
Else {
try {
Write-Verbose "$strVersion $strCipher $strBits is not in the array, so it will be added "
$objNewValue = New-Object -TypeName PSObject
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Version' -Value $strVersion
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Cipher' -Value $strCipher
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Bits' -Value $strBits
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'TimesSeen' -Value 1
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'Percentage' -Value 0
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'FirstSeen' -Value $dtTimestamp
Add-Member -InputObject $objNewValue -MemberType 'NoteProperty' -Name 'LastSeen' -Value $dtTimestamp
$PSOSSLOverall += $objNewValue
} #try
catch {
Write-Host "Something whent wrong while attempting to add a new object to the overall array"
Write-Host "Current line: $strtemp[$i]"
Write-Host "Current values: $dtTimestamp <-> $strVersion <-> $strCipher <-> $strBits"
Write-Host "Exception object:"
$_
} #catch
} #Else Check for existence in Overall array
However, when I have up to 10 or more arrays that I need to update, the result will be a lot of similar code as there's only relatively few lines that change each time - like the array being updated, the where clause, the variables used and number of columns in the arrays.
Would it be possible to create a function that can handle updating the different arrays?
Thanks in advance.
-Update-
To explain the code snippet above: All the variables are already set before this part of the script is run. $strtemp[$i] is actually where all the data comes from as that the current line in the log file from which I then extract the needed data and place it in various variables.
First I search the array in question, which in this case is $PSOSSLOverall, to see if the data from the current line is already in the array. If $objNewValue is not null, then the data is already there, and I then increment a counter and update a date stamp for that "row" of data. If $objNewValue is null, then the data is not already there, and then we added a now object (row) to the array with the data from various variables.
Each attempt is equipped with try/catch section for error handling.
The end result will be an array that looks like this (the percentage column is calculated elsewhere):
The other arrays have various number of columns, which I what I guess makes it difficult to make a common function to update them.
I have found myself in a similar situation once or twice. You'll have to refactor your code, converting inflexible static definitions into parameter variables. The idea is to separate the data from program logic so you can do something like pass a different set of attribute names and values into the same function for different circumstances.
Here were a couple places I found to improve flexibility:
Parameterize the Where-Object expressions used to find matching records so you don't write the same code for each combination of columns and values. See hasPropertyValues below. All it does is perform an -and concatenated -contains operation on each name-value pair you pass to it.
Parameterize the data you want changed for each eventuality. Your code does something when it finds matching records and when it finds no matching records. Pull those actions out of the script body and into an input parameter that can change when you're done working with the encryption dataset and have to move onto another. The UpdateRecords function takes hashtable parameters defining the shape of the data when new records are added and when matching records are found.
See below for the example. I think you can adapt some of the ideas in here to your code.
# this is a convenience function allowing us to test multiple name-value pairs against an object
function hasPropertyValues {
Param(
[object] $inputObject,
[hashtable] $properties
)
$result = $true
foreach($name in $properties.Keys){
$result = $result -and ($inputObject.$name -contains $properties[$name])
}
Write-Output $result
}
# this function evaluates each object in $inputDataset
# if an object matches the name-values defined in $matchProperties
# it updates the records according to $updateRecordProperties
# if no records are found which match $matchProperties
# a new object is created with the properties in both $matchProperties
# and $newRecordProperties
# All results are written to the pipeline, including unmodified objects
function UpdateRecords{
Param (
[object[]] $inputDataset,
[hashtable] $matchProperties,
[hashtable] $updateRecordProperties,
[hashtable] $newRecordProperties
)
$numberOfMatchingRecords = 0
foreach ($record in $inputDataset){
if ( hasPropertyValues -inputObject $record -properties $matchProperties) {
# a record with matching property values found.
# Update required attributes
$numberOfMatchingRecords++
foreach($name in $updateRecordProperties.Keys){
if ($updateRecordProperties[$name] -is 'ScriptBlock'){
# if the update is a scriptblock, we invoke the scriptblock
# passing the record as input. The result of the invocation
# will be set as the new attribute value
$newValue = & $updateRecordProperties[$name] $record
} else {
$newValue = $updateRecordProperties[$name]
}
$record | Add-Member -Force -MemberType NoteProperty -Name $name -Value $newValue
}
}
Write-Output $record
}
if ($numberOfMatchingRecords -eq 0) {
# no records found with the search parameters
$newRecord = New-Object -TypeName psobject -Property $newRecordProperties
foreach($key in $matchProperties.Keys){
$newRecord | Add-Member -MemberType NoteProperty -Name $key -Value $matchProperties[$key] -Force
}
Write-Output $newRecord
}
}
[object[]] $TestDataset= #(New-Object psobject -Property #{
version='TLSv1.2'
cipher='ECDHE-RSA-AES256-GCM-SHA384'
Bits=256
TimesSeen = 1833
Percentage = 87578
FirstSeen = [DateTime]::Now
LastSeen = [DateTime]::Now
})
function TestUpdateRecords{
$encryptionNewRecordDefaults = #{
TimesSeen = 1
Percentage = 0
FirstSeen = [DateTime]::Now
LastSeen = [DateTime]::Now
}
$encryptionUpdateAttributes = #{
LastSeen = [DateTime]::Now
TimesSeen = {$ARGS[0].TimesSeen + 1}
}
# test adding a new record
UpdateRecords -inputDataset $TestDataset `
-matchProperties #{ Version='TLSv1.0';cipher='unbreakable';bits=720} `
-updateRecordProperties $encryptionUpdateAttributes `
-newRecordProperties $encryptionNewRecordDefaults
# test modifying a matching record
UpdateRecords -inputDataset $things `
-matchProperties #{Version='TLSv1.2';cipher='ECDHE-RSA-AES256-GCM-SHA384';bits=256} `
-updateRecordProperties $encryptionUpdateAttributes `
-newRecordProperties $encryptionNewRecordDefaults
}
TestUpdateRecords
There are a lot of different ways to implement this kind of refactoring. You could, for instance, extract dataset-specific logic into scriptblocks and pass these to your main loop function.
Another possibility is to dig into the object-oriented features of PowerShell and try to build classes around each of your datasets. This could encapsulate the 'Update' and 'New' actions in a pleasant way. I'm not yet literate enough in powershell OO features to try.
Say I have a PowerShell array $Sessions = #() which I am going to fill with PSCustomObjects. How can I add a custom property to the array itself? E.g. so I can have $Sessions.Count which is built-in and $Sessions.Active which I want to set to the active session count.
I know that I can add properties to PSCustomObjects (in a dirty way) with
$MyCustomObject = "" | Select-Object Machine, UserName, SessionTime
but though doing so on an array would not result in the property being added.
So how can I achieve my goal? Is there any way to create a custom array?
The answer to your question as stated would be to just use Add-Member on the array object.
Add-Member -InputObject $sessions -MemberType NoteProperty -Name "State" -Value "Fabulous"
Adding a property to each element after you created the object is similar.
$sessions | ForEach-Object{
$_ | Add-Member -MemberType NoteProperty -Name "State" -Value "Fabulous"
}
This of course comes with a warning (that I forgot about). From comments
Beware, though, that appending to that array ($sessions += ...) will replace the array, thus removing the additional property.
Ansgar Wiechers
Depending on your use case there are other options to get you want you want. You can save array elements into distinct variables:
# Check the current object state
$state = $object.Property .....
# Add to the appropriate array.
if($state -eq "Active"){
$activeSessions += $object
} else {
$inactiveSessions += $object
}
Or you could still store your state property and post process with Where-Object as required:
# Process each inactive session
$sessions | Where-Object{$_.State -eq "Active"} | ForEach-Object{}
To avoid the destroying / recreating array issue, which can be a performance hog, you could also use an array list instead.
$myArray = New-Object System.Collections.ArrayList
Add-Member -InputObject $myArray -MemberType ScriptMethod -Name "NeverTellMeTheOdds" -Value {
$this | Where-Object{$_ % 2 -ne 0}
}
$myArray.AddRange(1..10)
$myArray.NeverTellMeTheOdds()
Notice that the array had its member added then we added its elements.
As Matt commented, you can use the Add-Member on an enumerable type by supplying it as a positional argument to the -InputObject parameter.
To allow for resizing after adding the new property, use a generic List instead of #():
$list = [System.Collections.Generic.List[psobject]]::new()
$list.AddRange(#(
[pscustomobject]#{SessionId = 1; Active = $true}
[pscustomobject]#{SessionId = 2; Active = $false}
[pscustomobject]#{SessionId = 3; Active = $true}
) -as [psobject[]])
Add-Member -InputObject $list -MemberType ScriptProperty -Name ActiveSessionCount -Value {
return #($this |? Active -eq $true).Count
}
Now you can retrieve the active session count easily:
PS C:\> $list.ActiveSessionCount
2
I have created a custom object called $info and moving it to an array $arr ,
How is it possible to remove one member along with its all properties ?
My script:
Get-Process | ForEach-Object{
$info = New-Object -TypeName PSObject
$info | Add-Member -Type NoteProperty -Name Process -Value $_.processname
$info | Add-Member -Type NoteProperty -Name ID -Value $_.id
$arr += $info
}
$arr | ft -AutoSize
The result looks like this :
Process ID
------- --
ApplicationFrameHost 38556
AppVShNotify 9792
armsvc 2336
atieclxx 6944
atiesrxx 1844
audiodg 59432
CcmExec 3988
chrome 46068
How can I remove one particular member for example "audiodg 59432" gets removed
audiodg 59432
Your terminology is a bit incorrect here. A member is on an individual object. When you use Add-Member above you're adding properties to each individual object, then you're returning an array of objects.
You're asking how to remove an individual object from the array.
In PowerShell you cannot remove an item from an array. You could instead filter the array based on some criteria and create a new one:
$newArr = $arr | Where-Object { $_.Name -ne 'audiodg' }
# or
$newArr = $arr | Where-Object { $_.ID -ne 59432 }
The purpose of my script is to get the entire list of VMs from our vcenter, check to see if they have DNS entries - if the dns entry is null then output the VM name and corresponding IP address. Pretty simple, but I'm getting tripped up combining the VM name array with the IP address array. Here's what I mean:
$mainArray = #()
$vmArray = #()
$ipArray = #()
$vms = Get-VM | select Name, #{N="DnsName"; E={$_.ExtensionData.Guest.Hostname}}, #{N="IPAddress"; E={$_.Guest.IPAddress[0]}}
foreach ($vm in $vms)
{
$dnsname = $vm.DnsName
$ipaddr = $vm.IPAddress
$vmname = $vm.Name
if (!$vm.DNSName) #if DNS name is null
{
$vmArray += $vmname
$ipArray += $ipaddr
}
}
$item = New-Object PSObject
$item | Add-Member -type NoteProperty -Name 'VM Name' -Value $vmArray
$item | Add-Member -type NoteProperty -Name 'IP Address' -Value $ipArray
$mainArray += $item
$mainArray | ft -wrap -autosize
This outputs the two columns next to each other, but the output is separated by commas and is truncated with an ellipse. I know that there's a better way to do this, so I'm open to suggestions. Also be curious to know how to make this work just for my own understanding. Appreciate the help everyone.
Your main issue is that you are populating an array outside your loop. I don't have access to PowerCLI right in front of me now but we should be able to remove most of that code as it appears to be redundant and needless. Continuing after $vms being declared
$vms | Where-Object{!$_.DNSName} | ForEach-Object{
New-Object PSObject -Property #{
'VM Name' = $_.Name
'IP Address' = $_.IPAddress
}
} | ft -wrap -autosize
Before you made a two arrays. One for names and the other ips. Then you took those 2 arrays and made a single object with those two properties. I removed the need for the if statement with the Where-Object{!$_.DNSName}. Now all the objects being processed are the ones you want.
You could really do this is in one/two lines using the same logic.
Get-VM | Select #{N='VM Name';E={$_.Name}}, #{N="DnsName"; E={$_.ExtensionData.Guest.Hostname}}, #{N="IP Address"; E={$_.Guest.IPAddress[0]}} |
Where-Object{!$_.DNSName} | Select "VM Name","IP Address" | Format-Table -wrap -autosize
Again, I cannot test but it should work. If there are any errors they should be easily correctable. Assuming I figured out your issue in the first place.
About the ellipses
PowerShell has a variable $FormatEnumerationLimit that governs how many array elements are display in console output. There is a good article that discusses it on TechNet
By default $FormatEnumerationLimit is set to 4
Matt, I had found that link earlier but didn't find it very helpful since it makes the output garbled and doesn't breakout the values by row. This is how I was able to make the original script work:
$i=0
write-output $vmArray |
foreach {
new-object psobject -property #{
VMName = $_
IPAddress = $ipArray[$i++]
}
} | ft -auto -wrap
Rather unconventional, but does work nonetheless. There really should be an easier way to combine array side by side, but I digress. Thanks a ton Matt.
Update: This is probably more conventional:
For($i=0;$i -lt $vmArray.Count;$i++)
{
$item = New-Object PSObject
$item | Add-Member -type NoteProperty -Name 'VM Name' -Value $vmArray[$i]
$item | Add-Member -type NoteProperty -Name 'IP Address' -Value $ipArray[$i]
$mainArray += $item
}
$mainArray | ft -wrap -autosize
I'm using custom objects to hold the name and schema from a set of SQL Server objects. I put the objects into an array, then I get another set of objects and put those into another array. What I'd like to do now is find all exact matches between the two arrays.
I'm currently using this:
$filteredSQLObjects = #()
foreach ($SQLObject1 in $SQLObjects1)
{
foreach ($SQLObject2 in $SQLObjects2)
{
if ($SQLObject1.Name -eq $SQLObject2.Name -and
$SQLObject1.Schema -eq $SQLObject2.Schema)
{
$filteredSQLObjects += $SQLObject1
}
}
}
Is there a better/faster/cleaner way to do this? Originally when I was just working with arrays of strings I could just loop through one of the arrays and use -contains on the second, but with objects that doesn't seem possible.
Thanks!
I think its better if you define the equality condition in an IsEqualTo method on your custom object. So something like this:
$myObject = New-Object PSObject
$myObject | Add-Member -MemberType NoteProperty -Name Name -Value $name
$myObject | Add-Member -MemberType NoteProperty -Name Schema -Value $schema
$myObject | Add-Member -MemberType ScriptMethod -Name IsEqualTo -Value {
param (
[PSObject]$Object
)
return (($this.Name -eq $Object.Name) -and ($this.Schema -eq $Object.Schema))
}
Then you can either do a one-liner like Keith showed us, or just do your double foreach iteration. Whichever you think is more readable:
$filteredSQLObjects = $SQLObjects1 | Where-Object { $SQLObject1 = $_; $SQLObjects2 | Where-Object { $_.IsEqualTo($SQLOBject1) } }
foreach ($SQLObject1 in $SQLObjects1)
{
foreach ($SQLObject2 in $SQLObjects2)
{
if ($SQLObject1.IsEqualTo($SQLObject2))
{
$filteredSQLObjects += $SQLObject1
}
}
}
EDIT
OK, for a start, you can't add an Equals member because it already exists on System.Object (doh!). So I guess IsEqualTo will have to do instead.
What you can do is define your own function called Intersect-Object (the equivalent of .NET's Enumerable.Intersect method) which accepts pipeline input and returns the set intersection of two sequences (the ones that appear in both sequences). Be aware that I haven't fully-implemented this function (assumes each item in the collection specified by Sequence has an IsEqualTo method, doesn't check for duplicates before adding to $filteredSequence etc), but I hope you get the idea.
function Intersect-Object
{
param (
[Parameter(ValueFromPipeline = $true)]
[PSObject]$Object,
[Parameter(Mandatory = $true)]
[PSObject[]]$Sequence
)
begin
{
$filteredSequence = #()
}
process
{
$Sequence | Where-Object { $_.IsEqualTo($Object) } | ForEach-Object { $filteredSequence += $_ }
}
end
{
return $filteredSequence
}
}
Then your double foreach loop turns into this:
$filteredSQLObjects = $SQLObjects1 | Intersect-Object -Sequence $SQLObjects2
You could condense this to a one-liner which would be appropriate if you were writing this at the console:
$filtered = $SQLObjects1 | ? {$o1=$_; $SQLObjects2 | ? {$_.Name -eq $o1.Schema `
-and $_.Name -eq $o1.Schema}}
But in a script, I would expand it out like you have it. It is more readable that way.