Skip to content

Instantly share code, notes, and snippets.

@schittli
Last active April 9, 2023 21:16
Show Gist options
  • Save schittli/a54b65b31e7e7bc9fcff9698d348bdc8 to your computer and use it in GitHub Desktop.
Save schittli/a54b65b31e7e7bc9fcff9698d348bdc8 to your computer and use it in GitHub Desktop.
LibGist: TomsBasicFuncs.ps1 #PowerShell #LibGist
# Basic PowerShell Funktionen
# Werden automatisch mit LibGist-Update-Gists.ps1 aktuell gehalten
#
# 001, 220126, Gist@jig.ch
# 002, 220129
# 003, 220130
# 004, 220205
# 003, 230409
# Funktionen ergänzt
### Config
$LibGistVersion_LibGist_TomBasicFuncs_ps1 = '004'
#Region Tom-Tools
#Region Tom-Tools: Log
### ℹ Log stumm schalten
#
# [CmdletBinding(SupportsShouldProcess)]
# Param (
# [Switch]$Silent
# )
#
# If ($Silent) { $Script:LogDisabled = $true }
# Log
# Prüft, ob $Script:LogColors definiert ist und nützt dann dieses zur Farbgebung
# $Script:LogColors =@('Cyan', 'Yellow')
#
# !Ex
# Log 1 'Test1' -NoNewline; Log 1 'Test2' -Append
# Log 1 'Test1' -NoNewline; Log -Message 'Test2' -Append
#
#
# 0: Thema - 1: Kapitel - 2: OK - 3: Error
# 200604 175016
# 200805 103305
# Neu: Optional BackgroundColor
# 211129 110213
# Fix -ClrToEol zusammen mit -ReplaceLine
# 220102 172537
# Fix Für PS7: Kommt irgendwie nicht mit NoNewLine zurecht
# 220118 095129
# ValueFromPipeline richtig angewendet
# 220123 222224
# Fixed: -ReplaceLine handling
# 220926 152200
# Neu: [Switch]IfVerbose
# Wir dur ausgegeben, wenn Verbose aktiv ist
# Fix bei NewLineBefore
# 230409 000309
# Wenn -Indent < 0: Keine Ausgabe
$Script:LogColors = @('Green', 'Yellow', 'Cyan', 'White', 'Red')
Function Log() {
Param (
[Parameter(Position = 0)]
[Int]$Indent,
[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[String]$Message = '',
[Parameter(Position = 2)]
[ConsoleColor]$ForegroundColor,
# Vor der Nachricht eine Leerzeile
[Parameter(Position = 3)]
[Switch]$NewLineBefore,
# True: Die aktuelle Zeile wird gelöscht und neu geschrieben
[Parameter(Position = 4)]
[Switch]$ReplaceLine = $false,
# True: Am Ende keinen Zeilenumbruch
[Parameter(Position = 5)]
[Switch]$NoNewline = $false,
# Append, also kein Präfix mit Ident
[Parameter(Position = 6)]
[Switch]$Append = $false,
# Löscht die Zeile bis zum Zeilenende
[Parameter(Position = 7)]
[Switch]$ClrToEol = $false,
# Ausgabe erfolgt nur, wenn Verbose aktiv ist
[Switch]$IfVerbose,
[Parameter(Position = 8)]
[ConsoleColor]$BackgroundColor
)
Begin {
$PSBoundParametersCopy = $PSBoundParameters
# Wenn Verbose gewünscht aber nicht aktiv, dann sind wir fertig
If ($IfVerbose -and (Is-Verbose) -eq $False) { Return }
# Wenn -Indent < 0, sind wir fertig
If ($Indent -lt 0) { Return }
If ($Script:LogDisabled -eq $true) { Return }
# Fix für PS7
If ($null -eq $Script:IsPS7) { $Script:IsPS7 = ($PSVersionTable).PSVersion.Major -eq 7 }
If ($null -eq $Script:DefaultBackgroundColor) { $Script:DefaultBackgroundColor = (Get-Host).UI.RawUI.BackgroundColor }
If ($Indent -eq $null) { $Indent = 0 }
If ($BackgroundColor -eq $null) { $BackgroundColor = $Script:DefaultBackgroundColor }
$WriteHostArgs = @{ }
If ($ForegroundColor -eq $null) {
If ($null -ne $Script:LogColors -and $Indent -le $Script:LogColors.Count -and $null -ne $Script:LogColors[$Indent]) {
Try {
$ForegroundColor = $Script:LogColors[$Indent]
} Catch {
Write-Host "Ungültige Farbe: $($Script:LogColors[$Indent])" -ForegroundColor Red
}
}
If ($null -eq $ForegroundColor) {
$ForegroundColor = [ConsoleColor]::White
}
}
If ($ForegroundColor) {
$WriteHostArgs += @{ ForegroundColor = $ForegroundColor }
}
$WriteHostArgs += @{ BackgroundColor = $BackgroundColor }
If ($NoNewline) {
$WriteHostArgs += @{ NoNewline = $true }
}
}
Process {
$PSBoundParametersCopy = $PSBoundParameters
# Wenn Verbose gewünscht aber nicht aktiv, dann sind wir fertig
If ($IfVerbose -and (Is-Verbose) -eq $False) { Return }
# Wenn -Indent < 0, sind wir fertig
If ($Indent -lt 0) { Return }
If ([String]::IsNullOrEmpty($Message)) { $Message = '' }
If ($NewLineBefore) { Write-Host '' }
If ($Append) {
$Msg = $Message
If ($ClrToEol) {
$Width = (get-host).UI.RawUI.MaxWindowSize.Width
If ($Msg.Length -lt $Width) {
$Spaces = $Width - $Msg.Length
$Msg = "$Msg$(' ' * $Spaces)"
}
}
} Else {
Switch ($Indent) {
0 {
$Msg = "* $Message"
If ($NoNewline -and $ClrToEol) {
$Width = (get-host).UI.RawUI.MaxWindowSize.Width
If ($Msg.Length -lt $Width) {
$Spaces = $Width - $Msg.Length
$Msg = "$Msg$(' ' * $Spaces)"
}
}
If (!($ReplaceLine)) {
$Msg = "`n$Msg"
}
}
Default {
$Msg = $(' ' * ($Indent * 2) + $Message)
If ($NoNewline -and $ClrToEol) {
# Rest der Zeile mit Leerzeichen überschreiben
$Width = (get-host).UI.RawUI.MaxWindowSize.Width
If ($Msg.Length -lt $Width) {
$Spaces = $Width - $Msg.Length
$Msg = "$Msg$(' ' * $Spaces)"
}
}
}
}
}
If ($ReplaceLine) { $Msg = "`r$Msg" }
Write-Host $Msg @WriteHostArgs
# Fix für PS7: Den Cursor ans Ende Positionieren
If ($Script:IsPS7 -and $NoNewline) {
$CursorPosition = (Get-Host).UI.RawUI.CursorPosition
$CursorPosition.X = $Msg.Length
(Get-Host).UI.RawUI.CursorPosition = $CursorPosition
}
# if (!([String]::IsNullOrEmpty($LogFile))) {
# "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Message" | Out-File $LogFile -Append
# }
}
}
#Endregion Tom-Tools: Log
#Rregion Tom-Tools: PS Processing / CmdletBinding
Function Is-Verbose() {
$VerbosePreference -eq 'Continue'
}
Function Is-WhatIf() {
$WhatIfPreference
}
#Endregion Tom-Tools: PS Processing / CmdletBinding
#Region Tom-Tools: PS Module
# Stellt sicher, dass Module installiert sind
# und lädt sie
Function Assert-Module-IsInstalled($ModuleName) {
If ((Get-Module -Name $ModuleName -ListAvailable).Count -gt 0) {
Return
}
Write-Host "Das PowerShell-Modul muss installiert werden: $ModulName"
Install-Module -Name $ModuleName
Import-Module -Name $ModuleName
}
#Endregion Tom-Tools: PS Module
#Region Tom-Tools: UI
# Stellt die Ja / Nein-Frage und liefert die Antwort
# True = ja
Function Ask_YesNo($Question) {
$Answer = $null
While ($Answer -eq $null) {
$Answer = Read-Host -Prompt $Question
$Answer = $Answer.Trim().ToLower()
Switch -regex ($Answer) {
'y|j' {
Return $true
}
'n' {
Return $false
}
Default { $Answer = $null }
}
}
}
#Endregion Tom-Tools: UI
#Region Tom-Tools: IO, File-Handling
# 210626
Function Test-FileIslocked {
Param (
[parameter(Mandatory = $true)]
[String]$File
)
If ((Get-Item $File) -is [System.IO.DirectoryInfo]) { Return $True }
If (!(Test-Path -Path $File)) { Return $False }
$oFile = New-Object System.IO.FileInfo $File
Try {
$oStream = $oFile.Open([IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
If ($oStream) { $oStream.Close() }
Return $False
} Catch [System.UnauthorizedAccessException] {
# $IsLocked = 'AccessDenied'
Return $True
} Catch {
# file is locked by a process.
Return $True
} Finally {
If ($oStream) { $oStream.Dispose() }
}
}
# Holt die erste Datei in $aDir, die den Namen $aScriptName hat
Function FindFirstFileName($aDir, $aScriptName) {
(Get-ChildItem -LiteralPath $aDir -Recurse -Filter $aScriptName | Select-Object -First 1).FullName
}
Function Backup-File($FileName, $NewExt = $null) {
If ($NewExt) {
$FileName = Replace-FileExt $FileName $NewExt
}
$BackupFile = Filename-Add-Suffix $FileName ($(get-date -f ' yyMMdd-HHmm'))
Copy-Item -LiteralPath $FileName -Destination $BackupFile
}
Function Is-File-NewerThan() {
Param (
[parameter(Mandatory)]
[String]$FileSrc,
[parameter(Mandatory)]
[String]$FileDst,
[Int]$DeltaSecondsTolerance = 0,
[Switch]$ShowInfo
)
# Konvertieren
[Double]$DeltaSecondsTolerance = $DeltaSecondsTolerance
$oFileSrc = [IO.FileInfo]($FileSrc)
If ($oFileSrc.Exists -eq $false) {
If ($ShowInfo) { Log 3 "Quelldatei existiert nicht:`n $($FileSrc)" }
# Die Quelldatei ist also älter, bzw. nicht versuchen, die Zieldatei zu generieren
Return $False
}
$oFileDst = [IO.FileInfo]($FileDst)
If ($oFileDst.Exists -eq $false) {
If ($ShowInfo) { Log 3 "Zieldatei existiert nicht:`n $($FileDst)" }
# Die Quelldatei ist also neuer
Return $True
}
# Zeitunterschied berechnen
$dtSec = (New-TimeSpan –Start $oFileSrc.LastWriteTime –End $oFileDst.LastWriteTime).TotalSeconds
$dtSecAbs = [Math]::Abs( $dtSec )
If ($dtSecAbs -le $DeltaSecondsTolerance) {
If ($ShowInfo) { Log 3 'Dateien sind gleich alt' }
Return 0
} Else {
If ($dtSec -lt 0) {
If ($ShowInfo) { Log 3 "Quelldatei ist $($dtSec)s älter als die Zieldatei" }
Return $false
} Else {
If ($ShowInfo) { Log 3 "Quelldatei ist $($dtSec)s jünger als die Zieldatei" }
Return $true
}
}
}
# Wipe File
# "C:\RemoveFileSecure\test1.txt" | Remove-FileSecure -DeleteAfterOverwrite #-Confirm:$false #-WhatIf
# Get-ChildItem c:\RemoveFileSecure -Filter *.txt | Remove-FileSecure -DeleteAfterOverwrite #-Confirm:$false #-WhatIf
# 200805
Function Remove-File-Secure {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact="High")]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
$File,
[Parameter()]
[Switch]$DeleteAfterOverwrite = $false
)
Begin {
$Rnd = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
}
Process {
$RetObj = $null
If ((Test-Path $File -PathType Leaf) -and $PSCmdlet.ShouldProcess($File)) {
$oFile = $File
if( !($oFile -is [System.IO.FileInfo]) ) {
$oFile = new-object System.IO.FileInfo($File)
}
$FileLen = $oFile.length
# write-host $oFile.FullName
$Stream = $oFile.OpenWrite()
Try {
$StopWatch = new-object system.diagnostics.stopwatch
$StopWatch.Start()
Write-Progress -Activity $oFile.FullName -Status "Write" -PercentComplete 0 -CurrentOperation ""
[long]$i = 0
$Buffer = new-object byte[](1024*1024)
While( $i -lt $FileLen ) {
$Rnd.GetBytes($Buffer)
$Rest = $FileLen - $i
If( $Rest -gt (1024*1024) ) {
$Stream.Write($Buffer, 0, $Buffer.length)
$i += $Buffer.LongLength
} Else {
$Stream.Write($Buffer, 0, $Rest)
$i += $Rest
}
[Double]$p = [double]$i / [double]$FileLen
[Long]$remaining = [double]$StopWatch.ElapsedMilliseconds / $p - [double]$StopWatch.ElapsedMilliseconds
Write-Progress -Activity $oFile.FullName -Status "Write" -PercentComplete ($p * 100) -CurrentOperation "" -SecondsRemaining ($remaining/1000)
}
$StopWatch.Stop()
} Finally {
$Stream.Close()
If( $DeleteAfterOverwrite ) {
$j = Remove-Item $oFile.FullName -Force -Confirm:$false
Start-Sleep -Milliseconds 250
$RetObj = new-object PSObject -Property @{File = $oFile; Wiped=$true; Deleted=(-Not (Test-Path $oFile)) }
} Else {
$RetObj = new-object PSObject -Property @{File = $oFile; Wiped=$true; Deleted=$false}
}
}
} Else {
$RetObj = new-object PSObject -Property @{File = $File; Wiped=$false; Deleted=$false}
}
return $RetObj
}
}
# Löscht Dateien, die älter als n Tage sind
# Unterstützt -WhatIf
# $PathFilter akzeptiert Muster
#
# Ex
# Remove-Old-Files c:\Tasks\AD-AAD-Group*.log -OlderThanDays 45 -WhatIf
# Remove-Old-Files c:\Tasks\AD-AAD-Group*.log -OlderThanDays 45
Function Remove-Old-Files() {
[CmdletBinding(SupportsShouldProcess = $True)]
Param (
[Parameter(Position = 0, Mandatory)]
[String]$PathFilter,
[Parameter(Position = 1, Mandatory)]
[Int]$OlderThanDays,
[Switch]$Recurse
)
If ($OlderThanDays -gt 0) { $OlderThanDays = -1* $OlderThanDays}
Get-ChildItem -Path $PathFilter -Recurse:$Recurse | ? { ($_.LastWriteTime -lt (Get-Date).AddDays($OlderThanDays)) } | Remove-Item -Force
}
# Sehr schnell, optional mit der expliziten Codierung
# http://www.happysysadm.com/2014/10/reading-large-text-files-with-powershell.html
Function Get-Content-Fast($LiteralPath, [Text.Encoding]$Encoding = $null) {
If ($Encoding) {
[System.IO.File]::ReadAllText($LiteralPath, $Encoding)
} Else {
[System.IO.File]::ReadAllText($LiteralPath)
}
}
# Sehr schnell, optional mit der expliziten Codierung
# 191028
Function Write-Content-Fast() {
Param (
[Parameter(Position = 0, Mandatory)]
$LiteralPath,
[Parameter(Position = 1, Mandatory, ValueFromPipeline)]
$Content,
[Parameter(Position = 2)]
[Text.Encoding]$Encoding,
[Switch]$Overwrite = $True
)
Begin {
If (Test-Path -LiteralPath $LiteralPath) {
If ($Overwrite) {
Remove-Item -Force -LiteralPath $LiteralPath
} Else {
Return
}
}
}
Process {
Switch ($Content.GetType()) {
([Object[]]) {
If ($Encoding) {
[System.IO.File]::WriteAllLines($LiteralPath, $Content, $Encoding)
} Else {
[System.IO.File]::WriteAllLines($LiteralPath, $Content)
}
}
([String]) {
If ($Encoding) {
[System.IO.File]::WriteAllText($LiteralPath, $Content, $Encoding)
} Else {
[System.IO.File]::WriteAllText($LiteralPath, $Content)
}
}
}
}
End {}
}
# 191028
Function Convert-File-Encoding() {
Param (
[Parameter(Position = 0, Mandatory)]
$LiteralPath,
[Parameter(Position = 1)]
[System.Text.Encoding]$InEncoding,
[Parameter(Position = 2)]
[System.Text.Encoding]$OutEncoding,
[Switch]$Overwrite = $True
)
$Content = Get-Content-Fast -LiteralPath $LiteralPath -Encoding $InEncoding
$Args = @{
Overwrite = $Overwrite
}
Write-Content-Fast -LiteralPath $LiteralPath -Content $Content -Encoding $OutEncoding @Args
}
# Erzeugt aus
# C:\Test und C:\Test\abc.txt » .\abc.txt
# C:\Test\ und C:\Test\abc.txt » .\abc.txt
# C:\Test\Dir und C:\Test\Dir\abc.txt » .\Dir\abc.txt
Function Get-Path-Delta($Base, $Target) {
# Allenfalls den Pfad-Delimiter zufügen: (Join-Path $Base '').Length
Return ".\{0}" -f $Target.SubString((Join-Path $Base '').Length )
}
#Endregion Tom-Tools: IO, File-Handling
#Region Tom-Tools: JSON
# Sortiert eine JSON Struktur inkl. Keys und Values,
# damit zwei Json Strukturen verglichen werden können
Function Sort-Json {
[CmdletBinding(DefaultParameterSetName = 'JsonString')]
[OutputType([Array])]
Param (
[Parameter(Mandatory, Position = 0, ParameterSetName = 'JsonString')]
[String]$JsonString,
[Parameter(Mandatory, Position = 0, ParameterSetName = 'JsonObj')]
[PSCustomObject]$JsonObj,
# Den Resultat-String komprimieren, wie ConvertTo-Json -Compress
[Switch]$Compress,
[Parameter(ParameterSetName = 'Debug')]
[Switch]$Dbg
)
# Write-Host $PsCmdlet.ParameterSetName
# Sortiert alle Json-Elemente
Function Sort-Json_ {
Param (
[PSObject] $SrcJsonObj,
[PSCustomObject]$ResSortedJsonObj,
[Int] $ThisLevel = 0,
[Int] $MaxLevel = 3
)
## Debug
## Alle Props von $ThisObj
# $DbgThisObjProps = $($SrcJsonObj.PSObject.Properties | Format-Table @{ Label = 'Type'; Expression = { "[$($($_.TypeNameOfValue).Split('.')[-1])]" } }, Name, Value)
# Das Resultat mit dem OrderedDictionary erzeugen
If ($null -eq $ResSortedJsonObj) {
$ResSortedJsonObj = [Ordered]@{}
}
# Alle Props übernehmen
$SrcJsonObj.PSObject.Properties | Sort-Object Name | % {
$ThisProp = $_
# System-Props überspringen
If (@('SyncRoot') -contains $ThisProp.Name) {
Continue
}
## Debug
If ($ThisProp.Name -eq 'defaultAudioLanguage' -and
$_.TypeNameOfValue -eq 'System.Object') {
$Stopper = 1
}
# Props übernehmen
# Und allenfalls nested OrderedDictionary erzeugen
Switch ($_.TypeNameOfValue) {
{ @('System.DateTime', 'System.String', 'System.Boolean',
'System.Int32', 'System.Int64', 'System.Double',
'System.TimeSpan') -contains $_ } {
# Das Prop übernehmen
$ResSortedJsonObj[$ThisProp.Name] = $ThisProp.Value
}
{ @('System.Object') -contains $_ } {
# Das Prop übernehmen
If ($null -eq $ThisProp.Value) {
$ResSortedJsonObj[$ThisProp.Name] = $Null
}
Else {
Write-Host 'Sort-Json(): Error #1: System.Object hat einen Wert != $Null:' -ForegroundColor Red
Write-Host " Typ : $($ThisProp.TypeNameOfValue)" -ForegroundColor Red
Write-Host " Name : $($ThisProp.Name)" -ForegroundColor Red
Write-Host " Value: $($ThisProp.Value)" -ForegroundColor Red
$Stopper = 1
}
}
{ @('System.Management.Automation.PSCustomObject',
'System.Object[]',
'Selected.System.Management.Automation.PSNoteProperty') -contains $_ } {
# Das Prop erzeugen: OrderedDictionary
$ResSortedJsonObj[$ThisProp.Name] = [Ordered]@{}
}
Default {
Write-Host "Sort-Json(): Skipping #1, weil noch nicht implementiert:" -ForegroundColor Red
Write-Host " Typ : $($ThisProp.TypeNameOfValue)" -ForegroundColor Red
Write-Host " Name : $($ThisProp.Name)" -ForegroundColor Red
Write-Host " Value: $($ThisProp.Value)" -ForegroundColor Red
}
}
}
# Die verschachtelten Props suchen und diese rekursiv übernehmen
ForEach ($Key in $ResSortedJsonObj.Keys) {
$Val = $ResSortedJsonObj[$Key]
## Debug
If ($Key -eq 'DurationTimeSpan') {
$Stopper = 1
}
# Write-Host "Key: $Key / Typ: $($Val.GetType().Name)"
If ($ThisLevel -le $MaxLevel) {
If ($Null -eq $Val) {
# Wenn der Wert $Null ist, müssen wir nichts tun,
# das wird hoffentlich richtig in Json konvertiert
}
Else {
Switch ($Val.GetType().Name) {
{ @('OrderedDictionary') -contains $_ } {
# Rekursiv das Prop übernehmen
# Die Quell-Daten bestimmen
$ThisSrcJsonObj = $SrcJsonObj.PSObject.Properties | ? Name -eq $Key | select -ExpandProperty Value
Sort-Json -SrcJsonObj $ThisSrcJsonObj -ResSortedJsonObj $ResSortedJsonObj[$Key] -ThisLevel ($ThisLevel + 1) -MaxLevel $MaxLevel
}
{ @('String', 'DateTime', 'Boolean', 'Int32', 'Int64',
'Double', 'TimeSpan') -contains $_ } {
# Skip gewollt: Props müssen nicht rekursiv übertragen werden
}
Default {
Write-Host "Sort-Json(): Skipping #2, weil noch nicht implementiert:" -ForegroundColor Red
Write-Host " Typ: $($Val.GetType().Name)" -ForegroundColor Red
Write-Host " Key: $($Key)" -ForegroundColor Red
}
}
}
}
Else {
Write-Host "MaxLevel $MaxLevel reached" -ForegroundColor Red
}
}
# Am Ende das Resultat zurückgeben
If ($ThisLevel -eq 0) {
Return $ResSortedJsonObj
}
}
### Prepare
# Allenfalls das Json Obj erzeugen
If ($PsCmdlet.ParameterSetName -eq 'JsonString') {
$JsonObj = $JsonString | ConvertFrom-Json -Depth 100
}
Else {
## !9 ConvertTo-Json / ConvertFrom-Json reagieren unterschiedlich,
# wenn das Ausgangsobjekt der String oder ein Objekt ist
# z.B. ist ein TimeSpan ein Objekt oder eine String-Liste mit den einzelnen Props
# Deshalb sicherstellen, dass wir immer mit aus string konvertierten Json-Daten arbeiten
# Das JsonObj in einen Json-String und wieder in das Obj konvertieren
$JsonObj = $JsonObj | ConvertTo-Json -Depth 100 | ConvertFrom-Json -Depth 100
}
### Main
# Json Obj sortieren
$SortedJsonObj = Sort-Json -SrcJsonObj $JsonObj
# Und als JSON String zurückgeben
If ($Compress) {
$SortedJsonObj | ConvertTo-Json -Depth 100 -Compress
}
Else {
$SortedJsonObj | ConvertTo-Json -Depth 100
}
}
# True, wenn JSON1 und JSON2 identisch sind
Function Is-JSON-Identical($oJson1, $oJson2) {
# Die Inhalte der JSON-Daten sortieren
$Json1_Compressed = & $SortJSON_ps1 -JsonObj $oJson1 -Compress
$Json2_Compressed = & $SortJSON_ps1 -JsonObj $oJson2 -Compress
# Sind die JSON-Daten identisch?
Return [String]::Compare($Json1_Compressed, $Json2_Compressed, [StringComparison]::InvariantCultureIgnoreCase) -eq 0
}
# Liest eine Json-Datei
# und liefert ein Json Obj
Function Read-Json-File() {
Param(
[parameter(Position = 0, Mandatory)]
[String]$JsonFileName,
[Text.Encoding]$Encoding
)
If (Test-Path -LiteralPath $JsonFileName -PathType Leaf) {
Get-Content-Fast -LiteralPath $JsonFileName -Encoding $Encoding | ConvertFrom-Json -Depth 100
}
}
# Schreibt eine Json-Datei
# Und überschreibt ein allenfalls existierendes File
Function Write-Json-File() {
Param(
[parameter(Position=0, Mandatory)]
[String]$JsonFileName,
[parameter(Position=1, Mandatory)]
[Object]$JsonData,
[Text.Encoding]$Encoding
)
$JsonData | ConvertTo-Json -Depth 100 | Write-Content-Fast -Overwrite -LiteralPath $JsonFileName -Encoding $Encoding
}
#Endregion Tom-Tools: JSON
#Region Tom-Tools: Cache
# Beispiel
#
# Function Get-YT-Channel-MetaData($Url) {
# $CacheVar = 'CacheMetaData'
# $ChannelMetaData = Cache-GetVal -CacheName $CacheVar -Key $Url
# If ($Null -eq $ChannelMetaData) {
# $ChannelMetaData = & $YouTubeRestAPI_ps1 -GetChannelMetaData $Url
# Cache-SetVal -CacheName $CacheVar -Key $Url -Value $ChannelMetaData
# }
# Return $ChannelMetaData
# }
# Liefert den Gecachten Wert oder $null
# Initialisiert allenfalls den Cache
Function Cache-GetVal($CacheName, $Key) {
$Cache = Get-Variable -Name $CacheName -ValueOnly -Scope Script -ErrorAction SilentlyContinue
# Allenfalls den Cache initialisieren
If ($null -eq $Cache) {
Set-Variable -Name $CacheName -Scope Script -Value @{}
} Else {
Return $Cache[$Key]
}
}
# Setzt einen Wert im Cache
Function Cache-SetVal($CacheName, $Key, $Value) {
$Cache = Get-Variable -Name $CacheName -ValueOnly -Scope Script -ErrorAction SilentlyContinue
# Allenfalls den Cache initialisieren
If ($null -eq $Cache) {
Set-Variable -Name $CacheName -Scope Script -Value @{}
$Cache = Get-Variable -Name $CacheName -Scope Script
}
Return $Cache.Add($Key, $Value)
}
#Endregion Tom-Tools: Cache
#Region Tom-Tools: Pipeline
# Liefert für die Obj-Liste
# die durch die PropNamen eindeutig identifizierten Objekte zurück
#
# !Ex
# Get-UniqueByProp -Data $Res $Properties | ft
# $Res | Get-UniqueByProp $Properties | ft
Function Get-UniqueByProp() {
[CmdletBinding()]
Param (
[Parameter(Position = 0, Mandatory)]
[String[]]$PropNames,
[Parameter(Position = 1, ValueFromPipeline)]
[Object[]]$Data
)
Begin {
$AllData = @()
}
Process {
$AllData += $Data
}
End {
$AllData |
Group-Object $PropNames | `
% { $_.Group | Select -First 1 } | `
Sort $PropNames
}
}
#Endregion Tom-Tools: Pipeline
#Region Tom-Tools: HashTables, Arrays
# Teilt ein Array in chunk of Arrays
# Ex
# [Int[]]$myArray = 1, 2, 3, 4, 5
# $Res = Get-Array-Chunks $myArray 2
# 230409 Alternative?:
# $Res = @(Get-Array-Chunks $myArray 2)
Function Get-Array-Chunks([Object[]]$SrcArr, [Int]$ChunkSize) {
# Wenn die SrcArr kleiner als $ChunkSize ist
If ($SrcArr.Count -le $ChunkSize) { Return $SrcArr }
# 230409 Alternative?:
# If ($SrcArr.Count -le $ChunkSize) { Return , @($SrcArr) }
Function Get-Arr($Type, $Size) {
Switch($Type) {
'String[]' { Return [String[]]::New($Size) }
'Int32[]' { Return [Int32[]]::New($Size) }
'Object[]' { Return [Object[]]::New($Size) }
Default { Write-Error "Keine Implementation für Typ: $Type" }
}
}
$SrcArrType = $SrcArr.GetType().Name
$AnzChunks = [Math]::Truncate($SrcArr.Count / $ChunkSize)
$LeftOver = $SrcArr.Count % $ChunkSize
[System.Collections.ArrayList]$Chunks = @()
For ($i = 0; $i -lt $AnzChunks; $i++) {
$ZielArr = Get-Arr $SrcArrType $ChunkSize
[Array]::Copy($SrcArr, $i * $ChunkSize, $ZielArr, 0, $ChunkSize)
$Chunks += ,@($ZielArr)
}
If ($LeftOver -gt 0) {
$ZielArr = @(Get-Arr $SrcArrType $LeftOver)
[Array]::Copy($SrcArr, ($AnzChunks * $ChunkSize), $ZielArr, 0, $LeftOver)
$Chunks += ,@($ZielArr)
}
$Chunks
}
# Erzeugt aus $Val ein Array
Function Get-Array($Val) {
If ($Val -isnot [System.Array]) {
Return, @($Val)
} Else {
Return $Val
}
}
# Konvertiert alle HashTable-Properties eines Objekts
# https://stackoverflow.com/a/55715823/4795779
# 200518 210715
Function Get-Hash-AsObject {
Param (
[HashTable]$hash,
[Switch]$Deep = $true
)
$NewHash = @{ }
ForEach ($k In $hash.Keys) {
If ($hash[$k] -is [HashTable] -and $Deep) {
$NewHash.Add($k, (Get-Hash-AsObject -Deep -hash $hash[$k]))
} ElseIf (($hash[$k] -is [Array])) {
$Items = @()
ForEach ($Item In $hash[$k]) {
$Items += (Get-Hash-AsObject -Deep -hash $Item)
}
$NewHash.Add($k, $Items)
} Else {
$NewHash.Add($k, $hash[$k])
}
}
Return [PSCustomObject]$NewHash
}
# Wenn $Obj $null ist, wird ein leeres Array zurückgegeben
# Anwendung:
# …
# Return-DataArr_OrEmptyArr $Obj
# }
# 200314 235243
Function Return-DataArr_OrEmptyArr($Obj) {
# Wenn wir kein Objekt haben, ein leeres Array zurückgeben
If ($Obj -eq $null) {
Return, @()
} Else {
# haben wir bereits ein Array?
Switch ($Obj.Gettype().Name) {
'Object[]' {
Return $Obj
}
default {
Return, @($Obj)
}
}
}
}
#Endregion Tom-Tools: HashTables, Arrays
#Region Tom-Tools: ScriptBlock in Texten
# Sucht in einem beliebigen Text nach allen ScriptBlocks
# Ex:
# $Test = @'
# …
# { @{ ShowDoc = $False } }
# …
# { @{ Prop = $True } }
# '@
Function Get-ScriptBlocks($Text) {
$RgxScriptBlock = @'
(?imsnx-)
(?<OuterScriptBlock>
\{ # Match {
(?<InnerScriptBlock>
[^{}]+ # all chars except {}
| (?<Level>\{) # or if { then Level += 1
| (?<-Level>\}) # or if } then Level -= 1
)+ # Repeat (to go from inside to outside)
(?(Level)(?!)) # zero-width negative lookahead assertion
\}
)
'@
# !9^9 Matches und nicht Match!
$MyMatches = [RegEx]::Matches($Text, $RgxScriptBlock, @('Compiled', 'CultureInvariant', 'ExplicitCapture', 'IgnoreCase', 'IgnorePatternWhitespace', 'Multiline', 'Singleline'))
If ($MyMatches.Count -gt 0) {
ForEach ($Match In $MyMatches) {
# Den äusseren Scriptblock holen
$OuterScriptBlock = $Match.Groups['OuterScriptBlock'].Value
# Den äusseren Scriptblock entfernen und zurückgeben
$OuterScriptBlock.Trim().Trim('{}').Trim()
}
}
}
# Evaluiert den Scriptblock und gibt das Resultat zurück
Function Invoke-ScriptBlock($Text) {
If ($SB = [Scriptblock]::Create($Text)) {
Return & $SB
}
}
# Startet alle ScriptBlocks und prüft, ob ein Proeprty gefunden wurde
#
# Retourniert das ScriptBlock-Resultat (für den Fall, dass weitere Properties von Interesse sind)
# und das gesuchte Property selber
# $ResSB, $ResSBProperty = Get-ScriptBlock-Prop …
Function Get-ScriptBlock-Prop([String[]]$TextScriptBlocks, $PropName) {
# Alle Text-ScriptBlocks prüfen
ForEach ($TextSB In $TextScriptBlocks) {
$ResSB = Start-ScriptBlock $TextSB
# Baben wir das gesuchte Property gefunden?
If ($ResSB -and $ResSB."$PropName") {
Return @($ResSB, $ResSB."$PropName")
}
}
}
#Endregion Tom-Tools: ScriptBlock in Texten
#Region Tom-Tools: Filenamen
Function Replace-FileExt($FileName, $NewFileExt) {
[IO.Path]::Combine( `
[IO.Path]::GetDirectoryName($FileName), `
[IO.path]::GetFileNameWithoutExtension($FileName) + $NewFileExt
)
}
# Ergänzt einen Dateinamen mit einem Pre- und/oder Suffix
# 200514 185133
Function Filename-Add-Prefix-Suffix($FileName, $Prefix, $Suffix) {
If ([System.String]::IsNullOrEmpty($Prefix)) { $Prefix = '' }
If ([System.String]::IsNullOrEmpty($Suffix)) { $Suffix = '' }
[IO.Path]::Combine( `
[IO.Path]::GetDirectoryName($FileName), `
$Prefix + [IO.Path]::GetFileNameWithoutExtension($FileName) + $Suffix + [IO.Path]::GetExtension($FileName)
)
}
# Ergänzt einen Dateinamen mit einem Suffix
Function Filename-Add-Suffix($FileName, $Suffix) {
[IO.Path]::Combine( `
[IO.Path]::GetDirectoryName($FileName), `
[IO.path]::GetFileNameWithoutExtension($FileName) + $Suffix + [IO.path]::GetExtension($FileName)
)
}
#Endregion Tom-Tools: Filenamen
#Region Tom-Tools: Pfade
# Bereinigt die Zeichen einer Bibelstelle, damit sie als Dateiname benützt werden kann
Function Clean-FileName-Str($Str) {
$CleanChars1 = $Str.Replace('/', '¬').Replace(':', '꞉').Replace('?', '¿').Replace('"', "'").Replace('|', '│').Trim('.')
# Removes any non-ASCII characters from the LHS string,
# which includes the problematic hidden control characters.
# https://stackoverflow.com/a/68328388/4795779
# $CleanChars2 = $CleanChars1 -replace '[^i\p{IsBasicLatin}]'
# Mehrfache Leerzeichen trimmen
$CleanChars2 = $CleanChars1 -replace '\s+', ' '
Return $CleanChars2.Trim()
}
# Erzeugt ein Verzeichnis
Function MKDir($Path, [Switch]$Silent = $True) {
Try {
If (-not (Test-Path -LiteralPath $Path)) {
If ($Silent) {
$null = New-Item -ItemType Directory -Path $Path -Force -ErrorAction SilentlyContinue
} Else {
$null = New-Item -ItemType Directory -Path $Path -Force -ErrorAction Stop
}
}
} Catch {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
$MessageId = ('{0:x}' -f $_.Exception.HResult).Trim([char]0)
$ErrorMessage = ($_.Exception.Message).Trim([char]0) # Illegal characters in path.
If ($ErrorMessage -like '*Illegal character*') {
Log 4 "MKDir: Ungültige Sonderzeichen:"
Log 4 $Path
Return
}
}
}
# Bereinigt FullFileName von ungültigen Zeichen
Function Clean-Path-Chars($FullFileName) {
If ($FullFileName.StartsWith('\\?\')) {
$Prefix = $FullFileName.SubString(0, '\\?\'.Length)
$Pfad = $FullFileName.SubString('\\?\'.Length)
} ElseIf ($FullFileName.StartsWith('\\')) {
$Prefix = $FullFileName.SubString(0, '\\'.Length)
$Pfad = $FullFileName.SubString('\\'.Length)
} ElseIf ($FullFileName[1] -eq ':' -and $FullFileName[2] -eq '\') {
$Prefix = $FullFileName.SubString(0, 'x:\'.Length)
$Pfad = $FullFileName.SubString('x:\'.Length)
} ElseIf ($FullFileName[1] -eq ':') {
$Prefix = $FullFileName.SubString(0, 'x:'.Length)
$Pfad = $FullFileName.SubString('x:'.Length)
} Else {
# Ein relativer Pfad
$Pfad = $FullFileName
}
# Die einzelnen Pfad-Elemente bereinigen
$Res = @()
$Items = $Pfad.Split('\')
ForEach ($Item In $Items) {
# Löscht ungültige Zeichen, mehrfache Leerzeichen und Underlines
$Res += $Item.Split([IO.Path]::GetInvalidFileNameChars()) -join '_' -replace '\s+', ' ' -replace '_+', ' '
}
# Das Resultat wieder zusammenfügen
$Prefix + ($Res -join '\')
}
# Liefert das Parent-Verzeichnis
Function Get-ParentDir($Dir) {
# [System.IO.Path]::GetDirectoryName('c:\temp\') » c:\temp
# [System.IO.Path]::GetDirectoryName('c:\temp') » c:\
# $Dir endet mit / oder \, oder $Dir hat keine Dateierweiterung
If (($Dir -match '/$|\\$') -or [String]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Dir))) {
# Wahrscheinlich ist Dir ein ein Verzeichnis
# Dir-Delimiter entfernen
$Dir = $Dir.TrimEnd('/\')
# Das ParentDir zurückgeben
If (Has-Value $Dir) {
[System.IO.Path]::GetDirectoryName($Dir)
}
} Else {
# Wahrscheinlich ist Dir ein Dateiname
$Dir = [System.IO.Path]::GetDirectoryName($Dir)
# Das ParentDir zurückgeben
If (Has-Value $Dir) {
[System.IO.Path]::GetDirectoryName($Dir)
}
}
}
#Endregion Tom-Tools: Pfade
#Region Tom-Tools: URI
# Verbinded zwei URLs
#
# Wenn $Append = $True, dann wird $Realtive dem ganzen Root-Pfad zugefügt
# Sonst wird Relative nur dem Host zugefügt
Function Join-URL ($Root, $Relative, [Switch]$Append = $True) {
# Sicherstellen, dass am Ende kein Slash existiert
$Root = $Root.TrimEnd('/').TrimEnd('\')
# Wenn $Relative zum ganzen Root-Pfad zugefügt werden muss, benötigt Root ein Slash
If ($Append) { $Root = $Root + '/' }
# https://msdn.microsoft.com/en-us/library/system.uri(v=vs.110).aspx
$RootUri = New-Object System.Uri($Root)
(New-Object System.Uri($RootUri, $Relative)).AbsoluteUri
}
# Parst $Str in ein [Uri]
# und ersetzt $Uri.Query mit einem Name/Value Array
Function Parse-Uri() {
Param (
[parameter(Position = 0, Mandatory)]
[String]$Str
)
# Laden ist schneller als der vorgängige Test
# ('Web.HttpUtility' -as [Type]) -ne $Null
Add-Type -AssemblyName System.Web
$Uri = [System.Uri]$Str
# $Uri.Query in $Uri.QueryStr ändern
$Uri | Add-Member -MemberType NoteProperty 'QueryStr' -Value $Uri.Query
$ParsedQueryString = [Web.HttpUtility]::ParseQueryString($Uri.Query)
$i = 0
$QryParams = @()
ForEach($QueryStringObject in $ParsedQueryString){
$QryParams += [PSCustomObject][Ordered]@{
Name = $QueryStringObject
Value = $ParsedQueryString[$i]
}
$i++
}
# $Uri.Query patchen / ersetzen
$Uri | Add-Member -MemberType NoteProperty 'Query' -Value $QryParams -Force
$Uri
}
#Endregion Tom-Tools: URI
#Region Tom-Tools: .URL Files
<#
.SYNOPSIS
Erzeugt eine .url - Shortcut-Datei
.PARAMETER ShortcutFile
Der Dateiname
.PARAMETER TargetURL
Die Ziel-URL
.NOTES
190301, tom-agplv3@jig.ch
230409, tom-agplv3@jig.ch
PS7
#>
Function New-Shortcut-URL() {
Param (
[Parameter(Position = 0, Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ShortcutFile,
[Parameter(Position = 1, Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$TargetURL
)
$Content = @"
[InternetShortcut]
URL=$TargetURL
"@
$Content | Out-File -LiteralPath $ShortcutFile -Encoding utf8
}
# Erzeugt für eine heruntergeladene Youtube-Datei die zugehörige URL-Datei
# *.url
Function Create-YouTube-URL-Video($ZielDir, $YTVideoID, [Int]$Ident = 2) {
# $UrlFile = Replace-FileExt $VideoFile '.url'
$UrlFile = Join-Path $ZielDir '!H.url'
$URL = "https://www.youtube.com/watch?v={0}" -f $YTVideoID
Log $Ident "Erzeuge: $([IO.Path]::GetFileName($UrlFile))" -ForegroundColor White
New-Shortcut-URL -ShortcutFile $UrlFile -TargetURL $URL
Return $UrlFile
}
#Endregion Tom-Tools: .URL Files
#Region Tom-Tools: Allgemeine Funktionen
# Join-Path für URLs
# Erlaubt den zwingenden abschliessenden Separator, weil manche Webseiten den brauchen
#
# Join-Parts -Parts @($HomeUrl,'login/')
# https://www.christ-sucht-christ.de/login/
Function Join-Parts {
param(
[String[]]$Parts = $null,
$Separator = '/'
)
# $Nuller löschen
($Parts | ? { $_ } | % { ([String]$_).TrimStart($Separator) } | ? { $_ } ) -Join $Separator
}
# ℹ Einzeiler für den Script-Anfang:
# $ScriptDir = [IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path)
# Liefert das Verzeichnis des ablaufenden Scriptes
Function Get-ScriptDir { "{0}\" -f $($MyInvocation.PSScriptRoot) }
# Macht, was man erwartet
# 220127
Function IsNullOrEmpty([String]$Str) {
[String]::IsNullOrWhiteSpace($Str)
}
Function Has-Value($Test) {
-not [String]::IsNullOrWhiteSpace($Test)
}
#Endegion Tom-Tools: Allgemeine Funktionen
#Region Tom-Tools: String-Tools
#220127
#230409
# Ergänzt Str mit einem Prefix und trennt sie per Default mit einem Space
Function Add-Prefix($Str, $Prefix, $Delimit = ' ', [Switch]$Trim) {
If (IsNullOrEmpty $Str -And IsNullOrEmpty $Prefix) {
Return ''
}
If (Has-Value $Str) { Return $Str }
If (Has-Value $Prefix) { Return $Prefix }
If ($Trim) {
Return $Prefix.Trim() + $Delimit + $Str.Trim()
}
Else {
Return $Prefix + $Delimit + $Str
}
}
# Ergänzt Str mit einem Suffix und trennt sie per Default mit einem Space
Function Add-Suffix($Str, $Suffix, $Delimit = ' ', [Switch]$Trim) {
If (IsNullOrEmpty $Str -And IsNullOrEmpty $Suffix) {
Return ''
}
If (Has-Value $Str) { Return $Str }
If (Has-Value $Suffix) { Return $Suffix }
If ($Trim) {
Return $Str.Trim() + $Delimit + $Suffix.Trim()
}
Else {
Return $Str + $Delimit + $Suffix
}
}
# Delayed expansion of variables String
# Alias: Expand-String
Function Invoke-String($str) {
$escapedString = $str -replace '"', '`"'
Invoke-Expression "Write-Output `"$escapedString`""
}
# Umschliesst einen String mit einem Character. idR mit "
Function EncloseString($Path, $EnclosingChar = '"') {
$EnclosingChar + $Path + $EnclosingChar
}
# Forciert, dass $Data die Windows-CRLF hat
Function Enforce-Win-CRLF($Data) {
$Data -replace "`r(?!`n)|`n(?!`r)", "`r`n"
}
# Forciert, dass $Data die Linux-LF hat
# Ex
# $f1 = 'c:\Certificate.crt'
# $F1Cnt = [IO.File]::ReadAllText($f1) | % { Enforce-Linux-LF $_ } | % { Enforce-LastLine-End $_ -Linux }
Function Enforce-Linux-LF($Data) {
$Data -replace "`r`n|`n`r", "`n"
}
# Stellt sicher, dass die letzte Zeile in $Data mit dem gewünschten NewLine endet
Function Enforce-LastLine-End() {
Param (
[Parameter(Position = 0, Mandatory)]
[String]$Data,
[Switch]$Windows,
[Switch]$Linux,
[Switch]$MacOS
)
If ($Data -eq $false -and $Windows -eq $false -and $Linux -eq $false -and $MacOS -eq $false) {
Return $Data
}
If ($Windows) {
If ($Data.EndsWith("`r`n")) {
Return $Data
}
Return $Data + "`r`n"
}
If ($Linux) {
If ($Data.EndsWith("`n")) {
Return $Data
}
Return $Data + "`n"
}
If ($MacOS) {
If ($Data.EndsWith("`r")) {
Return $Data
}
Return $Data + "`r"
}
}
# Data kann ein String oder ein Array von Strings sein
# Die Daten werden auf CRLF konvertiert
Function Trim-Lines() {
Param (
[Parameter(Position = 0, Mandatory)]
[String]$Data,
[Switch]$Trim,
[Switch]$TrimStart,
[Switch]$TrimEnd
)
If ($Trim -eq $false -and $TrimStart -eq $false -and $TrimEnd -eq $false) {
Return $Data
}
# Den Parameter verarbeiten
If ($Trim) { $Splat = @{ Trim = $true } }
If ($TrimStart) { $Splat = @{ TrimStar = $true } }
If ($TrimEnd) { $Splat = @{ TrimEnd = $true } }
Switch ($Data.GetType().Name) {
'String' {
Trim-Lines_ -String $Data @Splat
}
'Object[]' {
$Res = @()
ForEach ($Item In $Data) {
$Res += Trim-Lines_ -String $Item @Splat
}
$Res
}
default {
Write-Host "(076ef222): Unbekannter Typ: $($Data.GetType().Name)"
}
}
}
Function Trim-Lines_() {
Param (
[Parameter(Position = 0, Mandatory)]
[String]$String,
[Switch]$Trim,
[Switch]$TrimStart,
[Switch]$TrimEnd
)
If ($Trim -eq $false -and $TrimStart -eq $false -and $TrimEnd -eq $false) {
Return $String
}
$String = Enforce-Win-CRLF $String
$Lines = $String -split "`r`n"
$Res = $Lines | % {
If ($Trim) {
$_.Trim()
}
ElseIf ($TrimStart) {
$_.TrimStart()
}
ElseIf ($TrimEnd) {
$_.TrimEnd()
}
}
$Res -join "`r`n"
}
#Endregion Tom-Tools: String-Tools
#Region Tom-Tools: Programmlogik
# Beispiele:
# IIf ( --$Week % $nthWeek -eq 0 ) $true $false
# $a = IIf $Object {$_.Method()}
Function IIf($If, $IfTrue, $IfFalse) {
If ($If -IsNot "Boolean") {
$_ = $If
}
If ($If) {
If ($IfTrue -is "ScriptBlock") {
&$IfTrue
} Else {
$IfTrue
}
} Else {
If ($IfFalse -is "ScriptBlock") {
&$IfFalse
} Else {
$IfFalse
}
}
}
#Endregion Tom-Tools: Programmlogik
#Region Tom-Tools: PSCustomObject, PSObject
# Prüft, ob ein Objekt ein Property besitzt
# VORSICHT!, ist nicht ganz zuverlässig!, M$-Eigene Properties liefern manchmal falsche Ergebnisse
Function HasProperty ([object]$TestObject, [string]$propertyName) {
If (Get-Member -Force -InputObject $TestObject -Name $propertyName -MemberType Properties) {
$true
} Else { $false }
}
# Define the default DisplaySet
# Set-PSCustomObject_DisplaySet $result ('Server', 'BackupTime', 'Hostname')
Function Set-PSCustomObject_DisplaySet($Obj, $DisplaySet) {
If (-not ($Obj | Get-Member -Name 'PSStandardMembers' -Force)) {
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$DisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
$Obj.PSObject.TypeNames.Insert(0, 'User.Information')
$Obj | Add-Member MemberSet PSStandardMembers $PSStandardMembers
$Obj
}
}
#Endregion Tom-Tools: PSCustomObject
#Region Tom-Tools: Suchen und Einbinden von Scripts
### ℹ Anwendung
# $ScriptFileName = [IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Source)
# ## Config
# $BackupVeeam_ps1 = 'Backup-Veeam.ps1'
# $ConfigFile = "{0}-Config.ps1"
# ## Main
# $ScriptDir = Get-ScriptDir
# $ScriptName = Get-ScriptNameWithoutExtension
# $ConfigFile = $ConfigFile -f "$ScriptName"
# # $Res = [PSCustomObject][ordered]@{
# # ScriptFile = Das gefundene ScriptFile
# # HasErr = True: Das aufrufen des Scripts erzeugte einen Fehler
# # ScriptRes = Das Resultat des aufgerufenen Scripts
# $BackupVeeam_ps1 = . Get-Script $ScriptDir $BackupVeeam_ps1 -ShowError:$true -LocalCachePrefix LocalCachePrefix
# $ConfigFile_ps1 = . Get-Script $ScriptDir $ConfigFile -ShowError:$true -dotSourceScript -LocalCachePrefix LocalCachePrefix
# 200127: Get-Script(): Switch Parameter besser geprüft
# 200515: Get-Script(): Findet ein lokaler Cache
# 200519: Get-Script(): Neu: $ScriptParameters
# 220127: Info ScriptDir Einzeiler
# Sucht das PowerShell Verzeichnis
Function Get-ScriptPowerShellDir($SubDir) {
$Suche = '\powershell\'
$DirPos = $SubDir.IndexOf($Suche, [System.StringComparison]::CurrentCultureIgnoreCase)
If ($DirPos -gt 0) {
$SubDir.SubString(0, $DirPos + $Suche.Length)
}
}
# Sucht die Datei $aScriptName in:
# 1. In aScriptDir
# 2. In aScriptDir\* Rekursiv
# 3. In aScriptDir\powershell\* Rekursiv
# 4. Aufwärts in aScriptDir
#
# Res
# $FoundScript, $Res = Get-Script (Get-ScriptDir) $Get_O365_Config_ps1 -dotSourceScript
# $Res = $null, wenn das Script nicht geladen wird
#
# 200420 Switch Parameter besser geprüft
# 200519 Neu: $ScriptParameters
# Parameter, die dem aufgerufenen Script übergeben werden können
# Ex
# If ($Silent) { $ScriptArgs = @{ Silent = $true } }
# $FoundScript, $Res = . Get-Script (Get-ScriptDir) $Get_O365_Config_ps1 -dotSourceScript -ScriptParameters $ScriptArgs
Function Get-Script() {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
Param (
[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NoSwitches')]
[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'CallScript')]
[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'dotSourceScript')]
[String]$aScriptDir,
[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NoSwitches')]
[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'CallScript')]
[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'dotSourceScript')]
[String]$aScriptName,
# Aufruf mit &: Das Script aufrufen und nur das Resultat nützen
# https://ss64.com/ps/call.html
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'CallScript')]
[Switch]$CallScript,
# Aufruf mit .: Das Script einbetten, d.h. das Script im aktiven Scope aufrufen
# https://ss64.com/ps/source.html
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'dotSourceScript')]
[Switch]$dotSourceScript,
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NoSwitches')]
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'CallScript')]
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'dotSourceScript')]
[Switch]$ShowError = $true,
[HashTable]$ScriptParameters = $null
)
# Verbose deaktivieren und später wieder herstellen
$VerbosePreferenceBak = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
Try {
# Erkennen, ob die Funktion DotSourced aufgerufen wurde
Enum eCalledBy { Null; DotSource; CallOperator; Path; FunctionName }
[eCalledBy]$CalledBy = [eCalledBy]::Null
If ($MyInvocation.InvocationName -eq '&') {
$CalledBy = [eCalledBy]::CallOperator
} ElseIf ($MyInvocation.InvocationName -eq '.') {
$CalledBy = [eCalledBy]::DotSource
} Else {
$CalledBy = [eCalledBy]::FunctionName
}
If ($CalledBy -ne ([eCalledBy]::DotSource)) {
Write-Host 'Die Funktion(!) sollte DotSourced aufgerufen werden!: Get-Script()' -ForegroundColor Red
}
# 1. Sucht im ScriptDir selber
If ($FoundScript = (Get-ChildItem -LiteralPath $aScriptDir -Filter $aScriptName | Select-Object -First 1).FullName) {
Write-Verbose "Nütze Script: $FoundScript"
If ($ScriptParameters -ne $null) {
If ($CallScript) { Return @($FoundScript, (& $FoundScript @ScriptParameters)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript @ScriptParameters)) }
} Else {
If ($CallScript) { Return @($FoundScript, (& $FoundScript)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript)) }
}
Return $FoundScript
}
# 2. Sucht rekursiv im ScriptDir
If ($FoundScript = (Get-ChildItem -LiteralPath $aScriptDir -Recurse -Filter $aScriptName | Select-Object -First 1).FullName) {
Write-Verbose "Nütze Script: $FoundScript"
If ($ScriptParameters -ne $null) {
If ($CallScript) { Return @($FoundScript, (& $FoundScript @ScriptParameters)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript @ScriptParameters)) }
} Else {
If ($CallScript) { Return @($FoundScript, (& $FoundScript)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript)) }
}
Return $FoundScript
}
# 3. Sucht das Scripts\PowerShell Verzeichnis
# und dann in allen Unterverzeichnissen
If ($ScriptPowerShellDir = Get-ScriptPowerShellDir $aScriptDir) {
If ($FoundScript = (Get-ChildItem -LiteralPath $ScriptPowerShellDir -Recurse -Filter $aScriptName | Select-Object -First 1).FullName) {
Write-Verbose "Nütze Script: $FoundScript"
If ($ScriptParameters -ne $null) {
If ($CallScript) { Return @($FoundScript, (& $FoundScript @ScriptParameters)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript @ScriptParameters)) }
} Else {
If ($CallScript) { Return @($FoundScript, (& $FoundScript)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript)) }
}
Return $FoundScript
}
}
# 4. Aufwärts in aScriptDir
$ParentDir = Get-ParentDir $aScriptDir
While ($ParentDir) {
If ($FoundScript = (Get-ChildItem -LiteralPath $ParentDir -Filter $aScriptName | Select-Object -First 1).FullName) {
Write-Verbose "Nütze Script: $FoundScript"
If ($ScriptParameters -ne $null) {
If ($CallScript) { Return @($FoundScript, (& $FoundScript @ScriptParameters)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript @ScriptParameters)) }
} Else {
If ($CallScript) { Return @($FoundScript, (& $FoundScript)) }
If ($dotSourceScript) { Return @($FoundScript, (. $FoundScript)) }
}
Return $FoundScript
}
$ParentDir = Get-ParentDir $ParentDir
}
If ($ShowError) {
Write-Host "Script fehlt : $aScriptName"
Write-Host "In Verzeichnis: $aScriptDir"
}
} Finally {
$VerbosePreference = $VerbosePreferenceBak
}
}
# Liefert $True,
# wenn ein Script von einem anderen Script
# und nicht von der Shell aus gestartet wurde
Function Is-CalledFromScript($ThisScriptName) {
$CallStack = Get-PSCallStack | select -ExpandProperty Command
$CallingScriptName = $CallStack[($CallStack.count - 2)]
$CallingScriptName -ne $ThisScriptName
}
#Endregion Tom-Tools: Suchen und Einbinden von Scripts
#Region Tom-Tools: Network
# Ein schnelles Ping
# 220127
Function Test-Connection-Fast {
<#
.DESCRIPTION
Test-ComputerConnection sends a ping to the specified computer or IP Address specified in the ComputerName parameter. Leverages the System.Net object for ping
and measures out multiple seconds faster than Test-Connection -Count 1 -Quiet.
.PARAMETER ComputerName
The name or IP Address of the computer to ping.
.EXAMPLE
Test-ComputerConnection -ComputerName "THATPC"
Tests if THATPC is online and returns a custom object to the pipeline.
.EXAMPLE
$MachineState = Import-CSV .\computers.csv | Test-ComputerConnection -Verbose
Test each computer listed under a header of ComputerName, MachineName, CN, or Device Name in computers.csv and
and stores the results in the $MachineState variable.
.NOTES
001, 220127
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelinebyPropertyName)]
[Alias('CN', 'MachineName', 'Device Name')]
[String]$ComputerName,
[Int]$TimeoutMs = 500,
[Int]$NoOfPings = 5,
# Versucht stilldie Pings und bricht bei Erfolg ab
[Switch]$TestForSuccess,
# Maximum number of times the ICMP echo message can be forwarded before reaching its destination.
# Results in: TtlExpired
# Range is 1-255. Default is 64
[Int]$TTL = 64,
# Buffer used with this command. Default 32
[Int]$Buffersize = 32,
# Wenn true und das Paket ist für einen Router oder gateway zum Host
# grösser als die MTU: Status: PacketTooBig
[Switch]$DontFragment = $false,
[Switch]$PassThru
)
Begin {
$options = New-Object System.Net.Networkinformation.PingOptions
$options.TTL = $TTL
$options.DontFragment = $DontFragment
$buffer=([System.Text.Encoding]::ASCII).getbytes('a' * $Buffersize)
$ping = New-Object System.Net.NetworkInformation.Ping
# mind. 1 Ping
$NoOfPings = [Math]::Max($NoOfPings, 1)
$DestinationReachedOnce = $False
$ResPing = @()
}
Process {
For ($Cnt = 0; $Cnt -lt $NoOfPings; $Cnt++) {
Try {
$reply = $ping.Send($ComputerName, $TimeoutMs, $buffer, $options)
} Catch {
$ErrorMessage = $_.Exception.Message
Write-Host ($_ | Out-String)
$Res = [PSCustomObject][Ordered]@{
Message = ($_.ToString())
ComputerName = $ComputerName
Success = $False
Timeout = $True
Status = $ErrorMessage
}
}
If ($reply.status -eq 'Success') {
$Res = @{
ComputerName = $ComputerName
Success = $True
Timeout = $False
Status = $reply.status
}
} Else {
$Res = [PSCustomObject][Ordered]@{
ComputerName = $ComputerName
Success = $False
Timeout = $True
Status = $reply.status
}
}
If ($Res.Success) { $DestinationReachedOnce = $True }
If ($TestForSuccess) {
# Die Resultate sammeln
$ResPing +=$Res
# Bei Erfolg stoppen
If ($DestinationReachedOnce) {
If ($PassThru) {
Return $ResPing
} Else {
Return $True
}
}
} Else {
If ($PassThru) {
$Res
} Else {
$Res.Success
}
}
}
If ($TestForSuccess) {
If ($PassThru) {
Return $ResPing
} Else {
Return $DestinationReachedOnce
}
}
}
End{}
}
#EndRegion Tom-Tools: Network
#Region Tom-Tools: Enums
# !Ex Enum mit System.Flags und All / None Valie
#
# # Flag prüfen:
# ([eMediaType]::Video).HasFlag( [eMediaType]::Video )
# Add-Type -TypeDefinition @"
# [System.Flags]
# public enum eMediaType {
# None = 0,
# Audio = 1,
# Video = 2,
# All = (Audio | Video)
# }
# "@
# Setzt ein Flag und stellt sicher, dass $Flag zu $Enum passt
Function Set-Enum-Flag() {
[CmdletBinding()]
Param(
[Enum]$Enum,
[Enum]$Flag
)
If ($Enum.GetType().Name -ne $Flag.GetType().Name) {
Write-Error "Inkompatible Enums: $($Enum.GetType().Name) und $($Flag.GetType().Name)"
} Else {
Return $Enum -bor $Flag
}
Return $Enum
}
# Löscht ein Flag und stellt sicher, dass $Flag zu $Enum passt
Function Clear-Enum-Flag() {
[CmdletBinding()]
Param(
[Enum]$Enum,
[Enum]$Flag
)
If ($Enum.GetType().Name -ne $Flag.GetType().Name) {
Write-Error "Inkompatible Enums: $($Enum.GetType().Name) und $($Flag.GetType().Name)"
} Else {
If ($Enum.HasFlag($Flag)) {
Return ($Enum -band (-bnot [Int]$Flag))
}
}
Return $Enum
}
#EndRegion Tom-Tools: Enums
#Region Tom-Tools: Debugger
# Liefert true, wenn die Console von VSCode odere Sapien ist
Function Is-DebuggerVsCodeOrSapien() {
switch ($Host.Name) {
'Visual Studio Code Host' {
Return $True
}
'PrimalScriptHostImplementation' {
Return $True
}
Default {
Return $False
}
}
}
#EndRegion Tom-Tools: Debugger
#Endregion Tom-Tools
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment