Adding a column to a datatable in powershell - powershell

I am trying to add a column to data I have imported (and will export) as a CSV.
I am importing a CSV:
What I want to do add another column, perhaps "10/15/22" when the process runs, and then update the values under that date.
In effect, the document will grow to the right, adding a column each time it is run.
I have an object called $test. It will have values like:
$test.users = "7"
$test.SomeBSValue = "22"
$test.Computers = "52"
When all is said and done, my output should look like:
Adding to the list any values I have that are not part of the list already, but recording the values I have under the heading for the date.
So, if the script is run and collects 100 data points, those data point would all be in the CSV under the date.
I would have thought this would be easy, but now I am drawing a complete blank.
I've considered (but have not coded) even trying to put into a GUI grid view and then reading the data back and writing the CSV (but there should be an easier way, right?)

Since you don't actually use it as a CSV we can treat it like regular content.
Say we have a file in C:\test called test.csv that looks as follows:
"Settings","08/15/22","09/15/22"
"Users",0,0
"Computers",0,1
"SomeValue1",0,2
"SomeValue2",0,2
"SomeValue3",0,2
"Stat1",0,10
"Stat2",7,0
"Stat3",0,0
"SomeBSValue",1,2
We can import it, add the row from the object to each corresponding row and right the file to test2.csv.
$test = #{
Settings = "10/15/22"
users = "7"
Computers = "52"
SomeValue1 = "22"
SomeValue2 = "24"
SomeValue3 = "25"
Stat1 = "4"
Stat2 = "3"
Stat3 = "2"
SomeBSValue = "1"
}
$content = Get-Content "C:\test\test.csv"
$newContent = #()
foreach($row in $content){
foreach($key in $test.Keys){
if($row -like "*$key*"){
$row = $row + "," + $test."$key"
$newContent += $row
}
}
}
$newContent | Out-File "C:\test\test2.csv"
After running the script it will have added the values from the object:
"Settings","08/15/22","09/15/22",10/15/22
"Users",0,0,7
"Computers",0,1,52
"SomeValue1",0,2,22
"SomeValue2",0,2,22
"SomeValue3",0,2,22
"Stat1",0,10,4
"Stat2",7,0,4
"Stat3",0,0,4
Edit:
If you want the date between quotes, replace $row = $row + "," + $test."$key" with this:
if($key -eq "Settings"){
$row = $row + "," + '"' + $test."$key" + '"'
}else{
$row = $row + "," + $test."$key"
}

This idea is pretty terrible idea, as you stated, "grow to the right" is definitely not a good approach and you should consider a better way of doing it, data should always expand vertically.
As for the solution, you can create new columns easily with Select-Object and dynamically generated calculated properties.
Note, this should definitely not be considered an efficient approach. This will be slow because Select-Object is slow.
function Add-Column {
param(
[Parameter(ValueFromPipeline, DontShow, Mandatory)]
[object] $InputObject,
[Parameter(Mandatory)]
[string] $ColumnName,
[Parameter(Mandatory)]
[string] $ReferenceProperty,
[Parameter(Mandatory)]
[hashtable] $Values
)
begin {
$calculatedProp = #{ N = $ColumnName }
}
process {
$calculatedProp['E'] = { 0 }
if($value = $InputObject.$ReferenceProperty) {
if($Values.ContainsKey($value)) {
$calculatedProp['E'] = { $Values[$value] }
}
}
$InputObject | Select-Object *, $calculatedProp
}
}
Usage
Import-Csv path\to\csv | Add-Column -ColumnName '09/15/22' -ReferenceProperty Settings -Values #{
users = "7"
SomeBSValue = "22"
Computers = "52"
}
Result
Settings 08/15/22 09/15/22
-------- -------- --------
Users 0 7
Computers 0 52
SomeValue1 0 0
SomeValue2 0 0
SomeValue3 0 0
Stat1 0 0
Stat2 7 0
Stat3 0 0
SomeBSValue 1 22
This function allows then pipe into Export-Csv at ease:
Import-Csv path\to\csv | Add-Column ... | Export-Csv path\to\newCsv

Related

Iterating over multiple find and replace object

I know the solution below is quite simple but I am not able to get it to work. I created an object with a find and replace string property. The goal to replace all strings in the text file but it is not working properly. The code below would only replace the last object in the list. I've searched several stack over flow questions but was only able to find multiple files, not multiple find/replace strings.
Input File:
TEMPERATURE 1
TEMPERATURE 2
AVERAGE 1
AVERAGE 2
Expected Output:
TEMP 1
TEMP 2
AVG 1
AVG 2
Actual Output:
TEMP 1
TEMP 2
AVERAGE 1
AVERAGE 2
class FindReplace {
[string]$FindString;
[string]$ReplaceString;
}
[System.Collections.Generic.List[FindReplace]]$FindReplaceList = #()
$Obj1 = New-Object FindReplace
$Obj1.FindString = "AVERAGE"
$Obj1.ReplaceString = "AVG"
$Obj2 = New-Object FindReplace
$Obj2.FindString = "TEMPERATURE"
$Obj2.ReplaceString = "TEMP"
$FindReplaceList.Add($Obj1);
$FindReplaceList.Add($Obj2);
write-host $FindReplaceList.Count
for($i = 0; $i -lt $FindReplaceList.Count; $i++)
{
Write-Host "Replacing string" $FindReplaceList[$i].FindString "to" $FindReplaceList[$i].ReplaceString " where input file:" $inputTextFilePath "and output file:" $outputTextFilePath
(Get-Content $inputTextFilePath).Replace($FindReplaceList[$i].FindString, $FindReplaceList[$i].ReplaceString) | Set-Content $outputTextFilePath
}
As #AdminOfThings mentioned in the comments. The problem is that you are reading the file once, and then writing twice. So you don't end up with all of the replacements like you are expecting.
See below. I've done a little bit of cleanup, but this is what you could do...
I'm reading the contents of the file into memory once, storing it as a variable. Then performing the replacements on the variable, and then writing the contents of the variable to file:
$inputTextFilePath = 'D:\StackOverflow\fart\test.txt'
$outputTextFilePath = 'D:\StackOverflow\fart\test_output.txt'
class FindReplace {
[string]$FindString;
[string]$ReplaceString;
}
[System.Collections.Generic.List[FindReplace]]$FindReplaceList = #()
$Obj1 = New-Object FindReplace
$Obj1.FindString = "AVERAGE"
$Obj1.ReplaceString = "AVG"
$Obj2 = New-Object FindReplace
$Obj2.FindString = "TEMPERATURE"
$Obj2.ReplaceString = "TEMP"
$FindReplaceList.Add($Obj1);
$FindReplaceList.Add($Obj2);
write-host $FindReplaceList.Count
$fileContent = Get-Content $inputTextFilePath;
foreach ($item in $FindReplaceList)
{
Write-Host "Replacing string $($item.FindString) to $($item.ReplaceString) where input file: $inputTextFilePath and output file: $outputTextFilePath";
$fileContent = $fileContent.Replace($item.FindString, $item.ReplaceString);
}
Set-Content -Value $fileContent -Path $outputTextFilePath;
And of course, since "[F]ind [A]nd [R]eplace [T]ext" questions only come up every so often...I had to make use of that fancy acronym :)

comparision of 2 csv files using powershell

The format of two files is same and as follows:
ServiceName Status computer State
AdobeARMservice OK NEE Running
Amazon Assistan OK NEE Running
the requirement is, i have to check the service name and computer name..if both are same, then i have to check whether the state of particular service is same in both the files or not. And if it is not same then display it..
$preser = import-csv C:\info.csv
$postser = import-csv C:\serviceinfo.csv
foreach($ser1 in $preser)
{
foreach($ser2 in $postser)
{
if(($ser1.computer -eq $ser2.computer) -and ($ser1.ServiceName -eq $ser2.ServiceName))
{
if($ser1.State -eq $ser2.State)
{
}
else
{
write-host $ser1,$ser2
}
}
}
}
This code is working fine but as the files length is very large, the time of execution is more.
Is there any alternative method to reduce the time of execution..?
Thank you
Although Import-Csv on very large files will take its time, maybe this will be faster:
$preser = Import-Csv -Path 'C:\info.csv'
$postser = Import-Csv -Path 'C:\serviceinfo.csv'
# build a lookup Hashtable for $preser
$hash = #{}
foreach ($item in $preser) {
# combine the ServiceName and Computer to form the hash key
$key = '{0}#{1}' -f $item.ServiceName, $item.computer
$hash[$key] = $item
}
# now loop through the items in $postser
foreach ($item in $postser) {
$key = '{0}#{1}' -f $item.ServiceName, $item.computer
if ($hash.ContainsKey($key)) {
if ($hash[$key].State -ne $item.State) {
# create a new object for output
$out = $hash[$key] | Select-Object * -ExcludeProperty State
$out | Add-Member -MemberType NoteProperty -Name 'State in Preser' -Value $hash[$key].State
$out | Add-Member -MemberType NoteProperty -Name 'State in Postser' -Value $item.State
$out
}
}
}
The output on screen will look something like this:
ServiceName : AdobeARMservice
Status : OK
computer : NEE
State in Preser : Running
State in Postser : Stopped
Of course, you can capture this output and save it as new csv if you do
$result = foreach ($item in $postser) {
# rest of the above foreach loop
}
# output on screen
$result
# output to new csv
$result | Export-Csv -Path 'C:\ServiceInfoDifference.csv' -NoTypeInformation
There are a few ways to do this:
1. Sorting the columns
If the columns are unsorted in the files, sort them first, and then try finding a match by using linear search.
2. Binary search
What you are currently doing is an implementation of a linear search. You can implement binary search (works best on sorted lists) to find a result faster.
Taken from dfinkey's github repo
function binarySearch {
param($sortedArray, $seekElement, $comparatorCallback)
$comparator = New-Object Comparator $comparatorCallback
$startIndex = 0
$endIndex = $sortedArray.length - 1
while ($startIndex -le $endIndex) {
$middleIndex = $startIndex + [Math]::floor(($endIndex - $startIndex) / 2)
# If we've found the element just return its position.
if ($comparator.equal($sortedArray[$middleIndex], $seekElement)) {
return $middleIndex
}
# Decide which half to choose for seeking next: left or right one.
if ($comparator.lessThan($sortedArray[$middleIndex], $seekElement)) {
# Go to the right half of the array.
$startIndex = $middleIndex + 1
}
else {
# Go to the left half of the array.
$endIndex = $middleIndex - 1
}
}
return -1
}
3. Hashes
I am not completely sure of this method, but, you can load the columns into hashes and then compare them. Hash comparisons are generally faster than array comparisons.

Parsing and modifying a powershell object

I'm parsing HTML from a webserver (specifically a Fanuc controller) and assigning the innerText to a object.
#Make sure the controller respons
if ($webBody.StatusCode -eq 200) {
Write-Host "Response is Good!" -ForegroundColor DarkGreen
$preBody = $webBody.ParsedHtml.body.getElementsByTagName('PRE') | Select -ExpandProperty innerText
$preBody
}
The output looks a little like so:
[1-184 above]
[185] = 0 ''
[186] = 0 ''
[187] = 0 ''
[188] = 0 ''
[189] = 0 ''
[and so on]
I only want to read the data from 190, 191, 193 for example.
What's the best way to do this? I'm struggling to sanitize the unwanted data in the object.
Currently I have a vbscript app that outputs to a txt file, cleans the data then reads it back and manipulates it in to a sql insert. I'm trying to improve on it with powershell and keen to try and keep everything within the program if possible.
Any help greatly appreciated.
With the assumption that the data set is not too large to place everything into memory. You could parse with regex into a PowerShell Object, then you can use Where-Object to filter.
#Regex with a capture group for each important value
$RegEx = "\[(.*)\]\s=\s(\d+)\s+'(.*)'"
$IndexesToMatch = #(190, 191, 193)
$ParsedValues = $prebody.trim | ForEach-Object {
[PSCustomObject]#{
index = $_ -replace $regex,'$1'
int = $_ -replace $regex,'$2'
string = $_ -replace $regex,'$3'
}
}
$ParsedValues | Where-Object { $_.index -in $IndexesToMatch }
Input :
[190] = 1 'a'
[191] = 2 'b'
[192] = 3 'c'
[193] = 4 'd'
[194] = 5 'e'
Output :
index int string
----- --- ------
190 1 a
191 2 b
193 4 d

Powershell fixed width export

I am having a text file wich uses fixed width for separating columns.
I'm loading the file and create a new column which concatinates the values of the first two columns.
The problem I have that when exporting the data I need to define a fixed column width of 13 for Column C.
Column A (3) Column B(9) Column C(13)
MMA 12345 12345_MMA
MMO 987222 987222_MMO
Basically for this example in the export I am missing 4 spaces for the first row and 3 for the second row.
Thisis my current code, which also includes a new row for MD5 creation.
# Load input data
$PreSystem = [IO.File]::ReadAllText("C:\FILE.txt")
# Initiate md5-hashing
$md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
$utf8 = new-object -TypeName System.Text.UTF8Encoding
# Split input data by lines
$all = $PreSystem.split("`n")
# Loop over lines
for($i = 0; $i -lt $all.length-1; $i += 1) {
# Access distinct lines
$entry = "$($all[$i])"
# Get the different parameters
$market_code = $entry.substring(1,3)
$soc = $entry.substring(4,9)
# Hash the SOC element
$hash = [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($soc)))
# Create desired format for each entry
$output = $hash.Replace("-","")+$soc.Replace(" ","") + "_" + $market_code + $all[$i]
# Write to file
"$output" | Out-File -Filepath C:\"C:\FILE.txt" -Append -encoding ASCII
}
Thanks in advance
You can create a custom table format using the tip explained here. Here is an example for Get-Process:
$a = #{Expression={$_.Name};Label="Process Name";width=25}, `
#{Expression={$_.ID};Label="Process ID";width=15}, `
#{Expression={$_.MainWindowTitle};Label="Window Title";width=40}
Get-Process | Format-Table $a
Basically, you build an expression through wich Format-Table will pipe
each row. Instead of taking care of the formating yourself for each row, you build a hash and pipe it through Format-Table.
It's still not quite clear to me what output you actually want to achieve, but maybe this will give you some idea.
One of the most convenient ways to get formatted string output is using the format operator (-f). You specify a format string with placeholders in curly brackets, and fill it with the values of an array:
PS C:\> '_{0}:{1}:{2}_' -f 'foo', 'bar', 'baz'
_foo:bar:baz_
Column widths can be specified in the format string like this:
PS C:\> '_{0,-5}:{1,7}:{2,-9}_' -f 'foo', 'bar', 'baz'
_foo : bar:baz _
As you can see, negative values align the column to the left, positive values align it to the right.
If there's a chance that a value is too long for the give column width you need to truncate it, e.g. with the Substring() method:
PS C:\> $s = 'barbarbar'
PS C:\> $len = [math]::Min(7, $s.Length)
PS C:\> '_{0,-5}:{1,7}:{2,-9}_' -f 'foo', $s.Substring(0, $len), 'baz'
_foo :barbarb:baz _
You can quickly have a fixed size left-aligned content string using the following code:
Write-Host "$myvariable $(" " * 60)".Substring(0,60)
this will give you a fixed width of 60 characters with the contents aligned to the left
One of the solutions is for each of the rows use this mechanism when concatenating:
$a = "MMA"
$b = "12345"
$str = "$($b)_$($a)"
if (($str.Length) -ge 13 ) {
Write-Host "$($str)"
} else {
$padStr = " " * (13 - ($str.Length))
Write-Host "$($str)$($padStr)"
}
So instead of Write-Host CmdLet you can use the appropriate CmdLet for your purpose.
Edit, after adding actual code. So the above logic would translate into:
$market_code = $entry.subString(1,3)
$soc = $entry.subString(4,9)
$str = $soc.Replace(" ", "") + "_" + $market_code
if (($str.Length) -ge 13 ) {
$output = $hash.Replace("-","") + $str + $all[$i]
} else {
$padStr = " " * (13 - ($str.Length))
$output = $hash.Replace("-","") + $str + $padStr + $all[$i]
}
You can do fixed size using next code:
$data = "Some text"
$size = 20
$str = [string]::new(' ',$size).ToCharArray()
$data.CopyTo(0,$str,0,$data.Length)
$str = $str -join ''

How to create ForEach loop to go through multiple variables Powershell

I've got a problem with ForEach loop. Im trying to loop through multiple variables of same kind just increment different.
Im trying to change the TextBox Text depending on if Label from same row has text.
This is how I could make it to just write and IF sentence for each Label but I was looking a way to loop each of these blocks through ForEach loop. I've got total of 8 Labels and Textboxes.
Here is the code: ( Im sure you'll figure out what I'm after :) )
IF ( $Label1.Text.Length -ne 0 )
{
$Label1.Visible = $true
$TextBox1.Visible = $true
$TextBox1.Text = ( "Enter new name for " + $Label1.Text )
}
example of ForEach
$Count = 1..8
$Count | ForEach-Object {
IF ( $Label($_).Text.Length -ne 0 )
{
$Label($_).Visible = $true
$TextBox($_).Visible = $true
$TextBox($_).Text = ( "Enter new name for " + $Label($_).Text )
}
}
etc...
I tried putting variables in array and loop through that way but ofcourse array changes the type to string and it doesnt work...
Give this a try, I can't test it using label & textbox object but it can work tuning it better:
1..8 | ForEach-Object {
IF ( (iex "`$Label$_.Text.Length") -ne 0 )
{
iex "`$Label$_.Visible = `$true"
iex "`$TextBox$_.Visible = `$true"
iex "`$TextBox$_.Text = 'Enter new name for ' + `$Label$_.Text"
}
}
You can use the Get-Variable cmdlet for that purpose:
1..8 | ForEach-Object {
if ( (Get-Variable "Label$_").Value.Text.Length -ne 0 )
{
(Get-Variable "Label$_").Value.Visible = $true
(Get-Variable "Label$_").Value.Visible = $true
(Get-Variable "Label$_").Value.Text = ( "Enter new name for " + (Get-Variable "Label$_").Value.Text )
}
}