How to add a column to an existing CSV row in PowerShell? - powershell

I'm trying to write a simple usage logger into my script that would store information about the time when user opened the script, finished using the script and the user name.
The first part of the logger where I gather the first two data works fine and adds two necessary columns with values to the CSV file. Yet when I run the second part of the logger it does not add a new column to my existing CSV file.
#Code I will add at the very beginning of my script
$FileNameDate = Get-Date -Format "MMM_yyyy"
$FilePath = "C:\Users\Username\Desktop\Script\Logs\${FileNameDate}_MonthlyLog.csv"
$TimeStamp = (Get-Date).toString("dd/MMM/yyyy HH:mm:ss")
$UserName = [string]($env:UserName)
$LogArray = #()
$LogArrayDetails = #{
Username = $UserName
StartDate = $TimeStamp
}
$LogArray += New-Object PSObject -Property $LogArrayDetails | Export-Csv $FilePath -Notypeinformation -Append
#Code I will add at the very end of my script
$logArrayFinishDetails = #{FinishDate = $TimeStamp}
$LogCsv = Import-Csv $FilePath | Select Username, StartDate, #{$LogArrayFinishDetails} | Export-Csv $FilePath -NoTypeInformation -Append
CSV file should look like this when the script is closed:
Username StartDate FinishDate
anyplane 08/Apr/2018 23:47:55 08/Apr/2018 23:48:55
Yet it looks like this:
StartDate Username
08/Apr/2018 23:47:55 anyplane
The other weird thing is that it puts the StartDate first while I clearly stated in $LogArrayDetails that Username goes first.

Assuming that you only ever want to record the most recent run [see bottom if you want to record multiple runs] (PSv3+):
# Log start of execution.
[pscustomobject] #{ Username = $env:USERNAME; StartDate = $TimeStamp } |
Export-Csv -Notypeinformation $FilePath
# Perform script actions...
# Log end of execution.
(Import-Csv $FilePath) |
Select-Object *, #{ n='FinishDate'; e={ (Get-Date).toString("dd/MMM/yyyy HH:mm:ss") } } |
Export-Csv -Notypeinformation $FilePath
As noted in boxdog's helpful answer, using -Append with Export-Csv won't add additional columns.
However, since you're seemingly attempting to rewrite the entire file, there is no need to use
-Append at all.
So as to ensure that the old version of the file has been read in full before you attempt to replace it with Export-Csv, be sure to enclose your Import-Csv $FilePath call in (...), however.
This is not strictly necessary with a 1-line file such as in this case, but a good habit to form for such rewrites; do note that this approach is somewhat brittle in general, as something could go wrong while rewriting the file, resulting in potential data loss.
#{ n='FinishDate'; e={ (Get-Date).toString("dd/MMM/yyyy HH:mm:ss") } is an example of a calculated property/column that is appended to the preexisting columns (*)
The other weird thing is that it puts the StartDate first while I clearly stated in $LogArrayDetails that Username goes first.
You've used a hashtable (#{ ... }) to declare the columns for the output CSV, but the order in which a hashtable's entries are enumerated is not guaranteed.
In PSv3+, you can use an ordered hashtable instead ([ordered] #{ ... }) to achieve predictable enumeration, which you also get if you convert the hashtable to a custom object by casting to [pscustomobject], as shown above.
If you do want to append to the existing file, you can use the following, but note that:
this approach does not scale well, because the entire log file is read into memory every time (and converted to objects), though limiting the entries to a month's worth should be fine.
as stated, the approach is brittle, as things can go wrong while rewriting the file; consider simply writing 2 rows per execution instead, which allows you to append to the file line by line.
there's no concurrency management, so the assumption is that only ever one instance of the script is run at a time.
$FilePath = './t.csv'
$TimeStamp = (Get-Date).toString("dd/MMM/yyyy HH:mm:ss")
$env:USERNAME = $env:USER
# Log start of execution. Note the empty 'FinishDate' property
# to ensure all rows ultimately have the same column structure.
[pscustomobject] #{ Username = $env:USERNAME; StartDate = $TimeStamp; FinishDate = '' } |
Export-Csv -Notypeinformation -Append $FilePath
# Perform script actions...
# Log end of execution:
# Read the entire existing file...
$logRows = Import-Csv $FilePath
# ... update the last row's .FinishDate property
$logRows[-1].FinishDate = (Get-Date).toString("dd/MMM/yyyy HH:mm:ss")
# ... and rewrite the entire file, keeping only the last 30 entries
$logRows[-30..-1] | Export-Csv -Notypeinformation $FilePath

Because your CSV already has a structure (i.e. defined headers), PowerShell honours this when appending and doesn't add additional columns. It is (sort of) explained in this excerpt from the Export-Csv help:
When you submit multiple objects to Export-CSV, Export-CSV organizes
the file based on the properties of the first object that you submit.
If the remaining objects do not have one of the specified properties,
the property value of that object is null, as represented by two
consecutive commas. If the remaining objects have additional
properties, those property values are not included in the file.
You could include the FinishDate property in the original file (even though it would be empty), but the best option might be to export your output to a different CSV at the end, perhaps deleting the original after import then recreating it with the additional data. In fact, just removing the -Append will likely give the result you want.

Related

Parse MDT Log using PowerShell

I am trying to setup a log which would pull different information from another log file to log assets build by MDT using PowerShell. I can extract a line of log using simple get-content | select-string to get the lines i need so output looks like that
[LOG[Validate Domain Credentials [domain\user]]LOG]!
time="16:55:42.000+000" date="10-20-2017" component="Wizard"
context="" type="1" thread="" file="Wizard"
and I am curious if there is a way of capturing things like domain\user, time and date in a separate variables so it can be later passed with another data captured in a similar way in output file in a single line.
This is how you could do it:
$line = Get-Content "<your_log_path>" | Select-String "Validate Domain Credentials" | select -First 1
$regex = '\[(?<domain>[^\\[]+)\\(?<user>[^]]+)\].*time="(?<time>[^"]*)".*date="(?<date>[^"]*)".*component="(?<component>[^"]*)".*context="(?<context>[^"]*)".*type="(?<type>[^"]*)".*thread="(?<thread>[^"]*)".*file="(?<file>[^"]*)"'
if ($line -match $regex) {
$user = $Matches.user
$date = $Matches.date
$time = $Matches.time
# ... now do stuff with your variables ...
}
You might want to build in some error checking etc. (e.g. when no line is found or does not match etc.)
Also you could greatly simplify the regex if you only need those 3 values. I designed it so that all fields from the line are included.
Also, you could convert the values into more appropriate types, which (depending on what you want to do with them afterwards) might make handling them easier:
$type = [int]$Matches.type
$credential = New-Object System.Net.NetworkCredential($Matches.user, $null, $Matches.domain)
$datetime = [DateTime]::ParseExact(($Matches.date + $Matches.time), "MM-dd-yyyyHH:mm:ss.fff+000", [CultureInfo]::InvariantCulture)

CSV input, powershell pulling $null value rows from targeted column

I am trying to create a script to create Teams in Microsoft Teams from data in a CSV file.
The CSV file has the following columns: Team_name, Team_owner, Team_Description, Team_class
The script should grab Team_name row value and use that value to create a variable. Use that variable to query if it exists in Teams and if not, create it using the data in the other columns.
The problem I am having is my foreach loop seems to be collecting rows without values. I simplified the testing by first trying to identify the values and monitoring the output.
Here is the test script
$Team_infocsv = Import-csv -path $path Teams_info.csv
# $Team_infocsv | Foreach-object{
foreach($line in $Team_infocsv){
$owner = $line.Team_owner
Write-Host "Team Owner: $owner"
$teamname = $line.Team_name
Write-Host "Team Name: $teamname"
$team_descr = $line.Team_Description
Write-Host "Team Description: $team_descr"
$teamclass = $line.Team_class
Write-Host "Team Class: $teamclass"
}
I only have two rows of data but yet returned are the two lines as requested with extra output (from rows) without values.
There's no problem with your code per se, except:
Teams_info.csv is specified in addition to $path after Import-Csv -Path, which I presume is a typo, however.
$path could conceivably - and accidentally - be an array of file paths, and if the additional file(s) has entirely different columns, you'd get empty values for the first file's columns.
If not, the issue must be with the contents of Teams_info.csv, so I suggest you examine that; piping to Format-Custom as shown below will also you help you detect the case where $path is unexpectedly an array of file paths:
Here's a working example of a CSV file resembling your input - created ad hoc - that you can compare to your input file.
# Create sample file.
#'
"Team_name","Team_owner","Team_Description","Team_class"
"Team_nameVal1","Team_ownerVal1","Team_DescriptionVal1","Team_classVal1"
"Team_nameVal2","Team_ownerVal2","Team_DescriptionVal2","Team_classVal2"
'# > test.csv
# Import the file and examine the objects that get created.
# Note the use of Format-Custom.
Import-Csv test.csv test.csv | Format-Custom
The above yields:
class PSCustomObject
{
Team_name = Team_nameVal1
Team_owner = Team_ownerVal1
Team_Description = Team_DescriptionVal1
Team_class = Team_classVal1
}
class PSCustomObject
{
Team_name = Team_nameVal2
Team_owner = Team_ownerVal2
Team_Description = Team_DescriptionVal2
Team_class = Team_classVal2
}
Format-Custom produces a custom view (a non-table and non-list view) as defined by the type of the instances being output; in the case of the [pscustomobject] instances that Import-Csv outputs you get the above view, which is a convenient way of getting at least a quick sense of the objects' content (you may still have to dig deeper to distinguish empty strings from $nulls, ...).

Read a CSV in powershell with a variable number of columns

I have a CSV that contains a username, and then one or more values for the rest of the record. There are no headers in the file.
joe.user,Accounting-SG,CustomerService-SG,MidwestRegion-SG
frank.user,Accounting-SG,EastRegion-SG
I would like to read the file into a powershell object where the Username property is set to the first column, and the Membership property is set to either the remainder of the row (including the commas) or ideally, an array of strings with each element containing a single membership value.
Unfortunately, the following line only grabs the first membership and ignores the rest of the line.
$memberships = Import-Csv -Path C:\temp\values.csv -Header "username", "membership"
#{username=joe.user; membership=Accounting-SG}
#{username=frank.user; membership=Accounting-SG}
I'm looking for either of these outputs:
#{username=joe.user; membership=Accounting-SG,CustomerService-SG,MidwestRegion-SG}
#{username=frank.user; membership=Accounting-SG,EastRegion-SG}
or
#{username=joe.user; membership=string[]}
#{username=frank.user; membership=string[]}
I've been able to get the first result by enclosing the "rest" of the data in the csv file in quotes, but that doesn't really feel like the best answer:
joe.user,"Accounting-SG,CustomerService-SG,MidwestRegion-SG"
Well, the issue is that what you have isn't really a (proper) CSV. The CSV format doesn't support that notation.
You can "roll your own" and just process the file yourself, something like this:
$memberships = Get-Content -LiteralPath C:\temp\values.csv |
ForEach-Object -Process {
$user,$membership = $_.Split(',')
New-Object -TypeName PSObject -Property #{
username = $user
membership = $membership
}
}
You could do a half and half sort of thing. Using your modification, where the groups are all a single field in quotes, do this:
$memberships = Import-Csv -Path C:\temp\values.csv -Header "username", "membership" |
ForEach-Object -Process {
$_.membership = $_.membership.Split(',')
$_
}
The first example just reads the file line by line, splits on commas, then creates a new object with the properties you want.
The second example uses Import-Csv to create the object initially, then just resets the .membership property (it starts as a string, and we split the string so it's now an array).
The second way only makes sense if whatever is creating the "CSV" can create it that way in the first place. If you have to modify it yourself every time, just skip this and process it as it is.

String matching in PowerShell

I am new to scripting, and I would like to ask you help in the following:
This script should be scheduled task, which is working with Veritas NetBackup, and it creates a backup register in CSV format.
I am generating two source files (.csv comma delimited):
One file contains: JobID, FinishDate, Policy, etc...
The second file contains: JobID, TapeID
It is possible that in the second file there are multiple same JobIDs with different TapeID-s.
I would like to reach that, the script for each line in source file 1 should check all of the source file 2 and if there is a JobID match, if yes, it should have the following output:
JobID,FinishDate,Policy,etc...,TapeID,TapeID....
I have tried it with the following logic, but sometimes I have no TapeID, or I have two same TapeID-s:
Contents of sourcefile 1 is in $BackupStatus
Contents of sourcefile 2 is in $TapesUsed
$FinalReport =
foreach ($FinalPart1 in $BackupStatus) {
write-output $FinalPart1
$MediaID =
foreach ($line in $TapesUsed){
write-output $line.split(",")[1] | where-object{$line.split(",")[0] -like $FinalPart1.split(",")[0]}
}
write-output $MediaID
}
If the CSV files are not huge, it is easier to use Import-Csv instead of splitting the files by hand:
$BackupStatus = Import-Csv "Sourcefile1.csv"
$TapesUsed = Import-Csv "Sourcefile2.csv"
This will generate a list of objects for each file. You can then compare these lists quite easily:
Foreach ($Entry in $BackupStatus) {
$Match = $TapesUsed | Where {$_.JobID -eq $Entry.JobID}
if ($Match) {
$Output = New-Object -TypeName PSCustomObject -Property #{"JobID" = $Entry.JobID ; [...] ; "TapeID" = $Match.TapeID # replace [...] with the properties you want to use
Export-Csv -InputObject $Output -Path <OUTPUTFILE.CSV> -Append -NoTypeInformation }
}
This is a relatively verbose variant, but I prefer it like this.
I am checking for each entry in the first file whether there is a matching entry in the second. If there is one I combine the required fields from the entry of the first list with the ones from the entry in the second list into one object that I can then export very comfortably using Export-Csv.

Working with CSV - DataSplits

I have a script written that pulls a number of CSV files from an FTP server and downloads to a network location.
The content of this CSV file follows the example I have provided in this link
File Example
In short working with this file I need to:
Using the 12 characters (alpha-numeric) which follow Ords: on line two define a variable which will used later in a query. (A)
GB0000000001
Would become
$OrderVariable = "GB0000000001"
I have read about
.TrimStart([Characters_to_remove])
but am unsure how it would skip the first row and then how I would remove everything following the next 12 letters.
Using the entire line two information excluding the Ords: define this as a variable e.g.
GB0000000001 – Promotion Event
would become
$TitleEvent = "GB0000000001 – Promotion Event"
The CSV contains all the customers that an email needs to be sent to e.g.
D|300123123|BBA
D|300321312|DDS
D|A0123950|BBA
D|A0999950|ZZG
These items I would expect to be written into a hashtable which I thought would be simple enough except I cannot find any way to exclude everything which precedes it.
$mytable = Import-Csv -Path $filePath -Header D,Client,Suffix
$HashTable = #{}
foreach ($r in $mytable) {
$HashTable[$r.Client] = $r.Data
}
UPDATE
I have managed to get most of this element into a variable with the following
$target = "\\Messaging"
cd $target
$Clients = Import-Csv example.txt | where {$_ -like "*D|*"}
$Clients = $Clients[1..($Clients.count - 1)]
$Clients | Export-Csv "Test.csv" -NoTypeInformation
But I cannot get it to import with custom headers or without the first "H|" delimitation...
End of update 1
I believe this is roughly what is going to be required as the only element that I will need to define and use in a later query is the Client themselves.
The next would define all the text that remains as the message content
This is a Promotion Event and action needs to be taken by you. The
deadline for your instruction is 2pm on 12 September 2016.
The deadline for this event has been extended.
To notify us of your instruction you can send a secure message.
This can differ on each occasion massively so cannot simply be a removal of X numbers of lines the content will always follow the Ords: (line two) and End at the start for the D| delimitation.
Most of the other code I need to put together I am 'fairly confident' with (famous last words) and have a fully working script that is pulling the files I need, I am just not great at working with .csv's when I have them.
The data format is flexible without a global table/grid structure so let's use regexps (the breakdown), which is quite a universal method of parsing such texts.
$text = [IO.File]::ReadAllText('inputfile.txt', [Text.Encoding]::UTF8)
$data = ([regex]('ORDS: (?<order>.+?) [-–—] (?<title>.+)[\r\n]+' +
'(?<info>[\s\S]+?)[\r\n]+' +
'(?<clients>D\|[\s\S]+?)[\r\n]+' +
'T\|(?<T>\d+)')
).Matches($text) |
forEach {
$g = $_.groups
#{
order = $g['order'].value
info = $g['info'].value -join ' '
clients = $g['clients'].value -split '[\r\n]+' |
where { $_ -match 'D\|(.+?)\|(.+)' } |
forEach {
#{
id = $matches[1]
suffix = $matches[2]
}
}
T = $g['T']
}
}
$data is now a record (or an array of records if the file has multiple entries):
Name Value
---- -----
T 000004
info This is a Promotion Event and action needs to be take...
order GB0000000001
clients {System.Collections.Hashtable, System.Collections.Has...
$data.clients is an array of records:
Name Value
---- -----
id 300123123
suffix BBA
id 300321312
suffix DDS
id A0123950
suffix BBA
id A0999950
suffix ZZG