A Compare-Object cmdlet segítségével két tetszőleges gyűjteményt hasonlíthatunk össze, kimenetül a gyűjtemények közötti különbséget leíró objektumokat kapunk. Az alábbi példában egy változóba mentettem a gépen futó folyamatok listáját. Ezután leállítottam, illetve elindítottam néhány folyamatot, majd ezt az új állapotot egy másik változóba írtam. A Compare-Object-nek odaadtam a két változót, ő pedig kilistázta a különbségeket:
[44] PS C:\> $a = Get-Process # bezár notepad, xmlnotepad
[45] PS C:\> $b = Get-Process # megnyit másik notepad példány, mspaint
[46] PS C:\> Compare-Object $a $b
InputObject SideIndicator
----------- -------------
System.Diagnostics.Process (mspaint) =>
System.Diagnostics.Process (XmlNo... <=
A [44]-es sor futtatásakor a Notepad egy példánya futott, meg az XMLnotepad program. A [45]-ös sor futtatása előtt becsuktam a Notepadet és újra megnyitottam (másik processz lett belőle), és becsuktam az XMLnotepad-et és megnyitottam az MSPaint-et. A [46]-os sorban összehasonlítottam a két állapotban mintavételezett processzek listáját. Valamilyen szempontból jó eredményt kaptunk, de ha igazán belegondolunk, és precízek szerettünk volna lenni, akkor ez mégsem jó eredmény, hiszen a két notepad.exe folyamat az nem ugyanaz. Vajon hogyan gondolkodott a PowerShell? Szegény compare-object bármilyen bonyolult objektumok gyűjteményét kaphatja paraméterként, így ha az objektumok összes tulajdonságának összehasonlításával döntené el az egyezőséget, akkor nagyon sokat kellene dolgoznia. Így alaphelyzetben nagyon egyszerű algoritmust alkalmaz: veszi az objektumok szöveggé alakított formáját. A ToString metódus minden objektumnál kötelező elem, így ez garantáltan meghívható. Nézzük meg, hogy ez mit az a fenti esetben:
[47] PS C:\> $a | ForEach-Object {$_.tostring()}
System.Diagnostics.Process (conhost)
System.Diagnostics.Process (csrss)
System.Diagnostics.Process (csrss)
System.Diagnostics.Process (dfsrs)
System.Diagnostics.Process (dfssvc)
Hiszen pont ilyen adatokat láthatunk a [46]-os sor futtatása után, és ebben tényleg csak a processz objektumok típusa és neve látszik, azaz elrejtődik, ha egy processzt újra nyitunk. Hogyan lehetne precízebbé tenni a compare-object-et? Használjuk a –property paramétert, ahol felsorolhatjuk, hogy pontosan mely tulajdonság(ok) összehasonlításán alapuljon az egyes objektumok egyformaságának eldöntése. A mi esetünkben legyen a processzazonosító és a processz neve:
[48] PS C:\> Compare-Object $a $b -Property id, name
id name SideIndicator
-- ---- -------------
2076 mspaint =>
1632 notepad =>
2944 notepad <=
396 XmlNotepad <=
Ez már precízebb eredményt adott!
Mire használható ez még? Szeretnénk például megtudni, hogy az internetről gyűjtött csodaprogram telepítője pontosan mit garázdálkodik a gépünkön? Semmi gond, készítsünk pillanatfelvételt az érzékeny területekről (futó folyamatok, fájlrendszer, registry) a telepítés előtt, majd hasonlítsuk össze a telepítőprogram lefutása utáni állapottal. Lesz nagy meglepetés! Nem kell elaprózni, bátran lekérhetjük például a teljes c: meghajtó állapotát, a gép majd beleizzad kicsit az összehasonlításba, de így mindenre fény derül:
PS C:\> $a = Get-ChildItem c: -recurse
PS C:\> $b = Get-ChildItem c: -recurse
PS C:\> Compare-Object $a $b
Vigyázzunk azonban a compare-object-tel! Ha túl sok a különbség a két gyűjtemény között, akkor elég sokáig eltarthat az összehasonlítgatás, hiszen az első kupac minden eleméhez megnézi, hogy van-e egyező elem a másik kupacban. Ha mindkét kupac közel azonos számú elemből áll és az elemek sorrendben vannak, akkor használhatjuk –SyncWindow paramétert, mellyel leszűkíthetjük azt a tartományt, amin belül egyezést keres a másik kupacban. Nézzünk erre egy példát:
[56] PS C:\> Compare-Object 1,2,3 3,4,5 -SyncWindow 1
InputObject SideIndicator
----------- -------------
3 =>
1 <=
4 =>
2 <=
5 =>
3 <=
A fenti példában az egyik gyűjteményem 1-től 3-ig a számok, a másik gyűjteményem 3-tól 5-ig. Azaz a 3 valójában nem különbség a két gyűjtemény között, mégis az eredményben, ami ugye az eltéréseket adja meg, a 3-as is szerepel, ráadásul kétszer is. Ennek az az oka, hogy a compare‑object 1-es synwindow paraméterrel nem minden elemhez néz meg minden elemet, hanem alaphelyzetben ±1 elem távolságra. Azaz az első halmaz 3-asát összehasonlítja a második tömbbeli 4-gyel, 5-tel, de az ottani első 3-assal már nem. A compare-object alaphelyzetben ±maxint távolságra vizsgál, azaz gyakorlatilag minden elemhez minden elemet megkeres:
[57] PS C:\> Compare-Object 1,2,3 3,4,5
InputObject SideIndicator
----------- -------------
4 =>
5 =>
1 <=
2 <=
Így megtalálta, hogy mindkét halmazban benne van a 3-as.
A compare-object akár több tulajdonság együttállását is képes összehasonlítani:
PS C:\> $a = Get-Process
PS C:\> $b = Get-Process
PS C:\> Compare-Object $a $b -Property id, workingset
id workingset SideIndicator
-- ---------- -------------
3564 59248640 =>
348 62193664 =>
2904 6524928 =>
3564 61214720 <=
348 62160896 <=
2904 6508544 <=
A fenti példában 1-2 másodperces eltéréssel vettem mintát a futó processzeimből, ha csak ID-re vizsgáltam volna, akkor nem lett volna különbség a két kupac között, de így, hogy ID-re és workingset-re együtt vizsgáltam, így már látható, hogy három processznek változott meg a memóriafelhasználása ilyen rövid idő alatt.
Ha tovább vizsgáljuk az így kapott kimenetet, akkor láthatjuk, hogy annak szerkezete eléggé eltávolodott az eredeti objektumok típusától:
PS C:\> Compare-Object $a $b -Property id, workingset | gm
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
id NoteProperty System.Int32 id=3564
SideIndicator NoteProperty System.String SideIndicator==>
workingset NoteProperty System.Int32 workingset=59248640
De vajon hogyan lehetne valahogy megőrizni a kimenetben is a processz objektumokat? Erre –PassThru paraméter ad megoldást:
PS C:\> Compare-Object $a $b -Property id, workingset -PassThru
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
366 23 58216 57860 574 1,98 3564 powershell
1493 77 49944 60736 507 348 svchost
76 7 2864 6372 31 2904 unsecapp
463 24 60192 59780 574 2,00 3564 powershell
1481 77 49840 60704 506 348 svchost
74 7 2836 6356 30 2904 unsecapp
Itt viszont nem látom a változás irányát! De szerencsére azért az ott van a mélyén:
PS C:\> Compare-Object $a $b -Property id, workingset -PassThru | ft id, proces
sname, workingset, sideindicator
Id ProcessName WorkingSet SideIndicator
-- ----------- ---------- -------------
3564 powershell 59248640 =>
348 svchost 62193664 =>
2904 unsecapp 6524928 =>
3564 powershell 61214720 <=
348 svchost 62160896 <=
2904 unsecapp 6508544 <=
Azaz ezzel a kapcsolóval minden kimeneti objektum megőrizte eredeti mivoltát, csak egy plusz SideIndicator tulajdonságot kapott.
Ha precízebb akarok lenni, akkor a compare-object első két paraméterének neve ReferenceObject és DifferenceObject:
PS C:\> $a = Get-ChildItem -Path C:\PowerShell\Egy
PS C:\> $b = Get-ChildItem -Path C:\PowerShell\Kettő
PS C:\> Compare-Object -ReferenceObject $a -DifferenceObject $b -PassThru | For
mat-Table -Property directoryname, name, sideindicator
DirectoryName Name SideIndicator
------------- ---- -------------
C:\PowerShell\Kettő Advent2018_16input.ps1 =>
C:\PowerShell\Kettő Advent2018_18.ps1 =>
C:\PowerShell\Kettő LinkedList.ps1 =>
C:\PowerShell\Kettő test-linkedlist.ps1 =>
C:\PowerShell\Egy Advent2018_13.ps1 <=
C:\PowerShell\Egy Advent2018_3.ps1 <=
C:\PowerShell\Egy Advent2018_3_test.ps1 <=
Ha felcserélem a két paraméter értékét attól még az eredmény ugyanaz marad alaphelyzetben, kivéve az eredményelemek sorrendjét:
PS C:\> Compare-Object -ReferenceObject $b -DifferenceObject $a -PassThru | For
mat-Table -Property directoryname, name, sideindicator
DirectoryName Name SideIndicator
------------- ---- -------------
C:\PowerShell\Egy Advent2018_13.ps1 =>
C:\PowerShell\Egy Advent2018_3.ps1 =>
C:\PowerShell\Egy Advent2018_3_test.ps1 =>
C:\PowerShell\Kettő Advent2018_16input.ps1 <=
C:\PowerShell\Kettő Advent2018_18.ps1 <=
C:\PowerShell\Kettő LinkedList.ps1 <=
C:\PowerShell\Kettő test-linkedlist.ps1 <=
Felmerülhet a kérdés, hogy miért ez a neve ezeknek a paramétereknek, miért nem egyszerűen ObjectA és ObjectB? A paramétereknek igazából akkor van jelentőségük, ha nem a különbségeket, hanem pont az egyezőségeket vizsgáljuk:
PS C:\> Compare-Object -ReferenceObject $a -DifferenceObject $b -PassThru -Incl
udeEqual -ExcludeDifferent | Format-Table -Property directoryname, name, sidein
dicator
DirectoryName Name SideIndicator
------------- ---- -------------
C:\PowerShell\Egy Advent2018_13b.ps1 ==
C:\PowerShell\Egy Advent2018_16.ps1 ==
C:\PowerShell\Egy Advent2018_16b.ps1 ==
A fenti példában, ha a referencia objektum az $a, akkor az egyforma objektumok ebből kerültek ki, ha megcseréljük, és a referecia a $b, akkor azok lesznek az eredményhalmaz részei:
PS C:\> Compare-Object -ReferenceObject $b -DifferenceObject $a -PassThru -Incl
udeEqual -ExcludeDifferent | Format-Table -Property directoryname, name, sidein
dicator
DirectoryName Name SideIndicator
------------- ---- -------------
C:\PowerShell\Kettő Advent2018_13b.ps1 ==
C:\PowerShell\Kettő Advent2018_16.ps1 ==
C:\PowerShell\Kettő Advent2018_16b.ps1 ==
Így például ha ki szeretnénk törölni az Egy könyvtárból azokat a fájlokat, amik a Kettőben is megvannak, akkor az Egy kell legyen a referencia objektum és a Kettő a differencia:
PS C:\> Compare-Object -ReferenceObject $a -DifferenceObject $b -PassThru -Inc
ludeEqual -ExcludeDifferent | Remove-Item
Ha megnézzük mi maradt, akkor láthatjuk, hogy tényleg az Egy elemei tűntek el:
PS C:\> $a = Get-ChildItem -Path C:\PowerShell\Egy
PS C:\> $b = Get-ChildItem -Path C:\PowerShell\Kettő
PS C:\> Compare-Object -ReferenceObject $a -DifferenceObject $b -PassThru -Incl
udeEqual | Format-Table -Property directoryname, name, sideindicator
DirectoryName Name SideIndicator
------------- ---- -------------
C:\PowerShell\Kettő Advent2018_13b.ps1 =>
C:\PowerShell\Kettő Advent2018_16.ps1 =>
C:\PowerShell\Kettő Advent2018_16b.ps1 =>
C:\PowerShell\Kettő Advent2018_16input.ps1 =>
C:\PowerShell\Kettő Advent2018_18.ps1 =>
C:\PowerShell\Kettő LinkedList.ps1 =>
C:\PowerShell\Kettő test-linkedlist.ps1 =>
C:\PowerShell\Egy Advent2018_13.ps1 <=
C:\PowerShell\Egy Advent2018_3.ps1 <=
C:\PowerShell\Egy Advent2018_3_test.ps1 <=
Megjegyzés
Nem tökéletes a Compare-Object szerintem. Sajnos, ha vagy a -ReferenceObject, vagy a ‑DifferenceObject $null, akkor hibát kapok, holott simán kaphatnám a másik gyűjtemény összes elemét különbségként:
PS C:\> Compare-Object -ReferenceObject 1,2,3,4 -DifferenceObject $null
Compare-Object : Cannot bind argument to parameter 'DifferenceObject' because
it is null.
At line:1 char:60
+ Compare-Object -ReferenceObject 1,2,3,4 -DifferenceObject $null
+ ~~~~~
+ CategoryInfo : InvalidData: (:) [Compare-Object], ParameterBin
dingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,
Microsoft.PowerShell.Commands.CompareObjectCommand
Erre megoldást a 2.5.7 Meglevő cmdletek kiegészítése, átalakítása fejezetben tárgyal módon viszonylag egyszerűen lehet:
function Compare-Object2{
[CmdletBinding(HelpUri='https://go.microsoft.com/fwlink/?LinkID=113286', RemotingCapability='None')]
param(
[Parameter(Mandatory=$true, Position=0)]
[AllowNull()]
[AllowEmptyCollection()]
[psobject[]]
${ReferenceObject},
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[AllowNull()]
[AllowEmptyCollection()]
[psobject[]]
${DifferenceObject},
[ValidateRange(0, 2147483647)]
[int]
${SyncWindow},
[System.Object[]]
${Property},
[switch]
${ExcludeDifferent},
[switch]
${IncludeEqual},
[switch]
${PassThru},
[string]
${Culture},
[switch]
${CaseSensitive})
begin
{
if($null -eq $ReferenceObject){
$PSBoundParameters.ReferenceObject = @()
}
if($null -eq $DifferenceObject){
$PSBoundParameters.DifferenceObject = @()
}
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Compare-Object', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
<#
.ForwardHelpTargetName
Microsoft.PowerShell.Utility\Compare-Object
.ForwardHelpCategory
Cmdlet
#>
}
Vastagon kiemeltem azokat a sorokat, amiket ténylegesen én írtam bele.
Ezzel már jól működik:
PS C:\> Compare-Object2 -ReferenceObject 1,2,3,4 -DifferenceObject $null
InputObject SideIndicator
----------- -------------
1 <=
2 <=
3 <=
4 <=