How do I add a column of incrementing values to cmdlet output? - powershell

Suppose I call Get-Service and want to assign a new column ID with the cmdlet output that prints incrementing integers so that:
ID Status Name DisplayName
-- ------ ---- -----------
0 Running AdobeARMservice Adobe Acrobat Update Service
1 Stopped AeLookupSvc Application Experience
2 Stopped ALG Application Layer Gateway Service
I'm trying to use Select-Object right now to add this column, but I don't quite understand how to iterate a variable in this sort of expression. Here's what I've got:
Get-Service |
Select-Object #{ Name = "ID" ; Expression= { } }, Status, Name, DisplayName |
Format-Table -Autosize
Is there a way to iterate integers within Expression= { }, or am I going about this problem the wrong way?

You can do it this way, though you will need to maintain some counter variable outside of the main expression.
$counter = 0
Get-Service |
Select-Object #{ Name = "ID" ; Expression= {$global:counter; $global:counter++} }, Status, Name, DisplayName |
Format-Table -Autosize
Another option, which is perhaps cleaner
Get-Service `
|% {$counter = -1} {$counter++; $_ | Add-Member -Name ID -Value $counter -MemberType NoteProperty -PassThru} `
| Format-Table ID

I asked the same question a different way and got the following answer
$x = 10
Get-Service |
Select-Object #{ Name = "ID" ; Expression={ (([ref]$x).Value++) }}, Status, Name, DisplayName | Format-Table -Autosize
It wasn't at all clear to me that the expression is being invoked within Select-Object's scope, not the pipe's. The [ref] qualifier bumps the increment's result up to the pipe's scope achieving the same result as explicitly specifying the variable as global.

Related

PowerShell Export-CSV - Missing Columns [duplicate]

This question already has an answer here:
Not all properties displayed
(1 answer)
Closed 1 year ago.
This is a follow-up question from PowerShell | EVTX | Compare Message with Array (Like)
I changed the tactic slightly, now I am collecting all the services installed,
$7045 = Get-WinEvent -FilterHashtable #{ Path="1system.evtx"; Id = 7045 } | select
#{N=’Timestamp’; E={$_.TimeCreated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')}},
Id,
#{N=’Machine Name’; E={$_.MachineName}},
#{N=’Service Name’; E={$_.Properties[0].Value}},#{N=’Image Path’;E=$_.Properties[1].Value}},
#{N=’RunAsUser’; E={$_.Properties[4].Value}},#{N=’Installed By’; E={$_.UserId}}
Now I match each object for any suspicious traits and if found, I add a column 'Suspicious' with the value 'Yes'. This is because I want to leave the decision upto the analyst and pretty sure the bad guys might use something we've not seen before.
foreach ($Evt in $7045)
{
if ($Evt.'Image Path' -match $sus)
{
$Evt | Add-Member -MemberType NoteProperty -Name 'Suspicious' -Value 'Yes'
}
}
Now, I'm unable to get PowerShell to display all columns unless I specifically Select them
$7045 | Format-Table
Same goes for CSV Export. The first two don't include the Suspicious Column but the third one does but that's because I'm explicitly asking it to.
$7045 | select * | Export-Csv -Path test.csv -NoTypeInformation
$7045 | Export-Csv -Path test.csv -NoTypeInformation
$7045 | Select-Object Timestamp, Id, 'Machine Name', 'Service Name', 'Image Path', 'RunAsUser', 'Installed By', Suspicious | Export-Csv -Path test.csv -NoTypeInformation
I read the Export-CSV documentation on MS. Searched StackOverFlow for some tips, I think it has something to do with PS checking the first Row and then compares if the property exists for the second row and so on.
Thank you
The issue you're experiencing is partially because of how objects are displayed to the console, the first object's Properties determines the displayed Properties (Columns) to the console.
The bigger problem though, is that Export-Csv will not export those properties that do not match with first object's properties unless they're explicitly added to the remaining objects or the objects are reconstructed, for this one easy way is to use Select-Object as you have pointed out in the question.
Given the following example:
$test = #(
[pscustomobject]#{
A = 'ValA'
}
[pscustomobject]#{
A = 'ValA'
B = 'ValB'
}
[pscustomobject]#{
C = 'ValC'
D = 'ValD'
E = 'ValE'
}
)
Format-Table will not display the properties B to E:
$test | Format-Table
A
-
ValA
ValA
Format-List can display the objects properly, this is because each property with it's corresponding value has it's own console line in the display:
PS /> $test | Format-List
A : ValA
A : ValA
B : ValB
C : ValC
D : ValD
E : ValE
Export-Csv and ConvertTo-Csv will also miss properties B to E:
$test | ConvertTo-Csv
"A"
"ValA"
"ValA"
You have different options as a workaround for this, you could either add the Suspicious property to all objects and for those events that are not suspicious you could add $null as Value.
Another workaround is to use Select-Object explicitly calling the Suspicious property (this works because you know the property is there and you know it's Name).
If you did not know how many properties your objects had, a dynamic way to solve this would be to discover their properties using the PSObject intrinsic member.
using namespace System.Collections.Generic
function ConvertTo-NormalizedObject {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, Mandatory)]
[object[]] $InputObject
)
begin {
$list = [List[object]]::new()
$props = [HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase)
}
process {
foreach($object in $InputObject) {
$list.Add($object)
foreach($property in $object.PSObject.Properties) {
$null = $props.Add($property.Name)
}
}
}
end {
$list | Select-Object ([object[]] $props)
}
}
Usage:
# From Pipeline
$test | ConvertTo-NormalizedObject | Format-Table
# From Positional / Named parameter binding
ConvertTo-NormalizedObject $test | Format-Table
Lastly, a pretty easy way of doing it thanks to Select-Object -Unique:
$prop = $test.ForEach{ $_.PSObject.Properties.Name } | Select-Object -Unique
$test | Select-Object $prop
Using $test for this example, the result would become:
A B C D E
- - - - -
ValA
ValA ValB
ValC ValD ValE
Continuing from my previous answer, you can add a column Suspicious straight away if you take out the Where-Object filter and simply add another calculated property to the Select-Object cmdlet:
# create a regex for the suspicious executables:
$sus = '(powershell|cmd|psexesvc)\.exe'
# alternatively you can join the array items like this:
# $sus = ('powershell.exe','cmd.exe','psexesvc.exe' | ForEach-Object {[regex]::Escape($_)}) -join '|'
$7045 = Get-WinEvent -FilterHashtable #{ LogName = 'System';Id = 7045 } |
Select-Object Id,
#{N='Timestamp';E={$_.TimeCreated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')}},
#{N='Machine Name';E={$_.MachineName}},
#{N='Service Name'; E={$_.Properties[0].Value}},
#{N='Image Path'; E={$_.Properties[1].Value}},
#{N='RunAsUser'; E={$_.Properties[4].Value}},
#{N='Installed By'; E={$_.UserId}},
#{N='Suspicious'; E={
if ($_.Properties[1].Value -match $sus) { 'Yes' } else {'No'}
}}
$7045 | Export-Csv -Path 'X:\Services.csv' -UseCulture -NoTypeInformation
Because you have many columns, this will not fit the console width anymore if you do $7045 | Format-Table, but the CSV file will hold all columns you wanted.
I added switch -UseCulture to the Export-Csv cmdlet, which makes sure you can simply double-click the csv file so it opens correctly in your Excel.
As sidenote: Please do not use those curly so-called 'smart-quotes' in code as they may lead to unforeseen errors. Straighten these ’ thingies and use normal double or single quotes (" and ')

Merge two PSCustomObjects into one- PowerShell

I need help in PowerShell to combine two outputs or two PSCustomObjects into One.
For example,
$services = Get-Service | Select Name, Starttype,Status
$processes = Get-Process | Select ID
I need the output with the table headers
Name, Starttype, Status, ID
I have already tried creating CSV and joining them but the problem is Process ID starts when the entire output ends for the services. I need them to a parallel.
Second I have tried to create PSCustomObjects but no luck.
Please help me with the PowerShell code.
Actual code that I'm trying to achieve.
**$exclusionItems = #()
$OasHighItems = #()
foreach($item in $items){
$exclusionItems += [PSCustomObject]#{
EXCLUSION_BY_NAME_OR_LOCATION = $item.EXCLUSION_BY_NAME_OR_LOCATION
EXCLUSION_EXCLUDE_SUBFOLDERS = $item.EXCLUSION_EXCLUDE_SUBFOLDERS
EXCLUSION_ON_READ= $item.EXCLUSION_ON_READ
}
}
foreach($oas in $oashigh){
$oashighItems += [PSCustomObject]#{
OAS_PROCESSES_LIST = $oas
}
}
$Array = #()
$Array = $exclusionItems,$oashighItems
$Array | Update-FirstObjectProperties | Export-Excel $ExcelParams -TableName Table -Show**
I'm assuming you want to join the two objects by their names, i.e. match the Process-Name with the Service-Name. For this you can loop over all processes & services, keep only those where service-name equals process-name, and use a calculated property to merge the result into one object:
$services = Get-Service;
Get-Process | ForEach-Object {$p = $_; $services |
Where-Object{$p.ProcessName -eq $_.Name} |
Select-Object Name,StartType,Status,#{n='ID';e={$p.ID}}}
The output on my machine is:
Name StartType Status ID
---- --------- ------ --
CcmExec Automatic Running 14856
CmRcService Automatic Running 5748
FusionInventory-Agent Automatic Running 5996
IBMPMSVC Automatic Running 3540
IntelAudioService Automatic Running 6104
... and so on ...

Powershell filtering a property value without expanding the property name

Is it possible to filter the "Roles" property without using -expandproperty Roles? The first set of statement shows me what I needed except i couldn't filter Roles -eq "db_owner" and users -ne "user3"
#import-module dbatools
$servers = server1,server2
Get-DbaDatabase -SqlInstance $servers -Status Normal | Select SQLInstance, Name, Owner,Roles, Users
Results:
SqlInstance : server1
Name : testdb
Owner : sa
Roles : {db_accessadmin, db_backupoperator, db_datareader, db_datawriter, db_ddladmin, db_denydatareader, db_denydatawriter,
db_owner, db_securityadmin, public}
Users : {user1,user2,user3}
If I run another statement
Get-DbaDatabase -SqlInstance $servers -Status Normal | select -expandproperty Roles| Where Name -eq 'db_owner', the property for db_owner is now called "Name" and it will return the results where "Roles" -eq db_owner.
However, it is now missing the other properties that i needed from the first statement. Do i have to use an array to store the values from each statement then join the results together or is there another way to filter a property value inside "Roles"?
Property of "Roles"
Name MemberType Definition
Roles Property Microsoft.SqlServer.Management.Smo.DatabaseRoleCollection Roles {get;}
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
Would you be against using ForEach-Object?
The below script should return the information you need. You can then filter these results down further if you want.
Get-DbaDatabase -SqlInstance $servers -Status Normal | ForEach-Object -Process {
[PSCustomObject]#{
SqlInstance = $_.SqlInstance
Name = $_.Name
Owner = $_.Owner
Roles = $_.Roles | Where-Object Name -eq 'db_owner'
Users = $_.Users | Where-Object Name -ne 'user3'
}
}
$servers = server1,server2
Get-DbaDatabase -SqlInstance $servers -Status Normal | Select SQLInstance, Name, Owner,Roles, Users | Where-Object { (#($_.Roles)) -contains 'db_owner' }
Assuming the Roles property is returning a base array type (I can't check since I don't have that env available atm), you can use the -contains operator to see if a string value is contained in the array.
If the Roles property is a base-Collection type (you can check by using .GetType()), you can explicitly cast Collections to Arrays by wrapping the collection in array notation: ... | Where-Object { (#($_.Roles)) -contains 'db_owner' }

Concatenating non-unique values alongside a unique field in PowerShell

I have the following set of queries which retrieve a set of data, merge it down so I just get the unique values and add a number to each of them (so I can select that particular item later).
$allMoveRequests = Get-MoveRequest -DomainController server |
select Alias,Status,TargetDatabase,BatchName
$optNum=1
$AllMoveBatches = #($allMoveRequests | Sort-Object -Property BatchName |
Select-Object BatchName,TargetDatabase -Unique) |
Select #{Name="Option";Expression={$optNum;$optNum++}},BatchName,TargetDatabase
$AllMoveBatches | Format-Table -AutoSize | Out-String|% {Write-Host $_}
This returns :
Option BatchName TargetDatabase
------ --------- --------------
1 Batch1 Database1
2 Batch2 Database2
etc. That works as it should, but what I'd like to add is the Status value from those batches, combined where there is more than one rather than creating duplicate entries. For instance, if I simply add Status into the second bit of code I end up with :
Option BatchName TargetDatabase Status
------ --------- -------------- ------
1 Batch1 Database1 Completed
2 Batch1 Database1 In Progress
3 Batch2 Database2 Completed
while what I'd ideally like would be :
Option BatchName TargetDatabase Status
------ --------- -------------- ------
1 Batch1 Database1 Completed,InProgress
2 Batch2 Database2 Completed
I've tried using an expression in the select statement to query all the relevant Status entries and apply -Unique to them, but that just returns all Status entries across all batches, not just those relevant to the current Batch line.
Is there a way to achieve this?
It's not pretty, and it might not be very performant with lots of data, but here's one way to do it...
First, lets create some sample data:
$data = #(
(new-object PSObject -Property ([ordered] #{
"BatchName" = "Batch1"
"TargetDatabase" = "Database1"
"Status" = "Completed"
})),
(new-object PSObject -Property ([ordered] #{
"BatchName" = "Batch1"
"TargetDatabase" = "Database1"
"Status" = "In Progress"
})),
(new-object PSObject -Property ([ordered] #{
"BatchName" = "Batch2"
"TargetDatabase" = "Database2"
"Status" = "Completed"
}))
)
now, process it:
Set-Variable -Name "optNum" -Option AllScope -Value 1
$results = #( $data | group-object BatchName, TargetDatabase ) `
| select-object #{Name="Option";Expression={$optNum; $optNum++}},
#{Name="BatchName";Expression={$_.Group[0].BatchName}},
#{Name="TargetDatabase";Expression={$_.Group[0].TargetDatabase}},
#{Name="Status";Expression={$_.Group.Status -join ", "}} `
| sort-object -Property BatchName
and show the result:
PS> $results
Option BatchName TargetDatabase Status
------ --------- -------------- ------
1 Batch1 Database1 Completed, In Progress
2 Batch2 Database2 Completed
What it's doing is grouping to select the unique combinations of BatchName and DatabaseName and then to make the results it's selecting the BatchName and DatabaseName from the first item in each group, and concatenating all of the Status properties from the items in that group (you could also process the statuses in the Status Expression if you wanted to e.g. sort, filter or de-dupe them within each group).
Note that I've moved your original sort-object BatchName to the end of the pipeline. There's no point sorting, say 1000 objects only to throw half of them away - you might as well sort at the end.
And I could only get your "Option" counter to work by using Set-Variable to make it AllScope as the $optNum++ wasn't incrementing the variable properly when I used $optNum = 1 to initialise it.
mclayton's answer should be the accepted one, but here's a slightly more concise version that uses one of my favorite Powershell idioms: Foreach with a -Begin scriptblock {$i=1} that executes only once.
[pscustomobject]#{BatchName = 'Batch1';TargetDatabase='Database1';Status='Completed'},
[pscustomobject]#{BatchName = 'Batch1';TargetDatabase='Database1';Status='In Progress'},
[pscustomobject]#{BatchName = 'Batch2';TargetDatabase='Database2';Status='Completed'} |
Group BatchName, TargetDatabase |
%{$i=1}{ [pscustomobject]#{Option = $i++
BatchName = $_.Group[0].BatchName
TargetDatabase = $_.Group[0].TargetDatabase
Status = $_.Group.Status -join ','}
}

PowerShell Select-Object returning more than specified?

Why does the following command return more attributes than in the Select?
$obj = Get-VM | Select VMName,State; $obj
Output:
VMName : blah-blah
State : Off
PSComputerName : host
RunspaceId : URI
And how do I just get the value for State?
$obj = Get-VM | Select VMName,State; $obj.State
The above should do the trick but doesn't :(
That should work, but it sounds like for some reason the assignment of the results of Get-VM to $obj is happening before anything gets piped to select. Try Get-VM | select VMName, State by itself at the prompt, and if you get the expected results, try this:
$obj = (Get-VM | select VMName, State); $obj
To get just the State property, you can do this:
(Get-VM).State
Problem found :)
The above command is just fine as it is, however when it get's wrapped in an Invoke-Command such that it's run against another machine then the results are modified to include run specific information and a 'follow-on Select' is required like this:
Invoke-Command -Comuptername XXXX -ScriptBlock {$obj = Get-VM | Select VMName,State; $obj} | Select State