Calculate time differences in log file using Powershell based on linux code - powershell

EDITED:
Text1.txt:
123.456.789.189:12345
222.222.222.444:56789
451.200.111.321:55555
333.333.333.111:11223
I want to compare ID with IP that weren't registered.
ERROR:
Exception calling "Add" with "2" argument(s): "Item has already been added. Key in dictionary: '123.456.789.189:12345' Key being added: '123.456.789.189:12345'" + $nameHash.Add( $data3[4], $data3[3] )
I think this error is due to the existence of duplicates.
How do I solve an issue with duplicates in Hash Table?
My function to calculate time takes in a startdate and an end date.
Function calTimeDiff( $StartDate, $EndDate )
{
"which is = " + (NEW-TIMESPAN –Start $StartDate –End $EndDate).Hours + " hours, " +
(NEW-TIMESPAN –Start $StartDate –End $EndDate).Minutes + " minutes, " +
(NEW-TIMESPAN –Start $StartDate –End $EndDate).Seconds + " seconds, " +
"diff = " + (NEW-TIMESPAN –Start $StartDate –End $EndDate).TotalSeconds + " sec"
}
$lines1 = Get-Content "C:\Temp\Text1.txt" | Select-Object -Unique
$lines2 = Get-Content "C:\Temp\Text2.txt" | Select-Object -Unique
ForEach( $line2 in $lines2 )
{
$list = ( $date, $time, $client, $clientIP )
$list = $line2.Split( "" )
ForEach( $line1 in $lines1 )
{
$disconnectIP = $line1
If( $disconnectIP -match $list[3] )
{
$date = $list[0]
$time = $list[1]
$client = $list[2]
$clientIP = $list[3]
If( $client -eq "serviceClient" )
{
$start = $date + " " + $time
}
If( $client -eq "Unregistered" )
{
$end = $date + " " + $time
}
calTimeDiff $start $end
}
}
}

How about something along these lines? I think it's basically behaving the way you were asking for (although you might want to tweak the display-span function a bit...)
#requires -Version 3
function parse-log
{
param(
[string]$line
)
$data = $line.split(' ')
$dateString = '{0} {1}' -f $data[0], $data[1]
$timeStamp = Get-Date -Date $dateString
[pscustomobject]#{
TimeStamp = $timeStamp
Client = $data[2]
IPAddress = $data[3]
}
}
function display-span
{
param(
$logSpan
)
'{0} ({1}) ==> {2}' -f $logSpan.IPAddress, $nameHash.Get_Item( $logSpan.IPAddress), $logSpan.Start
'{0} ({1}) ==> {2}' -f $logSpan.IPAddress, $nameHash.Get_Item( $logSpan.IPAddress), $logSpan.End
'Start = {0}, End = {1}, diff = {2}' -f $logSpan.Start, $logSpan.End, $logSpan.TimeSpan
''
}
$ipStateHash = #{}
$nameHash = #{}
$logArray = #()
$lines1 = Get-Content -Path '.\Text1.txt'
$lines2 = Get-Content -Path '.\Text2.txt'
$lines3 = Get-Content -Path '.\Text3.txt'
# Build Name Hash
foreach( $line3 in $lines3 )
{
$data3 = $line3.Split( ' ' )
$nameHash.Add( $data3[4], $data3[3] )
}
foreach( $line2 in $lines2 )
{
$entry = parse-log -line $line2
switch( $entry.Client ) {
'serviceClient'
{
if( $lines1 -contains $entry.IPAddress )
{
if( $ipStateHash.ContainsKey( $entry.IPAddress ) -eq $false )
{
$ipStateHash.Add( $entry.IPAddress, $entry.TimeStamp )
}
}
}
'Unregistered'
{
if( $ipStateHash.ContainsKey( $entry.IPAddress ) -eq $true )
{
$start = $ipStateHash.Get_Item( $entry.IPAddress )
$ipStateHash.Remove( $entry.IPAddress )
$timespan = $entry.TimeStamp - $start
$logArray += [pscustomobject]#{
IPAddress = $entry.IPAddress
Start = $start
End = $entry.TimeStamp
TimeSpan = $timespan
}
}
}
}
}
$logArray | ForEach-Object -Process {
display-span -logSpan $_
}
"IPs that weren't Unregistered:"
$ipStateHash.GetEnumerator() | Sort-Object -Property TimeStamp | ForEach-Object -Process {
'{0} ==> {1}' -f $nameHash.Get_Item( $_.Key ), $_.Value
}
Using your updated data files from above, the script outputs:
123.456.789.189:12345 (BOB) ==> 7/29/2015 6:00:13 AM
123.456.789.189:12345 (BOB) ==> 7/29/2015 6:00:19 AM
Start = 7/29/2015 6:00:13 AM, End = 7/29/2015 6:00:19 AM, diff = 00:00:06
222.222.222.444:56789 (ALICE) ==> 7/29/2015 6:00:18 AM
222.222.222.444:56789 (ALICE) ==> 7/29/2015 6:00:22 AM
Start = 7/29/2015 6:00:18 AM, End = 7/29/2015 6:00:22 AM, diff = 00:00:04
451.200.111.321:55555 (TOM) ==> 7/29/2015 6:20:03 AM
451.200.111.321:55555 (TOM) ==> 7/29/2015 6:21:19 AM
Start = 7/29/2015 6:20:03 AM, End = 7/29/2015 6:21:19 AM, diff = 00:01:16
IPs that weren't Unregistered:
BOB ==> 7/29/2015 6:01:00 AM

Related

Making a box menu. How do I color/highlight the selection?

So I've pieced together a menu that contains a title and options you can select from. Selecting an item is done by using the arrow keys. I want to have the selected item highlighted so you can tell which item you are currently selecting. I'm new to PowerShell and am familiar as to how to change colors with write-host but in this example I am clueless. The line in question that I believe I need to inject the color options into is the one that starts with $Width. I'd really appreciate some insight! This is my last hiccup before beginning to add actual code!
Function Create-Menu (){
Param(
[Parameter(Mandatory=$True)][String]$MenuTitle,
[Parameter(Mandatory=$True)][array]$MenuOptions
)
$MaxValue = $MenuOptions.count-1
$Selection = 0
$EnterPressed = $False
Clear-Host
While($EnterPressed -eq $False){
For ($i=0; $i -le $MaxValue; $i++){
$Width = if($Title){$Length = $Title.Length;$Length2 = $MenuOptions|%{$_.length}|Sort -Descending|Select -First 1;$Length2,$Length|Sort -Descending|Select -First 1}else{$MenuOptions|%{$_.length}|Sort -Descending|Select -First 1}
$Buffer = if(($Width*1.5) -gt 78){(78-$width)/2}else{$width/4}
if($Buffer -gt 6){$Buffer = 6}
$MaxWidth = $Buffer*2+$Width+$($MenuOptions.count).length
$Menu = #()
$Menu += "╔"+"═"*$maxwidth+"╗"
if($MenuTitle){
$Menu += "║"+" "*[Math]::Floor(($maxwidth-$MenuTitle.Length)/2)+$MenuTitle+" "*[Math]::Ceiling(($maxwidth-$MenuTitle.Length)/2)+"║"
$Menu += "╟"+"─"*$maxwidth+"╢"
}
For($i=1;$i -le $MenuOptions.count;$i++){
$Item = "$i`. "
$Menu += "║"+" "*$Buffer+$Item+$MenuOptions[$i-1]+" "*($MaxWidth-$Buffer-$Item.Length-$MenuOptions[$i-1].Length)+"║"
}
$Menu += "╚"+"═"*$maxwidth+"╝"
$menu
}
$KeyInput = $host.ui.rawui.readkey("NoEcho,IncludeKeyDown").virtualkeycode
Switch($KeyInput){
13{
$EnterPressed = $True
$script:Selection = "$Selection"
Clear-Host
break
}
38{
If ($Selection -eq 0){
$Selection = $MaxValue
} Else {
$Selection -= 1
}
Clear-Host
break
}
40{
If ($Selection -eq $MaxValue){
$Selection = 0
} Else {
$Selection +=1
}
Clear-Host
break
}
Default{
Clear-Host
}
}
}
}
#MainMenu
Function MainMenu (){
Create-Menu -MenuTitle "Tool" -MenuOptions "Lookup","Prep","Tools","Settings","Cleanup and Exit"
If($script:Selection -eq 0) {Lookup}
If($script:Selection -eq 1) {PrepMenu}
If($script:Selection -eq 2) {ToolsMenu}
If($script:Selection -eq 3) {SettingsMenu}
If($script:Selection -eq 4) {CleanupAndExit}
}
MainMenu
pause
The code outputs the following:
╔═════════════════════════╗
║ Tool ║
╟─────────────────────────╢
║ 1. Lookup ║
║ 2. Prep ║
║ 3. Tools ║
║ 4. Settings ║
║ 5. Cleanup and Exit ║
╚═════════════════════════╝
You can add color in (I used Green, but that's up to you) with some minor adjustments to your code:
function Create-Menu {
Param(
[Parameter(Mandatory=$True)][String]$MenuTitle,
[Parameter(Mandatory=$True)][array]$MenuOptions
)
# test if we're not running in the ISE
if ($Host.Name -match 'ISE') {
Throw "This menu must be run in PowerShell Console"
}
$MaxValue = $MenuOptions.Count-1
$Selection = 0
$EnterPressed = $False
While(!$EnterPressed) {
# draw the menu
Clear-Host
for ($i = 0; $i -le $MaxValue; $i++){
[int]$Width = [math]::Max($MenuTitle.Length, ($MenuOptions | Measure-Object -Property Length -Maximum).Maximum)
[int]$Buffer = if (($Width * 1.5) -gt 78) { (78 - $width) / 2 } else { $width / 4 }
$Buffer = [math]::Min(6, $Buffer)
$MaxWidth = $Buffer * 2 + $Width + $MenuOptions.Count.ToString().Length
Write-Host ("╔" + "═" * $maxwidth + "╗")
# write the title if present
if (!([string]::IsNullOrWhiteSpace($MenuTitle))) {
$leftSpace = ' ' * [Math]::Floor(($maxwidth - $MenuTitle.Length)/2)
$rightSpace = ' ' * [Math]::Ceiling(($maxwidth - $MenuTitle.Length)/2)
Write-Host ("║" + $leftSpace + $MenuTitle + $rightSpace + "║")
Write-Host ("╟" + "─" * $maxwidth + "╢")
}
# write the menu option lines
for($i = 0; $i -lt $MenuOptions.Count; $i++){
$Item = "$($i + 1). "
$Option = $MenuOptions[$i]
$leftSpace = ' ' * $Buffer
$rightSpace = ' ' * ($MaxWidth - $Buffer - $Item.Length - $Option.Length)
$line = "║" + $leftSpace + $Item + $Option + $rightSpace + "║"
if ($Selection -eq $i) {
Write-Host $line -ForegroundColor Green
}
else {
Write-Host $line
}
}
Write-Host ("╚" + "═" * $maxwidth + "╝")
}
# wait for an accepted key press
do {
$KeyInput = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode
} while (13, 38, 40 -notcontains $KeyInput)
Switch($KeyInput){
13{
$EnterPressed = $True
return $Selection
}
38 {
$Selection--
if ($Selection -lt 0){ $Selection = $MaxValue }
break
}
40 {
$Selection++
if ($Selection -gt $MaxValue) { $Selection = 0 }
# or: $Selection = ($Selection + 1) % ($MaxValue + 1)
break
}
}
}
}
#MainMenu
function MainMenu {
$selected = Create-Menu -MenuTitle "Tools" -MenuOptions "Lookup","Prep","Tools","Settings","Cleanup and Exit"
switch ($selected) {
0 {"Lookup"}
1 {"PrepMenu"}
2 {"ToolsMenu"}
3 {"SettingsMenu"}
4 {"CleanupAndExit"}
}
}
MainMenu
Edit
Just for the fun of it, the above code uses Clear-Host before each redraw of the menu which results in a workable, but flickering menu.
Below the same menu, but this time it redraws without first clearing the console window resulting in a much smoother menu.
function Create-Menu {
Param(
[Parameter(Mandatory=$false)][string] $MenuTitle = $null,
[Parameter(Mandatory=$true)] [string[]]$MenuOptions
)
# test if we're not running in the ISE
if ($Host.Name -match 'ISE') {
Throw "This menu must be run in PowerShell Console"
}
$MaxValue = $MenuOptions.Count-1
$Selection = 0
$EnterPressed = $False
[console]::CursorVisible = $false # prevents cursor flickering
Clear-Host
while(!$EnterPressed) {
# draw the menu without Clear-Host to prevent flicker
[console]::SetCursorPosition(0,0)
for ($i = 0; $i -le $MaxValue; $i++){
[int]$Width = [math]::Max($MenuTitle.Length, ($MenuOptions | Measure-Object -Property Length -Maximum).Maximum)
[int]$Buffer = if (($Width * 1.5) -gt 78) { (78 - $width) / 2 } else { $width / 4 }
$Buffer = [math]::Min(6, $Buffer)
$MaxWidth = $Buffer * 2 + $Width + $MenuOptions.Count.ToString().Length
Write-Host ("╔" + "═" * $maxwidth + "╗")
# write the title if present
if (!([string]::IsNullOrWhiteSpace($MenuTitle))) {
$leftSpace = ' ' * [Math]::Floor(($maxwidth - $MenuTitle.Length)/2)
$rightSpace = ' ' * [Math]::Ceiling(($maxwidth - $MenuTitle.Length)/2)
Write-Host ("║" + $leftSpace + $MenuTitle + $rightSpace + "║")
Write-Host ("╟" + "─" * $maxwidth + "╢")
}
# write the menu option lines
for($i = 0; $i -lt $MenuOptions.Count; $i++){
$Item = "$($i + 1). "
$Option = $MenuOptions[$i]
$leftSpace = ' ' * $Buffer
$rightSpace = ' ' * ($MaxWidth - $Buffer - $Item.Length - $Option.Length)
$line = "║" + $leftSpace + $Item + $Option + $rightSpace + "║"
if ($Selection -eq $i) {
Write-Host $line -ForegroundColor Green
}
else {
Write-Host $line
}
}
Write-Host ("╚" + "═" * $maxwidth + "╝")
}
# wait for an accepted key press
do {
$KeyInput = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode
} while (13, 38, 40 -notcontains $KeyInput)
Switch($KeyInput){
13{
$EnterPressed = $True
[console]::CursorVisible = $true # reset the cursors visibility
return $Selection
}
38 {
$Selection--
if ($Selection -lt 0){ $Selection = $MaxValue }
break
}
40 {
$Selection++
if ($Selection -gt $MaxValue) { $Selection = 0 }
# or: $Selection = ($Selection + 1) % ($MaxValue + 1)
break
}
}
}
}
#MainMenu
function MainMenu {
$selected = Create-Menu -MenuTitle "Tools" -MenuOptions "Lookup","Prep","Tools","Settings","Cleanup and Exit"
switch ($selected) {
0 {"Lookup"}
1 {"PrepMenu"}
2 {"ToolsMenu"}
3 {"SettingsMenu"}
4 {"CleanupAndExit"}
}
}
MainMenu

How to handle looping to create file using powershell?

I have INI file and I want to get specific section. The items in the section that I choose are 24 items. I want to use all the item to write in a file. I tried this, It works, but it looks like bad way to write 24 times to do the process. Is there any other way to do that more beautiful? The section of my INI file like this
Input ini:
[Code]
A1=12,34,56
A2=23,45,67
A3=34,56,78,9,10
...
A24=a1,b2,c3,d4,e5
Script:
Function F_ML
{
$FilePath = "C:\Users\File.ini"
$section = "Code"
$R_1 = "A1"
$R_2 = "A2"
$R_3 = "A3"
$R_4 = "A4"
$R_5 = "A5"
$R_6 = "A6"
$R_7 = "A7"
$R_8 = "A8"
$R_9 = "A9"
$R_10 = "A10"
$R_11 = "A11"
$R_12 = "A12"
$R_13 = "A13"
$R_14 = "A14"
$R_15 = "A15"
$R_16 = "A16"
$R_17 = "A17"
$R_18 = "A18"
$R_19 = "A19"
$R_20 = "A20"
$R_21 = "A21"
$R_22 = "A22"
$R_23 = "A23"
$R_24 = "A24"
$store = "C:\Users\"
$FilePath
$input_file = $FilePath
$ini_file = #{}
Get-Content $input_file | ForEach-Object {
$_.Trim()
} | Where-Object {
$_ -notmatch '^(;|$)'
} | ForEach-Object {
if ($_ -match '^\[.*\]$') {
$section = $_ -replace '\[|\]'
$ini_file[$section] = #{}
} else {
$key, $value = $_ -split '\s*=\s*', 2
$ini_file[$section][$key] = $value
}
}
#--------
$Path_Store = $store
#---------
$Get_1 = $ini_file.($section).($R_1)
$L_1 = $Get_1.Substring(0,3)
$Get_2 = $ini_file.($section).($R_2)
$L_2 = $Get_2.Substring(0,3)
$Get_3 = $ini_file.($section).($R_3)
$L_3 = $Get_3.Substring(0,3)
#---------
$Outer = ";********************"
$Header = ";*******************"
$ML = "12345"
$FB = ";Initial=1a2b"
#----------
$B_ID_1 = ";Build=" + $ML + "#" + "S" + $L_1 + "#" + "D" + $L_1
$CRM_1 = ";CRM=" + $R_1
$Output_1 = $Header, $B_ID_1, $FB, $CRM_1 , $Outer | Out-File $Path_Store\A1
$B_ID_2 = ";Build=" + $ML + "#" + "S" + $L_2 + "#" + "D" + $L_2
$CRM_2 = ";CRM=" + $R_2
$Output_2 = $Header, $B_ID_2, $FB, $CRM_2 , $Outer | Out-File $Path_Store\A2
$B_ID_3 = ";Build=" + $ML + "#" + "S" + $L_3 + "#" + "D" + $L_3
$CRM_3 = ";CRM=" + $R_3
$Output_3 = $Header, $B_ID_3, $FB, $CRM_3 , $Outer | Out-File $Path_Store\A3
#---------
}
$call = F_ML
My expectation, I can make this way shorter and the output is getting 24 output file.
Output Sample
Output File 1
;********************
;Build=12345#S12#D12
;Initial=1a2b
;CRM=A1
;********************
Output File 2
;********************
;Build=12345#S23#D23
;Initial=1a2b
;CRM=A2
;********************
Try below...
$IniContent = Get-Content -Path $IninPath
$IniContent | ForEach-Object -Process {
$Split = $_ -split '=';
$OutPutfilePath = "c:\temp\$($Split[0]).txt"
$first = ($Split[1] -split ',')[0]
$append = "#S{0}D{1}" -f $first,$first
# create a herestring to build the output
#"
;********************
;Build=$Ml$append
$FB
;CRM=$($Split[0])
;********************
"# | Out-File -Path $OutPutfilePath -Force
}
Use a ForEach to manipulate a string and Invoke the Expression.
1..24 | ForEach-Object { Invoke-Expression -Command (
'$R_{0} = "A{0}"' -f $_
)}
.
.
.
1..24 | ForEach-Object { Invoke-Expression -Command (
'$Get_{0} = $ini_file.($section).($R_{0}) `
$L_{0} = $Get_{0}.Substring(0,3)' -f $_
)}
.
.
.
1..24 | ForEach-Object { Invoke-Expression -Command (
'$B_ID_{0} = ";Build=" + $ML + "#" + "S" + $L_{0} + "#" + "D" + $L_{0}
$CRM_{0} = ";CRM=" + $R_{0}
$Output_{0} = $Header, $B_ID_{0}, $FB, $CRM_{0} , $Outer | Out-File $Path_Store\A{0}' -f $_
)}
{0} will be replaced by the value behind -f so it will be replaced Foreach number from 1 to 24...
Would this work?
All I did here was use your existing code to run everything in a loop from 1 to 24, removing the duplicated code. I reformatted it a little so it was a little easier for me to read.
Essentially, the variable $i will be a number from 1 to 24, while the variable $R will be "A" and whatever number is in $i (essentially A + $i)
Function F_ML
{
$section = "Code"
$store = "C:\Users\"
$input_file = "C:\Users\File.ini"
$ini_file = #{}
for ($i=1; $i -le 24; $i++)
{
$R = "A$($i)"
Get-Content $input_file |
ForEach-Object `
{
$_.Trim()
} |
Where-Object `
{
$_ -notmatch '^(;|$)'
} |
ForEach-Object `
{
if ($_ -match '^\[.*\]$')
{
$section = $_ -replace '\[|\]'
$ini_file[$section] = #{}
}
else
{
$key, $value = $_ -split '\s*=\s*', 2
$ini_file[$section][$key] = $value
}
}
#--------
$Path_Store = $store
#---------
$Get = $ini_file.($section).($R)
$L = $Get.Substring(0,3)
#---------
$Outer = ";********************"
$Header = ";*******************"
$ML = "12345"
$FB = ";Initial=1a2b"
#----------
$B_ID = ";Build=" + $ML + "#" + "S" + $L + "#" + "D" + $L
$CRM = ";CRM=" + $R
$Output = $Header, $B_ID, $FB, $CRM , $Outer | Out-File $Path_Store\$R
}
}

How can I add a set number of Decimals?

I have a powershell script, I want that it'll always return the number with 2 decimal place (I.E 1 = 1.00, 1.1 = 1.10, 1.11 = 1.11)
I've managed to do it for 1 and 1.11, but I can think of a solution that wont take too much space for 1.1.
Thats what I got:
$Result = $shippingprice % 2
IF ($Result -ne 1 -and $Result -ne 0)
{
$shippingpricestring = "$" + $shippingprice.ToString()
}
else {
$shippingpricestring = "$" + ($shippingprice | % { '{0:0.00}' -f $_ })
}
$Result = $itemprice % 2
IF ($Result -ne 1 -and $Result -ne 0)
{
$itempricestring = "$" + $itemprice.ToString()
}
else {
$itempricestring = "$" + ($itemprice | % { '{0:0.00}' -f $_ })
}
IF ($Result -ne 1 -and $Result -ne 0)
{
$taxstring = "$" + $tax.ToString()
}
else {
$taxstring = "$" + ($tax | % { '{0:0.00}' -f $_ })
}
$Result = $finalrice % 2
IF ($Result -ne 1 -and $Result -ne 0)
{
$finalricestring = "$" +$finalrice.ToString()
}
else {
$finalricestring = "$" + ($finalrice | % { '{0:0.00}' -f $_ })
}
$shippingpricestring = "$" + $shippingprice.ToString("#.##")
Scripting Guy Reference: Hey Scripting Guy
$shippingprice = 17.1111
$ItemPrice = 4.1211
$tax = 94.919
$finalrice =10.3211
$Result = $shippingprice % 2
IF ($Result -ne 1 -and $Result -ne 0)
{
$shippingpricestring = "$" + $shippingprice.ToString("#.##")
}
else {
$shippingpricestring = "$" + ($shippingprice | % { '{0:0.00}' -f $_ })
}
Write-Output $shippingpricestring
IF ($Result -ne 1 -and $Result -ne 0)
{
$itempricestring = "$" + $itemprice.ToString("#.##")
}
else {
$itempricestring = "$" + ($itemprice | % { '{0:0.00}' -f $_ })
}
Write-Output $itempricestring
IF ($Result -ne 1 -and $Result -ne 0)
{
$taxstring = "$" + $tax.ToString("#.##")
}
else {
$taxstring = "$" + ($tax | % { '{0:0.00}' -f $_ })
}
Write-Output $taxstring
$Result = $finalrice % 2
IF ($Result -ne 1 -and $Result -ne 0)
{
$finalricestring = "$" +$finalrice.ToString("#.##")
}
else {
$finalricestring = "$" + ($finalrice | % { '{0:0.00}' -f $_ })
}
Write-Output $finalricestring
RESULT
$17.11
$4.12
$94.92
$10.32
*** The above assumes that all the variables have more than 2 decimal spaces. If say $tax = 9.9 $taxstring would appear as 9.9 and NOT as 9.90
If you want to format the number with .ToString you can go with .ToString(format)
Example
$list = 1, 1.1, 1.111, 1.1111
$list | %{$_.ToString("0.00")}
Output
1.00
1.10
1.11
1.11

How to track path/key during recursion through hash of hashes in PowerShell?

I've been trying to compare two hashes of hashes against each other. Unfortunately even with the great help from here I struggled to pull together what I was trying to do.
So, I've resorted to searching the internet again and I was lucky enough to find Edxi's code (full credit to him and Dbroeglin who looks to have written it originally)https://gist.github.com/edxi/87cb8a550b43ec90e4a89d2e69324806 His compare-hash function does exactly what I needed in terms of its comparisons. However, I'd like to report of the full path of the hash in the final output. So I've tried to update the code to allow for this. My thinking being that I should be able to take the Key (aka path) while the code loops over itself but I'm clearly going about it in the wrong manner.
Am I going about this with the right approach, and if not, would someone suggest another method please? The biggest problem I've found so far is that if I change the hash, my code changes don't work e.g. adding 'More' = 'stuff' So it becomes $sailings.Arrivals.PDH083.More breaks the path in the way I've set it.
My version of the code:
$Sailings = #{
'Arrivals' = #{
'PDH083' = #{
'GoingTo' = 'Port1'
'Scheduled' = '09:05'
'Expected' = '10:11'
'Status' = 'Delayed'
}
}
'Departures' = #{
'PDH083' = #{
'ArrivingFrom' = 'Port1'
'Scheduled' = '09:05'
'Expected' = '09:05'
'Status' = 'OnTime'
'Other' = #{'Data' = 'Test'}
}
}
}
$Flights = #{
'Arrivals' = #{
'PDH083' = #{
'GoingTo' = 'Port'
'Scheduled' = '09:05'
'Expected' = '10:20'
'Status' = 'Delayed'
}
}
'Departures' = #{
'PDH083' = #{
'ArrivingFrom' = 'Port11'
'Scheduled' = '09:05'
'Expected' = '09:05'
'Status' = 'NotOnTime'
'Other' = #{'Data' = 'Test_Diff'}
}
}
}
function Compare-Hashtable {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Hashtable]$Left,
[Parameter(Mandatory = $true)]
[Hashtable]$Right,
[string] $path,
[boolean] $trackpath = $True
)
write-host "PAth received as: $path"
function New-Result($Key, $LValue, $Side, $RValue) {
New-Object -Type PSObject -Property #{
key = $Key
lvalue = $LValue
rvalue = $RValue
side = $Side
}
}
[Object[]]$Results = $Left.Keys | ForEach-Object {
if ($trackpath ) {
write-host "Working on Path: " $path + $_
}
if ($Left.ContainsKey($_) -and !$Right.ContainsKey($_)) {
write-host "Right key not matched. Report path as: $path"
New-Result $path $Left[$_] "<=" $Null
}
else {
if ($Left[$_] -is [hashtable] -and $Right[$_] -is [hashtable] ) {
if ($path -ne $null -or $path -ne "/") {
$path = $path + "/" + $_
write-host "Sending Path to function as: $path"
}
Compare-Hashtable $Left[$_] $Right[$_] $path
}
else {
$LValue, $RValue = $Left[$_], $Right[$_]
if ($LValue -ne $RValue) {
$path = $path + "/" + $_
write-host "Not a hash so must be a value at path:$path"
New-Result $path $LValue "!=" $RValue
}
else {
Write-Host "Before changing path: $path "
if (($path.Substring(0, $path.lastIndexOf('/'))).length >0) {
$path = $path.Substring(0, $path.lastIndexOf('/'))
Write-Host "After changing path: $path "
}
}
}
}
# if (($path.Substring(0, $path.lastIndexOf('/'))).length >0) {
# Tried to use this to stop error on substring being less than zero
# but clearly doesnt work when you add more levels to the hash
$path = $path.Substring(0, $path.lastIndexOf('/'))
# } else { $path = $path.Substring(0, $path.lastIndexOf('/')) }
}
$Results += $Right.Keys | ForEach-Object {
if (!$Left.ContainsKey($_) -and $Right.ContainsKey($_)) {
New-Result $_ $Null "=>" $Right[$_]
}
}
if ($Results -ne $null) { $Results }
}
cls
Compare-Hashtable $Sailings $Flights
outputs
key lvalue side rvalue
--- ------ ---- ------
/Arrivals/PDH083/Expected 10:11 != 10:20
/Arrivals/GoingTo Port1 != Port
/Departures/PDH083/ArrivingFrom Port1 != Port11
/Departures/PDH083/Status OnTime != NotOnTime
/Departures/PDH083/Other/Data Test != Test_Diff
I won't do that much string manipulation on the $Path but threat $Path as an array and join it to a string at the moment you assign it as a property (key = $Path -Join "/") to the object:
function Compare-Hashtable {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Hashtable]$Left,
[Parameter(Mandatory = $true)]
[Hashtable]$Right,
[string[]] $path = #(),
[boolean] $trackpath = $True
)
write-host "Path received as: $path"
function New-Result($Path, $LValue, $Side, $RValue) {
New-Object -Type PSObject -Property #{
key = $Path -Join "/"
lvalue = $LValue
rvalue = $RValue
side = $Side
}
}
$Left.Keys | ForEach-Object {
$NewPath = $Path + $_
if ($trackpath ) {
write-host "Working on Path: " $NewPath
}
if ($Left.ContainsKey($_) -and !$Right.ContainsKey($_)) {
write-host "Right key not matched. Report path as: $NewPath"
New-Result $NewPath $Left[$_] "<=" $Null
}
else {
if ($Left[$_] -is [hashtable] -and $Right[$_] -is [hashtable] ) {
Compare-Hashtable $Left[$_] $Right[$_] $NewPath
}
else {
$LValue, $RValue = $Left[$_], $Right[$_]
if ($LValue -ne $RValue) {
New-Result $NewPath $LValue "!=" $RValue
}
}
}
}
$Right.Keys | ForEach-Object {
$NewPath = $Path + $_
if (!$Left.ContainsKey($_) -and $Right.ContainsKey($_)) {
New-Result $NewPath $Null "=>" $Right[$_]
}
}
}
cls
Compare-Hashtable $Sailings $Flights
side-note: Write Single Records to the Pipeline (SC03), see: Strongly Encouraged Development Guidelines. In other words, don't do the $Results += ... but intermediately put any completed result in the pipeline for the next cmdlet to be picked up (besides, it unnecessarily code)...

Why does PowerShell intermittently not find List[string].Add() method?

I've got a build script (PowerShell 4 on Windows 2012 R2) that runs NUnit in a background job and returns NUnit's output. This output is collected in a Collections.Generic.List[string].
$nunitJob = Start-Job -ScriptBlock {
param(
[string]
$BinRoot,
[string]
$NUnitConsolePath,
[string[]]
$NUnitParams,
[string]
$Verbose
)
Set-Location -Path $BinRoot
$VerbosePreference = $Verbose
Write-Verbose -Message ('{0} {1}' -f $NUnitConsolePath,($NUnitParams -join ' '))
& $NUnitConsolePath $NUnitParams 2>&1
$LASTEXITCODE
} -ArgumentList $binRoot,$nunitConsolePath,$nunitParams,$VerbosePreference
$nunitJob | Wait-Job -Timeout ($timeoutMinutes * 60) | Out-Null
$jobKilled = $false
if( $nunitJob.JobStateInfo.State -eq [Management.Automation.JobState]::Running )
{
$jobKilled = $true
$errMsg = 'Killing {0} tests: exceeded {1} minute timeout.' -f $assembly.Name,$timeoutMinutes
Write-Error -Message $errMsg
}
$output = New-Object 'Collections.Generic.List[string]'
$nunitJob |
Stop-Job -PassThru |
Receive-Job |
ForEach-Object {
if( -not $_ )
{
[void]$output.Add( '' )
return
}
switch -Regex ( $_ )
{
'^Tests run: (\d+), Errors: (\d+), Failures: (\d+), Inconclusive: (\d+), Time: ([\d\.]+) seconds$'
{
$testsRun = $Matches[1]
$errors = $Matches[2]
$failures = $Matches[3]
$inconclusive = $Matches[4]
$duration = New-Object 'TimeSpan' 0,0,$Matches[5]
break
}
'^ Not run: (\d+), Invalid: (\d+), Ignored: (\d+), Skipped: (\d+)$'
{
$notRun = $Matches[1]
$invalid = $Matches[2]
$ignored = $Matches[3]
$skipped = $Matches[4]
break
}
}
# Error happens here:
[void] $output.Add( $_ )
}
Intermittently, our build will fail with this error:
Cannot find an overload for "Add" and the argument count: "1".
At line:XXXXX char:XXXXX
+ [void] $output.Add( $_ )
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
Any idea why PowerShell would not be able to find List[string]'s Add method?
I've opened a console Window and played around with passing different typed objects to Add without getting an error.
> $o = New-Object 'Collections.Generic.List[string]'
> $o.Add( '' )
> $o.Add( $null )
> $o.Add( 1 )
> $o
1
When you redirect stderr to stdout, you can get System.Management.Automation.ErrorRecords interspersed with strings. Stderr is automatically converted to ErrorRecords while stdout is strings.
So you'll probably want to look to see if the output contains ErrorRecords of interest and if not then filter them out.