Is there a PowerShell Script that can Compare CSVs Side by Side? - powershell

I have two CSVs with data populated from running a query. I intend to compare them both to see what are the differences using Compare-Object cmdlet.
The CSV files looks like this:
CSV1
TableName ColumnName
------------------------------
Container ContainerName
Container Location
Container ReceivedDate
Container TopMark
CSV2
TableName ColumnName
------------------------------
Container Containername
Container Location
Container DateReceived
Container BackMark
Right now, what I'm using is the Compare-Object cmdlet which is readily available in PowerShell. It runs great and I'm getting the results I wanted. However, the results might be difficult to understand for people who do not know how to read results generated by Compare-Object cmdlet. I've tried to simplify the results by comparing each of the properties but my end users somehow still don't understand the results. I even changed the SideIndicator to note if an object is existing on the Reference or Difference copy.
$compareResult = (Compare-Object -ReferenceObject $file1 -DifferenceObject $file2 -Property TableName, ColumnName |
ForEach-Object {
$_.SideIndicator = $_.SideIndicator -replace '=>',$onlyD -replace '<=',$onlsyG
$_
})
What they wanted to see is something like this:
LeftSideInputObject RightSideInputObject
-----------------------------------------------
Container,ContainerName Container,ContainerName
Container,Location Container,Location
Container,ReceivedDate
ContainerDateReceived
Container,TopMark
Container,BackMark
Is there by any chance that I can do this in PowerShell? Thank you!

You can use Select-Object to create a customized object.
Compare-Object $file1 $file2 -Property TableName,ColumnName -IncludeEqual | Select-Object #(
#{ n = "Left"; e = { if ($_.SideIndicator -in "==","<=") { $_.TableName,$_.ColumnName -join "," } } }
#{ n = "Right"; e = { if ($_.SideIndicator -in "==","=>") { $_.TableName,$_.ColumnName -join "," } } }
)

Check this:
Add-Type -AssemblyName System.Collections
#-----------------------------------------------------
class comparerClass { # class for result object
#-----------------------------------------------------
comparerClass(
[string]$leftSide,
[string]$rightSide
)
{
$this.leftSide = $leftSide
$this.rightSide = $rightSide
}
[string]$leftSide = ''
[string]$rightSide = ''
}
# Collections: File1, File2 and result list
[System.Collections.Generic.List[string]]$contentFile1 = #()
[System.Collections.Generic.List[string]]$contentFile2 = #()
[System.Collections.Generic.List[comparerClass]]$comparerList= #()
# Files to process
$pathFile1 = 'D:\csv1.txt'
$pathFile2 = 'D:\csv2.txt'
# read files to generic lists
$contentFile1.AddRange( [System.IO.File]::ReadAllLines( $pathFile1 ) )
$contentFile2.AddRange( [System.IO.File]::ReadAllLines( $pathFile2 ) )
# pointer for generic lists
$ptrFile1 = 0
$ptrFile2 = 0
# process lists, mainloop
while( $ptrFile1 -lt $contentFile1.Count ) {
# equal, easy is this
if( $contentFile1[ $ptrFile1 ] -eq $contentFile2[ $ptrFile2 ] ) {
$tmpComparer = New-Object comparerClass -ArgumentList $contentFile1[ $ptrFile1 ], $contentFile2[ $ptrFile2 ]
$comparerList.Add( $tmpComparer )
$ptrFile1++
$ptrFile2++
}
else { # not equal, check if entry list 1 comes later in list 2
$ptr = $ptrFile2 + 1
$found = $false
while( $ptr -lt $contentFile2.Count ) {
if( $contentFile1[ $ptrFile1 ] -eq $contentFile2[ $ptr ] ) { # entry found later in list2!
for( $i = $ptrFile2; $i -lt $ptr; $i++ ) {
$tmpComparer = New-Object comparerClass -ArgumentList '', $contentFile2[ $i ]
$comparerList.Add( $tmpComparer )
}
$ptrFile2 = $ptr + 1
$found = $true
$ptrFile1++
break
}
$ptr++
}
if( !$found ) { # entry not found, this entry only exists in list1
$tmpComparer = New-Object comparerClass -ArgumentList $contentFile1[ $ptrFile1 ], ''
$comparerList.Add( $tmpComparer )
$ptrFile1++
}
}
}
# process remaining entries in list2
while( $ptrFile2 -lt $contentFile2.Count ) {
$tmpComparer = New-Object comparerClass -ArgumentList '', $contentFile2[ $ptrFile2 ]
$comparerList.Add( $tmpComparer )
$ptrFile2++
}
# show result
$comparerList

Related

Powershell - Exchange JSON output without needing to write to a file

EDIT: Added Setupconfigfiles.ps1
I'm a bit new to detailed scripting so please bear with me.
I have two Powershell scripts
Setupconfigfiles.ps1 generates JSON output to be fed to an API.
Script2 uses that JSON data to execute API commands.
Script 2 can call setupconfigfiles.ps1 as indicated below and use the output data.
.\SetupConfigFiles.ps1 -type $Type -outfile .\Templist.json
$servers = Get-Content -Raw -Path .\templist.json | ConvertFrom-Json
setupconfigfiles.ps1:
param (
# If this parameter is set, format the output as csv.
# If this parameter is not set, just return the output so that the calling program can use the info
[string]$outfile,
# this parameter can be 'production', 'development' or 'all'
[string]$type
)
enum MachineTypes {
production = 1
development = 2
all = 3
}
$Servers = Get-ADObject -Filter 'ObjectClass -eq "computer"' -SearchBase 'Obfuscated DSN' | Select-Object Name
$output = #()
$count = 0
# Set this to [MachineTypes]::production or [MachineTypes]::development or [MachineTypes]::all
if ($type -eq "all") {
$server_types = [MachineTypes]::all
}
ElseIf ($type -eq "production") {
$server_types = [MachineTypes]::production
}
else {
$server_types = [MachineTypes]::development
}
ForEach ($Server in $Servers)
{
$count = $count + 1
$this_server = #{}
$this_server.hostname = $Server.Name
$this_server.id = $count
$this_server."site code" = $this_server.hostname.substring(1,3)
$this_server."location code" = $this_server.hostname.substring(4,2)
if ($this_server.hostname.substring(7,1) -eq "P") {
$this_server.environment = "Production"
}
ElseIf ($this_server.hostname.substring(7,1) -eq "D") {
$this_server.environment = "Development"
}
Else {
$this_server.environment = "Unknown"
}
if (($server_types -eq [MachineTypes]::production ) -and ($this_server.environment -eq "Production")) {
$output += $this_server
}
ElseIf (($server_types -eq [MachineTypes]::development ) -and ($this_server.environment -eq "Development")) {
$output += $this_server
}
Else {
if ($server_types -eq [MachineTypes]::all ) {
$output += $this_server
}
}
}
if ($outfile -eq "")
{
ConvertTo-Json $output
}
else {
ConvertTo-Json $output | Out-File $outfile
}
How can I do it without needing to write to the Templist.json file?
I've called this many different ways. The one I thought would work is .\SetupConfigFiles.ps1 $servers
Y'all are great. #Zett42 pointed me in a direction and #Mathias rounded it out.
The solution was to change:
"ConvertTo-Json $output" to "Write-Output $output"
Then it's handled in the calling script.
thanks!

How would you generate a unique sequence from 000 to 9ZZ

How would you generate a unique sequence from 000 to 9ZZ. Lastly, my export-csv is not working. Please see the data and export output below.
The alphanumeric sequence starts from 0 through 9 and then A through Z.
Please be advised, my PowerShell skill are a bit new. :)
$i = #()
$a = 0..9
$b = 65..90 | Foreach{[Char]$_}
$i = $a + $b
For($d = 0; $d -le $i.count; $d++){
$g = $i[$d]
For($e = 0; $e -le $i.count; $e++){
$h = $i[$e]
For($f = 0; $f -le $i.count; $f++){
$j = $i[$f]
$k = "{0}{1}{2}" -f $g, $h, $j
$k #| Export-Csv -Path .\List.csv -NoTypeInformation -Append
If($k -eq '9ZZ'){
Break
}
}
}
}
Data Output:
000
001
.
.
|
V
009
00A
.
.
00Z
00 <-- I don't get this.
010
Export:
Length
3 <-- I don't get this either.
.
.
|
v
3
Any and all help is appreciated. Thank you in Advanced. ;)
I would do it this way, with 3 foreach loops and a labeled break:
$digits = 0..9
$chars = (65..90).ForEach([char])
$dict = $digits + $chars
$result = :outer foreach($i in $dict)
{
foreach($x in $dict)
{
foreach($z in $dict)
{
'{0}{1}{2}' -f $i,$x,$z
if($i -eq 9 -and $x -eq 'Z' -and $z -eq 'Z')
{
break outer
}
}
}
}
$result | Export-Csv ...
This is a classic off-by-1 error - your loops run from 0 through $i.count (included), which is exactly 1 longer than $i.
Change -le $i.count to -lt $i.count in all 3 loop conditions and it'll work.
You could simplify your code by generating the full range of digits/characters up front a little differently, and then use 3 nested foreach loops instead:
$digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.ToCharArray()
$ranges =
:outerLoop
foreach($a in $digits){
foreach($b in $digits){
foreach($c in $digits){
# save and output new value
($label = "${a}${b}${c}")
# exit the label generation completely if we've reached the desired upper boundary
if($label -eq '9ZZ'){ break outerLoop }
}
}
}
$ranges now contain the correct range of labels from 000 through 9ZZ
To complement the helpful existing answers with a generalized solution for producing permutations of characters with a given number of places, using recursive helper function Get-Permutations:
function Get-Permutations {
param(
[Parameter(Mandatory)]
[string] $Chars, # e.g. '0123456789'
[Parameter(Mandatory)]
[uint] $NumPlaces # e.g. 2, to produce '00', '01', ..., '99'
)
switch ($NumPlaces) {
0 { return }
1 { [string[]] $Chars.ToCharArray() }
default {
Get-Permutations -Chars $Chars -NumPlaces ($NumPlaces-1) | ForEach-Object {
foreach ($c in $chars.ToCharArray()) { $_ + $c }
}
}
}
}
You'd call it as follows (to get '000', '001', ..., but all the way up to 'ZZZ' - you can post-filter with ... | Where-Object { $_ -le '9ZZ' })
Get-Permutations '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 3
Note:
In PowerShell (Core) 7+ you can create the string of characters with
-join ('0'..'9' + 'A'..'Z')
In Windows PowerShell, where you cannot use [char] instances with the range operator, .., you can use the following, more concise alternative to your approach:
-join ([char[]] (48..57) + [char[]] (65..90)), based on the chars.' code points obtained with, e.g. [int] [char] 'A'
similarly you could cheat since '009' -le '00A'
$sb = [System.Text.StringBuilder]::new()
function inc([string]$s){
[byte[]]$v = $s.ToCharArray()
$radix = $v.Count-1
$carry = $true
while($carry -and $radix -ge 0){
$carry = $false
switch(++$v[$radix]){
91{$v[$radix--] = 48; $carry = $true}
58{$v[$radix] = 65}
}
}
$null = $sb.clear()
if($carry){$null = $sb.Append('1')}
return $sb.Append([char[]]$v).ToString()
}
Describe 'Test inc function' {
It 'increments 000' {inc '000' | should be '001'}
It 'increments 001' {inc '001' | should be '002'}
It 'increments 008' {inc '008' | should be '009'}
It 'increments 009' {inc '009' | should be '00A'}
It 'increments 00A' {inc '00A' | should be '00B'}
It 'increments 00Y' {inc '00Y' | should be '00Z'}
It 'increments 00Z' {inc '00Z' | should be '010'}
It 'increments 010' {inc '010' | should be '011'}
It 'increments 0ZZ' {inc '0ZZ' | should be '100'}
It 'increments 1ZZ' {inc '1ZZ' | should be '200'}
It 'increments ZZZ' {inc 'ZZZ' | should be '1000'}
}
measure-command {for($i = '000'; $i -le '9ZZ'; $i = inc $i){$i}}
&{for($i = '000'; $i -le '9ZZ'; $i = inc $i){
$i
}} | set-content -Path .\List.csv
Edit: unnecessarily faster
Inspired by mklement0's awesome answer
$cache = #{}
function Get-Permutations{
param(
[Parameter(Mandatory=$true)][string]$Alphabet # e.g. '0123456789'
,[Parameter(Mandatory=$true)][uint32]$Length # e.g. 2, to produce '00', '01', ..., '99'
)
if($Length -eq 0)
{return}
if($Length -eq 1)
{return $Alphabet.ToCharArray()}
if($Alphabet -NotIn $cache.Keys){
$cache.$Alphabet = new-object String[][] $Length
$cache.$Alphabet[1] = $Alphabet.ToCharArray()
}elseif($cache.$Alphabet.Count -lt $Length){
$tmp = new-object String[][] $Length
[Array]::Copy($cache.$Alphabet,$tmp,$cache.$Alphabet.Count)
$cache.$Alphabet = $tmp
}
Get-Permutations-Helper $Alphabet $Length
}
function Get-Permutations-Helper{
param(
[string]$Alphabet
,[uint32]$Length
)
$TailLength = $Length-1
if($cache.$Alphabet[$TailLength] -eq $null){
$cache.$Alphabet[$TailLength] = Get-Permutations-Helper $Alphabet $TailLength
}
foreach($head in $Alphabet.ToCharArray()){
foreach($tail in $cache.$Alphabet[$TailLength]){
$head + $tail
}
}
}
Describe 'Test Get-Permutations function' {
It 'Makes an ordered ABC,2 permutation' {
$valid = #('AA','AB','AC','BA','BB','BC','CA','CB','CC')
$cache = #{}; $i = 0
Get-Permutations 'ABC' 2 | foreach-object {
$_ | should be $valid[$i++]
}
}
It 'Extends the cache' {
$cache = #{}
$null = Get-Permutations 'ABC' 2
$cache.'ABC'[1][0] | should be 'A'
$cache.'ABC'[1][0] = 'test'
$results = Get-Permutations 'ABC' 3
$cache.'ABC'[2][0] | should be 'Atest'
$results[0] | should be 'AAtest'
}
}
$digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
$spin = 10
$ttl=$null;for($i=0;$i -lt $spin;$i++){
$cache = #{}
$ttl+=measure-command {
Get-Permutations $digits 3
}
};$ttl;$ttl.TotalMilliseconds/$spin
$ttl=$null;for($i=0;$i -lt $spin;$i++){
$cache = #{}
$ttl+=measure-command {
$enu = (Get-Permutations $digits 3).GetEnumerator();
while($enu.MoveNext() -and ($v = $enu.Current) -le '9ZZ'){$v}
}
};$ttl;$ttl.TotalMilliseconds/$spin
$enu = (Get-Permutations $digits 3).GetEnumerator();
&{while($enu.MoveNext() -and ($v = $enu.Current) -le '9ZZ'){
$v
}} | set-content -Path .\List.csv

How can I check if a 2 numbers have the same digits PowerShell?

I want to compare two int so if they contain the same digits it outputs a true,for example:
$a=1260
$b=2106
and then because both of them contain: 0126 it outputs true how can this be made?
And if it's possible with the fewest possible lines
Here's one technique:
$null -eq (Compare-Object -ReferenceObject ([char[]][String]1260) -DifferenceObject ([char[]][String]2601))
Which returns true or false, depending on if the digits are the same or not.
Here is another, a bit lengthier solution:
( $a.ToString().ToCharArray() | ForEach-Object { $c = $true } { if ( $b.ToString() -notmatch $_.ToString() ) { $c = $false } } { $c } ) -and ( $b.ToString().ToCharArray() | ForEach-Object { $c = $true } { if ( $a.ToString() -notmatch $_.ToString() ) { $c = $false } } { $c } )
This compares the int's as arrays and thus need to be run bothways.

Powershell Return only TRUE if All Values are the same

I have the script below to read registry values from a certain key(taking no credit for it). My end goal is to only return TRUE if all the values in the array Match. However I'm not quite getting it as
Example Registry Entry
$array = #()
$regval = Get-Item -Path HKLM:\SOFTWARE\Runner\Event
$regval.GetValueNames() |
ForEach-Object {
$name = $_
$rv.Value
$array += New-Object psobject -Property #{'Value' = $rv.Value }
}
$Matchvalue = 'A'
Foreach ($v in $array){
if ($v -match $Matchvalue){
$true
}
}
Update: I've just tried again and it appears my array is empty. So any tips welcome for me.
How about this:
$regkey = Get-Item HKLM:\SOFTWARE\Runner\Event
$matchPattern = 'A'
$values = $regkey.GetValueNames()
$matchingValues = $values | Where { $regkey.GetValue($_) -match $matchPattern }
# this is going to be true or false
$values.Count -eq $matchingValues.Count
Note that by default, Powershell is case-insensitive. So $matchPattern = 'A' and $matchPattern = 'a' will behave the same.
Here's my attempt to do something like Haskell's "all".
function foldr ($sb, $accum, $list) {
if ($list.count -eq 0) { $accum }
else { & $sb $list[0] (foldr $sb $accum $list[1..$list.length]) }
}
function and ($list) {
foldr { $args[0] -and $args[1] } $true $list
}
function all ($list, $sb) {
and ( $list | foreach $sb )
}
all 1,1,1 { $_ -eq 1 }
True
all 1,2,1 { $_ -eq 1 }
False

reconstructing path from outlined directory structure

I have a csv file in the form:
Address,L0,L1,L2,L3,L4
01,Species,,,,
01.01,,Mammals,,,
01.01.01,,,Threatened,,
...
I want to use it to create a matching directory structure. I'm new to scripting and PowerShell, and in this case I'm not sure if I'm on totally the wrong track. Should I use a separate array to store each level's Name/Address pairs and then use those arrays like a lookup table to build the path? If so, I guess I'm stuck on how to set up if-thens based on a row's Address. This is as far as I've got so suggestions on general strategy or links to similar kinds of problem would be really welcome:
$folders = Import-Csv "E:\path\to\file.csv"
$folders | foreach {
$row = new-object PSObject -Property #{
Address = $_.Address;
Level = ([regex]::Matches($_.Address, "\." )).count;
L0 = $_.L0
L1 = $_.L1
L2 = $_.L2
L3 = $_.L3
}
$array += $row
}
#top level directories
$0 = $array | ?{$_.Level -eq 0} |
Select-Object #{n="Address";e={$_.Address;}},#{n="Name";e={$_.L0}}
#2nd level directories
$1 = $array | ?{$_.Level -eq 1} |
Select-Object #{n="Number";e={$_.Address.split(".")[-1];}},#{n="Name";e={$_.L1}}
Not tested, but I think this might do what you want:
$root = 'E:\SomeDirName'
Switch -Regex (Get-Content "E:\path\to\file.csv")
{
'^01,(\w+),,,,$' { $L1,$L2,$L3,$L4 = $null; $L0=$matches[1];mkdir "$root\$L0" }
'^01\.01,,(\w+),,,$' { $L1=$matches[1];mkdir "$root\$L0\$L1" }
'^01\.01\.01,,,(\w+),,$' { $L2=$matches[1];mkdir "$root\$L0\$L1\$L2" }
'^01\.01\.01\.01,,,,(\w+),$' { $L3=$matches[1];mkdir "$root\$L0\$L1\$L2\$L3" }
'^01\.01\.01\.01\.01,,,,,(\w+)$' { $L4=$matches[1];mkdir "$root\$L0\$L1\$L2\$L3\$L4" }
}
To solve that kind of problem a programming concept called recursion is often used.
In short a recursive function is a function that call itself.
I successfully tested this code with you CSV input:
$csvPath = 'C:\Temp\test.csv'
$folderRoot = 'C:\Temp'
$csv = Import-Csv $csvPath -Delimiter ',' | Sort-Object -Property Address
# Recursive function
function Recurse-Folder( $folderAddress, $basePath )
{
# Getting the list of current folder subfolders
$childFolders = $null
if( $folderAddress -like '' )
{
$childFolders = $csv | Where-Object { $_.Address -like '??' }
}
else
{
$childFolders = $csv | Where-Object { $_.Address -like $( $folderAddress + '.??' ) }
}
# For each child folder
foreach( $childFolder in $childFolders )
{
# Get the folder name
$dotCount = $childFolder.Address.Split('.').Count - 1
$childFolderName = $childFolder.$('L'+$dotCount)
if( $childFolderName -ne '')
{
$childFolderPath = $basePath + '\' + $childFolderName
# Creating child folder and calling recursive function for it
New-Item -Path $childFolderPath -ItemType Directory
Recurse-Folder $childFolder.Address $childFolderPath
}
}
}
Recurse-Folder '' $folderRoot