PowerShell : compare 2 excel files or 2 sheets - powershell

I have to make a script which can compare 2 excel files or sheets and if one of the cells isn't de the same it tells me which one it is but i don't know how to do this, I watched another situation like this one but i didn't manage to do it can you help me ?
my files are test1.csv and test2.csv

Try this.
$file1 = Import-Csv test1.csv
$file2 = Import-Csv test2.csv
Compare-Object $file1 $file2 -property "HeaderProperty" -IncludeEqual

#Vivek Kumar : Be careful, Compare-Object has a -SyncWindow parameter which has a value by default and that can give only a part of the results.
A very good explanation here : http://community.idera.com/powershell/powershell_com_featured_blogs/b/tobias/posts/tipps-amp-tricks-using-compare-object
One way to bypass this "problem" is to set the -SyncWindow by dividing by 2 the -ReferenceObject :
$file1 = Import-Csv test1.csv
$file2 = Import-Csv test2.csv
Compare-Object -ReferenceObject $file1 -DifferenceObject $file2 -SyncWindow ($file1.length / 2)

Since you mention the files are CSV, you can do all you need with standard PS functions.
However, if using Excel (XLSX/XLS) files, you may be interested in this library: https://github.com/RamblingCookieMonster/PSExcel. Just switch Import-CSV for Import-XLSX.
Below's a very basic example of how this could be done.
Code
function Report-OffendingCell { #NB: doesn't follow good naming conventions
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline = $true)]
[long]$ColumnIndex
,
[Parameter(Mandatory=$true)]
[long]$RowIndex
,
[Parameter(Mandatory=$true)]
[string]$SheetName
,
[Parameter(Mandatory=$false)]
[string]$Explanation
)
process {
#If you want column letters instead of numbers, use something like Convert-NumberToA1 from https://gallery.technet.microsoft.com/office/Powershell-function-that-88f9f690
#"[{0}]!{1}{2}" -f $SheetName, (Convert-NumberToA1 $ColumnIndex + 1), ($RowIndex + 1)
#I've returned an object instead, since that's more useful for any further PS automation
(New-Object -TypeName PSObject -Property #{
ColumnNo = $ColumnIndex + 1
RowNo = $RowIndex + 1
SheetName = $SheetName
Explanation = $Explanation
})
}
}
function Compare-Tables {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[PSObject[]]$Table1
,
[Parameter(Mandatory=$true)]
[PSObject[]]$Table2
,
[Parameter(Mandatory=$false)]
[string]$Table1Name = 'Table1'
,
[Parameter(Mandatory=$false)]
[string]$Table2Name = 'Table2'
)
begin {
[long]$t1Cols = ($Table1[0].PSObject.Properties | Measure-Object).Count - 1
[long]$t2Cols = ($Table2[0].PSObject.Properties | Measure-Object).Count - 1
[long]$t1Rows = $Table1.Count - 1
[long]$t2Rows = $Table2.Count - 1
[long]$minCols = [System.Math]::Min($t1Cols, $t2Cols)
[long]$maxCols = [System.Math]::Max($t1Cols, $t2Cols)
[long]$minRows = [System.Math]::Min($t1Rows, $t2Rows)
[long]$maxRows = [System.Math]::Max($t1Rows, $t2Rows)
[string]$offendingColTable = if ($maxCols -eq $t1Cols){$Table1Name}else{$Table2Name}
[string]$offendingRowTable = if ($maxRows -eq $t1Rows){$Table1Name}else{$Table2Name}
write-verbose $offendingColTable
write-verbose $offendingRowTable
write-verbose $maxCols
write-verbose $t1Cols
write-verbose $t2Cols
}
process {
0..$minRows | %{ #loop through each row which is populated in both sheets
[long]$row = $_
0..$minCols |
?{(#($Table1[$row].PSObject.Properties)[$_].Value) -ne (#($Table2[$row].PSObject.Properties)[$_].Value)} |
Report-OffendingCell -RowIndex $row -SheetName $Table2Name -Explanation 'Values differ between sheets!' #sheetname could be Table1 or Table2 here; since the cell exists in both sheets
($minCols + 1)..$maxCols | Report-OffendingCell -RowIndex $row -SheetName $offendingColTable -Explanation 'Entire Column only exists on one sheet!'
}
($minRows + 1)..$maxRows | %{ #for any rows which don't exist in one of the sheets, output that
[long]$row = $_
0..$maxCols | Report-OffendingCell -RowIndex $row -SheetName $offendingRowTable -Explanation 'Entire Row only exists on one sheet!'
}
}
}
$test1 = Import-CSV -Path '.\test1.csv'
$test2 = Import-CSV -Path '.\test2.csv'
Compare-Tables -Table1 $test1 -Table2 $test2 -Table1Name 'test1' -Table2Name 'test2' -Verbose | ft SheetName, ColumnNo, RowNo, Explanation
#just so I don't mess up your session with my mock
if((Get-Command Import-Csv).Source -ne 'Microsoft.PowerShell.Utility') {
Remove-Item 'function:Import-Csv'
}
Code for Testing
To provide the example output below, you can use the following code. This overwrites the Import-CSV function with a mocked version of that function which simply returns fixed value data. This code is not required for the real-world scenario; just for those who don't have suitable test CSV files who want something to experiment with.
#region 'Mocked Standard Functions'
#you don't need this function; this is just to make testing simple
function Import-CSV {
param($Path)
switch ($Path) {
'.\test1.csv' {
#(
#{
'Column A Heading'='Row 1 Cell 1';
'Column B Heading'='Row 1 Cell 2';
'Column C Heading'='Row 1 Cell 3';
'Column D Heading'='Row 1 Cell 4';
}
, #{
'Column A Heading'='Row 2 Cell 1';
'Column B Heading'='Row 2 Cell 2';
'Column C Heading'='Row 2 Cell 3';
'Column D Heading'='Row 2 Cell 4';
}
, #{
'Column A Heading'='Row 3 Cell 1';
'Column B Heading'='Row 3 Cell 2';
'Column C Heading'='Row 3 Cell 3';
'Column D Heading'='Row 3 Cell 4';
}
) | %{(New-Object -TypeName PSObject -Property $_)} | select 'Column A Heading', 'Column B Heading', 'Column C Heading', 'Column D Heading' #select needed to ensure columns are returned in the correct order
}
'.\test2.csv' {
#(
#{
'Column Heading 1'='Row 1 Cell 1';
'Column B Heading'='Row 1 Cell 2';
'Column C Heading'='Row 1 Cell 3 difference';
'Column D Heading'='Row 1 Cell 4';
}
, #{
'Column Heading 1'='Row 2 Cell 1';
'Column B Heading'='Row 2 Cell 2';
'Column C Heading'='Row 2 Cell 3';
'Column D Heading'='Row 2 Cell 4';
'Column E Heading'='Row 2 Cell 5 bonus ball!'; #note that though we've not defined on the previous "row", the import function assumes a table, so we'll still have a property on the previous row; only it'll be null
}
) | %{(New-Object -TypeName PSObject -Property $_)}| select 'Column Heading 1', 'Column B Heading', 'Column C Heading', 'Column D Heading', 'Column E Heading' #select needed to ensure columns are returned in the correct order
}
default {throw "no dummy data defined for $Path"}
}
}
#endregion 'Mocked Standard Functions'
Example Output
SheetName ColumnNo RowNo Explanation
--------- -------- ----- -----------
test2 3 1 Values differ between sheets!
test2 5 1 Entire Column only exists on one sheet!
test2 5 2 Entire Column only exists on one sheet!
test1 1 3 Entire Row only exists on one sheet!
test1 2 3 Entire Row only exists on one sheet!
test1 3 3 Entire Row only exists on one sheet!
test1 4 3 Entire Row only exists on one sheet!
test1 5 3 Entire Row only exists on one sheet!

Function Compare-WorkSheet {
<#
.Synopsis
Compares two worksheets with the same name in different files.
.Description
This command takes two file names, a worksheet name and a name for a key column.
It reads the worksheet from each file and decides the column names.
It builds as hashtable of the key column values and the rows they appear in
It then uses PowerShell's compare object command to compare the sheets (explicity checking all column names which have not been excluded)
For the difference rows it adds the row number for the key of that row - we have to add the key after doing the comparison,
otherwise rows will be considered as different simply because they have different row numbers
We also add the name of the file in which the difference occurs.
If -BackgroundColor is specified the difference rows will be changed to that background.
.Example
Compare-WorkSheet -Referencefile 'Server56.xlsx' -Differencefile 'Server57.xlsx' -WorkSheetName Products -key IdentifyingNumber -ExcludeProperty Install* | format-table
The two workbooks in this example contain the result of redirecting a subset of properties from Get-WmiObject -Class win32_product to Export-Excel
The command compares the "products" pages in the two workbooks, but we don't want to register a differnce if if the software was installed on a
different date or from a different place, so Excluding Install* removes InstallDate and InstallSource.
This data doesn't have a "name" column" so we specify the "IdentifyingNumber" column as the key.
The results will be presented as a table.
.Example
compare-WorkSheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView
This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel
Here the -Differencefile and -Referencefile parameter switches are assumed , and the default setting for -key ("Name") works for services
This will display the differences between the "services" sheets using a grid view
.Example
Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName Services -BackgroundColor lightGreen
This version of the command outputs the differences between the "services" pages and also highlights any different rows in the spreadsheet files.
.Example
Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -FontColor Red -Show
This builds on the previous example: this time Where two changed rows have the value in the "name" column (the default value for -key),
this version adds highlighting of the changed cells in red; and then opens the Excel file.
.Example
Compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description"
This time the reference file and the difference file are the same file and two different sheets are used. Because the tests include the
machine name and time the test was run the command specifies a limited set of columns should be used.
.Example
Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -GridView -ExcludeDifferent
The "General" page has a title and two unlabelled columns with a row forCPU, Memory, Domain, Disk and so on
So the command is instructed to starts at row 2 to skip the title and to name the columns: the first is "label" and the Second "Value";
the label acts as the key. This time we interested the rows which are the same in both sheets,
and the result is displayed using grid view. Note that grid view works best when the number of columns is small.
.Example
Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -BackgroundColor White -Show -AllDataBackgroundColor LightGray
This version of the previous command lightlights all the cells in lightgray and then sets the changed rows back to white; only
the unchanged rows are highlighted
#>
[cmdletbinding(DefaultParameterSetName)]
Param(
#First file to compare
[parameter(Mandatory=$true,Position=0)]
$Referencefile ,
#Second file to compare
[parameter(Mandatory=$true,Position=1)]
$Differencefile ,
#Name(s) of worksheets to compare.
$WorkSheetName = "Sheet1",
#Properties to include in the DIFF - supports wildcards, default is "*"
$Property = "*" ,
#Properties to exclude from the the search - supports wildcards
$ExcludeProperty ,
#Specifies custom property names to use, instead of the values defined in the column headers of the TopRow.
[Parameter(ParameterSetName='B', Mandatory)]
[String[]]$Headername,
#Automatically generate property names (P1, P2, P3, ..) instead of the using the values the top row of the sheet
[Parameter(ParameterSetName='C', Mandatory)]
[switch]$NoHeader,
#The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row.
[int]$Startrow = 1,
#If specified, highlights all the cells - so you can make Equal cells one colour, and Diff cells another.
[System.Drawing.Color]$AllDataBackgroundColor,
#If specified, highlights the DIFF rows
[System.Drawing.Color]$BackgroundColor,
#If specified identifies the tabs which contain DIFF rows (ignored if -backgroundColor is omitted)
[System.Drawing.Color]$TabColor,
#Name of a column which is unique and will be used to add a row to the DIFF object, default is "Name"
$Key = "Name" ,
#If specified, highlights the DIFF columns in rows which have the same key.
[System.Drawing.Color]$FontColor,
#If specified opens the Excel workbooks instead of outputting the diff to the console (unless -passthru is also specified)
[Switch]$Show,
#If specified, the command tries to the show the DIFF in a Gridview and not on the console. (unless-Passthru is also specified). This Works best with few columns selected, and requires a key
[switch]$GridView,
#If specified -Passthrough full set of diff data is returned without filtering to the specified properties
[Switch]$PassThru,
#If specified the result will include equal rows as well. By default only different rows are returned
[Switch]$IncludeEqual,
#If Specified the result includes only the rows where both are equal
[Switch]$ExcludeDifferent
)
#if the filenames don't resolve, give up now.
try { $oneFile = ((Resolve-Path -Path $Referencefile -ErrorAction Stop).path -eq (Resolve-Path -Path $Differencefile -ErrorAction Stop).path)}
Catch { Write-Warning -Message "Could not Resolve the filenames." ; return }
#If we have one file , we mush have two different worksheet names. If we have two files we can a single string or two strings.
if ($onefile -and ( ($WorkSheetName.count -ne 2) -or $WorkSheetName[0] -eq $WorkSheetName[1] ) ) {
Write-Warning -Message "If both the Reference and difference file are the same then worksheet name must provide 2 different names"
return
}
if ($WorkSheetName.count -eq 2) {$worksheet1 = $WorkSheetName[0] ; $WorkSheet2 = $WorkSheetName[1]}
elseif ($WorkSheetName -is [string]) {$worksheet1 = $WorkSheet2 = $WorkSheetName}
else {Write-Warning -Message "You must provide either a single worksheet name or two names." ; return }
$params= #{ ErrorAction = [System.Management.Automation.ActionPreference]::Stop }
foreach ($p in #("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}}
try {
$Sheet1 = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 #params
$Sheet2 = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 #Params
}
Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile and/or $Differencefile." ; return }
#Get Column headings and create a hash table of Name to column letter.
$headings = $Sheet1[-1].psobject.Properties.name # This preserves the sequence - using get-member would sort them alphabetically!
$headings | ForEach-Object -Begin {$columns = #{} ; $i=65 } -Process {$Columns[$_] = [char]($i ++) }
#Make a list of property headings using the Property (default "*") and ExcludeProperty parameters
if ($Key -eq "Name" -and $NoHeader) {$key = "p1"}
$propList = #()
foreach ($p in $Property) {$propList += ($headings.where({$_ -like $p}) )}
foreach ($p in $ExcludeProperty) {$propList = $propList.where({$_ -notlike $p}) }
if (($headings -contains $key) -and ($propList -notcontains $Key)) {$propList += $Key}
$propList = $propList | Select-Object -Unique
if ($propList.Count -eq 0) {Write-Warning -Message "No Columns are selected with -Property = '$Property' and -excludeProperty = '$ExcludeProperty'." ; return}
#Add RowNumber, Sheetname and file name to every row
$FirstDataRow = $startRow + 1
if ($Headername -or $NoHeader) {$FirstDataRow -- }
$i = $FirstDataRow ; foreach ($row in $Sheet1) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++)
Add-Member -InputObject $row -MemberType NoteProperty -Name "_Sheet" -Value $worksheet1
Add-Member -InputObject $row -MemberType NoteProperty -Name "_File" -Value $Referencefile}
$i = $FirstDataRow ; foreach ($row in $Sheet2) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++)
Add-Member -InputObject $row -MemberType NoteProperty -Name "_Sheet" -Value $worksheet2
Add-Member -InputObject $row -MemberType NoteProperty -Name "_File" -Value $Differencefile}
if ($ExcludeDifferent -and -not $IncludeEqual) {$IncludeEqual = $true}
#Do the comparison and add file,sheet and row to the result - these are prefixed with "_" to show they are added the addition will fail if the sheet has these properties so split the operations
[PSCustomObject[]]$diff = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual:$IncludeEqual -ExcludeDifferent:$ExcludeDifferent |
Sort-Object -Property "_Row","File"
#if BackgroundColor was specified, set it on extra or extra or changed rows
if ($diff -and $BackgroundColor) {
#Differences may only exist in one file. So gather the changes for each file; open the file, update each impacted row in the shee, save the file
$updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property "_File"
foreach ($file in $updates) {
try {$xl = Open-ExcelPackage -Path $file.name }
catch {Write-warning -Message "Can't open $($file.Name) for writing." ; return}
if ($AllDataBackgroundColor) {
$file.Group._sheet | Sort-Object -Unique | ForEach-Object {
$ws = $xl.Workbook.Worksheets[$_]
if ($headerName) {$range = "A" + $startrow + ":" + $ws.dimension.end.address}
else {$range = "A" + ($startrow + 1) + ":" + $ws.dimension.end.address}
Set-Format -WorkSheet $ws -BackgroundColor $AllDataBackgroundColor -Range $Range
}
}
foreach ($row in $file.group) {
$ws = $xl.Workbook.Worksheets[$row._Sheet]
$range = $ws.Dimension -replace "\d+",$row._row
Set-Format -WorkSheet $ws -Range $range -BackgroundColor $BackgroundColor
}
if ($TabColor) {
foreach ($tab in ($file.group._sheet | Select-Object -Unique)) {
$xl.Workbook.Worksheets[$tab].TabColor = $TabColor
}
}
$xl.save() ; $xl.Stream.Close() ; $xl.Dispose()
}
}
#if font colour was specified, set it on changed properties where the same key appears in both sheets.
if ($diff -and $FontColor -and ($propList -contains $Key) ) {
$updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | Where-Object {$_.count -eq 2}
if ($updates) {
$XL1 = Open-ExcelPackage -path $Referencefile
if ($oneFile ) {$xl2 = $xl1}
else {$xl2 = Open-ExcelPackage -path $Differencefile }
foreach ($u in $updates) {
foreach ($p in $propList) {
if($u.Group[0].$p -ne $u.Group[1].$p ) {
Set-Format -WorkSheet $xl1.Workbook.Worksheets[$u.Group[0]._sheet] -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor
Set-Format -WorkSheet $xl2.Workbook.Worksheets[$u.Group[1]._sheet] -Range ($Columns[$p] + $u.Group[1]._Row) -FontColor $FontColor
}
}
}
$xl1.Save() ; $xl1.Stream.Close() ; $xl1.Dispose()
if (-not $oneFile) {$xl2.Save() ; $xl2.Stream.Close() ; $xl2.Dispose()}
}
}
elseif ($diff -and $FontColor) {Write-Warning -Message "To match rows to set changed cells, you must specify -Key and it must match one of the included properties." }
#if nothing was found write a message which wont be redirected
if (-not $diff) {Write-Host "Comparison of $Referencefile::$worksheet1 and $Differencefile::$WorkSheet2 returned no results." }
if ($show) {
Start-Process -FilePath $Referencefile
if (-not $oneFile) { Start-Process -FilePath $Differencefile }
if ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" }
}
elseif ($GridView -and $propList -contains $key) {
if ($IncludeEqual -and -not $ExcludeDifferent) {
$GroupedRows = $diff | Group-Object -Property $key
}
else { #to get the right now numbers on the grid we need to have all the rows.
$GroupedRows = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual |
Group-Object -Property $key
}
#Additions, deletions and unchanged rows will give a group of 1; changes will give a group of 2 .
#If one sheet has extra rows we can get a single "==" result from compare, but with the row from the reference sheet
#but the row in the other sheet might so we will look up the row number from the key field build a hash table for that
$Sheet2 | ForEach-Object -Begin {$Rowhash = #{} } -Process {$Rowhash[$_.$key] = $_._row }
$ExpandedDiff = ForEach ($g in $GroupedRows) {
#we're going to create a custom object from a hash table. We want the fields to be ordered
$hash = [ordered]#{}
foreach ($result IN $g.Group) {
# if result indicates equal or "in Reference" set the reference side row. If we did that on a previous result keep it. Otherwise set to "blank"
if ($result.sideindicator -ne "=>") {$hash["<Row"] = $result._Row }
elseif (-not $hash["<Row"]) {$hash["<Row"] = "" }
#if we have already set the side, this is the second record, so set side to indicate "changed"
if ($hash.Side) {$hash.side = "<>"} else {$hash["Side"] = $result.sideindicator}
#if result is "in reference" and we don't have a matching "in difference" (meaning a change) the lookup will be blank. Which we want.
$hash[">Row"] = $Rowhash[$g.Name]
#position the key as the next field (only appears once)
$Hash[$key] = $g.Name
#For all the other fields we care about create <=FieldName and/or =>FieldName
foreach ($p in $propList.Where({$_ -ne $key})) {
if ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P}
else {$hash[($result.SideIndicator+$P)] =$result.$P}
}
}
[Pscustomobject]$hash
}
#Sort by reference row number, and fill in any blanks in the difference-row column
$ExpandedDiff = $ExpandedDiff | Sort-Object -Property "<row"
for ($i = 1; $i -lt $ExpandedDiff.Count; $i++) {if (-not $ExpandedDiff[$i].">row") {$ExpandedDiff[$i].">row" = $ExpandedDiff[$i-1].">row" } }
#Sort by difference row number, and fill in any blanks in the reference-row column
$ExpandedDiff = $ExpandedDiff | Sort-Object -Property ">row"
for ($i = 1; $i -lt $ExpandedDiff.Count; $i++) {if (-not $ExpandedDiff[$i]."<row") {$ExpandedDiff[$i]."<row" = $ExpandedDiff[$i-1]."<row" } }
#if we had to put the equal rows back, take them out; sort, make sure all the columns are present in row 1 so the grid puts them in, and output
if ( $ExcludeDifferent) {$ExpandedDiff = $ExpandedDiff.where({$_.side -eq "=="}) | Sort-Object -Property "<row" ,">row" }
elseif ( $IncludeEqual) {$ExpandedDiff = $ExpandedDiff | Sort-Object -Property "<row" ,">row" }
else {$ExpandedDiff = $ExpandedDiff.where({$_.side -ne "=="}) | Sort-Object -Property "<row" ,">row" }
$ExpandedDiff | Update-FirstObjectProperties | Out-GridView -Title "Comparing $Referencefile::$worksheet1 (<=) with $Differencefile::$WorkSheet2 (=>)"
}
elseif ($GridView ) {Write-Warning -Message "To use -GridView you must specify -Key and it must match one of the included properties." }
elseif (-not $PassThru) {return ($diff | Select-Object -Property (#(#{n="_Side";e={$_.SideIndicator}},"_File" ,"_Sheet","_Row") + $propList))}
if ( $PassThru) {return $diff }
}

Related

Comparing all rows of a table Powershell

I have a DataTable ($dt = New-Object system.Data.datatable) which contains entries as below:
My objective is :
Find servers which same names ,ie , from ServerID column trim the part after underscore (_) (which I achieved via Split()) and then compare with rest of the rows.
If the Server Name is same, check the value of all respective "Status" column
If none of the columns have "IN PROCESS" in them for the respective server, then print the ServerID.
This is what I came up with but got stuck since values are not returned correctly:
foreach($backupid in ($dt.'ServerID' | %{foreach ($y in $_){$y.Split('_')[0]}} | sort -Unique)){
foreach ($row in $dt){
if ($row.'ServerID ' -match "^$backupid" -and $row.Status -ne "IN PROCESS" ){
$row.'ServerID '
}
}
}
Just use a hash table to check whether a server id is (not) IN PROCESS, like:
$dt = ConvertFrom-Csv #'
Server,Status
abc_123,"IN PROCESS"
abc_345,"INACTIVE"
abc_546,"INACTIVE"
xyz_123,"INACTIVE"
xyz_457,"INACTIVE"
xyz_230,"INACTIVE"
'#
$InProcess = #{}
$dt | Foreach-Object {
$Id = $_.Server.Split('_')[0]
if (!$InProcess.Contains($Id)) { $InProcess[$Id] = $False }
if ($_.Status -eq 'IN PROCESS') { $InProcess[$Id] = $True }
}
$dt | Foreach-Object {
$Id = $_.Server.Split('_')[0]
if ($InProcess[$Id] -eq $False) { $_ }
}
Server Status
------ ------
xyz_123 INACTIVE
xyz_457 INACTIVE
xyz_230 INACTIVE
Instead of nested loops, try Group-Object!
# Group objects by the first part of the Server ID
$dt |Group { $_.ServerID.Split('_')[0] } |Where-Object {
# Then find only the groups with no objects where Status is IN_PROGRESS
$_.Group.Status -notcontains 'IN_PROGRESS'
} |ForEach-Oject -MemberName ServerID # Output just the Server value

PowerShell: list CSV file rows where at least one value between the 3rd and last column is equal to "0" or "1"

In my PowerShell script, I'm working with a CSV file that looks like this (with a number of rows and columns that can vary, but there will always be at least the headers and the first 2 columns):
OS;IP;user0;user1;user3
Windows;10.0.0.1;;;
Linux;hostname2;0;;1
Linux;10.0.0.3;;0;0
Linux;hostname4;;;
Windows;hostname5;1;1;1
I basically list servers in the first column and users in the first row (CSV header). This represents a user "access granting" matrix to servers (1 for "give access", 0 for "remove access", and void for "don't change").
I'm looking for a way to extract only the rows that include a value equal to "1" or "0" between (and including) the 3rd and last column. (= to eventually get the list of servers where access rights should be changed)
So taking the above example, I only want the following lines returned:
Linux;hostname2;0;;1
Linux;10.0.0.3;;0;0
Windows;hostname5;1;1;1
Any hints to make this possible? Or the opposite (getting the ones without any 0 or 1)?
Even if it means using "Get-Content" instead of "Import-CSV". I don't care about the 1st (headers) row; I know how to exclude that.
Thank you!
--- Final solution, thanks to #Tomalak's answer:
$AccessMatrix = Import-CSV $CSVfile -delimiter ';'
$columns = $AccessMatrix | Get-Member -MemberType NoteProperty | Select-Object -Skip 2 -ExpandProperty Name
$AccessMatrix = $AccessMatrix | ForEach-Object {
$row = $_
foreach ($col in $columns) {
if ($row.$col.trim() -eq "1" -OR $row.$col.trim() -eq "0") {
$row # this pushes the $row onto the pipeline
break
}
}
}
The following uses Get-Member to select the names of all columns after the first two.
Then, using ForEach-Object, we can output only those rows that have a value in any of those columns.
$data = ConvertFrom-Csv "OS;IP;user0;user1;user3
Windows;10.0.0.1;;;
Linux;hostname2;0;;1
Linux;10.0.0.3;;0;0
Linux;hostname4;;;
Windows;hostname5;1;1;1" -Delimiter ";"
$columns = $data | Get-Member -MemberType NoteProperty | Select-Object -Skip 2 -ExpandProperty Name
$data | ForEach-Object {
$row = $_
foreach ($col in $columns) {
if ($row.$col -ne "") {
$row # this pushes the $row onto the pipeline
break
}
}
}
The break statement stops the execution of the inner foreach loop because there is no point in further checking as soon as the first column with any value is found.
This is equivalent to the above, if you prefer Where-Object:
$data | Where-Object {
$row = $_
foreach ($col in $columns) {
if ($row.$col -ne "") {
return $true
}
}
}

Powershell - Exporting Non-string Multivalued Properties

I have the following to dynamically expand properties of objects I feed into the function, but ToString() is often spitting out the property types instead:
Function ExpandMultivaluedProperties
{
Param(
[PSObject]$InputObject
)
$results= $InputObject |
ForEach-Object {
$properties = New-Object PSObject
$_.PSObject.Properties |
ForEach-Object {
$propertyName = $_.Name
$propertyValue = $_.Value
If ($propertyValue -NE $NULL) {
$values = #()
ForEach ($value In $propertyValue) {
$values += $value.ToString()
}
Add-Member -inputObject $properties NoteProperty -name $propertyName -value "$([String]::Join(";",$values))"
} Else {
Add-Member -inputObject $properties NoteProperty -name $propertyName -value $NULL
}
}
$properties
}
return $results
}
ExpandMultivaluedProperties -InputObject (Get-ExchangeCertificate) | Export-CSV -path "Cert.csv" -NoTypeInformation
In particular with the results of Get-ExchangeCertificate, what I end up with is
System.Security.Cryptography.X509Certificates.X500DistinguishedName
as the IssuerName for each certificate.
The code works fine for any string-friendly properties, but I'm aiming for it to be robust enough to handle any property that would normally show up correctly with a | Format-List
Any thoughts on how to print/expand similar properties programmatically without having to use a "Select" expression?
In a string context, the instances of many types default to simply printing their type name - e.g., System.Security.Cryptography.X509Certificates.X500DistinguishedName - which is often not helpful.
However, you can use Out-String instead, which applies PowerShell's default output formatting (the same output format you'd see in the console):
$values += Out-String -InputObject $value
One caveat is that the resulting strings often have leading or trailing empty lines. You can remove leading and/or trailing empty lines with the following variation:
$values += (Out-String -InputObject $value).Trim()
Caveats:
The default width of the output lines for non-string data is based on the PowerShell host, and defaults to the width of the window buffer in the regular console minus 1 (excluding the line break); i.e., 79 in with the default console window size.
Use -Width to specify an output line width (excluding the line break) explicitly.
Longer lines are truncated, i.e., the information is lost.
Note that [string] values are not affected - they are output as-is.
Conversely, if the output is in table format (implicit use of Format-Table), shorter lines are right-padded with spaces to the implied or specified width.
Given that lines can be padded, you shouldn't use something like Out-String -Width ([int]::MaxValue]) to prevent truncation (in fact, you may run out of memory).
Instead, use a reasonably high value such as -Width 255, as #Abraxas000 did in his own answer.
If you wanted to condense the typically multi-line output to a one-liner, use something like the the following, though :
$values += ($value | Out-String).Trim() -replace '\s*\r?\n', '; ' -replace '\s+', ' '
;  was chosen to replace the line breaks here, and line-internal runs of whitespace are normalized to a single space each - whether the results are still readable probably both depends on the specific output format and the eye of the beholder.
To give a concrete example:
> $ht = #{ one = 1; two = 2; three = 3 } # sample hashtable
> $val = "$ht"; "ht: $val" # hashtable is stringified -> type name only
ht: System.Collections.Hashtable
> $val = Out-String -InputObject $ht; "ht: $val" # Out-String creates meaningful representation
ht:
Name Value
---- -----
one 1
two 2
three 3
> ($ht | Out-String).Trim() -replace '\s*\r?\n', '; ' -replace '\s+', ' '
Name Value; ---- -----; one 1; two 2; three 3
#mklement0 Gave me an answer that lead me as close to ideal as I have time for, so go up-vote it!
My code as of posting this:
Function ExpandMultivaluedProperties
{
Param(
[PSObject]$InputObject
)
$results= $InputObject |
ForEach-Object {
$properties = New-Object PSObject
$_.PSObject.Properties |
ForEach-Object {
$propertyName = $_.Name
$propertyValue = $_.Value
If ($propertyValue -NE $NULL) {
$values = #()
ForEach ($value In $propertyValue) {
if (($value.ToString()).StartsWith("System."))
{
$values += (Out-String -InputObject $value -Width 255).Trim()
}
else
{
$values += $value.ToString()
}
}
Add-Member -inputObject $properties NoteProperty -name $propertyName -value "$([String]::Join(";",$values))"
} Else {
Add-Member -inputObject $properties NoteProperty -name $propertyName -value $NULL
}
}
$properties
}
return $results
}
This gives me the one-line value I expect to see for the simpler properties while properly expanding the complex properties.
Even if the property is simple and the value starts with "System.", the data will still be present, just not in the simpler, one-liner format I prefer for CSVs.
Thanks again, #mklement0!

compare two csv using powershell and return matching and non-matching values

I have two csv files, i want to check the users in username.csv matches with userdata.csv copy
to output.csv. If it does not match return the name alone in the output.csv
For Ex: User Data contains 3 columns
UserName,column1,column2
Hari,abc,123
Raj,bca,789
Max,ghi,123
Arul,987,thr
Prasad,bxa,324
username.csv contains usernames
Hari
Rajesh
Output.csv should contain
Hari,abc,123
Rajesh,NA,NA
How to achieve this. Thanks
Sorry for that.
$Path = "C:\PowerShell"
$UserList = Import-Csv -Path "$($path)\UserName.csv"
$UserData = Import-Csv -Path "$($path)\UserData.csv"
foreach ($User in $UserList)
{
ForEach ($Data in $UserData)
{
If($User.Username -eq $Data.UserName)
{
# Process the data
$Data
}
}
}
This returns only matching values. I also need to add the non-matching values in output
file. Thanks.
something like this will work:
$Path = "C:\PowerShell"
$UserList = Import-Csv -Path "$($path)\UserName.csv"
$UserData = Import-Csv -Path "$($path)\UserData.csv"
$UserOutput = #()
ForEach ($name in $UserList)
{
$userMatch = $UserData | where {$_.UserName -eq $name.usernames}
If($userMatch)
{
# Process the data
$UserOutput += New-Object PsObject -Property #{UserName =$name.usernames;column1 =$userMatch.column1;column2 =$userMatch.column2}
}
else
{
$UserOutput += New-Object PsObject -Property #{UserName =$name.usernames;column1 ="NA";column2 ="NA"}
}
}
$UserOutput | ft
It loops through each name in the user list. Line 9 does a search of the userdata CSV for a matching user name if it finds it it adds the user data for that user to the output if no match is found it adds the user name to the output with NA in both columns.
had to change your userList csv:
usernames
Hari
Rajesh
expected output:
UserName column1 column2
-------- ------- -------
Hari abc 123
Rajesh NA NA
I had a similar situation, where I needed a "changed record collection" holding the entire record when the current record was either new or had any changes when compared to the previous record. This was my code:
# get current and previous CSV
$current = Import-Csv -Path $current_file
$previous = Import-Csv -Path $previous_file
# collection with new or changed records
$deltaCollection = New-Object Collections.Generic.List[System.Object]
:forEachCurrent foreach ($row in $current) {
$previousRecord = $previous.Where( { $_.Id -eq $row.Id } )
$hasPreviousRecord = ($null -ne $previousRecord -and $previousRecord.Count -eq 1)
if ($hasPreviousRecord -eq $false) {
$deltaCollection.Add($current)
continue forEachCurrent
}
# check if value of any property is changed when compared to the previous
:forEachCurrentProperty foreach ($property in $current.PSObject.Properties) {
$columnName = $property.Name
$currentValue = if ($null -eq $property.Value) { "" } else { $property.Value }
$previousValue = if ($hasPreviousRecord) { $previousRecord[0]."$columnName" } else { "" }
if ($currentValue -ne $previousValue -or $hasPreviousRecord -eq $false) {
$deltaCollection.Add($currentCenter)
continue forEachCurrentProperty
}
}
}

compare columns in two csv files

With all of the examples out there you would think I could have found my solution. :-)
Anyway, I have two csv files; one with two columns, one with 4. I need to compare one column from each one using powershell. I thought I had it figured out but when I did a compare of my results, it comes back as false when I know it should be true. Here's what I have so far:
$newemp = Import-Csv -Path "C:\Temp\newemp.csv" -Header login_id, lastname, firstname, other | Select-Object "login_id"
$ps = Import-Csv -Path "C:\Temp\Emplid_LoginID.csv" | Select-Object "login id"
If ($newemp -eq $ps)
{
write-host "IDs match" -forgroundcolor green
}
Else
{
write-host "Not all IDs match" -backgroundcolor yellow -foregroundcolor black
}
I had to specifiy headers for the first file because it doesn't have any. What's weird is that I can call each variable to see what it holds and they end up with the same info but for some reason still comes up as false. This occurs even if there is only one row (not counting the header row).
I started to parse them as arrays but wasn't quite sure that was the right thing. What's important is that I compare row1 of the first file with with row1 of the second file. I can't just do a simple -match or -contains.
EDIT: One annoying thing is that the variables seem to hold the header row as well. When I call each one, the header is shown. But if I call both variables, I only see one header but two rows.
I just added the following check but getting the same results (False for everything):
$results = Compare-Object -ReferenceObject $newemp -DifferenceObject $ps -PassThru | ForEach-Object { $_.InputObject }
Using latkin's answer from here I think this would give you the result set you're looking for. As per latkin's comment, the property comparison is redundant for your purposes but I left it in as it's good to know. Additionally the header is specified even for the csv with headers to prevent the header row being included in the comparison.
$newemp = Import-Csv -Path "C:\Temp\_sotemp\Book1.csv" -Header loginid |
Select-Object "loginid"
$ps = Import-Csv -Path "C:\Temp\_sotemp\Book2.csv" -Header loginid |
Select-Object "loginid"
#get list of (imported) CSV properties
$props1 = $newemp | gm -MemberType NoteProperty | select -expand Name | sort
$props2 = $ps | gm -MemberType NoteProperty | select -expand Name | sort
#first check that properties match
#omit this step if you know for sure they will be
if(Compare-Object $props1 $props2){
throw "Properties are not the same! [$props1] [$props2]"
}
#pass properties list to Compare-Object
else{
Compare-Object $newemp $ps -Property $props1
}
In the second line, I see there a space "login id" and the first line doesn't have it. Could that be an issue. Try having the same name for the headers in the .csv files itself. And it works for without providing header or select statements. Below is my experiment based upon your input.
emp.csv
loginid firstname lastname
------------------------------
abc123 John patel
zxy321 Kohn smith
sdf120 Maun scott
tiy123 Dham rye
k2340 Naam mason
lk10j5 Shaan kelso
303sk Doug smith
empids.csv
loginid
-------
abc123
zxy321
sdf120
tiy123
PS C:\>$newemp = Import-csv C:\scripts\emp.csv
PS C:\>$ps = Import-CSV C:\scripts\empids.csv
PS C:\>$results = Compare-Object -ReferenceObject $newemp -DifferenceObject $ps | foreach { $_.InputObject}
Shows the difference objects that are not in $ps
loginid firstname lastname SideIndicator
------- --------- -------- -------------
k2340 Naam mason <=
lk10j5 Shaan kelso <=
303sk Doug smith <=
I am not sure if this is what you are looking for but i have used the PowerShell to do some CSV formatting for myself.
$test = Import-Csv .\Desktop\Vmtools-compare.csv
foreach ($i in $test) {
foreach ($n in $i.name) {
foreach ($m in $test) {
$check = "yes"
if ($n -eq $m.prod) {
$check = "no"
break
}
}
if ($check -ne "no") {$n}
}
}
this is how my excel csv file looks like:
prod name
1 3
2 5
3 8
4 2
5 0
and script outputs this:
8
0
so basically script takes each number under Name column and then checks it against prod column. If the number is there then it won't display else it will display that number.
I have also done it the opposite way:
$test = Import-Csv c:\test.csv
foreach ($i in $test) {
foreach ($n in $i.name) {
foreach ($m in $test) {
$check = "yes"
if ($n -eq $m.prod) {echo $n}
}
}
}
this is how my excel csv looks like:
prod name
1 3
2 5
3 8
4 2
5 0
and script outputs this:
3
5
2
so script shows the matching entries only.
You can play around with the code to look at different columns.