Private/Wissen/B_Basic/B53_Objekt-Handling.ps1

<#
 
# Objekt-Handling
 
Der richtige Umgang mit Objekten in der PowerShell.
 
- **Hashtags** Object Where Select Sort Group Measure Compare ForEach New Analyse static inherits
 
- **Version** 2020.02.29
 
#>


# READ Weiterführende und Nachschlage-Informationen:

Get-Help -Name 'about_Objects' -ShowWindow
Get-Help -Name 'about_Object_Creation' -ShowWindow
Get-Help -Name 'about_Pipelines' -ShowWindow
Get-Command -Noun 'Object' -Module 'Microsoft.PowerShell.*'

#region Objekte-Analyse

# ! siehe .\AKPT\Private\Wissen\B_Basic\B52_Objekt-Analyse.md

#endregion

#region Mit den Default-Cmdlets (*-Object) Rückgabe-Objekte zu managen

# ? Eine Übersicht von Cmdlets die Rückgabe-Objekte managen können:
Get-Command -Name Get-Command -Noun Object -Module Microsoft.PowerShell.*

Get-Command -Name Where-Object   -Syntax # Filtern (Zeile)
Get-Command -Name Select-Object  -Syntax # Filtern (Spalten, First, Last, ExpandProperty, ...)
Get-Command -Name Sort-Object    -Syntax # Sortieren
Get-Command -Name Group-Object   -Syntax # Gruppieren
Get-Command -Name Measure-Object -Syntax # Messen (Count, Sum, Min, Max, Avg)
Get-Command -Name Compare-Object -Syntax # Vergleichen
Get-Command -Name ForEach-Object -Syntax # Schleife
Get-Command -Name New-Object     -Syntax # Erstellen
Get-Command -Name Tee-Object     -Syntax # Verzweigen & loggen

#region Sort-Object

Get-Process | Sort-Object -Property 'Name'
Get-Process | Sort-Object -Property 'WorkingSet64' -Descending
Get-Process | Sort-Object -Property 'WorkingSet64' -Descending | Select-Object -First 3

#endregion

#region Select-Object

Get-Process | Select-Object -Property 'Name', 'WorkingSet64' # ALLE anderen Eigenschaften werden entfernt und spart Speicherplatz!
Get-Process | Select-Object -Skip 3 -First 2 # Der 4. und 5. Prozess wird zurück gegeben
Get-Process | Select-Object -SkipLast 5 # Alle außer die letzten 5
Get-Process | Select-Object -Last 2
Get-Process | Select-Object -Index 5 # 0-Basierend; Identisch mit folgendem Aufruf: (Get-Process)[5]
Get-Process | Select-Object -Property 'Name' -Unique

Get-Process | Select-Object -ExpandProperty 'Name' -First 2
; <# vs.#> ; 
Get-Process | Select-Object -Property 'Name' -First 2

#endregion

#region Measure-Object

Get-Process | Measure-Object
Get-Process | Measure-Object -Property 'WorkingSet64' -Sum -Average -Minimum -Maximum

#endregion

#region Where-Object

# Einfache Schreibweise

Get-Command -Name 'Where-Object' -Syntax

Get-Process | Where-Object -Property 'Company'    -Like -Value 'Microsoft*'
Get-Process | Where-Object           'Company'    -Like        'Microsoft*'
Get-Process | Where-Object -Like     'Company'                 'Microsoft*'
Get-Process | Where-Object           'Microsoft*' -Like       'Company'
Get-Process | Where-Object -Like     'Microsoft*'             'Company'

# -or | -and: Ausführliche Schreibweise seit PS1.0
Get-Process | Where-Object -FilterScript { $_.Company -like 'Microsoft*' -or $_.Company -like 'Oracle*' }
# ! Die Variable {$_} ist die Laufzeit-Variable in der Pipeline und stellt das übergebene Pipeline-Objekt dar!
Get-Process -PipelineVariable 'prozess' | Where-Object -FilterScript { $prozess.Company -like 'Microsoft*' -or $prozess.Company -like 'Oracle*' }

#endregion

#region Group-Object (Gruppiert zu Objektgruppen)

Get-Service | Group-Object -Property 'Status' 

Get-ChildItem -Path 'C:\Windows' -File -Force | Group-Object -Property 'Extension' | Sort-Object -Property 'Count' -Descending | Select-Object -First 3

#endregion

#region Compare-Object

Start-Process -FilePath 'calc'
$x = Get-Process
Stop-Process -Name '*Calc*'
Start-Process -FilePath 'notepad.exe'
$y = Get-Process

Compare-Object -ReferenceObject  $x  -DifferenceObject $y -Property 'Name'
Compare-Object -DifferenceObject $y  -ReferenceObject  $x -Property 'Name'

# TIPP: Doppelte Dateien finden s. Get-FileHash

Compare-Object -ReferenceObject $x -DifferenceObject $y -ExcludeDifferent -IncludeEqual

#endregion

#region New-Object

Add-Type -AssemblyName 'System.Windows.Forms'
$o1 = New-Object -TypeName 'System.Windows.Forms.Form' # Erstellt ein Objekt aus der .NET-Framework-Klasse 'Form'
$o1.ShowDialog() # .NET Wissen

$o2 = New-Object -ComObject 'Excel.Application'
$mappe = $o2.Workbooks.Add()
$sheet = $mappe.Worksheets.Add()
$cellA1 = $sheet.Range('A1')
$cellA1.Value2 = 'Greetings from PowerShell'
$o2.Visible = $true # VBA for Application

# TIPP Besser per ImportExcel-Modul .XLSX-Dateien erzeugen!

#endregion

#region ForEach-Object

31, 32, 33, 34, 35, 36 | ForEach-Object -Process { "192.168.178.$_" }
31..36                 | %                       { "192.168.178.$_" }

# ? Eine Übersicht aus Name, Größe und Besitzer von c:\Temp:

Get-ChildItem -Path 'C:\Temp' -File | Get-Member            # ! Kein Datei-Owner vorhanden.
Get-ChildItem -Path 'C:\Temp' -File | Get-Acl | Get-Member  # ! Keine Datei-Length vorhanden.

# ? LÖSUNG:
Get-ChildItem -Path 'C:\Temp' -File | ForEach-Object -Process {
    $dateiname = $_.Name
    $dateigröße = $_.Length
    $dateibesitze = $_ | Get-Acl | Select-Object -ExpandProperty Owner
    "DATEI-NAME: $dateiname | DATEI-GRÖSSE: $dateigröße | DATEI-BESITZER: $dateibesitze"
}
# TODO Aus ForEach-Object besser komplexe Objekte zurück geben:
Get-ChildItem -Path 'C:\Temp' -File | ForEach-Object -Process {
    $datei = New-Object -TypeName PSCustomObject
    $datei | Add-Member -Name Dateiname    -Value $_.Name                                              -MemberType NoteProperty
    $datei | Add-Member -Name Dateigröße   -Value $_.Length                                            -MemberType NoteProperty
    $datei | Add-Member -Name Dateibesitze -Value ($_ | Get-Acl | Select-Object -ExpandProperty Owner) -MemberType NoteProperty -PassThru
}
# TODO doer:
Get-ChildItem -Path 'C:\Temp' -File | ForEach-Object -Process {
    $_ | Add-Member -Name Dateibesitze -Value ($_ | Get-Acl | Select-Object -ExpandProperty Owner) -MemberType NoteProperty -PassThru
} | Select-Object -Property Name, Length, Dateibesitze 

# ! Was ist "besser"?

$prozesse = Get-Process
foreach ($prozess in $prozesse) {
    $prozess.Name
}

# vs.

Get-Process | ForEach-Object -Process { $_.Name }

#endregion

#region Tee-Object

Get-Process | Tee-Object -FilePath 'c:\temp\EvtlBeendeteProzesse.txt' | Stop-Process -WhatIf
# ! NACHTEIL 1: Eine Umleitung ist nur in Dateien möglich
# ! NACHTEIL 2: Z.Bsp. Die Auswirkungen von Stop-Process sind im 'Log' nicht enthalten!

# ? Eine andere Lösung könnte sein:
Get-Process | Stop-Process -WhatIf -PassThru | Set-Content -Path c:\temp\BeendeteProzesseInklFehler.txt
# ... Oder per ForEach-Object

#endregion

#endregion

#region Vorhandene Typen/Objekte erweitern

# ! siehe .\AKPT\Private\Wissen\B_Basic\B54_Objekte-erweitern.md

#endregion

#region Auf Ereignisse reagieren

#region Asynchrones Ereignis-Handling

$np = Start-Process -FilePath notepad -WindowStyle Minimized -PassThru
$np | Get-Member -MemberType Event
$job = Register-ObjectEvent -InputObject $np -EventName Exited -Action { 'Der Notepad-Prozess wurde beendet.' | Write-Warning }
$np | Stop-Process -Force
$job | Get-Job
$job | Get-EventSubscriber | Unregister-Event # oder:
$job | Remove-Job -Force

#endregion

#region Synchrones Ereignis-Handling

$np = Start-Process -FilePath notepad -WindowStyle Minimized -PassThru
Register-ObjectEvent -InputObject $np -EventName Exited -SourceIdentifier np.Exited
Wait-Event -SourceIdentifier np.Exited
"... Die Verarbeitung nach dem Beenden von Notepad wurde aufgenommen." | Write-Warning
Unregister-Event -SourceIdentifier  np.Exited

#endregion

#region Hintegrundjobs überwachen

$job = Start-Job -Name gci -ScriptBlock { Get-ChildItem -Path c:\*.log -Recurse -Force -ErrorAction Ignore | Select-Object -Property Name, Length, DirectoryName }
$job | Get-Member -MemberType Event
Register-ObjectEvent -SourceIdentifier job.StateChanged -InputObject $job -EventName StateChanged -Action {
    "GCI-Job ist fertig, STATUS = $($job.State)" | Write-Warning # TIPP - oder Send-MailMessage
    Unregister-Event -SourceIdentifier job.StateChanged
    $job | Remove-Job
}
$job | Receive-Job -Keep

#endregion

#region Ordner überwachen

# ? Dateien- / Ordner auf Veränderung überwachen
$fsw = New-Object -TypeName System.IO.FileSystemWatcher
$fsw.Path = "c:\temp"
$fsw.Filter = "*.txt"
$fsw | Get-Member -MemberType Event
$action = {
    "Dateiänderung: {0} {1}" -f $eventArgs.FullPath, $eventArgs.ChangeType | Write-Warning
}
Register-ObjectEvent -InputObject $fsw -EventName Changed -Action $action -SourceIdentifier fsw_Changed
Register-ObjectEvent -InputObject $fsw -EventName Created -Action $action -SourceIdentifier fsw_Created
Register-ObjectEvent -InputObject $fsw -EventName Deleted -Action $action -SourceIdentifier fsw_Deleted
Register-ObjectEvent -InputObject $fsw -EventName Error   -Action $action -SourceIdentifier fsw_Error
Register-ObjectEvent -InputObject $fsw -EventName Renamed -Action $action -SourceIdentifier fsw_Renamed
# ! Aufräumen
Get-EventSubscriber
Unregister-Event -SourceIdentifier fsw_*  -Force
$fsw.Dispose()
$fsw = $null
Remove-Variable -Name fsw -Force

#endregion

#region CIM (früher WMI) Events

$action = {
    $e = $Event.SourceEventArgs.NewEvent
    "Neuer Process gestartet, ID: {0} | NAME: {1}" -f $e.ProcessId, $e.ProcessName | Write-Warning
}
Register-CimIndicationEvent -ClassName "Win32_ProcessStartTrace" -SourceIdentifier "ProcessStarted" -Action $action # ! ADMIN-Rechte
Get-EventSubscriber -SourceIdentifier "ProcessStarted"
Start-Process -FilePath "calc"
Unregister-Event -SourceIdentifier "ProcessStarted" 

#endregion

#region Eigene Events auslösen

Register-EngineEvent -SourceIdentifier myEvent -Action { "Möööp!" | Write-Warning }
New-Event -SourceIdentifier myEvent
Unregister-Event -SourceIdentifier myEvent

#endregion

#endregion

#region PowerShell 7 - Update-List

# ! Das neue Cmdlet Update-List aktualisiert Listen-Einträge (Add / Remove) von Listen-Objekt-Eigenschaften:

class OsDetails {
    [System.Collections.ArrayList]$ProcessItems
    OsDetails() {
        $this.ProcessItems = New-Object -TypeName "System.Collections.ArrayList"
    }
}

$osd = New-Object -TypeName "OsDetails"
$osd.ProcessItems.AddRange((Get-Process | Select-Object -First 3))
$osd.ProcessItems

$notepad = Start-Process "notepad" -PassThru

$osd | Update-List -Property "ProcessItems" -Add $notepad | Out-Null
$osd.ProcessItems

$osd | Update-List -Property "ProcessItems" -Remove $notepad | Out-Null
$osd.ProcessItems

#endregion

# TODO QUIZ - https://attilakrick.com/schlagwort/powershell-objekte/

<# TODO Übung A
 
    Finden Sie alle inkl. versteckte und System-Dateien in C:\ und darunter die größer 100MB sind.
    # TIPP - Get-ChildItem ; Where-Object
#>


<# TODO Übung B
 
    Beenden Sie alle Notepad- und Taschenrechner-Prozesse.
    # TIPP - Get-Process ; Where-Object -IN ; Stop-Process -WhatIf
#>


<# TODO Übung C
 
    1. Öffnen Sie sämtliche .log-Dateien in C:\Windows\Logs\*.
    2. Anschließend filtern Sie den Inhalt nach dem Wort "Error".
    3. Und jetzt schreiben Sie diese Error-Zeile in eine neue zentral MasterError.log-Datei weg. Folgende Informationen sollen enthalten sein: Quell-Dateiname, Zeilennummer, Fehlertext-Zeile.
    # TIPP - Select-String ; Where-Object ; Set-Content ; Get-ChildItem
#>


<# TODO Übung D
    Wie alt (in Tagen) ist die älteste Datei (CreationTime) im C:\Windows-Ordner?
    # TIPP - Get-ChildItem ; New-TimeSpan ; Select-Object ; Sort-Object
#>