<# .SYNOPSIS Princess Pi's Magical CHECKSUM! - Computes and verifies message digests / file checksums. .DESCRIPTION Computes or verifies checksums using cryptographic hash algorithms (SHA256, SHA512, SHA1, MD5, SHA384). Supports parallel processing, recursive scanning, direct hash matching, and verification file parsing. .NOTES Requires PowerShell 7.0 or higher. #> [CmdletBinding(DefaultParameterSetName='Generate')] param( # File(s) or wildcard pattern to compute checksum for [Parameter(Position = 0, ParameterSetName = 'Generate', ValueFromPipeline = $true)] [Parameter(Position = 0, ParameterSetName = 'Match', ValueFromPipeline = $true)] [Parameter(Position = 0, ParameterSetName = 'PrincessPi', ValueFromPipeline = $false)] [Alias('i', 'input', 'if', 'InputFile')] [string[]]$FilePath, # Custom output file (-f / -OutFile / -of) [Parameter(Mandatory = $false, ParameterSetName = 'Generate')] [Alias('f', 'of')] [string]$OutFile, # Auto output file switch (-s / -Save / -o) [Parameter(Mandatory = $false, ParameterSetName = 'Generate')] [Alias('s', 'o')] [switch]$Save, # File containing checksums to verify (-c / -Check) [Parameter(Mandatory = $false, ParameterSetName = 'Verify')] [Alias('c')] [string]$Check, # Direct checksum hash string to match against (-m / -Match) [Parameter(Mandatory = $true, ParameterSetName = 'Match')] [Alias('m')] [string]$Match, # Hashing algorithm to use (Defaults to SHA256) [Parameter(Mandatory = $false)] [Alias('a', 'algo')] [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')] [string]$Algorithm, # Recursive flag (-r / -Recursive) [Parameter(Mandatory = $false)] [Alias('r')] [switch]$Recursive, # Princess Pi art flag (-pp / -PrincessPi) [Parameter(Mandatory = $true, ParameterSetName = 'PrincessPi')] [Alias('pp')] [switch]$PrincessPi, # Help flag (-h / -Help) [Parameter(Mandatory = $false)] [Alias('h')] [switch]$Help ) # --- ENVIRONMENT SANITY CHECK --- # Ensure PowerShell 7+ is used since ForEach-Object -Parallel is required. if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Error "checksum: This script requires PowerShell 7 or higher for parallel execution (-Parallel support). Current version is $($PSVersionTable.PSVersion)." exit 1 } begin { function Show-Help { @' Princess Pi's Magical CHECKSUM! NAME checksum - Compute and verify message digests / file checksums SYNOPSIS .\checksum.ps1 [-FilePath] [-s] [-f ] [-Algorithm ] [-r] .\checksum.ps1 [-FilePath] -Match [-Algorithm ] [-r] .\checksum.ps1 -Check [-Algorithm ] [-r] .\checksum.ps1 -PrincessPi .\checksum.ps1 -Help DESCRIPTION Computes or verifies checksums using cryptographic hash algorithms (SHA256, SHA512, SHA1, MD5, SHA384). Outputs hash values in standard GNU coreutils format ( ). Ignores checksum metadata files (.md5, .sha1, .sha256, .sha384, .sha512) during wildcards unless passed explicitly. PARAMETERS -FilePath, -i, -input, -if, -InputFile Specifies the target file(s) or wildcard pattern (*). -s, -S, -Save, -o, -O Auto-saves checksum results. (Takes no arguments). - If input filename contains a wildcard (*), saves to 'checksums.'. - If input filename does NOT contain a wildcard, saves to '.'. When combined with -r and *, recursively auto-generates checksums per directory. -f, -F, -of, -OF, -OutFile Saves checksum results to a specific custom filename. Overrides the auto-naming behavior of -s if both are passed. -c, -C, -Check Reads hashes from a specified checksum file and verifies target files on disk. If -r is specified, recursively looks for and validates checksum files by name across subdirectories. -m, -M, -Match Compares the computed hash of target file(s) directly against a given hash string. -a, -A, -Algo, -Algorithm Specifies the hash algorithm to use. Supported options: MD5, SHA1, SHA256 (default), SHA384, SHA512. In -c or -m mode, auto-detects algorithm from hash length if omitted. -r, -R, -Recursive Processes target files or verification files recursively across directory structures. -pp, -PP, -PrincessPi Displays Princess Pi! -h, -H, -Help Displays this help documentation. Also displays on any error. EXAMPLES 1. Compute SHA256 for a single file and auto-save as 'myfile.iso.sha256': .\checksum.ps1 myfile.iso -s 2. Compute SHA512 recursively and auto-save as 'checksums.sha512' in each folder: .\checksum.ps1 * -r -s -a SHA512 3. Compute SHA256 checksums and save to a specific custom file: .\checksum.ps1 * -f my_hashes.txt 4. Verify checksums against a verification file: .\checksum.ps1 -c checksums.sha256 5. Compare file directly against an expected hash string: .\checksum.ps1 myfile.bin -m 406653a68bd3538975656831f33f031b Princess Pi LOVES YOU!~ https://linktr.ee/princesspi3 '@ }; function Write-ErrorWithHelp { param([string]$ErrorMessage); Show-Help; Write-Host ""; Write-Error $ErrorMessage; }; function Show-PrincessPiArt { $art = @' =#= -- ##*+++++ *+ -#=#- +# -*%%= ##++++++++++# ##*********# #==#= *** %%++%% -++# #++++++++++++*+ *#***************# #===#= %***+%++#*%%@+++++% #++++++++++++**+++#******++********# ====+#%@*******###%=%++##++% #+++++++++++*++++++++#***====+****++# =#=*==**#*#########%++#*+++@ *++++++++#+++++++++++#****==+******# *#=====%####***%%++++#*+++**% ++++++++*+++++++++++++#************#******* @****#==%####**#*+++++++#+++#**# #+++++++++++++++++++#####+*****************# @****#######%**@+*%**#%+++++%##*** #+++++++++++++++++#+++++#****+======*****+ #****##%####@**%+*******@@+++@###**% #+++++++++++++*++++++++++****========****# ****#####%%*%*%+***@@@%%@*+++@####** *+++++++++++++++++++++#****=======****# %***####%%**+%%*++*@=##@@%@++++%###*** ++++*+++++++++++#+**************# @%%%@@##%%#++++++++==**@@**+++%*###**% +++++++++++#*+++++#*********#### %###%++++++++====**#++++%**##**% @% ++++#+++++++++++++**************# %###%++++++++++++++++++%**%##**%@###@ %++++++++++++++++***=====******** ###%%++++++++%++++++*%%%%###***##%##@ +++++++++++++##**************# ###%%%%+++++##+++++++#*+%###***%%%##@ ++++++++++++++%********+############%@ @%%*@*%%%%###****+*%%****++%##***%##@@@ #+++++++++++++++*********###*****#########* #%%###***#####%**++++##**%#@@#@ ##+++++++++*###%#+***+***##*************####### @#%%####%%####%+++++++##*######%++++++++++++++#*+++++#%###%#######*********######% ##%########@%@+++@###%*%#####@+++++++++++%**++++++++++%%%%%%########***==**###%##% %#%###%###########@@%#%#@@##++++++++****++++********+++ %########**===*###%*# #**##%####@@####%@######%*++++++++++++++++#++**+*#++++# ########******###% % @#***# ###%@@##%#######@+++++++++++++++++++++%*++**++++# *########******##% @ *******% %##% =@#########++++++++++++++++++#++++*%++*#%#++# ########******##% #@%@# %% *@#@@###*+++++++++++++++++++%+++##+++###++* ##*#####******##@ **##%*++%++++++++++++++++++++++++++++++++% +#**#####******### ##%#######*@***+#+++++++++++++++#***#++++++++++++ %**#####*******#% *+++++$****##****++++++++##% #******#+++++++++++ %**#####*******#@ #++++++++*******- ++++++++# *******#++++++++++# @**#####*******# #@@+++++++******- *+++++++# #******#+++++++++* **####%**==+**@ @@@@@+++ %+++++++# ******# %+++++++ %*###%***===**@ %= @@@@@@@@@ #+++++++# =****** #++++++% *###%#***=+**% %** @@@@@@@@@@ ++++++++# ******# #++++++# *##@%********% @*@ @@+@@@++@@+ ++++++++# ******# #++++++# @## #*********@ #***# @@+@@++@@@++ @@@++++++* @@@%**** #@@++++% +## %#****************% @*@**@+++# @@@@+++++ @@@@@@**# @@@@@++++# @# %#**************** +#+++++# @@@@@@+++% @@@@@@@@# @@@@@@@++ @ %##************% #+++** @@@@@@@@@@ @@@@@@@@@ @@@@@@@@@ %###********%* @@@@@@@@@@+ @@@@@@@@@@ =@@@@@@@@@ %%#####%% *@@@@@@@@@@ @+*@@@@@@@= @@++@@@@@@ @++@@@@@@@@ #**@**@@@@@ *@@*#@@@@@@ -= @%#@@@@@@@@ *#***@@@@@@ @+#+@++#@@@ #+++@++@@@@@ **********# #+++++@+@@@@ ##+#+++@@@@@@ ##****#### *+#+##+++@@%% #+++++++++++# #++++++++++# #########* ____ ____ ____ _ _ ___ ____ ___ ___ ____ ____ ( _ \( _ \(_ _)( \( )/ __)( ___)/ __)/ __) ( _ \(_ _)/\/ )___/ ) / _)(_ ) (( (__ )__) \__ \\__ \ )___/ _)(_ (__) (_)\_)(____)(_)\_)\___)(____)(___/(___/ (__) (____) Princess Pi https://linktr.ee/princesspi3 '@ Write-Host $art -ForegroundColor Blue; }; function Get-AlgorithmFromHashLength { param([string]$HashString); switch ($HashString.Trim().Length) { 32 { return 'MD5'; } 40 { return 'SHA1'; } 64 { return 'SHA256'; } 96 { return 'SHA384'; } 128 { return 'SHA512'; } default { Write-ErrorWithHelp "checksum: Could not auto-detect algorithm for hash length $($HashString.Length). Please specify -Algorithm."; return $null; } }; }; } process { $helpInPath = $false; if ($FilePath) { foreach ($p in ($FilePath)) { if ($p -match '^(?i)[-/]{1,2}(h|help|\?)$') { $helpInPath = $true; break; } }; }; if ($Help.IsPresent -or $helpInPath) { Show-Help; return; }; if ($PrincessPi.IsPresent) { Show-PrincessPiArt; return; }; if (-not [string]::IsNullOrWhiteSpace($Algorithm)) { $Algorithm = $Algorithm.ToUpper(); }; # Determine if progress bar should display (-r, -c/-Check, or wildcard input '*') $isWildcardGlobal = $false; if ($FilePath) { foreach ($p in $FilePath) { if ($p -match '\*') { $isWildcardGlobal = $true; } } } $showProgress = $Recursive.IsPresent -or $isWildcardGlobal -or ($PSCmdlet.ParameterSetName -eq 'Verify'); # --- ITEM GATHERING (Generate & Match Modes) --- if ($PSCmdlet.ParameterSetName -in @('Generate', 'Match')) { if (-not $FilePath -or $FilePath.Count -eq 0) { Write-ErrorWithHelp "checksum: Missing file path parameter. Use -Help for usage."; return; }; $allItems = @(); $skipExtensions = @('.md5', '.sha1', '.sha256', '.sha384', '.sha512'); foreach ($pathEntry in ($FilePath)) { $isWildcard = ($pathEntry -match '\*'); $resolvedItems = @(); if ($Recursive) { $resolvedItems = Get-ChildItem -Path ($pathEntry) -Recurse -File -ErrorAction 'SilentlyContinue'; } else { if ($isWildcard -or ($pathEntry -match '\?')) { $resolvedItems = Get-ChildItem -Path ($pathEntry) -File -ErrorAction 'SilentlyContinue'; } else { $resolvedItems = Get-Item -Path ($pathEntry) -ErrorAction 'SilentlyContinue'; } }; if (-not $resolvedItems) { Write-ErrorWithHelp "checksum: $($pathEntry): No such file or directory"; continue; }; $explicitFileFound = $false; if ((-not $isWildcard) -and (Test-Path -Path ($pathEntry) -PathType 'Leaf')) { $explicitFileFound = $true; }; foreach ($item in ($resolvedItems)) { if ($item.PSIsContainer) { continue; }; if (-not $explicitFileFound) { if ($skipExtensions -contains $item.Extension.ToLower()) { continue; } }; $allItems += $item; }; }; }; # --- DIRECT MATCH MODE (-m / -Match) --- if ($PSCmdlet.ParameterSetName -eq 'Match') { if ([string]::IsNullOrWhiteSpace($Algorithm)) { $Algorithm = Get-AlgorithmFromHashLength -HashString ($Match); if (-not $Algorithm) { return; }; }; $expectedHash = $Match.Trim().ToLower(); $totalFiles = $allItems.Count; $currentIndex = 0; # Stream parallel results and update progress bar in real time $allItems | ForEach-Object -Parallel { $item = $_ $algo = $using:Algorithm $exp = $using:expectedHash $actualHash = (Get-FileHash -Path ($item.FullName) -Algorithm ($algo)).Hash.ToLower(); $targetFile = Resolve-Path -Path ($item.FullName) -Relative -ErrorAction 'SilentlyContinue'; if (-not $targetFile) { $targetFile = $item.Name; }; if ($targetFile.StartsWith(".\") -or $targetFile.StartsWith("./")) { $targetFile = $targetFile.Substring(2); }; [PSCustomObject]@{ TargetFile = $targetFile IsMatch = ($actualHash -eq $exp) } } -ThrottleLimit 16 | ForEach-Object { $currentIndex++ if ($showProgress -and $totalFiles -gt 0) { Write-Progress -Activity "Matching Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); } if ($_.IsMatch) { Write-Host "$($_.TargetFile): " -NoNewline; Write-Host "OK" -ForegroundColor Green; } else { Write-Host "$($_.TargetFile): " -NoNewline; Write-Host "FAILED" -ForegroundColor Red; } }; if ($showProgress) { Write-Progress -Activity "Matching Checksums ($Algorithm)" -Completed; } } # --- VERIFICATION MODE (-c / -Check) --- elseif ($PSCmdlet.ParameterSetName -eq 'Verify') { $checkFiles = @(); if ($Recursive) { $checkFileName = Split-Path -Path ($Check) -Leaf; $checkFiles = Get-ChildItem -Path '.\' -Filter ($checkFileName) -Recurse -File -ErrorAction 'SilentlyContinue'; } else { if (Test-Path -Path ($Check) -PathType 'Leaf') { $checkFiles = @(Get-Item -Path ($Check)); } }; if (-not $checkFiles -or $checkFiles.Count -eq 0) { Write-ErrorWithHelp "checksum: $($Check): No such file or directory"; return; }; foreach ($cf in ($checkFiles)) { $lines = Get-Content -Path ($cf.FullName); $baseDir = $cf.DirectoryName; $fileAlgo = $Algorithm; if ([string]::IsNullOrWhiteSpace($fileAlgo)) { foreach ($line in ($lines)) { if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) { continue; }; if ($line -match '^([a-fA-F0-9]+)\s+') { $fileAlgo = Get-AlgorithmFromHashLength -HashString ($Matches[1]); break; } elseif ($line -match '^([a-fA-F0-9]{128}|[a-fA-F0-9]{96}|[a-fA-F0-9]{64}|[a-fA-F0-9]{40}|[a-fA-F0-9]{32})') { $fileAlgo = Get-AlgorithmFromHashLength -HashString ($Matches[1]); break; } }; if ([string]::IsNullOrWhiteSpace($fileAlgo)) { $fileAlgo = 'SHA256'; }; }; # Parse verification items $verifyItems = @(); foreach ($line in ($lines)) { if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) { continue; }; $expectedHash = $null; $targetFile = $null; if ($line -match '^([a-fA-F0-9]+)\s+[* ]?(.*)$') { $expectedHash = $Matches[1].ToLower(); $targetFile = $Matches[2].Trim().TrimStart('*'); } elseif ($line -match '^([a-fA-F0-9]{128}|[a-fA-F0-9]{96}|[a-fA-F0-9]{64}|[a-fA-F0-9]{40}|[a-fA-F0-9]{32})[* ]?(.*)$') { $expectedHash = $Matches[1].ToLower(); $targetFile = $Matches[2].Trim().TrimStart('*'); }; if ($expectedHash -and $targetFile) { $targetFullPath = Join-Path -Path ($baseDir) -ChildPath ($targetFile); $verifyItems += [PSCustomObject]@{ ExpectedHash = $expectedHash TargetFile = $targetFile TargetFullPath = $targetFullPath BaseDir = $baseDir }; } } $totalFiles = $verifyItems.Count; $currentIndex = 0; # Run verification items in parallel and stream progress live $verifyItems | ForEach-Object -Parallel { $item = $_ $algo = $using:fileAlgo if (Test-Path -Path ($item.TargetFullPath) -PathType 'Leaf') { $actualHash = (Get-FileHash -Path ($item.TargetFullPath) -Algorithm ($algo)).Hash.ToLower(); $displayPath = Resolve-Path -Path ($item.TargetFullPath) -Relative -ErrorAction 'SilentlyContinue'; if (-not $displayPath) { $displayPath = $item.TargetFile; }; if ($displayPath.StartsWith(".\") -or $displayPath.StartsWith("./")) { $displayPath = $displayPath.Substring(2); }; [PSCustomObject]@{ DisplayPath = $displayPath Status = if ($actualHash -eq $item.ExpectedHash) { "OK" } else { "FAILED" } IsError = $false } } else { $displayPath = $item.TargetFile; if ($item.BaseDir -ne (Get-Location).Path) { $displayPath = Join-Path -Path (Resolve-Path -Path ($item.BaseDir) -Relative -ErrorAction 'SilentlyContinue') -ChildPath ($item.TargetFile); if ($displayPath.StartsWith(".\") -or $displayPath.StartsWith("./")) { $displayPath = $displayPath.Substring(2); }; }; [PSCustomObject]@{ DisplayPath = $displayPath Status = "FAILED open or read" IsError = $true } } } -ThrottleLimit 16 | ForEach-Object { $currentIndex++ if ($totalFiles -gt 0) { Write-Progress -Activity "Verifying Checksums ($fileAlgo)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); } Write-Host "$($_.DisplayPath): " -NoNewline; if ($_.IsError) { Write-Host "$($_.Status)" -ForegroundColor Red; } elseif ($_.Status -eq "OK") { Write-Host "OK" -ForegroundColor Green; } else { Write-Host "FAILED" -ForegroundColor Red; } }; Write-Progress -Activity "Verifying Checksums ($fileAlgo)" -Completed; }; } # --- GENERATION MODE --- else { if ([string]::IsNullOrWhiteSpace($Algorithm)) { $Algorithm = 'SHA256'; }; $wantCustomOut = $PSBoundParameters.ContainsKey('OutFile'); $wantAutoOut = $Save.IsPresent; if ($wantCustomOut -or $wantAutoOut) { # Recursive wildcard auto-saves per directory if ($Recursive -and $isWildcardGlobal) { $groupedItems = $allItems | Group-Object DirectoryName; foreach ($group in ($groupedItems)) { $dirPath = $group.Name; $filesInDir = $group.Group; if ($wantCustomOut) { $currentOutFile = Join-Path -Path ($dirPath) -ChildPath ($OutFile); } else { $currentOutFile = Join-Path -Path ($dirPath) -ChildPath "checksums.$($Algorithm.ToLower())"; }; $totalFiles = $filesInDir.Count; $currentIndex = 0; $linesToWrite = @(); $filesInDir | ForEach-Object -Parallel { $item = $_ $algo = $using:Algorithm $hash = (Get-FileHash -Path ($item.FullName) -Algorithm ($algo)).Hash.ToLower(); $relativePath = Resolve-Path -Path ($item.FullName) -Relative -ErrorAction 'SilentlyContinue'; if (-not $relativePath) { $relativePath = $item.Name; }; if ($relativePath.StartsWith(".\") -or $relativePath.StartsWith("./")) { $relativePath = $relativePath.Substring(2); }; [PSCustomObject]@{ Item = $item Hash = $hash RelativePath = $relativePath OutputLine = "${hash} ${relativePath}" ItemOutFile = "$($item.FullName).$($algo.ToLower())" } } -ThrottleLimit 16 | ForEach-Object { $currentIndex++; if ($showProgress -and $totalFiles -gt 0) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Directory: $(Split-Path $dirPath -Leaf) [$currentIndex/$totalFiles]" -PercentComplete (($currentIndex / $totalFiles) * 100); } $linesToWrite += $_.OutputLine; Write-Output $_.OutputLine; }; if ($showProgress) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Completed; } Set-Content -Path ($currentOutFile) -Value $linesToWrite -Encoding utf8; }; } else { # Custom file OR Auto file with a wildcard if ($wantCustomOut -or ($wantAutoOut -and $isWildcardGlobal)) { if ($wantCustomOut) { $singleOutFile = $OutFile; } else { $singleOutFile = "checksums.$($Algorithm.ToLower())"; }; $totalFiles = $allItems.Count; $currentIndex = 0; $linesToWrite = @(); $allItems | ForEach-Object -Parallel { $item = $_ $algo = $using:Algorithm $hash = (Get-FileHash -Path ($item.FullName) -Algorithm ($algo)).Hash.ToLower(); $relativePath = Resolve-Path -Path ($item.FullName) -Relative -ErrorAction 'SilentlyContinue'; if (-not $relativePath) { $relativePath = $item.Name; }; if ($relativePath.StartsWith(".\") -or $relativePath.StartsWith("./")) { $relativePath = $relativePath.Substring(2); }; [PSCustomObject]@{ Item = $item Hash = $hash RelativePath = $relativePath OutputLine = "${hash} ${relativePath}" ItemOutFile = "$($item.FullName).$($algo.ToLower())" } } -ThrottleLimit 16 | ForEach-Object { $currentIndex++; if ($showProgress -and $totalFiles -gt 0) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); } $linesToWrite += $_.OutputLine; Write-Output $_.OutputLine; }; if ($showProgress) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Completed; } Set-Content -Path ($singleOutFile) -Value $linesToWrite -Encoding utf8; } # Auto file for explicit individual files (No wildcard) else { $totalFiles = $allItems.Count; $currentIndex = 0; $allItems | ForEach-Object -Parallel { $item = $_ $algo = $using:Algorithm $hash = (Get-FileHash -Path ($item.FullName) -Algorithm ($algo)).Hash.ToLower(); $relativePath = Resolve-Path -Path ($item.FullName) -Relative -ErrorAction 'SilentlyContinue'; if (-not $relativePath) { $relativePath = $item.Name; }; if ($relativePath.StartsWith(".\") -or $relativePath.StartsWith("./")) { $relativePath = $relativePath.Substring(2); }; [PSCustomObject]@{ Item = $item Hash = $hash RelativePath = $relativePath OutputLine = "${hash} ${relativePath}" ItemOutFile = "$($item.FullName).$($algo.ToLower())" } } -ThrottleLimit 16 | ForEach-Object { $currentIndex++; if ($showProgress -and $totalFiles -gt 0) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); } Write-Output $_.OutputLine; Set-Content -Path ($_.ItemOutFile) -Value $_.OutputLine -Encoding utf8; }; if ($showProgress) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Completed; } }; }; } # Standard console output (No save flags passed) else { $totalFiles = $allItems.Count; $currentIndex = 0; $allItems | ForEach-Object -Parallel { $item = $_ $algo = $using:Algorithm $hash = (Get-FileHash -Path ($item.FullName) -Algorithm ($algo)).Hash.ToLower(); $relativePath = Resolve-Path -Path ($item.FullName) -Relative -ErrorAction 'SilentlyContinue'; if (-not $relativePath) { $relativePath = $item.Name; }; if ($relativePath.StartsWith(".\") -or $relativePath.StartsWith("./")) { $relativePath = $relativePath.Substring(2); }; [PSCustomObject]@{ Item = $item Hash = $hash RelativePath = $relativePath OutputLine = "${hash} ${relativePath}" ItemOutFile = "$($item.FullName).$($algo.ToLower())" } } -ThrottleLimit 16 | ForEach-Object { $currentIndex++; if ($showProgress -and $totalFiles -gt 0) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); } Write-Output $_.OutputLine; }; if ($showProgress) { Write-Progress -Activity "Computing Checksums ($Algorithm)" -Completed; } }; } } end {}