diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 deleted file mode 100644 index 01ae37e..0000000 --- a/Merge-worksheet.ps1 +++ /dev/null @@ -1,538 +0,0 @@ -Function Merge-Worksheet { - <# - .Synopsis - Merges two Worksheets (or other objects) into a single Worksheet with differences marked up. - .Description - The Compare-Worksheet command takes two Worksheets and marks differences in the source document, and optionally outputs a grid showing the changes. - By contrast the Merge-Worksheet command takes the Worksheets and combines them into a single sheet showing the old and new data side by side. - Although it is designed to work with Excel data it can work with arrays of any kind of object; so it can be a merge *of* Worksheets, or a merge *to* a Worksheet. - .Example - Merge-Worksheet "Server54.xlsx" "Server55.xlsx" -WorksheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -show - The workbooks contain audit information for two servers, one sheet contains - a list of services. This command creates a worksheet named "54-55" in a - workbook named "services.xlsx" which shows all the services and their - differences, and opens the new workbook in Excel. - .Example - Merge-Worksheet "Server54.xlsx" "Server55.xlsx" -WorksheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -HideEqual -AddBackgroundColor LightBlue -show - This modifies the previous command to hide the equal rows in the output - sheet and changes the color used to mark rows added to the second file. - .Example - Merge-Worksheet -OutputFile .\j1.xlsx -OutputSheetName test11 -ReferenceObject (dir .\ImportExcel\4.0.7) -DifferenceObject (dir .\ImportExcel\4.0.8) -Property Length -Show - This version compares two directories, and marks what has changed. - Because no "Key" property is given, "Name" is assumed to be the key - and the only other property examined is length. Files which are added - or deleted or have changed size will be highlighed in the output sheet. - Changes to dates or other attributes will be ignored. - .Example - Merge-Worksheet -RefO (dir .\ImportExcel\4.0.7) -DiffO (dir .\ImportExcel\4.0.8) -Pr Length | Out-GridView - This time no file is written and the results - which include all properties, - not just length, are output and sent to Out-Gridview. This version uses - aliases to shorten the parameters, (OutputFileName can be "outFile" and - the Sheet can be"OutSheet"; DifferenceObject & ReferenceObject can be - DiffObject & RefObject respectively). - #> - [cmdletbinding(SupportsShouldProcess=$true)] - Param( - #First Excel file to compare. You can compare two Excel files or two other objects or a reference obhct against a difference file, but not a reference file against an object. - [parameter(ParameterSetName='A',Mandatory=$true,Position=0)] #A = Compare two files default headers - [parameter(ParameterSetName='B',Mandatory=$true,Position=0)] #B = Compare two files user supplied headers - [parameter(ParameterSetName='C',Mandatory=$true,Position=0)] #C = Compare two files headers P1, P2, P3 etc - $Referencefile , - - #Second Excel file to compare. - [parameter(ParameterSetName='A',Mandatory=$true,Position=1)] - [parameter(ParameterSetName='B',Mandatory=$true,Position=1)] - [parameter(ParameterSetName='C',Mandatory=$true,Position=1)] - [parameter(ParameterSetName='E',Mandatory=$true,Position=1)] #D Compare two objects; E = Compare one object one file that uses default headers - [parameter(ParameterSetName='F',Mandatory=$true,Position=1)] #F = Compare one object one file that uses user supplied headers - [parameter(ParameterSetName='G',Mandatory=$true,Position=1)] #G Compare one object one file that uses headers P1, P2, P3 etc - $Differencefile , - - #Name(s) of Worksheets to compare. - [parameter(ParameterSetName='A',Position=2)] #Applies to all sets EXCEPT D which is two objects (no sheets) - [parameter(ParameterSetName='B',Position=2)] - [parameter(ParameterSetName='C',Position=2)] - [parameter(ParameterSetName='E',Position=2)] - [parameter(ParameterSetName='F',Position=2)] - [parameter(ParameterSetName='G',Position=2)] - $WorksheetName = "Sheet1", - - #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. - [parameter(ParameterSetName='A')] #Applies to all sets EXCEPT D which is two objects (no sheets, so no start row ) - [parameter(ParameterSetName='B')] - [parameter(ParameterSetName='C')] - [parameter(ParameterSetName='E')] - [parameter(ParameterSetName='F')] - [parameter(ParameterSetName='G')] - [int]$Startrow = 1, - - #Specifies custom property names to use, instead of the values defined in the column headers of the Start ROw. - [Parameter(ParameterSetName='B',Mandatory=$true)] #Compare object + sheet or 2 sheets with user supplied headers - [Parameter(ParameterSetName='F',Mandatory=$true)] - [String[]]$Headername, - - #Automatically generate property names (P1, P2, P3, ..) instead of using the values the top row of the sheet. - [Parameter(ParameterSetName='C',Mandatory=$true)] #Compare object + sheet or 2 sheets with headers of P1, P2, P3 ... - [Parameter(ParameterSetName='G',Mandatory=$true)] - [switch]$NoHeader, - - #Reference object to compare if a Worksheet is NOT being used. Reference object can combine with a difference sheet or difference object - [parameter(ParameterSetName='D',Mandatory=$true)] - [parameter(ParameterSetName='E',Mandatory=$true)] - [parameter(ParameterSetName='F',Mandatory=$true)] - [parameter(ParameterSetName='G',Mandatory=$true)] - [Alias('RefObject')] - $ReferenceObject , - #Difference object to compare if a Worksheet is NOT being used for either half. Can't have a reference sheet and difference object. - [parameter(ParameterSetName='D',Mandatory=$true,Position=1)] - [Alias('DiffObject')] - $DifferenceObject , - [parameter(ParameterSetName='D',Position=2)] - [parameter(ParameterSetName='E',Position=2)] - [parameter(ParameterSetName='F',Position=2)] - [parameter(ParameterSetName='G',Position=2)] - #If there isn't a filename to use to label data from the "Difference" side, DiffPrefix is used, it defaults to "=>" - $DiffPrefix = "=>" , - #File to hold merged data. - [parameter(Position=3)] - [Alias('OutFile')] - $OutputFile , - #Name of Worksheet to output - if none specified will use the reference Worksheet name. - [parameter(Position=4)] - [Alias('OutSheet')] - $OutputSheetName = "Sheet1", - #Properties to include in the DIFF - supports wildcards, default is "*". - $Property = "*" , - #Properties to exclude from the the search - supports wildcards. - $ExcludeProperty , - #Name of a column which is unique used to pair up rows from the refence and difference side, default is "Name". - $Key = "Name" , - #Sets the font color for the "key" field; this means you can filter by color to get only changed rows. - $KeyFontColor = [System.Drawing.Color]::DarkRed , - #Sets the background color for changed rows. - $ChangeBackgroundColor = [System.Drawing.Color]::Orange, - #Sets the background color for rows in the reference but deleted from the difference sheet. - $DeleteBackgroundColor = [System.Drawing.Color]::LightPink, - #Sets the background color for rows not in the reference but added to the difference sheet. - $AddBackgroundColor = [System.Drawing.Color]::PaleGreen, - #if specified, hides the rows in the spreadsheet that are equal and only shows changes, added or deleted rows. - [switch]$HideEqual , - #If specified, outputs the data to the pipeline (you can add -WhatIf so the command only outputs to the pipeline). - [switch]$Passthru , - #If specified, opens the output workbook. - [Switch]$Show - ) - - #region Read Excel data - if ($Differencefile -is [System.IO.FileInfo]) {$Differencefile = $Differencefile.FullName} - if ($Referencefile -is [System.IO.FileInfo]) {$Referencefile = $Referencefile.FullName} - if ($Referencefile -and $Differencefile) { - #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 must have two different Worksheet names. If we have two files $WorksheetName can be 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) {$Worksheet2 = $DiffPrefix = $WorksheetName[1] ; $Worksheet1 = $WorksheetName[0] ; } - elseif ($WorksheetName -is [string]) {$Worksheet2 = $Worksheet1 = $WorksheetName ; - $DiffPrefix = (Split-Path -Path $Differencefile -Leaf) -replace "\.xlsx$","" } - 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 { - $ReferenceObject = Import-Excel -Path $Referencefile -WorksheetName $Worksheet1 @params - $DifferenceObject = Import-Excel -Path $Differencefile -WorksheetName $Worksheet2 @Params - } - Catch {Write-Warning -Message "Could not read the Worksheet from $Referencefile::$Worksheet1 and/or $Differencefile::$Worksheet2." ; return } - if ($NoHeader) {$firstDataRow = $Startrow } else {$firstDataRow = $Startrow + 1} - } - elseif ( $Differencefile) { - if ($WorksheetName -isnot [string]) {Write-Warning -Message "You must provide a single Worksheet name." ; return } - $params = @{WorksheetName=$WorksheetName; Path=$Differencefile; ErrorAction=[System.Management.Automation.ActionPreference]::Stop } - foreach ($p in @("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} - try {$DifferenceObject = Import-Excel @Params } - Catch {Write-Warning -Message "Could not read the Worksheet '$WorksheetName' from $Differencefile::$WorksheetName." ; return } - if ($DiffPrefix -eq "=>" ) { - $DiffPrefix = (Split-Path -Path $Differencefile -Leaf) -replace "\.xlsx$","" - } - if ($NoHeader) {$firstDataRow = $Startrow } else {$firstDataRow = $Startrow + 1} - } - else { $firstDataRow = 1 } - #endregion - - #region Set lists of properties and row numbers - #Make a list of properties/headings using the Property (default "*") and ExcludeProperty parameters - $propList = @() - $DifferenceObject = $DifferenceObject | Update-FirstObjectProperties - $headings = $DifferenceObject[0].psobject.Properties.Name # This preserves the sequence - using get-member would sort them alphabetically! There may be extra properties in - if ($NoHeader -and "Name" -eq $Key) {$Key = "p1"} - if ($headings -notcontains $Key -and - ('*' -ne $Key)) {Write-Warning -Message "You need to specify one of the headings in the sheet '$Worksheet1' as a key." ; return } - foreach ($p in $Property) { $propList += ($headings.where({$_ -like $p}) )} - foreach ($p in $ExcludeProperty) { $propList = $propList.where({$_ -notlike $p}) } - if (($propList -notcontains $Key) -and - ('*' -ne $Key)) { $propList += $Key} #If $key isn't one of the headings we will have bailed by now - $propList = $propList | Select-Object -Unique #so, prolist must contain at least $key if nothing else - - #If key is "*" we treat it differently , and we will create a script property which concatenates all the Properties in $Proplist - $ConCatblock = [scriptblock]::Create( ($proplist | ForEach-Object {'$this."' + $_ + '"'}) -join " + ") - - #Build the list of the properties to output, in order. - $diffpart = @() - $refpart = @() - foreach ($p in $proplist.Where({$key -ne $_}) ) {$refPart += $p ; $diffPart += "$DiffPrefix $p" } - $lastRefColNo = $proplist.count - $FirstDiffColNo = $lastRefColNo + 1 - - if ($key -ne '*') { - $outputProps = @($key) + $refpart + $diffpart - #If we are using a single column as the key, don't duplicate it, so the last difference column will be A if there is one property, C if there are two, E if there are 3 - $lastDiffColNo = (2 * $proplist.count) - 1 - } - else { - $outputProps = @( ) + $refpart + $diffpart - #If we not using a single column as a key all columns are duplicated so, the Last difference column will be B if there is one property, D if there are two, F if there are 3 - $lastDiffColNo = (2 * $proplist.count ) - } - - #Add RowNumber to every row - #If one sheet has extra rows we can get a single "==" result from compare, with the row from the reference sheet, but - #the row in the other sheet might be different so we will look up the row number from the key field - build a hash table for that here - #If we have "*" as the key ad the script property to concatenate the [selected] properties. - - $Rowhash = @{} - $rowNo = $firstDataRow - foreach ($row in $ReferenceObject) { - if ($null -eq $row._row) {Add-Member -InputObject $row -MemberType NoteProperty -Value ($rowNo ++) -Name "_Row" } - else {$rowNo++ } - if ($Key -eq '*' ) {Add-Member -InputObject $row -MemberType ScriptProperty -Value $ConCatblock -Name "_All" } - } - $rowNo = $firstDataRow - foreach ($row in $DifferenceObject) { - Add-Member -InputObject $row -MemberType NoteProperty -Value $rowNo -Name "$DiffPrefix Row" -Force - if ($Key -eq '*' ) { - Add-Member -InputObject $row -MemberType ScriptProperty -Value $ConCatblock -Name "_All" - $Rowhash[$row._All] = $rowNo - } - else {$Rowhash[$row.$key] = $rowNo } - $rowNo ++ - } - if ($DifferenceObject.count -gt $Rowhash.Keys.Count) { - Write-Warning -Message "Difference object has $($DifferenceObject.Count) rows; but only $($Rowhash.keys.count) unique keys" - } - if ($Key -eq '*') {$key = "_ALL"} - #endregion - #We need to know all the properties we've met on the objects we've diffed - $eDiffProps = [ordered]@{} - #When we do a compare object changes will result in two rows so we group them and join them together. - $expandedDiff = Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject -Property $propList -PassThru -IncludeEqual | - Group-Object -Property $key | ForEach-Object { - #The value of the key column is the name of the Group. - $keyval = $_.name - #we're going to create a custom object from a hash table. - $hash = [ordered]@{} - foreach ($result in $_.Group) { - if ($result.SideIndicator -ne "=>") {$hash["_Row"] = $result._Row } - elseif (-not $hash["$DiffPrefix Row"]) {$hash["_Row"] = "" } - #if we have already set the side, this must be the second record, so set side to indicate "changed"; if we got two "Same" indicators we may have a classh of keys - if ($hash.Side) { - if ($hash.Side -eq $result.SideIndicator) {Write-Warning -Message "'$keyval' may be a duplicate."} - $hash.Side = "<>" - } - else {$hash["Side"] = $result.SideIndicator} - switch ($hash.side) { - '==' { $hash["$DiffPrefix is"] = 'Same' } - '=>' { $hash["$DiffPrefix is"] = 'Added' } - '<>' { if (-not $hash["_Row"]) { - $hash["$DiffPrefix is"] = 'Added' - } - else { - $hash["$DiffPrefix is"] = 'Changed' - } - } - '<=' { $hash["$DiffPrefix is"] = 'Removed'} - } - #find the number of the row in the the "difference" object which has this key. If it is the object is only in the reference this will be blank. - $hash["$DiffPrefix Row"] = $Rowhash[$keyval] - $hash[$key] = $keyval - #Create FieldName and/or =>FieldName columns - foreach ($p in $result.psobject.Properties.name.where({$_ -ne $key -and $_ -ne "SideIndicator" -and $_ -ne "$DiffPrefix Row" })) { - if ($result.SideIndicator -eq "==" -and $p -in $propList) - {$hash[("$p")] = $hash[("$DiffPrefix $p")] = $result.$P} - elseif ($result.SideIndicator -eq "==" -or $result.SideIndicator -eq "<=") - {$hash[("$p")] = $result.$P} - elseif ($result.SideIndicator -eq "=>") { $hash[("$DiffPrefix $p")] = $result.$P} - } - } - - foreach ($k in $hash.keys) {$eDiffProps[$k] = $true} - [Pscustomobject]$hash - } | Sort-Object -Property "_row" - - #Already sorted by reference row number, fill in any blanks in the difference-row column. - for ($i = 1; $i -lt $expandedDiff.Count; $i++) {if (-not $expandedDiff[$i]."$DiffPrefix Row") {$expandedDiff[$i]."$DiffPrefix Row" = $expandedDiff[$i-1]."$DiffPrefix Row" } } - - #Now re-Sort by difference row number, and fill in any blanks in the reference-row column. - $expandedDiff = $expandedDiff | Sort-Object -Property "$DiffPrefix Row" - for ($i = 1; $i -lt $expandedDiff.Count; $i++) {if (-not $expandedDiff[$i]."_Row") {$expandedDiff[$i]."_Row" = $expandedDiff[$i-1]."_Row" } } - - $AllProps = @("_Row") + $OutputProps + $eDiffProps.keys.where({$_ -notin ($outputProps + @("_row","side","SideIndicator","_ALL" ))}) - - if ($PassThru -or -not $OutputFile) {return ($expandedDiff | Select-Object -Property $allprops | Sort-Object -Property "_row", "$DiffPrefix Row" ) } - elseif ($PSCmdlet.ShouldProcess($OutputFile,"Write Output to Excel file")) { - $expandedDiff = $expandedDiff | Sort-Object -Property "_row", "$DiffPrefix Row" - $xl = $expandedDiff | Select-Object -Property $OutputProps | Update-FirstObjectProperties | - Export-Excel -Path $OutputFile -Worksheetname $OutputSheetName -FreezeTopRow -BoldTopRow -AutoSize -AutoFilter -PassThru - $ws = $xl.Workbook.Worksheets[$OutputSheetName] - for ($i = 0; $i -lt $expandedDiff.Count; $i++ ) { - if ( $expandedDiff[$i].side -ne "==" ) { - Set-ExcelRange -Worksheet $ws -Range ("A" + ($i + 2 )) -FontColor $KeyFontColor - } - elseif ( $HideEqual ) {$ws.row($i+2).hidden = $true } - if ( $expandedDiff[$i].side -eq "<>" ) { - $range = $ws.Dimension -replace "\d+", ($i + 2 ) - Set-ExcelRange -Worksheet $ws -Range $range -BackgroundColor $ChangeBackgroundColor - } - elseif ( $expandedDiff[$i].side -eq "<=" ) { - $rangeR1C1 = "R[{0}]C[1]:R[{0}]C[{1}]" -f ($i + 2 ) , $lastRefColNo - $range = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1($rangeR1C1,0,0) - Set-ExcelRange -Worksheet $ws -Range $range -BackgroundColor $DeleteBackgroundColor - } - elseif ( $expandedDiff[$i].side -eq "=>" ) { - if ($propList.count -gt 1) { - $rangeR1C1 = "R[{0}]C[{1}]:R[{0}]C[{2}]" -f ($i + 2 ) , $FirstDiffColNo , $lastDiffColNo - $range = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1($rangeR1C1,0,0) - Set-ExcelRange -Worksheet $ws -Range $range -BackgroundColor $AddBackgroundColor - } - Set-ExcelRange -Worksheet $ws -Range ("A" + ($i + 2 )) -BackgroundColor $AddBackgroundColor - } - } - Close-ExcelPackage -ExcelPackage $xl -Show:$Show - } -} - -Function Merge-MultipleSheets { - <# - .Synopsis - Merges Worksheets into a single Worksheet with differences marked up. - .Description - The Merge Worksheet command combines two sheets. Merge-MultipleSheets is - designed to merge more than two. So if asked to merge sheets A,B,C which - contain Services, with a Name, Displayname and Start mode, where "Name" is - treated as the key, Merge-MultipleSheets calls Merge-Worksheet to merge - "Name", "Displayname" and "Startmode" from sheets A and C; the result has - column headings "_Row", "Name", "DisplayName", "Startmode", "C-DisplayName", - "C-StartMode", "C-Is" and "C-Row". - Merge-MultipleSheets then calls Merge-Worksheet again passing it the - intermediate result and sheet B, comparing "Name", "Displayname" and - "Start mode" columns on each side, and gets a result with columns "_Row", - "Name", "DisplayName", "Startmode", "B-DisplayName", "B-StartMode", "B-Is", - "B-Row", "C-DisplayName", "C-StartMode", "C-Is" and "C-Row". Any columns on - the "reference" side which are not used in the comparison are added on the - right, which is why we compare the sheets in reverse order. - - The "Is" columns hold "Same", "Added", "Removed" or "Changed" and is used for - conditional formatting in the output sheet (these columns are hidden by default), - and when the data is written to Excel the "reference" columns, in this case - "DisplayName" and "Start" are renamed to reflect their source, so become - "A-DisplayName" and "A-Start". - - Conditional formatting is also applied to the Key column ("Name" in this - case) so the view can be filtered to rows with changes by filtering this - column on color. - - Note: the processing order can affect what is seen as a change. For example - if there is an extra item in sheet B in the example above, Sheet C will be - processed and that row and will not be seen to be missing. When sheet B is - processed it is marked as an addition, and the conditional formatting marks - the entries from sheet A to show that a values were added in at least one - sheet. However if Sheet B is the reference sheet, A and C will be seen to - have an item removed; and if B is processed before C, the extra item is - known when C is processed and so C is considered to be missing that item. - .Example - dir Server*.xlsx | Merge-MulipleSheets -WorksheetName Services -OutputFile Test2.xlsx -OutputSheetName Services -Show - Here we are auditing servers and each one has a workbook in the current - directory which contains a "Services" Worksheet (the result of - Get-WmiObject -Class win32_service | Select-Object -Property Name, Displayname, Startmode) - No key is specified so the key is assumed to be the "Name" column. - The files are merged and the result is opened on completion. - .Example - dir Serv*.xlsx | Merge-MulipleSheets -WorksheetName Software -Key "*" -ExcludeProperty Install* -OutputFile Test2.xlsx -OutputSheetName Software -Show - The server audit files in the previous example also have "Software" worksheet, - but no single field on that sheet works as a key. Specifying "*" for the key - produces a compound key using all non-excluded fields (and the installation - date and file location are excluded). - .Example - Merge-MulipleSheets -Path hotfixes.xlsx -WorksheetName Serv* -Key hotfixid -OutputFile test2.xlsx -OutputSheetName hotfixes -HideRowNumbers -Show - This time all the servers have written their hotfix information to their own - worksheets in a shared Excel workbook named "Hotfixes.xlsx" (the information was - obtained by running Get-Hotfix | Sort-Object -Property description,hotfixid | Select-Object -Property Description,HotfixID) - This ignores any sheets which are not named "Serv*", and uses the HotfixID as - the key; in this version the row numbers are hidden. - #> - [cmdletbinding()] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives when initializing variable in begin block")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification="MultipleSheet would be incorrect")] - #[Alias("Merge-MulipleSheets")] #There was a spelling error in the first release. This was there to ensure things didn't break but intelisense gave the alias first. - param ( - #Paths to the files to be merged. Files are also accepted - [Parameter(Mandatory=$true,ValueFromPipeline=$true)] - $Path , - #The row from where we start to import data, all rows above the Start row are disregarded. By default this is the first row. - [int]$Startrow = 1, - - #Specifies custom property names to use, instead of the values defined in the column headers of the Start row. - [String[]]$Headername, - - #If specified, property names will be automatically generated (P1, P2, P3, ..) instead of using the values from the start row. - [switch]$NoHeader, - - #Name(s) of Worksheets to compare. - $WorksheetName = "Sheet1", - #File to write output to. - [Alias('OutFile')] - $OutputFile = ".\temp.xlsx", - #Name of Worksheet to output - if none specified will use the reference Worksheet name. - [Alias('OutSheet')] - $OutputSheetName = "Sheet1", - #Properties to include in the comparison - supports wildcards, default is "*". - $Property = "*" , - #Properties to exclude from the the comparison - supports wildcards. - $ExcludeProperty , - #Name of a column which is unique used to pair up rows from the reference and difference sides, default is "Name". - $Key = "Name" , - #Sets the font color for the Key field; this means you can filter by color to get only changed rows. - $KeyFontColor = [System.Drawing.Color]::Red, - #Sets the background color for changed rows. - $ChangeBackgroundColor = [System.Drawing.Color]::Orange, - #Sets the background color for rows in the reference but deleted from the difference sheet. - $DeleteBackgroundColor = [System.Drawing.Color]::LightPink, - #Sets the background color for rows not in the reference but added to the difference sheet. - $AddBackgroundColor = [System.Drawing.Color]::Orange, - #If specified, hides the columns in the spreadsheet that contain the row numbers. - [switch]$HideRowNumbers , - #If specified, outputs the data to the pipeline (you can add -whatif so it the command only outputs to the pipeline). - [switch]$Passthru , - #If specified, opens the output workbook. - [Switch]$Show - ) - begin { $filestoProcess = @() } - process { $filestoProcess += $Path} - end { - if ($filestoProcess.Count -eq 1 -and $WorksheetName -match '\*') { - Write-Progress -Activity "Merging sheets" -CurrentOperation "Expanding * to names of sheets in $($filestoProcess[0]). " - $excel = Open-ExcelPackage -Path $filestoProcess - $WorksheetName = $excel.Workbook.Worksheets.Name.where({$_ -like $WorksheetName}) - Close-ExcelPackage -NoSave -ExcelPackage $excel - } - - #Merge identically named sheets in different work books; - if ($filestoProcess.Count -ge 2 -and $WorksheetName -is "string" ) { - Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty','WorksheetName' -ErrorAction SilentlyContinue | - Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} - - Write-Progress -Activity "Merging sheets" -CurrentOperation "comparing '$WorksheetName' in $($filestoProcess[-1]) against $($filestoProcess[0]). " - $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[-1] - $nextFileNo = 2 - while ($nextFileNo -lt $filestoProcess.count -and $merged) { - Write-Progress -Activity "Merging sheets" -CurrentOperation "comparing '$WorksheetName' in $($filestoProcess[-$nextFileNo]) against $($filestoProcess[0]). " - $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[-$nextFileNo] - $nextFileNo ++ - - } - } - #Merge different sheets from one workbook - elseif ($filestoProcess.Count -eq 1 -and $WorksheetName.Count -ge 2 ) { - Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty' -ErrorAction SilentlyContinue | - Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} - - Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($WorksheetName[-1]) against $($WorksheetName[0]). " - $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[0] -WorksheetName $WorksheetName[0,-1] - $nextSheetNo = 2 - while ($nextSheetNo -lt $WorksheetName.count -and $merged) { - Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($WorksheetName[-$nextSheetNo]) against $($WorksheetName[0]). " - $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[0] -WorksheetName $WorksheetName[-$nextSheetNo] -DiffPrefix $WorksheetName[-$nextSheetNo] - $nextSheetNo ++ - } - } - #We either need one Worksheet name and many files or one file and many sheets. - else { Write-Warning -Message "Need at least two files to process" ; return } - #if the process didn't return data then abandon now. - if (-not $merged) {Write-Warning -Message "The merge operation did not return any data."; return } - - $orderByProperties = $merged[0].psobject.properties.where({$_.name -match "row$"}).name - Write-Progress -Activity "Merging sheets" -CurrentOperation "creating output sheet '$OutputSheetName' in $OutputFile" - $excel = $merged | Sort-Object -Property $orderByProperties | - Export-Excel -Path $OutputFile -Worksheetname $OutputSheetName -ClearSheet -BoldTopRow -AutoFilter -PassThru - $sheet = $excel.Workbook.Worksheets[$OutputSheetName] - - #We will put in a conditional format for "if all the others are not flagged as 'same'" to mark rows where something is added, removed or changed - $sameChecks = @() - - #All the 'difference' columns in the sheet are labeled with the file they came from, 'reference' columns need their - #headers prefixed with the ref file name, $colnames is the basis of a regular expression to identify what should have $refPrefix appended - $colNames = @("^_Row$") - if ($key -ne "*") - {$colnames += "^$Key$"} - if ($filesToProcess.Count -ge 2) { - $refPrefix = (Split-Path -Path $filestoProcess[0] -Leaf) -replace "\.xlsx$"," " - } - else {$refPrefix = $WorksheetName[0] } - Write-Progress -Activity "Merging sheets" -CurrentOperation "applying formatting to sheet '$OutputSheetName' in $OutputFile" - #Find the column headings which are in the form "diffFile is"; which will hold 'Same', 'Added' or 'Changed' - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { - #Work leftwards across the headings applying conditional formatting which says - # 'Format this cell if the "IS" column has a value of ...' until you find a heading which doesn't have the prefix. - $prefix = $cell.value -replace "\sIS$","" - $columnNo = $cell.start.Column -1 - $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) - while ($sheet.cells[$cellAddr].value -match $prefix) { - $condFormattingParams = @{RuleType='Expression'; BackgroundPattern='Solid'; Worksheet=$sheet; StopIfTrue=$true; Range=$([OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[1]C[$columnNo]:R[1048576]C[$columnNo]",0,0)) } - Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Added"' ) -BackgroundColor $AddBackgroundColor - Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Changed"') -BackgroundColor $ChangeBackgroundColor - Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Removed"') -BackgroundColor $DeleteBackgroundColor - $columnNo -- - $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) - } - #build up a list of prefixes in $colnames - we'll use that to set headers on rows from the reference file; and build up the "if the 'is' cell isn't same" list - $colNames += $prefix - $sameChecks += (($cell.Address -replace "1","2") +'<>"Same"') - } - - #For all the columns which don't match one of the Diff-file prefixes or "_Row" or the 'Key' columnn name; add the reference file prefix to their header. - $nameRegex = $colNames -Join '|' - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -Notmatch $nameRegex}) ) { - $cell.Value = $refPrefix + $cell.Value - $condFormattingParams = @{RuleType='Expression'; BackgroundPattern='Solid'; Worksheet=$sheet; StopIfTrue=$true; Range=[OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[2]C[$($cell.start.column)]:R[1048576]C[$($cell.start.column)]",0,0)} - Add-ConditionalFormatting @condFormattingParams -ConditionValue ("OR(" +(($sameChecks -join ",") -replace '<>"Same"','="Added"' ) +")" ) -BackgroundColor $DeleteBackgroundColor - Add-ConditionalFormatting @condFormattingParams -ConditionValue ("AND(" +(($sameChecks -join ",") -replace '<>"Same"','="Changed"') +")" ) -BackgroundColor $ChangeBackgroundColor - } - #We've made a bunch of things wider so now is the time to autofit columns. Any hiding has to come AFTER this, because it unhides things - $sheet.Cells.AutoFitColumns() - - #if we have a key field (we didn't concatenate all fields) use what we built up in $sameChecks to apply conditional formatting to it (Row no will be in column A, Key in Column B) - if ($Key -ne '*') { - Add-ConditionalFormatting -Worksheet $sheet -Range "B2:B1048576" -ForeGroundColor $KeyFontColor -BackgroundPattern 'None' -RuleType Expression -ConditionValue ("OR(" +($sameChecks -join ",") +")" ) - $sheet.view.FreezePanes(2, 3) - } - else {$sheet.view.FreezePanes(2, 2) } - #Go back over the headings to find and hide the "is" columns; - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { - $sheet.Column($cell.start.Column).HIDDEN = $true - } - - #If specified, look over the headings for "row" and hide the columns which say "this was in row such-and-such" - if ($HideRowNumbers) { - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "Row$"}) ) { - $sheet.Column($cell.start.Column).HIDDEN = $true - } - } - if ($Passthru) {$excel} - else {Close-ExcelPackage -ExcelPackage $excel -Show:$Show} - Write-Progress -Activity "Merging sheets" -Completed - } -} diff --git a/Send-SqlDataToExcel.ps1 b/Send-SqlDataToExcel.ps1 deleted file mode 100644 index 874ee87..0000000 --- a/Send-SqlDataToExcel.ps1 +++ /dev/null @@ -1,175 +0,0 @@ -Function Send-SQLDataToExcel { - <# - .SYNOPSIS - Inserts a DataTable - returned by a SQL query - into an ExcelSheet - .DESCRIPTION - This command takes a SQL statement and run it against a database connection; for the connection it accepts either - * an object representing a session with a SQL server or ODBC database, or - * a connection string to make a session (if -MSSQLServer is specified it uses the SQL Native client, - and -Connection can be a server name instead of a detailed connection string. Without this switch it uses ODBC) - The command takes all the parameters of Export-Excel, except for -InputObject (alias TargetData); after - fetching the data it calls Export-Excel with the data as the value of InputParameter and whichever of - Export-Excel's parameters it was passed; for details of these parameters see the help for Export-Excel. - .PARAMETER Session - An active ODBC Connection or SQL connection object representing a session with a database which will be queried to get the data . - .PARAMETER Connection - A database connection string to be used to create a database session; either - * A Data source name written in the form DSN=ODBC_Data_Source_Name, or - * A full ODBC or SQL Native Client Connection string, or - * The name of a SQL server. - .PARAMETER MSSQLServer - Specifies the connection string is for SQL server, not ODBC. - .PARAMETER SQL - The SQL query to run against the session which was passed in -Session or set up from -Connection. - .PARAMETER Database - Switches to a specific database on a SQL server. - .PARAMETER QueryTimeout - Override the default query time of 30 seconds. - .PARAMETER DataTable - A System.Data.DataTable object containing the data to be inserted into the spreadsheet without running a query. - This remains supported to avoid breaking older scripts, but if you have a DataTable object you can pass the it - into Export-Excel using -InputObject. - .EXAMPLE - C:\> Send-SQLDataToExcel -MsSQLserver -Connection localhost -SQL "select name,type,type_desc from [master].[sys].[all_objects]" -Path .\temp.xlsx -WorkSheetname master -AutoSize -FreezeTopRow -AutoFilter -BoldTopRow - - Connects to the local SQL server and selects 3 columns from [Sys].[all_objects] and exports then to a sheet named master with some basic header management - .EXAMPLE - C:\> $dbPath = 'C:\Users\James\Documents\Database1.accdb' - C:\> $Connection = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=$dbPath;" - C:\> $SQL="SELECT top 25 Name,Length From TestData ORDER BY Length DESC" - - C:\> Send-SQLDataToExcel -Connection $connection -SQL $sql -path .\demo1.xlsx -WorkSheetname "Sizes" -AutoSize - - This creates an ODBC connection string to read from an Access file and a SQL Statement to extracts data from it, - and sends the resulting data to a new worksheet - - .EXAMPLE - C:\> $dbPath = 'C:\users\James\Documents\f1Results.xlsx' - C:\> $Connection = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};Dbq=$dbPath;" - C:\> $SQL="SELECT top 25 DriverName, Count(RaceDate) as Races, Count(Win) as Wins, Count(Pole) as Poles, Count(FastestLap) as Fastlaps " + - " FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC" - - C:\> Send-SQLDataToExcel -Connection $connection -SQL $sql -path .\demo2.xlsx -WorkSheetname "Winners" -AutoSize -AutoNameRange -ConditionalFormat @{DataBarColor="Blue"; Range="Wins"} - - Similar to the previous example this creates a connection string, this time for an Excel file, and runs - a SQL statement to get a list of motor-racing results, outputting the resulting data to a new spreadsheet. - The spreadsheet is formatted and a data bar added to show make the drivers' wins clearer. - (the F1 results database is available from https://1drv.ms/x/s!AhfYu7-CJv4ehNdZWxJE9LMAX_N5sg ) - .EXAMPLE - C:\> $dbPath = 'C:\users\James\Documents\f1Results.xlsx' - C:\> $SQL = "SELECT top 25 DriverName, Count(RaceDate) as Races, Count(Win) as Wins, Count(Pole) as Poles, Count(FastestLap) as Fastlaps " + - " FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC" - C:\> $null = Get-SQL -Session F1 -excel -Connection $dbPath -sql $sql -OutputVariable Table - - C:\> Send-SQLDataToExcel -DataTable $Table -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -autosize -TableName winners -TableStyle Light6 -show - - This uses Get-SQL (at least V1.1 - download from the PowerShell gallery with Install-Module -Name GetSQL - - note the function is Get-SQL the module is GetSQL without the "-" ) - Get-SQL simplify making database connections and building /submitting SQL statements. - Here Get-SQL uses the same SQL statement as before; -OutputVariable leaves a System.Data.DataTable object in $table - and Send-SQLDataToExcel puts $table into the worksheet and sets it as an Excel table. - The command is equivalent to running - C:\> Export-Excel -inputObject $Table -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -autosize -TableName winners -TableStyle Light6 -show - This is quicker than using - C:\> Get-SQL | export-excel -ExcludeProperty rowerror,rowstate,table,itemarray,haserrors - (the F1 results database is available from https://1drv.ms/x/s!AhfYu7-CJv4ehNdZWxJE9LMAX_N5sg ) - .EXAMPLE - C:\> $SQL = "SELECT top 25 DriverName, Count(Win) as Wins FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC" - C:\> Send-SQLDataToExcel -Session $DbSessions["f1"] -SQL $sql -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -ClearSheet -autosize -ColumnChart - - Like the previous example, this uses Get-SQL (download from the gallery with Install-Module -Name GetSQL). - It uses the database session which Get-SQL created, rather than an ODBC connection string. - The Session parameter can either be a object (as shown here), or the name used by Get-SQL ("F1" in this case). - Here the data is presented as a quick chart. - .EXAMPLE - C:\> Send-SQLDataToExcel -path .\demo4.xlsx -WorkSheetname "LR" -Connection "DSN=LR" -sql "SELECT name AS CollectionName FROM AgLibraryCollection Collection ORDER BY CollectionName" - - This example uses an Existing ODBC datasource name "LR" which maps to an adobe lightroom database and gets a list of collection names into a worksheet - - .Link - Export-Excel - #> - [CmdletBinding(DefaultParameterSetName="none")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification="Allowed to use DBSessions Global variable from GETSQL Module")] - - param ( - [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] - [Parameter(ParameterSetName="ODBCConnection", Mandatory=$true)] - $Connection, - [Parameter(ParameterSetName="ExistingSession", Mandatory=$true)] - $Session, - [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] - [switch]$MsSQLserver, - [Parameter(ParameterSetName="SQLConnection")] - [String]$DataBase, - [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] - [Parameter(ParameterSetName="ODBCConnection", Mandatory=$true)] - [Parameter(ParameterSetName="ExistingSession", Mandatory=$true)] - [string]$SQL, - [int]$QueryTimeout, - [Parameter(ParameterSetName="Pre-FetchedData", Mandatory=$true)] - [System.Data.DataTable]$DataTable - ) -#Import the parameters from Export-Excel, we will pass InputObject, and we have the common parameters so exclude those, -#and re-write the [Parmameter] attribute on each one to avoid parameterSetName here competing with the settings in Export excel. -#The down side of this that impossible parameter combinations won't be filtered out and need to be caught later. - DynamicParam { - $ParameterAttribute = "System.Management.Automation.ParameterAttribute" - $RuntimeDefinedParam = "System.Management.Automation.RuntimeDefinedParameter" - $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary - $attributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] - $attributeCollection.Add((New-Object -TypeName $ParameterAttribute -Property @{ ParameterSetName = "__AllParameterSets" ;Mandatory = $false})) - foreach ($P in (Get-Command -Name Export-Excel).Parameters.values.where({$_.name -notmatch 'Verbose|Debug|Action$|Variable$|Buffer$|TargetData$|InputObject$'})) { - $paramDictionary.Add($p.Name, (New-Object -TypeName $RuntimeDefinedParam -ArgumentList $p.name, $p.ParameterType, $attributeCollection ) ) - } - return $paramDictionary - } - process { - #Dynamic params mean we can get passed parameter combination Export-Excel will reject, so throw here, rather than get data and then have Export-Excel error. - if ($PSBoundParameters.Path -and $PSBoundParameters.ExcelPackage) { - throw 'Parameter error: you cannot specify both a path and an Excel Package.' - return - } - if ($PSBoundParameters.AutoFilter -and ($PSBoundParameters.TableName -or $PSBoundParameters.TableStyle)) { - Write-Warning "Tables are automatically auto-filtered, -AutoFilter will be ignored" - $null = $PSBoundParameters.Remove('AutoFilter') - } - #We were either given a session object or a connection string (with, optionally a MSSQLServer parameter) - #If we got -MSSQLServer, create a SQL connection, if we didn't but we got -Connection create an ODBC connection - if ($MsSQLserver -and $Connection) { - if ($Connection -notmatch '=') {$Connection = "server=$Connection;trusted_connection=true;timeout=60"} - $Session = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $Connection - if ($Session.State -ne 'Open') {$Session.Open()} - if ($DataBase) {$Session.ChangeDatabase($DataBase) } - } - elseif ($Connection) { - $Session = New-Object -TypeName System.Data.Odbc.OdbcConnection -ArgumentList $Connection ; $Session.ConnectionTimeout = 30 - } - if ($Session) { - #A session was either passed in or just created. If it's a SQL one make a SQL DataAdapter, otherwise make an ODBC one - if ($Session -is [String] -and $Global:DbSessions[$Session]) {$Session = $Global:DbSessions[$Session]} - if ($Session.GetType().name -match "SqlConnection") { - $dataAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList ( - New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $SQL, $Session) - } - else { - $dataAdapter = New-Object -TypeName System.Data.Odbc.OdbcDataAdapter -ArgumentList ( - New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $SQL, $Session ) - } - if ($QueryTimeout) {$dataAdapter.SelectCommand.CommandTimeout = $QueryTimeout} - - #Both adapter types output the same kind of table, create one and fill it from the adapter - $DataTable = New-Object -TypeName System.Data.DataTable - $rowCount = $dataAdapter.fill($dataTable) - Write-Verbose -Message "Query returned $rowCount row(s)" - } - if ($DataTable.Rows.Count) { - #Call export-excel removing parameters which relate to the SQL query, and keeping the rest. - 'Connection' , 'Database' , 'Session' , 'MsSQLserver' , 'SQL' , 'DataTable' , 'QueryTimeout' | ForEach-Object {$null = $PSBoundParameters.Remove($_) } - Export-Excel @PSBoundParameters -InputObject $DataTable - } - else {Write-Warning -Message ' No Data to insert.' } - #If we were passed a connection and opened a session, close that session. - if ($Connection) {$Session.close() } - } -} \ No newline at end of file diff --git a/compare-worksheet.ps1 b/compare-worksheet.ps1 deleted file mode 100644 index fb8edf6..0000000 --- a/compare-worksheet.ps1 +++ /dev/null @@ -1,286 +0,0 @@ -Function Compare-WorkSheet { -<# - .Synopsis - Compares two worksheets and shows the differences. - .Description - This command takes two file names, one or two worksheet names and a name - for a "key" column. It reads the worksheet from each file and decides the - column names and builds a hashtable of the key-column values and the - rows in which they appear. - It then uses PowerShell's Compare-Object command to compare the sheets - (explicitly checkingall the 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 identical - rows at different positions in the file will not be considered a match. - We also add the name of the file and sheet in which the difference occurs. - If -BackgroundColor is specified the difference rows will be changed to - that background in the orginal file. - .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 difference if the software was installed on a - different date or from a different place, and 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 the command - 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 highlights any different rows in the spreadsheet files. - .Example - Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -FontColor Red -Show - - This example builds on the previous one: 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 that 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 in the two workbooks has a title and two unlabelled columns - with a row each for CPU, Memory, Domain, Disk and so on. So the command is - told to start at row 2 in order to skip the title and given names for the - columns: the first is "label" and the second "Value"; the label acts as the key. - This time we are interested in those 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 highlights all the cells in LightGray - and then sets the changed rows back to white. - Only the unchanged rows are highlighted. -#> - [cmdletbinding(DefaultParameterSetName)] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification="Write host used for sub-warning level message to operator which does not form output")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives when initializing variable in begin block")] - 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 comparison - supports wildcards, default is "*". - $Property = "*" , - #Properties to exclude from the comparison - supports wildcards. - $ExcludeProperty , - #Specifies custom property names to use, instead of the values defined in the starting row of the sheet. - [Parameter(ParameterSetName='B', Mandatory)] - [String[]]$Headername, - #Automatically generate property names (P1, P2, P3 ...) instead of the using the values the starting row of the sheet. - [Parameter(ParameterSetName='C', Mandatory)] - [switch]$NoHeader, - #The row from where we start to import data: all rows above the start row 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 color, and Different cells another. - $AllDataBackgroundColor, - #If specified, highlights the rows with differences. - $BackgroundColor, - #If specified identifies the tabs which contain difference rows (ignored if -BackgroundColor is omitted). - $TabColor, - #Name of a column which is unique and will be used to add a row to the DIFF object, defaults to "Name". - $Key = "Name" , - #If specified, highlights the DIFF columns in rows which have the same key. - $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 Grid-View and not on the console. (unless-PassThru is also specified). This works best with few columns selected, and requires a key. - [switch]$GridView, - #If specifieda 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 must have two different worksheet names. If we have two files we can have 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= 1 } -Process {$Columns[$_] = [OfficeOpenXml.ExcelAddress]::GetAddress(1,($i ++)) -replace "\d","" } - - #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 ($PSBoundParameters.ContainsKey("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 ($PSBoundParameters.ContainsKey("TabColor")) { - if ($TabColor -is [string]) {$TabColor = [System.Drawing.Color]::$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 color was specified, set it on changed properties where the same key appears in both sheets. - if ($diff -and $FontColor -and (($propList -contains $Key) -or ($key -is [hashtable])) ) { - $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]._file -eq $Referencefile) { - $ws1 = $xl1.Workbook.Worksheets[$u.Group[0]._sheet] - $ws2 = $xl2.Workbook.Worksheets[$u.Group[1]._sheet] - } - else { - $ws1 = $xl2.Workbook.Worksheets[$u.Group[0]._sheet] - $ws2 = $xl1.Workbook.Worksheets[$u.Group[1]._sheet] - } - if($u.Group[0].$p -ne $u.Group[1].$p ) { - Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor - Set-Format -WorkSheet $ws2 -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 will not 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"] = $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") {$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" } - elseif ( $IncludeEqual) {$ExpandedDiff = $ExpandedDiff | Sort-Object -Property "row" } - else {$ExpandedDiff = $ExpandedDiff.where({$_.side -ne "=="}) | Sort-Object -Property "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 } -} diff --git a/plot.ps1 b/plot.ps1 deleted file mode 100644 index 4a54ea1..0000000 --- a/plot.ps1 +++ /dev/null @@ -1,218 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives")] -param() -class PSPlot { - hidden $path - hidden $pkg - hidden $ws - hidden $chart - - PSPlot() { - $this.path=[System.IO.Path]::GetTempFileName() -replace "\.tmp", ".xlsx" - $this.pkg = New-Object OfficeOpenXml.ExcelPackage $this.path - $this.ws=$this.pkg.Workbook.Worksheets.Add("plot") - } - - [PSPlot] Plot($yValues) { - - $this.NewChart() - - $xValues = 0..$yValues.Count - - $xCol = 'A' - $yCol = 'B' - - $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) - $this.AddSeries($xCol,$yCol,$yValues) - $this.SetChartPosition($yCol) - - return $this - } - - [PSPlot] Plot($yValues,[string]$options) { - $this.NewChart() - - $xValues = 0..$yValues.Count - - $xCol = 'A' - $yCol = 'B' - - $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) - $this.AddSeries($xCol,$yCol,$yValues) - - $this.SetMarkerInfo($options) - $this.SetChartPosition($yCol) - - return $this - } - - [PSPlot] Plot($xValues,$yValues) { - - $this.NewChart() - - $xCol = 'A' - $yCol = 'B' - - $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) - $this.AddSeries($xCol,$yCol,$yValues) - - $this.SetChartPosition($yCol) - - return $this - } - - [PSPlot] Plot($xValues,$yValues,[string]$options) { - $this.NewChart() - - $xCol = 'A' - $yCol = 'B' - - $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) - $this.AddSeries($xCol,$yCol,$yValues) - - $this.SetMarkerInfo($options) - - $this.SetChartPosition($yCol) - - return $this - } - - [PSPlot] Plot($xValues,$yValues,$x1Values,$y1Values) { - - $this.NewChart() - - $xCol = 'A' - $yCol = 'B' - - $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) - $this.AddSeries($xCol,$yCol,$yValues) - - $xCol=$this.GetNextColumnName($yCol) - $yCol=$this.GetNextColumnName($xCol) - - $this.AddDataToSheet($xCol,$yCol,'x1','y1',$x1Values,$y1Values) - $this.AddSeries($xCol,$yCol,$y1Values) - - $this.SetChartPosition($yCol) - - return $this - } - - [PSPlot] Plot($xValues,$yValues,$x1Values,$y1Values,$x2Values,$y2Values) { - - $this.NewChart() - - $xCol = 'A' - $yCol = 'B' - - $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) - $this.AddSeries($xCol,$yCol,$yValues) - - $xCol=$this.GetNextColumnName($yCol) - $yCol=$this.GetNextColumnName($xCol) - - $this.AddDataToSheet($xCol,$yCol,'x1','y1',$x1Values,$y1Values) - $this.AddSeries($xCol,$yCol,$y1Values) - - $xCol=$this.GetNextColumnName($yCol) - $yCol=$this.GetNextColumnName($xCol) - - $this.AddDataToSheet($xCol,$yCol,'x2','y2',$x2Values,$y2Values) - $this.AddSeries($xCol,$yCol,$y2Values) - - $this.SetChartPosition($yCol) - - return $this - } - - [PSPLot] SetChartPosition($yCol) { - $columnNumber = $this.GetColumnNumber($yCol)+1 - $this.chart.SetPosition(1,0,$columnNumber,0) - - return $this - } - - AddSeries($xCol,$yCol,$yValues) { - $yRange = "{0}2:{0}{1}" -f $yCol,($yValues.Count+1) - $xRange = "{0}2:{0}{1}" -f $xCol,($yValues.Count+1) - $Series=$this.chart.Series.Add($yRange,$xRange) - } - - hidden SetMarkerInfo([string]$options) { - $c=$options.Substring(0,1) - $m=$options.Substring(1) - - $cmap=@{r='red';g='green';b='blue';i='indigo';v='violet';c='cyan'} - $mmap=@{Ci='Circle';Da='Dash';di='diamond';do='dot';pl='plus';sq='square';tr='triangle'} - - $this.chart.Series[0].Marker = $mmap.$m - $this.chart.Series[0].MarkerColor = $cmap.$c - $this.chart.Series[0].MarkerLineColor = $cmap.$c - } - - hidden [string]GetNextColumnName($columnName) { - return $this.GetColumnName($this.GetColumnNumber($columnName)+1) - } - - hidden [int]GetColumnNumber($columnName) { - $sum=0 - - $columnName.ToCharArray() | - ForEach-Object { - $sum*=26 - $sum+=[char]$_.tostring().toupper()-[char]'A'+1 - } - - return $sum - } - - hidden [string]GetColumnName($columnNumber) { - $dividend = $columnNumber - $columnName = @() - while($dividend -gt 0) { - $modulo = ($dividend - 1) % 26 - $columnName += [char](65 + $modulo) - $dividend = [int](($dividend -$modulo)/26) - } - - return ($columnName -join '') - } - - hidden AddDataToSheet($xColumn,$yColumn,$xHeader,$yHeader,$xValues,$yValues) { - $count=$yValues.Count - $this.ws.Cells["$($xColumn)1"].Value=$xHeader - $this.ws.Cells["$($yColumn)1"].Value=$yHeader - - for ($idx= 0; $idx-lt $count; $idx++) { - $row=$idx+2 - $this.ws.Cells["$($xColumn)$($row)"].Value=$xValues[$idx] - $this.ws.Cells["$($yColumn)$($row)"].Value=$yValues[$idx] - } - } - - hidden NewChart() { - $chartType="XYScatter" - #$chartType="line" - $this.chart=$this.ws.Drawings.AddChart("plot", $chartType) - $this.chart.Title.Text = 'Plot' - $this.chart.Legend.Remove() - $this.SetChartSize(300,300) - } - - [PSPlot] SetChartSize([int]$width,[int]$height){ - $this.chart.SetSize($width, $height) - - return $this - } - - [PSPlot] Title($title) { - $this.chart.Title.Text = $title - - return $this - } - - Show() { - $this.pkg.Save() - $this.pkg.Dispose() - Invoke-Item $this.path - } -} \ No newline at end of file