Wissen/A01_Spickzettel.ps1

# ? TITEL Spickzettel
# ? DESCRIPTION PowerShell Spickzettel
# ? TAGS Spickzettel Kochbuch Tips
# ? VERSION 2010.11.11

[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
param()

#region Analyse & Hilfe

# ? 1. PowerShell-Hilfe installieren / aktualisieren
Update-Help -Module * -UICulture en-US, de-DE -Force

# ? 2. Hilfe zur bzw. um die PowerShell (about-Seiten)
Get-Help -Name about_* | Out-GridView -OutputMode Multiple # ! Inhaltsverzeichnis
Get-Help -Name about_CommonParameters -ShowWindow          # ! about_CommonParameters öffnen
Get-Help -Name "Call operator" -Category HelpFile          # ! In allen about-Seiten suchen

# ? 3. Ein benötigtes Cmdlet finden
Get-Command -Name * -CommandType Cmdlet, Function                      # ! Ein Cmdlet über dessen Namen finden
Get-Command -Verb Stop                                                 # ! Ein Cmdlet über dessen Verb finden
Get-Verb | Sort-Object -Property Verb | Format-Wide -AutoSize          # ! Welche Verben gibt es
Get-Command -Noun Object                                               # ! Ein Cmdlet über dessen Substantiv finden
Get-Command -Name G*-*connection*                                      # ! ? Oder eine Suchkombination aus Verb und Noun
Get-Command -Name * -Module RemoteDesktop                              # ! Ein Cmdlet finden über den Modulnamen
Get-Module -ListAvailable | Out-GridView                               # ! Welche Module gibt es
Start-Process -FilePath https://www.google.de/search?q=powershell+ping # ! Ein Cmdlet über Google finden

# ? 4. Hilfe zu einem Cmdlet aufrufen, lesen und anwenden
Get-Help -Name Get-Process -ShowWindow            # ! Hilfe für Get-Process LOKAL öffnen
Get-Help -Name Get-Process -Online                # ! Hilfe für Get-Process ONLINE öffnen (VSCode: CTRL + F1)
Get-Help -Name about_CommonParameters -ShowWindow # ! Hilfe zu den Standard-Parametern

# ? 5. Objekt-Analyse:
# ? AUFGABENSTELLUNG: Welche Prozesse sind von der Firma Microsoft?
# ! Es ist immer wichtig zu analysieren (u.a. Get-Member, Select-Object)
# ! A) was ist das für ein Rückgabeobjekt,
Get-Process | Get-Member
Get-Process | Get-Member -View All
Get-Process | Get-Member -View Extended

# ! B) was kann ich mit diesem Objekte machen (Eigenschaften, Methoden, Ereignisse) und
Get-Process | Get-Member  # ! => ProcessName, Company, Description, Product, Site
# ! C) welche Werte enthalten diese Eigenschaften?
Get-Process | Select-Object -Property ProcessName, Company, Description, Product, Site
# ? LÖSUNG:
Get-Process | Where-Object -Property Company -EQ -Value "Microsoft Corporation"

# ? 6. Vererbung
# ! 1. Eine Klasse kann von EINER anderen Basisklasse ABGELEITET sein und ERBT so dessen Member/Funktionen.
# ! 2. Jede Klasse ist früher oder später von der Klasse 'Object' (alias PSObject) abgeleitet worden.
# ! 3. Eine abgeleitete Klasse A ist daher mit der Basisklasse B kompatibel,
# ! daher kann auch ein Objekt von Typ A an einen Parameter vom Typ B übergeben werden.
# ? Welche Typ A von welchem Typ B abgeleitet wurde erfahren Sie so:
"Köln!".PSTypeNames
(Get-Process)[0].PSTypeNames
(Get-Service)[0].PSTypeNames
(Get-Process).PSTypeNames

# ? 7. Enumerationen
# ! Enumerationen (Enum) bilden eine häufig benutzt Untermenge von Typen da.
# ! Ob ein Parameter eben nur diese Enumeration akzeptiert ist auf den ersten Blick ...
# ! ... nicht ersichtlich da die Hilfe nur den Typennamen ausgibt.
# ? Handelt es sich bei einem Type X um eine Enumeration?
Get-Service | Get-Member -Name StartType
[System.ServiceProcess.ServiceStartMode] | Get-Member
[System.ServiceProcess.ServiceStartMode].IsEnum
[System.ServiceProcess.ServiceStartMode].GetEnumNames()

# ? Vorhandene Enumerations-Typen finden
function Get-Enum {
    param (
        [string]$Value,
        [String]$Name,
        [switch]$All
    )
    $defaultManifestModules = 'CommonLanguageRuntimeLibrary', 
                              'Microsoft.CSharp.dll', 
                              'Microsoft.Management.Infrastructure.dll', 
                              'Microsoft.PowerShell.Commands.Management.dll', 
                              'Microsoft.PowerShell.commands.Utility.dll', 
                              'System.dll', 
                              'System.Configuration.dll',
                              'System.Configuration.Install.dll',
                              'System.Core.dll', 
                              'System.Data.dll', 
                              'System.DirectoryServices.dll', 
                              'System.Management.Automation.dll', 
                              'System.Management.dll',
                              'System.ServiceProcess.dll',
                              'System.Transactions.dll', 
                              'System.Xml.dll'
    
    [System.AppDomain]::CurrentDomain.GetAssemblies() | 
        Where-Object -FilterScript {$All -or ($defaultManifestModules -contains $_.ManifestModule)} |
        ForEach-Object -Process { try { $_.GetExportedTypes() } catch { "Keine ExportedTypes vorhanden" | Write-Verbose } } |
        Where-Object -FilterScript { $_.IsEnum -and $_.Name -imatch $Name } |
        ForEach-Object -Process {
            return [PSCustomObject]@{
                Name   = $_.FullName
                Source = $_.Module.ScopeName
                Values = [System.Enum]::GetNames($_)
            }
        } |
        Where-Object -Property Values -imatch -Value $Value
 }
Get-Enum -Value SilentlyContinue
Get-Enum -Name ServiceStartMode | Get-Member
Get-Enum -All | Measure-Object
 
# ? 8. Weitere hilfreiche Informationen über Objekte herausfinden
# ! Beide Methoden werden automatisch bei laden des AKPT-Moduls implementiert
$code = {
    Add-Type -AssemblyName System.Windows.Forms
  
    $propertyGrid                = New-Object System.Windows.Forms.PropertyGrid
    $propertyGrid.Dock           = [System.Windows.Forms.DockStyle]::Fill
    $propertyGrid.PropertySort   = [System.Windows.Forms.PropertySort]::Alphabetical
    $propertyGrid.SelectedObject = $this
  
    $window         = New-Object System.Windows.Forms.Form
    $window.Text    = $this.ToString()
    $window.Size    = New-Object System.Drawing.Size -ArgumentList @(600, 800)
    $window.TopMost = $true
    $window.Controls.Add($propertyGrid)
    $window.ShowDialog() | Out-Null
}
Update-TypeData -MemberType ScriptMethod -MemberName ShowObject -Value $code -TypeName System.Object -Force
$code = {
    $url = 'https://docs.microsoft.com/de-de/dotnet/api/{0}' -f $this.GetType().FullName
    Start-Process $url
}
Update-TypeData -MemberType ScriptMethod -MemberName GetHelp -Value $code -TypeName System.Object
$p1 = Get-Process | Select-Object -First 1
$p1.ShowObject()
$p1.GetHelp()

#endregion
#region Aliase

# ! Nachteil ohne Aliase
Get-Process | Where-Object -Property Company -Like -Value "Microsoft*" | Sort-Object -Property Name | Format-Table -Property Name, Company
# ! Vorteil mit Aliase
gp | ? Company -like "Microsoft*" | sort Name | ft Name, Company 

Get-Alias ; Get-Command -CommandType Alias     # ! Übersicht aller Aliase
Get-Alias -Name ls                             # ! Wie lautet das Cmdlet für den Alias 'ls'
Get-Alias -Definition Get-ChildItem            # ! Welche Aliase gibt es für das Cmdlet 'Get-ChildItem'
New-Alias -Name ping -Value Test-NetConnection # ! Eigene Aliase erstellen

# ! Für Parameter können auch Aliase definiert sein, welche es sein können erfahren Sie so:
Get-Command -Name Get-ChildItem | Select-Object -ExpandProperty Parameters | Select-Object -ExpandProperty Values | Select-Object -Property Name, Aliases

#endregion
#region Objekte in der Pipeline managen

Get-Command -Noun Object -Module Microsoft.PowerShell.*                   # ! Eine Übersicht von Cmdlets die Rückgabe-Objekte managen können:
Get-Process | Sort-Object -Property WorkingSet64 -Descending              # ! Sortieren
Get-Process | Select-Object -Property Name, WorkingSet64                  # ! Filtern
Get-Process | Select-Object -Property Name -Unique                        # ! Filtern
Get-Process | Select-Object -ExpandProperty Name                          # ! Filtern
Get-Process | Select-Object -Skip 3 -First 2                              # ! Filtern
Get-Process | Where-Object -Property Company -Like -Value "Microsoft*"    # ! Filtern
Get-Process | Where-Object -FilterScript {                                          
    $_.Company -like "Microsoft*" -or $_.Company -like "Oracle*"}         # ! Filtern
Get-ChildItem -Path C:\Windows -File -Force |                              
    Group-Object -Property Extension |                                    # ! Gruppieren
    Sort-Object -Property Count -Descending |                             # ! Sortieren
    Select-Object -First 3                                                # ! Filtern
Compare-Object -ReferenceObject 10,11,12,14 -DifferenceObject 10,12,13,14 # ! Vergleichen
31, 32, 33| ForEach-Object -Process {"192.168.178.$_"} # ! Objekt-Schleife

# ? Objekte konvertieren
Get-Command -Verb ConvertTo, ConvertFrom -Module Microsoft.PowerShell.* # ! Übersicht der Cmdlets
Get-Process | ConvertTo-Csv | out-File C:\temp\procs.csv                  # ! CSV
Get-Process | Select-Object Name, Company, WorkingSet64 | 
    ConvertTo-Csv -Delimiter ";" -NoTypeInformation |                     # ! CSV
    Set-Content C:\temp\procs.csv
Get-Process | Select-Object Name, Company, WorkingSet64 | 
    ConvertTo-Html | out-File C:\temp\procs.html                          # ! HTml
ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force             # ! SecureString


#endregion
#region Vorhandene Objekte erweitern

# ? Vorhandene Objekt-Member abfragen
Get-Process | Get-Member
Get-Process | Get-Member -Static
Get-Process | Get-Member -View All
Get-Process | Get-Member -View Extended

# ? Einem Process-Objekt temporär Member hinzufügen
$p1 = Get-Process | 
    Select-Object -First 1 | 
    Add-Member -Name Jetzt_A -Value (  Get-Date  ) -MemberType NoteProperty -PassThru | 
    Add-Member -Name Jetzt_B -Value ({ Get-Date }) -MemberType ScriptMethod -PassThru |
    Add-Member -Name Jetzt_C -Value ({ Get-Date }) -MemberType ScriptProperty -PassThru
$p1 | Get-Member -Name Jetzt*

# ?
# ? Für die aktuelle Session dauerhaft Erweiterungen vornehmen
# ?

# ! Beispiel 1:
Update-TypeData -TypeName System.Diagnostics.Process -MemberName WorkingSet64MB -Value { [int]($this.WorkingSet64 / 1MB) } -MemberType ScriptProperty -Force
Get-Process | Sort-Object -Property WorkingSet64MB -Descending | Select-Object -Property Name, WorkingSet64MB -First 10

# ! Beispiel 2:
$code = {
    Add-Type -AssemblyName System.Windows.Forms
  
    $propertyGrid                = New-Object System.Windows.Forms.PropertyGrid
    $propertyGrid.Dock           = [System.Windows.Forms.DockStyle]::Fill
    $propertyGrid.PropertySort   = [System.Windows.Forms.PropertySort]::Alphabetical
    $propertyGrid.SelectedObject = $this
  
    $window         = New-Object System.Windows.Forms.Form
    $window.Text    = $this.ToString()
    $window.Size    = New-Object System.Drawing.Size -ArgumentList @(600, 800)
    $window.TopMost = $true
    $window.Controls.Add($propertyGrid)
    $window.ShowDialog() | Out-Null
}
Update-TypeData -MemberType ScriptMethod -MemberName ShowObject -Value $code -TypeName System.Object -Force
$p1 = Get-Process | Select-Object -First 1
$p1.ShowObject()

# ! Beispiel 3:
$code = {
    $url = 'https://docs.microsoft.com/de-de/dotnet/api/{0}' -f $this.GetType().FullName
    Start-Process $url
}
Update-TypeData -MemberType ScriptMethod -MemberName GetHelp -Value $code -TypeName System.Object
$p1 = Get-Process | Select-Object -First 1
$p1.GetHelp()

#endregion
#region Auf Ereignisse reagieren

# ? Asynchrones Ereignis-Handling
$np = Start-Process -FilePath notepad -WindowStyle Minimized -PassThru
$regEvent = Register-ObjectEvent -InputObject $np -EventName Exited -Action { "Der Notepad-Prozess wurde beendet." | Write-Warning }
$np | Stop-Process -Force
$regEvent | Get-EventSubscriber | Unregister-Event

# ? 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

# ? 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
    Unregister-Event -SourceIdentifier job.StateChanged
    Remove-Job -Name job.StateChanged
}
$job | Receive-Job -Keep

# ? 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
Get-EventSubscriber
Unregister-Event -SourceIdentifier fsw_*  -Force
$fsw.Dispose()
$fsw = $null
Remove-Variable -Name fsw -Force

# ? 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 

# ? Eigene Events auslösen
Register-EngineEvent -SourceIdentifier myEvent -Action { "A L A R M!" | Write-Warning }
New-Event -SourceIdentifier myEvent
Unregister-Event -SourceIdentifier myEvent

#endregion
#region Objekt-Bindung in der Pipeline

@"
Benutzername;Passwort;Beschreibung
p.lustig;P@ssw0rd;Peter Lustig (IT)
e.gruen;Geh1imAbc;Eva Grün (HR)
"@
 | Set-Content -Path c:\temp\NewUsers.csv

Get-Content -Path C:\temp\NewUsers.csv |
  ConvertFrom-Csv -Delimiter ";" -Header Name, Password, Description | 
  Select-Object -Skip 1 -Property Name, `
                                  Description, `
                                  @{Label="Password"; Expression={$_.Password | ConvertTo-SecureString -AsPlainText -Force}} |
    New-LocalUser -WhatIf

#endregion
#region Splatting (Mehrere Parameter gleichzeitig an ein Cmdlet übergeben)

Get-EventLog -LogName System -Newest 5 -EntryType Error, Warning
# vs.
$argument = @{
    LogName   = 'System'
    Newest    = 5
    EntryType = 'Error', 'Warning'
}
Get-EventLog @argument

#endregion
#region Output

#region PowerShell

# ? Formatierte Ausgabe
Get-Command -Verb Format -Module Microsoft.PowerShell.*                                                               # ! Übersicht der Cmdlets
Get-Process | Format-Table -Property Name, Company, CPU, TotalProcessorTime, WorkingSet64 -AutoSize                   # ! Tabellenausgabe
Get-Process | Format-Table -Property Name, @{Label="WS (MB)"; Expression={"{0,7:0.0}" -f ($_.WorkingSet64 / 1MB)   }} # ! Tabellenausgabe
Get-EventLog -LogName System -Newest 10 -EntryType Error | Format-Table -Wrap                                         # ! Tabellenausgabe
Get-Process | Format-List -Property *                                                                                 # ! Listenausgabe
Get-Alias | Sort-Object -Property Name | Format-Wide -Property Name -Column 10                                        # ! Komprimierte Ausgabe
Get-Service -Name SsTpSvc | Format-Custom -Property Name, StartType, DependentServices -Depth 2                       # ! Verschachtelte Klassen-Ausgabe

# ? Div. Meldungen mit einem Status ausgeben
Get-Command -Verb Write -Module Microsoft.PowerShell.Utility                             # ! Übersicht der Cmdlets
# ! ACHTUNG: Write-Host nicht in Kombination mit PowerShell Core benutzen
# ! (Write-Host ist veraltet! Nachfolger Write-Output)
"Hallo Köln!" | Write-Output                                                             # ! Das Ziel kann angegeben werden z.B. Datei, etc.
"Achtung ich bin ein Fehler" | Write-Error                                               # ! Stop das Script wenn CommonParameter '-ErrorAction Stop' gesetzt
Write-Warning "und ich bin eine Warnung"                                                 # ! Evtl. wird das Script gestoppt CommonParameter -WarningAction Stop
Write-Debug "und ich eine Debug-Meldung"                                                 # ! CommonParameter -Debug
0..100 | ForEach-Object {
    Write-Progress -Activity "Verarbeite Daten" -Status "$_ Prozent" -PercentComplete $_ # ! Einen %-Fortschritt anzeigen
    Start-Sleep -Milliseconds 100 }

# ? Die Ausgabe umleiten
Get-Command -Verb Out -Module Microsoft.PowerShell.*                             # ! Übersicht der Cmdlets
Get-ChildItem -Path c:\ -Recurse | Out-Host -Paging
Get-ChildItem -Path c:\ -Directory | Select-Object -First 10 | Out-Printer       # ! Ausgabe umleiten zum Druckern
Get-ChildItem -Path c:\ -Directory | Select-Object -First 10 | Out-GridView      # ! Ausgabe umleiten in eine Fenster
Get-ChildItem -Path c:\ -Directory | Select-Object -First 10 | Out-File          # ! Ausgabe umleiten in eine Datei
Get-ChildItem -Path c:\ -Directory | Select-Object -First 10 | Out-String        # ! Ausgabe umleiten in einen String
Get-ChildItem -Path c:\ -Directory | Select-Object -First 10 | Out-Null          # ! Ausgabe unterdrücken

# ! Siehe auch:
Send-MailMessage ; Set-Content ; Invoke-SqlCmd -Query "INSERT INTO ..."

#endregion
#region .NET

#region eine ALARM-Meldung

Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Speech
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        WindowStartupLocation="CenterScreen"
        ResizeMode="NoResize"
        WindowStyle="None"
        mc:Ignorable="d"
        SizeToContent="WidthAndHeight"
        Title="MainWindow">
    <Grid Background="Red">
        <TextBlock FontFamily="Consolas" FontSize="100" FontWeight="ExtraBold" Foreground="White" Margin="50" HorizontalAlignment="Center" VerticalAlignment="Center">A L A R M !</TextBlock>
    </Grid>
</Window>
"@

$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [System.Windows.Markup.XamlReader]::Load($reader)
$window.Topmost = $true
$window.Show() | Out-Null
$speaker = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer -Property @{ Rate = 2; Volume = 100 }
$speaker.SpeakAsync('ALARM! ALARM! ALARM! ALARM!')
Start-Sleep -Seconds 5
$window.Close()

#endregion
#region .NET Framework WPF 'Hallo Welt'

function Invoke-WpfHalloWelt {

    [CmdletBinding(SupportsShouldProcess=$false)]
    param()

    Add-Type -AssemblyName PresentationFramework
    Add-Type -AssemblyName System

    [XML]$XAML = @"
    <Window x:Class="FSM.PS_WPF_Sample"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="PS_WPF_Sample" Height="200" Width="300">
        <Grid>
            <TextBox x:Name="PSText" Text="Hello World" HorizontalAlignment="Left" Margin="65,40,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="175"/>
            <Label x:Name="PSLabel" Content="Type in the box and click OK" HorizontalAlignment="Left" Margin="65,80,0,0" VerticalAlignment="Top" Width="175" BorderThickness="1"/>
            <Button x:Name="PSBtnOK" Content="OK" HorizontalAlignment="Left" Margin="65,130,0,0" VerticalAlignment="Top" Width="75"/>
            <Button x:Name="PSBtnExit" Content="Exit" HorizontalAlignment="Left" Margin="165,130,0,0" VerticalAlignment="Top" Width="75"/>
        </Grid>
    </Window>
"@


    $XAML.Window.RemoveAttribute("x:Class")
    $Reader = New-Object System.Xml.XmlNodeReader $XAML
    $Form = [Windows.Markup.XamlReader]::Load($Reader)

    $PSText    = $Form.FindName('PSText')
    $PSLabel   = $Form.FindName('PSLabel')
    $PSBtnOK   = $Form.FindName('PSBtnOK')
    $PSBtnExit = $Form.FindName('PSBtnExit')

    $btnOKClick   = {$PSLabel.Content = $PSText.Text}
    $btnExitClick = {$Form.Close()}

    $PSBtnOK.Add_Click($btnOKClick)
    $PSBtnExit.Add_Click($btnExitClick)

    $Form.ShowDialog()
}
Invoke-WpfHalloWelt

#endregion
#region Diagramm erstellen per DataVisualization

using namespace System.Windows.Forms.DataVisualization.Charting
using namespace System.Windows.Forms
using namespace System.Drawing

Add-Type -AssemblyName System.Windows.Forms.DataVisualization

$chartTitle = "Top Memory Usage"
$axisYTitle = "Memory (MB)"
$axisXTitle = "Process Name"
$dataSource = Get-Process | 
    Sort-Object PrivateMemorySize -Descending  | 
    Select-Object -First 10 | ForEach-Object -Process {
        [PSCustomObject]@{
            Name              = $_.Name
            PrivateMemorySizeMB = [int]($_.PrivateMemorySize / 1MB)
            VirtualMemorySizeMB = [int]($_.VirtualMemorySize / 1MB)
        }
    }

$chart1 = New-Object Chart
$chart1.Width = 1200
$chart1.Height = 1000
$chart1.BackColor = [Color]::White
$chart1.Dock = [DockStyle]::Fill

$chart1.Titles.Add($chartTitle) | Out-Null
$chart1.Titles[0].Font = "Arial,13pt"
$chart1.Titles[0].Alignment = [ContentAlignment]::TopCenter

$chartArea = New-Object ChartArea
$chartArea.Name = "ChartArea1"
$chartArea.AxisY.Title = $axisYTitle
$chartArea.AxisX.Title = $axisXTitle
$chartArea.AxisY.Interval = 100
$chartArea.AxisX.Interval = 1
$chart1.ChartAreas.Add($chartArea)

$legend = New-Object Legend
$legend.name = "Legend1"
$chart1.Legends.Add($legend)

$chart1.Series.Add("VirtualMem") | Out-Null
$chart1.Series["VirtualMem"].ChartType = "Column"
$chart1.Series["VirtualMem"].BorderWidth  = 3
$chart1.Series["VirtualMem"].IsVisibleInLegend = $true
$chart1.Series["VirtualMem"].ChartArea = "ChartArea1"
$chart1.Series["VirtualMem"].Legend = "Legend1"
$chart1.Series["VirtualMem"].color = [Color]::LightGreen
$dataSource | ForEach-Object {
    $chart1.Series["VirtualMem"].Points.AddXY( $_.Name, $_.VirtualMemorySizeMB)  | Out-Null
}

$chart1.Series.Add("PrivateMem") | Out-Null
$chart1.Series["PrivateMem"].ChartType = "Column"
$chart1.Series["PrivateMem"].IsVisibleInLegend = $true
$chart1.Series["PrivateMem"].BorderWidth  = 3
$chart1.Series["PrivateMem"].ChartArea = "ChartArea1"
$chart1.Series["PrivateMem"].Legend = "Legend1"
$chart1.Series["PrivateMem"].color = [Color]::Orange
$dataSource | ForEach-Object {
    $chart1.Series["PrivateMem"].Points.AddXY( $_.Name, $_.PrivateMemorySizeMB) | Out-Null
}
            
# z.B. als PNG speichern, oder
$chart1.SaveImage("c:\temp\$chartTitle.png", "png")

# z.B. im Fenster anzeigen
$Form = New-Object Form
$Form.Width = 1300
$Form.Height = 800
$Form.StartPosition = [FormStartPosition]::CenterScreen
$Form.controls.add($chart1)
$Form.Add_Shown({$Form.Activate()})
$Form.ShowDialog()

#endregion

# ! Siehe auch:
# ! Cmdlet-Grundgerüst per GUI bauen => https://poshgui.com/CmdletBuilder

#endregion

#endregion
#region Input

#region PowerShell

#region Read-Host

# ? Datum gem. den Ländereinstellungen abfragen
$ci = New-Object -TypeName System.Globalization.CultureInfo -ArgumentList $PSCulture
$benutzereingabe = Read-Host -Prompt "Bitte Datum eingeben ($($ci.DateTimeFormat.ShortDatePattern))"
$benutzereingabe.ToDateTime($ci)

# ? Credential verschlüsselt speichern
$username = Read-Host -Prompt "Benutzername eingaben"
$password = Read-Host -Prompt "Passwort für $username eingeben" -AsSecureString
[PSCustomObject]@{
    Username = $username
    Password = $password | ConvertFrom-SecureString
} | ConvertTo-Json | Set-Content -Path C:\Temp\Credential_Admin.json -Force
$admin = Get-Content -Path C:\Temp\Credential_Admin.json | ConvertFrom-Json | Select-Object Username, @{Label="Password"; Expression={$_.Password | ConvertTo-SecureString}}
$cred = [System.Management.Automation.PSCredential]::new($admin.Username, $admin.Password)
Enter-PSSession -ComputerName $ZielComputer -Credential $cred

#endregion
#region Out-GridView / Show-Command

# ? Eine InputBox simulieren
$antwort = "JA, Dateien archivieren", "NEIN, Dateien am Ort belassen" | Out-GridView -OutputMode Single
$antwort.StartsWith('NEIN')

# ? Ein Cmdlet per GUI mit dem Benutzer interagieren lassen
Import-Module -Name .\Modules\AKPT
Get-EuroExchange -ListCurrency | 
    Out-GridView -OutputMode Multiple | 
    Get-EuroExchange | 
    Out-GridView

#endregion
#region PromptForChoice

$titel      = "Aktion wählen"
$nachricht  = "Wie lautet Ihre Entscheidung?"
$ja         = New-Object System.Management.Automation.Host.ChoiceDescription "&Ja"        , "Hilfetext zu JA"
$nein       = New-Object System.Management.Automation.Host.ChoiceDescription "&Nein"      , "Hilfetext zu NEIN"
$vielleicht = New-Object System.Management.Automation.Host.ChoiceDescription "&Vielleicht", "Hilfetext zu VIELLEICHT"
$optionen   = [System.Management.Automation.Host.ChoiceDescription[]]($ja, $nein, $vielleicht)
$ergebnis   = $host.UI.PromptForChoice($titel, $nachricht, $optionen, 1) 
switch ($ergebnis)
{
    0 {"Die Auswahl ist JA"}
    1 {"Die Auswahl ist NEIN"}
    2 {"Die Auswahl ist VIELLEICHT"}
}

#endregion

#endregion
#region .NET

#region MessageBox

using namespace System.Windows.Forms # Betr. Namespace bekannt machen; MUSS IN DEN ERSTEN ZEILEN STEHEN
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null # Betr. DLL-Datei wird geladen
$title         = "Dateien archivieren"
$message       = "Alte Dateien archivieren? (Werden am Ursprungsort gelöscht auf ... kopiert)"
$buttons       = [MessageBoxButtons]::YesNo
$icon          = [MessageBoxIcon]::Question
$defaultButton = [MessageBoxDefaultButton]::Button1
$antwort       = [MessageBox]::Show($message, $title, $buttons, $icon, $defaultButton)
if($antwort -eq [DialogResult]::Yes)
{
    "OK, die Dateien werden archiviert..."
}

#endregion
#region Common Dialogs

# ? OpenFileDialog
using namespace System.Windows.Forms # Betr. Namespace bekannt machen; MUSS IN DEN ERSTEN ZEILEN STEHEN
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null # Betr. DLL-Datei wird geladen
$ofd = New-Object OpenFileDialog
$ofd.Filter = "PowerShell-Skripten (*.ps1)|*.ps1|Text-Dateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*"
$ofd.InitialDirectory = "C:\Temp"
$dialogResult = $ofd.ShowDialog()
if ($dialogResult -eq [DialogResult]::OK) {
    $ofd.Filename
}

# ? FolderBrowserDialog
using namespace System.Windows.Forms # Betr. Namespace bekannt machen; MUSS IN DEN ERSTEN ZEILEN STEHEN
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null # Betr. DLL-Datei wird geladen
$fbd = New-Object FolderBrowserDialog
$fbd.Description = "Bitte Ordner für das Archivieren auswählen"
$fbd.RootFolder = [Environment+SpecialFolder]::Desktop
$fbd.ShowNewFolderButton = $true
$dialogResult = $fbd.ShowDialog()
if ($dialogResult -eq [DialogResult]::OK) {
    $fbd.SelectedPath
}

#endregion
#region WPF 'Euro Rechner'

# Betr. Namespace bekannt machen; MUSS IN DATEIKOPF
using namespace System.PresentationFramework
using namespace System.Windows.Markup
using namespace System.Xml

Add-Type -AssemblyName PresentationFramework

$url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
$xml = [xml](Invoke-WebRequest -Uri $url | Select-Object -exp Content) 
$cubes = $xml.Envelope.Cube.Cube.Cube
$currencys = $cubes | Sort-Object currency | Select-Object -exp currency

function EuroRateRechnen() {
    try {
        $aktWährung    = $WährungenCtrl.SelectedItem
        [decimal]$rate = $cubes | Where-Object currency -eq $aktWährung | Select-Object -exp rate
        $euro          = $eurosCtrl.Text
        $ergebnis      = $rate * $euro
    }
    catch {
        $rate     = 0
        $ergebnis = 0
    }
    $RateCtrl.Text     = "{0:#,##0.0000} {1}" -f $rate    , $WährungenCtrl.SelectedItem
    $ErgebnisCtrl.Text = "{0:#,##0.0000} {1}" -f $ergebnis, $WährungenCtrl.SelectedItem
}

[XML]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="€CALC"
    Width="220"
    Height="280"
    WindowStartupLocation="CenterScreen">
    <Grid Margin="20">
        <StackPanel>
            <Label Margin="-4 0 0 -2">Währungssymbol</Label>
            <ComboBox x:Name="Währungen" />
            <Label Margin="-4 8 0 -2">Rate</Label>
            <TextBox x:Name="Rate" IsReadOnly="True" TextAlignment="Right" />
            <Label Margin="-4 8 0 -2">€ (Euros)</Label>
            <TextBox x:Name="Euros" TextAlignment="Right" />
            <Label Margin="-4 8 0 -2">Ergebnis</Label>
            <TextBox x:Name="Ergebnis" IsReadOnly="True" TextAlignment="Right" FontWeight="Bold" />
        </StackPanel>
    </Grid>
</Window>
"@


$reader = New-Object System.Xml.XmlNodeReader $xaml
$window = [System.Windows.Markup.XamlReader]::Load($Reader)

$WährungenCtrl = $window.FindName('Währungen')
$RateCtrl      = $window.FindName('Rate')
$EurosCtrl     = $window.FindName('Euros')
$ErgebnisCtrl  = $window.FindName('Ergebnis')

$currencys | ForEach-Object { $WährungenCtrl.Items.Add($_) | Out-Null }
$WährungenCtrl.Add_SelectionChanged({EuroRateRechnen})
$WährungenCtrl.SelectedIndex = 0

$EurosCtrl.Add_TextChanged({EuroRateRechnen})
$EurosCtrl.Text = "1"

$window.ShowDialog()

#endregion
#region Read-Window

Import-Module .\Modules\AKPT
Read-Window -Title "Alte Dateien finden" -Message "Alte Dateien sind älter als?" -Default "01.01.2010"

#endregion

# ! Siehe auch:
# ! Cmdlet-Grundgerüst per GUI bauen => https://poshgui.com/CmdletBuilder

#endregion

#endregion
#region Module

# ? Überblick
Get-Module -Name * -ListAvailable # ! Eine Übersicht aller INSTALLIERTEN Module, gruppiert nach Modul-Ordner
Get-Module -Name *                # ! Welche Module sind z.Zt. importiert/geladen

# ? Modul installieren
$env:PSModulePath -split ";"                                                                                         # ! Installationsorte
Get-ChildItem -Path C:\Windows\system32\WindowsPowerShell\v1.0\Modules                                               # ! Das Modul steht computerweit zur Verfügung, wenn es in dem Default-Ordner liegt:
Get-ChildItem -Path C:\Users\Alex\Documents\WindowsPowerShell\Modules                                                # ! Das Modul steht dem lokalen Benutzer zur Verfügung, wenn es in dem Default-Ordner liegt:
Copy-Item -Path .\AKPT -Destination C:\Users\Administrator\Documents\WindowsPowerShell\Modules\AKPT\ -Force -Recurse # ! Module per Copy installieren
Install-Module -Repository PSGallery -Name AKPT -SkipPublisherCheck -Scope CurrentUser -Force                        # ! Module per PowerShellGallery installieren

# ? Modul IMPORTIEREN und dadurch in der Session nutzbar machen
Import-Module -Name .\Modules\AKPT -Verbose # ! Modul AKPT laden/importieren soweit es den installiert wurde
Get-Hotkeys                                 # ! ... Seit der PowerShell 3.0 wird Import-Module automatisch ausgeführt
Get-Help -Name Get-Hotkeys -ShowWindow      # ! ... wenn die Hilfe zu einem Cmdlet aufgerufen wird
Get-Command -Module AKPT                    # ! ... wenn der Inhalt des Moduls angezeigt wird

# ? Ein Modul manuell entladen z.b. für das Debugging nötig
Remove-Module -Name AKPT -Force 

# ? Modul deinstallieren
Uninstall-Module -Name AKPT -AllVersions -Force

#region Ein Module von PowerShellGallery downloaden, analysieren und testhalber importieren

Find-Module -Tag AttilaKrick
Find-Module -Name AKPT | Format-List -Property Name, Version, Description, Author, PublishedDate, AdditionalMetadata, ReleaseNotes # TODO Übung AdditionalMetadata lesbar machen
Save-Module -Name 'AKPT' -Path "$env:USERPROFILE\Downloads" -Force -Verbose # -AcceptLicense

# ! Ein Module vom FileSystem laden
Import-Module -Name "$env:USERPROFILE\Downloads\AKPT\*\AKPT.psd1" -Verbose
Install-Module -Name AKPT -Scope CurrentUser -Verbose
Update-Module -Name AKPT -Verbose

#endregion

#endregion
#region PSProvider & PSDrive

Get-Command -Noun ChildItem, Item, ItemProperty, ItemPropertyValue, Location, Path -Module Microsoft.PowerShell.* # ! Übersicht aller Default-Cmdlets
Get-PSProvider                                                                                                    # ! Übersicht z.Zt. geladener PSProvider
Get-PSDrive                                                                                                       # ! Übersicht z.Zt. geladene PSDrives
Import-Module -Name SqlServer                                                                                     # ! Durch das importieren eines Modules können weitere PS-Provider aktiviert werden

# ? Beispiel 1
New-PSDrive -PSProvider FileSystem -Name DL -Root $env:USERPROFILE\Downloads
Get-ChildItem -Path DL:\
New-PSDrive -PSProvider Registry -Name HKCC -Root HKEY_CURRENT_CONFIG
Get-ChildItem -Path HKCC:

# ? Beispiel 2
New-Item         -Path C:\Temp                 -Name NeueDatei.txt                     -ItemType Directory 
New-Item         -Path HKCU:\Software          -Name NeuerKey                          -ItemType Key
New-Item         -Path variable:\              -Name NeueVariable -Value "Hallo Köln!" -ItemType Variable   # ! $NeueVariable = "Hallo Köln!" ; New-Variable -Name NeueVariable -Value "Hallo Köln!" -Option ReadOnly
New-ItemProperty -Path HKCU:\SOFTWARE\NeuerKey -Name Heute        -Value (Get-Date)    -PropertyType String

# ? Absolute Pfad-Angaben
Get-ChildItem -Path c:\
Get-ChildItem -Path variable:\    # $PsUiCulture
Get-ChildItem -Path cert:\
Get-ChildItem -Path env:\         # $env:WinDir
Get-ChildItem -Path hkcu:\
Get-ChildItem -Path SqlServer:\

# ? Relative Pfad-Angaben
Get-ChildItem -Path .  # . => Aktueller Ordner
Get-ChildItem -Path .. # .. => Übergeordneter
Get-ChildItem -Path \  # \ => Stammordner
Get-ChildItem -Path ~  # ~ => Home-Verzeichnis

# ? Handling mit den Laufwerken
Set-Location -Path HKCU:\SOFTWARE                         # ! Aktuellen Ordner setzen
Set-Location -Path C:\Windows\System32                    # ! Aktuellen Ordner setzen
Resolve-Path -Path .\MsInfo32.exe                         # ! Pfad auflösen
Test-Path -Path C:\Windows\System32\MsInfo32.exe          # ! Pfad testen (Nur Syntax-Prüfung mit: -IsValid)
Test-Path -Path Variable:\PSUICulture                     # ! Pfad testen
Split-Path -Path C:\Windows\System32\MsInfo32.exe -Parent # ! Pfad zerlegen
Split-Path -Path C:\Windows\System32\MsInfo32.exe -Leaf   # ! Pfad zerlegen
Join-Path -Path $env:WinDir -ChildPath System32           # ! Pfad zusammenbauen

#endregion
#region Remoting

# ? PowerShell-Remoting (WinRM) einrichten
Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private -PassThru
Enable-PSRemoting -Force
Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value * -Force

# ? Remoting nutzen: Simple
Enter-PSSession -ComputerName "192.168.0.100"
New-Item -Path C:\temp\AkWasHere.V1 -ItemType File
Exit-PSSession

# ? Remoting nutzen: Complex
$admin = Get-Credential -Message "Anmeldung für Server X & Y" -UserName Administrator
$serverX = New-PSSession -ComputerName "192.168.0.101" -Credential $admin -Name X
$serverY = New-PSSession -ComputerName "192.168.0.102" -Credential $admin -Name Y
Enter-PSSession -Session $serverX
Exit-PSSession
Enter-PSSession -Session $serverY
Exit-PSSession
Remove-PSSession -Session $serverX, $serverY
Get-PSSession

# ? Remoting nutzen: 1:n
Invoke-Command -ComputerName "192.168.0.103", "192.168.0.104" -ScriptBlock { Get-Process } -AsJob -JobName lfdProcXY 
Get-Job
Receive-Job -Name lfdProcXY -Keep

# ! Siehe auch:
# ! Remoting Troubleshooting => B09_Remoting.ps1

#endregion
#region Operatoren

# ? Übersicht aller Operatoren
Get-Help -Name about_Operators            -ShowWindow # ! Übersicht aller Operatoren
Get-Help -Name about_Operator_Precedence  -ShowWindow # ! Vorrangregeln
Get-Help -Name about_Arithmetic_Operators -ShowWindow # ! Arithmetische- Operatoren
Get-Help -Name about_Assignment_Operators -ShowWindow # ! Zuweisungs- Operatoren
Get-Help -Name about_Logical_Operators    -ShowWindow # ! Logische- Operatoren
Get-Help -Name about_Comparison_Operators -ShowWindow # ! Vergleichs- Operatoren
Get-Help -Name about_Type_Operators       -ShowWindow # ! Typen- Operatoren
Get-Help -Name about_Split                -ShowWindow # ! String-Zerlege- Operatoren
Get-Help -Name about_Join                 -ShowWindow # ! String-Zusammenfüge-Operatoren

# ? Beispiele zu: about_Arithmetic_Operators
1 + 1 ; 2 * 10 ; 9 % 2 ; 15 -shl 1

# ? Beispiele zu: about_Assignment_Operators
$zahl=1 ; $zahl++ ; $zahl+=2
$varB = @() ; $varB += (Get-Process)[0] ; $varB += (Get-Process)[1]

# ? Beispiele zu: about_Logical_Operators
$true -and $true ; !$true -and $true ; -not $true -and $true

# ? Beispiele zu: about_Comparison_Operators
"Köln" -eq "köln" ; "Köln" -ceq "köln" ; "Köln" -ieq "köln"
"Hallo Köln!" -iReplace "Köln", "Berlin"
"Köln", "Hamburg", "München" -contains "Köln"
"abc@abc.de" -like "*@*.*"
"AB678.docx" -match "^[A-Z]{2,2}[0-9]{3,3}\.doc[x]{0,1}$"
10,11,12,13,14,10 -ne 10

# ? Beispiele zu: about_Type_Operators
(Get-Process) -is [System.Array]
"2015-03-18" -is [DateTime]
"2015-03-18".PSTypeNames

# ? Beispiele zu: -f (Format-Operator) s. about_operators
"Wert 1 ist {0}, der letzte Wert ist {3} und die anderen sind: {1}, {2}" -f 10,11,99,230,1024
"Zahlenformat: {0:#,##0.00}" -f 123456.6789
"Zahlenformat: {0:c2}" -f 123456.6789
"Log_{0:yyyyMMddHH}.log" -f (Get-date)

# ? Beispiele zu: about_Split, about_Join,
"ADS001;127.0.0.1;1234.67;" -split ";"
("Hallo Köln!", "127.0.0.1", 123.45) -join ";"

# ? Beispiele zu: Spezial-Operator
$c = {Get-Service -Name Spooler} ; & $c
. c:\scripts\sample.ps1
[datetime]$birthday = "1/20/88" ; $birthday
1..10
[datetime]::now

#endregion
#region Typen und Variable

# ? String
$ort = "Köln"
"Hallo $ort!"
'Hallo $ort!'

# ? Ganzzahlen (Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64)
"{0:x}" -f 255
0xFF
[Convert]::ToByte(0xFF)

# ? DateTime, DateTimeOffset
[Convert]::ToDateTime("1.10.2019")
[Convert]::ToDateTime("10/1/2019", [CultureInfo]::new("en-US"))

# ? Unix-Time
$unixtime = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" | Select-Object -ExpandProperty InstallDate
$span = New-TimeSpan -Seconds $unixtime
$start = [datetime]"1970-01-01"
$start + $span

# ? EMail
[System.NET.Mail.MailAddress]'Attila Krick<a.krick@outlook.com>'

# ? TimeSpan (Zeitdauer)
[TimeSpan]1
[timespan]5d
New-TimeSpan -Minutes 1

# ? Array: Eindimensional
$array1 = 1, 2, 3, 4                                                             
$array1 = 1..4
$array1 = Get-Process
$array1[2]

# ? Array: Zweidimensionale
$array2 = (11,12,13), (21,22,23), (31,32,33)                                     
$array2[1][1]
$array2[$array2.Length-1]

# ? Array: Gemischte
$array3 = "Hallo", 12, (Get-Process)                                             
$array3.Length
$array3 | Select-Object -Last 1

# ? Array: Ein Multidimensionales
$arrayMD = New-Object -TypeName ‘Int32[, ]’ -ArgumentList 2, 2 

# ? Hashtable
@{ HashNameA="Wert"; HashNameB="Wert"; } # ! Syntax
Get-ChildItem -Path c:\temp -File | Select-Object -Property Name, @{Label="LengthKB"; Expression={$_.Length / 1KB}}
Get-ChildItem -Path c:\temp -File | ForEach-Object -Process {
    [PSCustomObject]@{
        Dateiname    = $_.Name
        DateigrößeKB =  [int]($_.Length / 1KB)
        Besitzer     = $_ | Get-Acl | Select-Object -ExpandProperty Owner
    }
}

# ? Ein Hashtable durch Benutzung erstellen
$person = @{}
$person.Age = 23
$person.Name = ‘Tobias’
$person.Status = ‘Online’
$person.Name
$person

# ? StringData
$LocalizedData = Data {
    ConvertFrom-StringData @'
    CannotDetermineTestName = Cannot determine test name
    InvalidTestInfo = TestInfo must contain the path and the list of tests
    InspectingModules = Inspecting Modules
    DiagnosticSearch = Searching for Diagnostics in {0}
    InvokingTest = Invoking tests in {0}
'@

}
$LocalizedData.InvalidTestInfo
$LocalizedData.InvokingTest 

# ? ScriptBlock
$sc   = {Get-Process} ; . $sc

# ? Die wichtigsten PowerShell Typen
[PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get.Keys | Sort-Object | ForEach-Object { “[$_]” }

# ? Eigenen Datentypen definieren
class Wechselkurs {
  [ValidatePattern('^[A-Z]{3,3}$')]
  [string]$Währung

  [ValidateRange(0, 10000)]
  [decimal]$Rate
}
New-Object -TypeName Wechselkurs -Property @{Währung = 'USD'; Rate = 1.4567}

# ? Werttypen (Primitive Typen)
$a = 10
$b = 20
$a = $b
$b = 99
'$a = {0} ; $b = {1}' -f $a, $b
[Object]::ReferenceEquals($a, $b)
$b.GetType().IsPrimitive

# ? Referenztype
$a = @(10)
$b = @(20)
$a = $b
$b[0] = 99
'$a = {0} ; $b = {1}' -f $a[0], $b[0]
[Object]::ReferenceEquals($a, $b)
$b.GetType().IsPrimitive

# ? Variable: Gültigkeitsbereiche

$Global:A  # Auf Ebene der PowerShell-Anwendung
$Script:B  # Auf Ebene des Scripts/Modules
$Private:C # Auf Ebene des aktuellen Gültigkeitsbereiches

# ? Call-Operator . und &
Set-Location -Path C:\Temp
'$nachricht = "Message from Test.ps1"' | Set-Content -Path .\Test.ps1 -Force

$nachricht = "Keine Nachricht!"
. .\Test.ps1 # ! Aufruf im aktuellen Gültigkeitsbereich
"Die akt. Nachricht lautet: $nachricht"

$nachricht = "Keine Nachricht!"
& .\Test.ps1 # ! Aufruf im eigenen Gültigkeitsbereich
"Die akt. Nachricht lautet: $nachricht"

# ? PowerShell-Provider für Variable
Get-ChildItem -Path Variable: -Force
Get-Item -Path Variable:\PSVersionTable
Set-Item -Path Variable:\PSUICulture -Value "en-US" -Force
Test-Path -Path Variable:\Error

# ? Variable erstellen
$planet = "Erde"
New-Variable -Name $planet -Value "Mars" -Force # ! FALSCH
New-Variable -Name  planet -Value "Mars" -Force # * RICHTIG
Get-ChildItem -Path C:\ -Force -Recurse -File -ErrorVariable sumGciErrors -ErrorAction SilentlyContinue

New-Variable -Name PI -Value 3.14 -Option ReadOnly
$PI = 99
New-Variable -Name PI2 -Value 3.14 -Option Constant
$PI2 = 99
Set-Variable -Name PI -Value 99 -Force
Set-Variable -Name PI2 -Value 99 -Force

# ? Typen-Umwandlung vs. Variablentyp festlegen
$b      = [int]"99" # Der Typ von $b kann sich ändern
[int]$c =      "99" # $c wird für IMMER auf int festgelegt

$b | Get-Member
$c | Get-Member

$b = "Hallo Welt!"
$b | Get-Member

$c = "Hallo Welt!"

# ? Variable: Sonderformen
$env:WinDir                        # Windows Umgebungsvariablen
${c:\temp\JetztIst.txt} = Get-Date # Datei als Variable-Speicher

# ? Prüfen ob eine Variable (PSProvider) vorhanden ist
Test-Path -Path variable:var1
Test-Path -Path variable:varX

# ! .PS1-Empfehlung => Variablen müssen vor dem Benutzen deklariert werden
Set-StrictMode -Version Latest
$password = "geheim"
$pasword -eq "geheim"

# ? Variablen evtl. in Skripten und Funktionen aufräumen
Remove-Variable -Name alter
Remove-Variable -Name PI -Force
Remove-Variable -Name * -Force -ErrorAction SilentlyContinue

#endregion
#region PowerShell-Dateien / -Dateierweiterungen

# ! .ps1 (PowerShell Script version 1.0)
# Ausführbare Skriptdatei (Beachte Ausführungsrichtlinien=

# ! .ps1xml
# PowerShell Format- und Typdefinitionen-Datei
Get-ChildItem -Path C:\Windows\System32\WindowsPowerShell\v1.0\*.ps1xml

# !.psc1 (PowerShell Console version 1)
Export-Console -Path c:\temp\myconsole.psc1
powershell.exe -PSConsoleFile c:\temp\myconsole.psc1

# ! .psd1 (PowerShell Definition version 1)
# Modul-Manifest-Datei
# Definiert die Eckdaten eines Modules und psdm1 den Inhalt des Modules, z.B. siehe:
Get-ChildItem -Path C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Archive

# ! .psm1 (PowerShell Modul version 1)
# Ausführbare Modul-Skriptdatei die beim importieren eines Moduls ausgeführt wird)
# Beachte Ausführungsrichtlinien

#endregion
#region PowerShell-Dateien-Ausführungsrichtlinien

Get-ExecutionPolicy -List                                            # ! Was ist eingestellt
$env:PSExecutionPolicyPreference                                     # ! Was gilt für den aktuellen Scripthost
Set-ExecutionPolicy -ExecutionPolicy AllSigned                       # ! Empfohlen Einstellung für Client und Server
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser # ! Empfohlen Einstellung für Test-/Entwicklungszwecke (Remote-Scripte müssen signiert sein)

#endregion
#region Kontrollstrukturen

# TODO Für die korrekte Syntax gibt es in Visual Studio Code Schnipsel zum einfügen (CTRL + ALT + J) oder über Autovervollständigung (Beachte Symbol)

Get-Help -Name about_if      -ShowWindow
Get-Help -Name about_switch  -ShowWindow
Get-Help -Name about_while   -ShowWindow
Get-Help -Name about_do      -ShowWindow
Get-Help -Name about_do      -ShowWindow
Get-Help -Name about_for     -ShowWindow
Get-Help -Name about_foreach -ShowWindow

Get-Help -Name about_Break    -ShowWindow
Get-Help -Name about_Continue -ShowWindow

#endregion
#region Debugging (Visual Studio Code)

# ? VSCode-Debug-Modus-Tastaturbefehle
# ! F9 => Haltepunkt setzen/lösen
# ! F5 => Debug-Modus starten, die Ausführung bleibt beim ersten Haltepunkt stehen
# ! F10 => Funktionsaufrufe werden übersprungen und im gesamt abgearbeitet
# ! F11 => Führt die nächste Code-Zeile aus (Gelbe Zeile => wurde NOCH NICHT ausgeführt)
# ! Ein erreichbarer Funktionsaufrufe wird angesprungen
# ! SHIFT + F5 => Beenden den Debug-Modus vorzeitig

# ? In den Debug-Modus wechseln wenn sich Variablen ändern (dynamische Haltepunkte)
Set-PSBreakpoint -Variable ipId -Mode Write                                                               # ! Anhalten wenn die Variable $ipId beschieben wird:
Set-PSBreakpoint -Variable ipAddress -Mode Write -Action { if ($ipAddress -ceq "192.168.178.3") {break} } # ! Anhalten wenn die Variable den Wert "192.168.178.5" enthält:

# ? Debug-Meldungen ausgeben
$DebugPreference = [System.Management.Automation.ActionPreference]::Continue # ! Script-Lokal aktivieren
Test-NetConnection -ComputerName 127.0.0.1 -Debug                            # ! oder Cmdlet lokal aktivieren

# ? Tracing
Trace-Command -Name ParameterBinding -Expression { Get-Process spoolsv } -PSHost # ! Cmdlet-Verarbeitung analysieren
Get-TraceSource | Select-Object -Property Name, Description | Out-GridView       # ! Welche Überwachungen es gibt liefert folgendes Cmdlet

# ? Der Common-Parameter Verbose
Remove-Module -Name AKPT -Verbose
Import-Module -Name .\Modules\AKPT -Verbose

# ? Die Dauer einer Ausführung messen
Measure-Command -Expression { 1..5 | ForEach-Object -Process { "Verarbeite Nummer $_"; Start-Sleep -Seconds 1 } | Out-Default }
1..5 | Measure-Command -Expression { "Verarbeite Nummer $_" | Out-Default; Start-Sleep -Seconds 1 }

#endregion
#region Error Handling

#region Meldungen unterdrücken

Get-Process -FileVersionInfo -ErrorAction Ignore -WarningAction Ignore -InformationAction Continue -Verbose # ! Lokal Unterdrückung
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # ! Globale Unterdrückung, Default => Continue
$WarningPreference     = [System.Management.Automation.ActionPreference]::Stop # ! Globale Unterdrückung, Default => Continue
$InformationPreference = [System.Management.Automation.ActionPreference]::Stop # ! Globale Unterdrückung, Default => SilentlyContinue
$VerbosePreference     = [System.Management.Automation.ActionPreference]::Stop # ! Globale Unterdrückung, Default => SilentlyContinue

# ? Meldungen in eigenen Cmdlets einbauen
function Test-CommonParameter {
    [CmdletBinding()] # ! WICHTIG um die Common-Parameters nutzen zu können
    param (

    )
    "Error Test"       | Write-Error
    "Warning Test"     | Write-Warning
    "Verbose test"     | Write-Verbose
    Write-Information -MessageData "Information Test"
    "Debug Test"       | Write-Debug
}
Test-CommonParameter
Test-CommonParameter -ErrorAction SilentlyContinue -WarningAction Ignore -Verbose -InformationAction Continue
Test-CommonParameter -Debug

#endregion
#region Fehler analysieren

# ! Hilfreiches Cmdlet zur Analyse von Fehler:
filter Get-ErrorDetails {
    $Category         = @{ Name = 'CategoryInfo'     ; Expression = { $_.CategoryInfo.Category           }}
    $Line             = @{ Name = 'LineNumber'       ; Expression = { $_.InvocationInfo.ScriptLineNumber }}
    $Script           = @{ Name = 'ScriptName'       ; Expression = { $_.InvocationInfo.ScriptName       }}
    $Target           = @{ Name = 'Target'           ; Expression = { $_.TargetObject                    }}
    $ErrorId          = @{ Name = 'ErrorId'          ; Expression = { $_.FullyQualifiedErrorID           }}
    $ExType           = @{ Name = 'ExceptionType'    ; Expression = { $_.Exception.GetType().FullName    }}
    $ExMessage        = @{ Name = 'ExceptionMessage' ; Expression = { $_.Exception.Message               }}
    $ExInnerException = @{ Name = 'ExceptionMessage' ; Expression = { $_.Exception.InnerException        }}

    $Input | Select-Object -Property $Category, $Line, $Script, $Target, $ErrorId, $ExType, $ExMessage, $ExInnerException
}

# ? Beispiel:
$myErrors = @()
Get-Process -FileVersionInfo -ErrorVariable myErrors             # ! Löst Fehler aus
Get-Content -Path C:\MichGibtEsNicht.txt -ErrorVariable myErrors # ! Löst Fehler aus
$myErrors | Get-ErrorDetails

#region Die $error-Systemvariable

$error # Enthält 256 Fehler seit beginn der Session
$MaximumErrorCount
$MaximumErrorCount = 999 # Default 256
$error.Count
$error | Select-Object -Last 1  # ! Der älteste Fehler
$error | Select-Object -First 1 # ! Der neuste Fehler ($error[0])
# ? Nutzen, z.B.:
$error.Clear()
42 / 0
"Aufgetretene Fehler: $($error.Count)"
$error | Get-ErrorDetails

#endregion

# ? Wurde der letzte Befehl erfolgreich ausgeführt:
Stop-Service -Name Audiosrv -Force
if ($?) {
    "'Stop-Service -Name Audiosrv -Force' wurde erfolgreich ausgeführt!"
}

#endregion
#region Fehler behandeln

#region per try, catch und finally

# ! try..catch nur benutzen wenn EINE LÖSUNG implentiert wird,
# ! da dieses Konstrukt der PowerShell signalisiert das der Fehler behoben wurde!

# ! #######################################
# ! ### EINE MELDUNG IST KEINE LÖSUNGEN ###
# ! #######################################

try {
    $ErrorActionPreferenceBackup = $ErrorActionPreference
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    # ! Code der evtl. einen Fehler auslöst:
    Get-Content c:\temp\wichtig.txt
}
catch [System.Management.Automation.ItemNotFoundException] {
    # ! Code der diesen Fehler behebt:
    New-Item c:\temp\wichtig.txt
}
catch [System.OutOfMemoryException] { 
    # ! Völlig spezifischer Fehler
    # ! Code der diesen Fehler behebt
}
catch [System.SystemException] { # Bereichsfehler

}
catch { # Völlig unspezifische Fehler
    # ! Code der diesen Fehler behebt => Was könnte das nur sein !?!?!?
    $_.GetType().FullName
    throw $_
}
finally {
    $ErrorActionPreference = $ErrorActionPreferenceBackup
}

#endregion
#region über trap

$ErrorActionPreferenceBackup = $ErrorActionPreference
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

trap [System.DivideByZeroException] {
    '[SIRENE] => Division durch 0 wurde festgestellt'
    continue
}

trap [System.Management.Automation.ActionPreferenceStopException] { 
    '[SIRENE] => Eine ActionPreferenceStopException ist aufgetreten, tatsächliche Exception auslösen und abfangen'
    throw $_.Exception
    continue

    trap [System.Management.Automation.ItemNotFoundException] {
        '[SIRENE] => Die Datei wurde nicht gefunden'
        continue
    }

    trap {
        '[SIRENE] => Irgend ein anderer Fehler in ActionPreferenceStopException ist aufgetreten'
        continue
    }
}

trap  {
    '[SIRENE] => Irgend ein anderer Fehler ist aufgetreten'
    continue
}

12/0
Get-Content -Path C:\MichGibtEsNicht.txt
Get-Process -Name wininit -FileVersionInfo

$ErrorActionPreference = $ErrorActionPreferenceBackup

#endregion

#endregion
#region Eigene Fehler auslösen

[byte]$hubraum = 5000                                                     # ! ... durch Typenfestlegung
"UPS, da hat etwas nicht geklappt" | Write-Error                          # ! ... durch Write-Error
[ValidateRange(5, 10)]$Hubraum                                            # ! ... durch div. Validate-Attributen für eigen Cmdlet-Parameter
throw "Geburtsdatum muss <= Heute $(Get-Date -Format dd.MM.yyyy) sein"    # ! ... durch throw String-Text
throw New-Object -TypeName System.ArgumentException `
                 -ArgumentList "Wert 'xyz' ist ungültig.", "Geburtsdatum" # ! ... durch das Auslösen einer vorhanden Exception-Klasse
# ! ... durch das Auslösen einer selbst deklarierte Exception-Klasse
[System.SerializableAttribute()]
class KundeVorhandenException : System.Exception {
    [string]$Message
    [int]$KundeId
    KundeVorhandenException([string]$message, [int]$kundeId) : base("$message (Kunde $kundeId)") {
        $this.Message = $message
    }
}
throw New-Object -TypeName KundeVorhandenException -ArgumentList "Der Kunde ist bereits vorhanden", 4711

#endregion

#endregion
#region Muster-Aufbau von ausführbaren Dateien (.PS1 & .PSM1)

# ! 1. Übersicht der benutzten Module (d.h. evtl. vorher installieren)
using Module Microsoft.PowerShell.Management
using Module Microsoft.PowerShell.Utility

# ! 2. using-Anweisung zum Verkürzen, z.B. [System.Management.Automation.ActionPreference]::Stop auf [ActionPreference]::Stop
using namespace System.Management.Automation
using namespace System.Windows.Forms

# ! 3. Beim auftreten eines Fehler => Script-Ausführung abbrechen
$ErrorActionPreference = [ActionPreference]::Stop # Default ist 'Continue'

# ! 4. Script-Übergabe-Parameter definieren
param ([string]$Path, [int]$BenutzerId) # ? Aufruf: MeinScript.ps1 -Path c:\temp -BenutzerId 47110815

# ! 5. Benötigte .NET Assembly laden
Add-Type -AssemblyName System.Windows.Forms

# ! 6. Sicherstellen das Variablen vor der ersten Verwendung deklariert wurden
Set-StrictMode -Version Latest

# ! 7. Vor der ersten Verwendung von Functions die Function-Definitionen importieren.
function Invoke-Irgendwas { param ([string]$Stadtname) "Stadtname: $Stadtname" }

# ! 8. Der eigentliche Logik-Code des Skriptes:
$title = Invoke-Irgendwas -Stadtname "Köln"
$fenster = New-Object -TypeName Form
$fenster.Text = $title
$fenster.ShowDialog()

# ! [9. Digitale Signatur]

#endregion
#region Benutzerdefinierte Cmdlets schreiben

# TODO Weiterführende und Nachschlage-Informationen
Get-Content -Path .\AKPT\Wissen\C07_UserCmdlets.ps1 
Get-Help about_Comment_Based_Help               -ShowWindow # Beschreibung der eigenen Cmdlet-Hilfe für das PS-Hilfesystem.
Get-Help about_Functions                        -ShowWindow # Grundlagen zu benutzerdefinierten Funktion
Get-Help about_Functions_Advanced               -ShowWindow # Grundlagen zu benutzerdefinierten Cmdlet
Get-Help about_Functions_Advanced_Methods       -ShowWindow 
Get-Help about_Functions_Advanced_Parameters    -ShowWindow # Beschreibung der eigenen Cmdlet-Parameter u.a. Validierung
Get-Help about_Functions_CmdletBindingAttribute -ShowWindow # Beschreibung der eigenen Cmdlet-Parameter bzgl. Pipeline-Verarbeitung
Get-Help about_Functions_OutputTypeAttribute    -ShowWindow # Beschreibung von Rückgabe-Objekte wie PSCustomObject

# ? Lernen von vorhanden Cmdlets anderer Programmierer
Get-ChildItem Function:\New-Guid | Select-Object -ExpandProperty ScriptBlock
Install-Module -Name .\AKPT ; Get-ChildItem Function:\* | Where-Object -Property ModuleName -IEQ -Value AKPT

#endregion
#region Scriptdateien signieren

# ! 1. Ausführungsrichtlinien auf 'AllSigned' setzen:
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope Process      -Force
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope CurrentUser  -Force
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine -Force

# ! 2. Signierer-Zertifikate erstellen (Oder über PKI-Admin):
$myPfxCert = New-SelfSignedCertificate -Subject "CN=_FirstName_LastName (PS Developer), E=v.nachname@abc.local" `
                                       -HashAlgorithm SHA512 `
                                       -KeyExportPolicy ExportableEncrypted `
                                       -CertStoreLocation Cert:\CurrentUser\My `
                                       -Type CodeSigningCert `
                                       -NotAfter (Get-Date).AddYears(5) 

# ! 3. Signierer-Zertifikate mit dem privaten Schlüssel exportieren
$myPfxCertPassword = Read-Host -Prompt "Das Passwort zum schützen des privaten Schlüssels angeben" -AsSecureString
$myPfxCert | Export-PfxCertificate -Password $myPfxCertPassword -FilePath .\MyCodeSigningCert.pfx

# ! 4. Aus dem Signierer-Zertifikate ein öffentliches Zertifikat erstellen:
Get-PfxCertificate -FilePath .\MyCodeSigningCert.pfx | Export-Certificate -FilePath .\PublicSignerCertificate.cer -Type CERT -Force

# ! 5. Vertrauensstellung einrichten:
Import-Certificate -FilePath .\PublicSignerCertificate.cer -CertStoreLocation Cert:\LocalMachine\Root
Import-Certificate -FilePath .\PublicSignerCertificate.cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher

# ! 6. .PS1-Datei signieren:
$cert = Get-ChildItem -Path .\*.pfx | Out-GridView -Title "Zertifikat wählen" -OutputMode Single | Get-PfxCertificate
Get-ChildItem -Path . | 
    Where-Object Extension -In '.PS1', '.PSM1' |
    Out-GridView -Title "Zu signierende .PS1 oder PSM1-Datei(en) auswählen" -OutputMode Multiple | 
    Set-AuthenticodeSignature -Certificate $cert `
                              -HashAlgorithm SHA512 `
                              -TimestampServer http://timestamp.globalsign.com/scripts/timstamp.dll

# ! Die digitale Signatur prüfen
Get-AuthenticodeSignature -FilePath '.\Test.ps1' | Format-List -Property *

#endregion


# TODO C09
# TODO C10
# TODO C11
# TODO C12