"Normalizing" a CSV file - arrays

I have a CSV file for help desk calls. The same ticket might have 1,2, or even 5 records based on the number of updates it has. (One field is different, all other fields are identical).
I want to take the mostly-duplicate records and create one record with the differences concatenated into it. Having programmed in the past, but being a newbie to PowerShell, I could use some help.
So, based on a previous question I asked, here's what I have so far. Assuming data like this:
ID, Date, Comment
345, 1/1/16, Moss has reported a fire in the I/T Room
345, 1/1/16, Never mind, he has sent an e-mail about it.
346, 1/2/16, Someone on the 5th floor is complaining about a man under her desk.
347, 2/1/16, Jen says she has broken the Internet.
347, 2/1/16, Douglas is very angry, we need a fix ASAP!
347, 2/1/16, Roy was playing a joke on her. Closing ticket.
I have the following code:
$FileList = Import-Csv "Call List.csv"
$incidents = $FileList | Group ID
foreach($group in $incidents)
{
# What goes here?
}
How do I take the comments from the 2nd, 3rd, etc. line in the group, concatenate it to the comment in the first, and write the file out?

The Group-Object produces an object with Name and Group, Group containing all the items in that group. You can extract them and create a new object using something like this:
$incidents = $FileList | Group-Object ID | % {
New-Object psobject -property #{
ID = $_.Name
Date = $_.Group[0].Date
Comment = ($_.Group | Select -Expandproperty Comment) -Join "`n"
}
}
(not tested as I am currently on a Mac)

I'd first get a list of the unique IDs, for example:
$Ids = $FileList | Select-Object -ExpandProperty Id -Unique
Then I'd look through the list of tickets and build up a "report" for each ID:
foreach($Id in $Ids){
# Get all incident logs for this Id:
$logs = $FileList | ?{$_.Id -eq $Id}
$report = ""
foreach($log in $logs){
$report += $log.Date + ": " + $log.Comment + "; "
}
# Now you can write the text out
$report | Out-File $Outfile -Append
}
Hope that gives you an idea.

Related

How does one add output from a cmdlet to an array?

I am trying to determine if specific Windows hotfixes are installed on our Windows servers. I am quite new to PowerShell scripting and this is what I have so far:
$servers = Get-ADComputer -Filter {(OperatingSystem -like "Windows Server 2019*") -and (enabled -ne $false)} -Property *
$result = #()
ForEach ($item in $servers) {
$testhotfix = Get-HotFix -Id KB4534310,KB4534314,KB4534283,KB4534288,KB4534297,KB4534309,KB4534271,KB4534273 -ComputerName $item.Name | `
select $item.Name,$item.CanonicalName,$item.OperatingSystem
$result += $testhotfix
}
$result | Export-Csv -Path C:\Users\user1\Desktop\Servers.csv -NoTypeInformation
The CSV file that is created includes one line with the information I'm looking for, followed by several lines of commas, like so:
Script Output
"SERVER1","somedomain.com/Servers/Non-Prod/New_Server_Staging/SERVER1","Windows Server 2019 Standard"
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
,,
We have several servers with at least one of the hotfixes installed. How do I add each server to the $result array?
Thank you
Generally speaking:
select $item.Name,$item.CanonicalName,$item.OperatingSystem
should be:
select Name, CanonicalName, OperatingSystem
That is, you need to pass the property names (e.g., Name), not the current input object's property values (e.g., $item.Name) to select (the Select-Objectcmdlet).
The net effect is that Select-Object creates custom objects whose properties are (mistakenly) named for the property values and themselves have no value, given that the input objects have no such properties.
This explains the output you saw.
However, the bigger problem is that even that won't work, given that the property names relate to the $item object, not to the objects output by Get-HotFix, which are the ones select operates on.
As it turns out, what you really need is to use the Get-HotFix call as a conditional, so as to only write a CSV row for the computer at hand if at least one of the specified hotfixes is installed:
$hotfixIds = 'KB4534310', 'KB4534314', 'KB4534283', 'KB4534288', 'KB4534297', 'KB4534309', 'KB4534271', 'KB4534273'
if (0 -ne (Get-HotFix -ErrorAction SilentlyContinue -Id $hotfixIds -ComputerName $item.Name).Count) {
$result += $item | select Name, CanonicalName, OperatingSystem
}
Note:
Note how it is now $item (the computer at hand) that is piped to select, to ensure that its properties are extracted (in the form of a custom object with these properties).
You could omit 0 -eq altogether and rely on PowerShell's implicit to-Boolean conversion, where any nonzero number evaluates to $true (see the bottom section of this answer for a summary of all rules.
If instead you want to test for all of the specified hotfixes being installed, replace 0 -ne with $hotfixIds.Count -eq.
-ErrorAction SilentlyContinue silences the errors from computers where none of the specified hotfixes are installed; you could examine the automatic $Error collection afterwards, or use -ErrorVariable err to collect all command-specific errors in variable $err.
Also, your overall command can be greatly streamlined - see the bottom section.
A solution for a different scenario, that may be of interest as well:
If you wanted to combine properties from the Get-HotFix output objects with properties from the $item objects (representing the computer at hand):
The following command:
selects all properties from the Get-HotFix output objects (-Property *)
adds the properties of interest from the current $item, using calculated properties
# Additional 'KB...' values omitted for brevity.
Get-HotFix -Id KB4534310, KB4534314 -ComputerName $item.Name |
Select-Object -Exclude Name -Property *,
#{ n = 'Name'; e = { $item.Name } },
#{ n = 'CanonicalName'; e = { $item.CanonicalName } },
#{ n = 'OperatingSystem'; e = { $item.OperatingSystem } }
Note that -Exclude Name excludes the Name property from the input objects (Get-HotFix output objects that have such a property, but it is empty), so that Name can be added as a property containing the computer name.
As for what you tried:
Aside from the Select-Object property-name problem mentioned above, your major problem was that you expected a pipeline segment as a conditional, which is not how pipelines work:
Get-HotFix ... | select ...
The above simply sends Get-HotFix's output objects to select (Select-Object), which then unconditionally processes them (and, as stated, looks for properties with the given names on these objects).
Now, if Get-HotFix produced no output, then conditional logic applies implicitly: the select command would then simply not be invoked.
Conversely, if Get-HotFix produces multiple outputs, select would be invoked on each.
That is, if we had naively tried to correct your command from:
Get-HotFix ... | select ...
to:
Get-HotFix ... | ForEach-Object { $item | select ... }
you would have potentially created multiple output objects per computer, namely whenever a given computer happens to have more than one among the given hotfixes installed.
A streamlined version of your (corrected) command:
Your command can be streamlined to use a single pipeline only, without the need for aux. variables:
Get-ADComputer -Filter '(OperatingSystem -like "Windows Server 2019*") -and (enabled -ne $false)' -Property * |
ForEach-Object {
if (0 -ne (Get-HotFix -ErrorAction SilentlyContinue -ComputerName $item.Name -Id KB4534310,KB4534314,KB4534283,KB4534288,KB4534297,KB4534309,KB4534271,KB4534273).Count) {
$item | select Name, CanonicalName, OperatingSystem
}
} | Export-Csv -Path C:\Users\user1\Desktop\Servers.csv -NoTypeInformation
Note:
If you end a line with |, you do not need a trailing ` to signal line continuation.
PowerShell [Core] v7.0+ now also allows placing | at the start of the very next line.
A single-quoted string ('...') is used instead of a script block ({ ... }) to pass the -Filter argument, because tt's best to avoid the use of script blocks ({ ... }) as -Filter arguments.
The output custom object instances created with $item | select Name, CanonicalName, OperatingSystem are sent directly to the pipeline.
I would use a PSCustomObject.
$array = foreach($item in $obj)
{
[PSCustomObject]#{
Name = $item.Name
CanonicalName = $item.CanonicalName
OS = $item.OperatingSystem
}
}

Selecting Distinct Items within Array using PowerShell and Linq

I have been banging my head on this problem for a few hours.
I have a multi-dimensional array and I need to select the unique items based on two "columns".
Is there an efficient .Net or otherwise way to do this and achieve the desired output?
The data looks something like:
ComputerName, IPAddress, MacAddress, FirstObserved
I would like to determine unique values based on MacAddress and ComputerName and keep the unique value based on the oldest FirstObserved date.
I have tried the PowerShell ways of doing this but it's horribly slow to say the least.
$data | Group-Object -Property ComputerName,MacAddress | ForEach-Object{$_.Group | Sort-Object -Property FirstObserved | Select-Object -First 1}
In a perfect world I would have a list of items no duplicates with the oldest entry based on FirstObserved date.
You can implement the grouping manually with a hashtable:
$FirstObserved = #{}
Import-Csv .\data.csv |ForEach-Object {
$key = $_.Computername,$_.MacAddress -join ','
$date = $_.FirstObserved -as [datetime]
# Check if we already have an entry for this Name + MAC combo
if($FirstObserved.Contains($key))
{
# Make sure the current date is older than what we have already
if($FirstObserved[$key] -gt $date)
{
$FirstObserved[$key] = $date
}
}
else
{
# First instance of the Name + MAC combo, save the date
$FirstObserved[$key] = $date
}
}
Now you can eaily look up the first date for a combination with:
$FirstObserved["Computer-01,11:22:33:44:55:66"]
If you want to export the list to another csv, all you need to do is turn the key-value pairs of the hashtable into objects:
$FirstObserved |Select-Object #{Name='Identity';Expression={$_.Key}},#{Name='FirstObserved';Expression={$_.Value}} |Export-Csv -Path output.csv

Get the latest date

I have an array that I call $data.
I want to get the earliest dates from the $data array for each host in my csv file. The user will input a host and it will find the earliest date it was modified.
Hostname LastModified
HD 9/8/2012
LOG 9/15/2004
NETMAN 12/25/2004
NETMAN 5/5/2015
LOG 1/4/2013
LOG 6/6/2011
So if they input LOG, I want it to give me the earliest date.
Created on 9/15/2004
Code:
$data= import-csv ".\Earliest Date Template.csv"
$Hostname=Read-Host "Please enter Host Name"
$data | Foreach-Object {$_."Last Modified" = [DateTime]$_."Last Modified"; $_} | Group-Object Hostname| Foreach-Object {$_.Group | Sort-Object LastModified | Select-Object -First 1}
Grouping them does seem like the way to go but you don't need to do that. Just sort the entire list by date then select the last option from the list (that matches the host name you are looking for).
$hostname = Read-Host "Please enter Hostname"
$data | Sort-Object {[DateTime]$_."LastModified"} | Where-Object{$_.Hostname -eq $hostname} | Select -Last 1
You might need to do some user validation but something like this seems to work with your sample data:
Please enter Hostname: log
Hostname LastModified
-------- ------------
LOG 1/4/2013
If you then only want the date it would just be a matter of expanding the value from the result.
$data |
Sort-Object {[DateTime]$_."LastModified"} |
Where-Object{$_.Hostname -eq $hostname} |
Select -Last 1 -ExpandProperty LastModified

How to Parse the Results of CMDlet into Array

I am sure there is an easy answer to this question, but I cannot find it anywhere. I would like to know how to parse the results of Get-Mailbox | select Alias into an array so each time I use the array it does not show the items as "#{Alias=username}".
I have tired this, but it seems to make the values not text:
$arrayname = Get-Mailbox | select Alias
I am sure this question has been asked before, but I cannot find it.
Essentially I would like to get the Alias' from the Get-Mailbox command into an array so that I can use the foreach cmdlet to get specific folder information from a user like so:
>> $aliases = Get-Mailbox | select Alias
>> Foreach ($username in $aliases) {Get-MailboxFolderStatistics -identity $username | select FolderPath | where {$_.FolderPath -like '*Deleted*'} | Export-CSV "C:\users\username\desktop\allusers-deletedfolder-postarchive.csv" -NoTypeInformation}
The cmdlet already produces an array, only that its elements are mailbox objects, not strings. Selecting the Alias property restricts the object properties, but still leaves you with an array of objects (hence the output you observed). You need to expand the property:
$arrayname = Get-Mailbox | select -Expand Alias
or echo it in a loop:
$arrayname = Get-Mailbox | % { $_.Alias }

PowerShell script to list items in collection

I'm new to PowerShell and am trying to query against my SQL server. I get the idea of creating a new-psdrive and then navigating to databases etc and have a line of code as
$dbs = (get-childitem
sqlserver:\sql\SERVER\INSTANCE\databases)
when I pipe the $dbs to a foreach, how would I get results of a collection of the database object? I am trying to read the extendedproperties of my test database.
This single query gives the results I want repeated for each database.
set-location
DRIVENAME:\databases\beagle_test\extendedproperties
get-childitem | select displayname,
value
any help very much appreciated.
I dont have SQL server handy to try this. Let me know the result
Set-Location DRIVENAME:\Databases
Get-ChildItem | % { Get-ChildItem $("$_.Name\extendedproperties") | Select DisplayName, Value }
Try this
Set-Location DRIVENAME:\Databases
Get-ChildItem | foreach-object { if (Test-Path $("$.Name\extendedproperties")) { Get-ChildItem $("$.Name\extendedproperties") | Select DisplayName, Value } }
The second line here is a single statement. What I am doing is to check if Extendedproperties exist and then get child item.
How about:
dir sqlserver:\sql\ServerName\InstanceName\Databases\*\ExtendedProperties\* |
select #{Name="Database";Expression={$_.Parent.Name}}, Name, Value
How about just:
dir SQLSERVER:\SQL\Server\Instance\databases\*\extendedproperties\* | % {select $_.displayname, $_.value}
so, many years later I am looking into my SO stats and see this old question and figure that my powershell skills have grown a little since 2010.
The use-case has long gone but I think what I was trying to achieve is this:
foreach ($db in $SMOServer.databases | Where-Object status -eq 'normal') {
$db.ExtendedProperties | Select-Object #{name = "DBName"; expression = {$db.Name}}, name, value
}
which gives results like this:
DBName Name Value
------ ---- -----
AdventureWorks2014 MS_Description AdventureWorks 2014 Sample OLTP Database
AdventureWorks2016 MS_Description AdventureWorks 2016 Sample OLTP Database

Resources