[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)] [string[]]$FilePath, # Custom output file (-f / -OutFile / -of) [Parameter(Mandatory = $false, ParameterSetName = 'Generate')] [Alias('f', 'of')] [string]$OutFile, # Auto output file switch (-s / -Save) [Parameter(Mandatory = $false, ParameterSetName = 'Generate')] [Alias('s', 'S')] [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 ) begin { function Show-Help { @' 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 Specifies the target file(s) or wildcard pattern (*). -s, -S, -Save 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, -OutFile, -of, -OF 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, -PrincessPi Displays Princess Pi ASCII art in magenta. -h, -H, -Help Displays this help documentation. 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 '@ }; function Write-ErrorWithHelp { param([string]$ErrorMessage); Write-Error $ErrorMessage; Write-Host ""; Show-Help; }; function Show-PrincessPiArt { $art = @' .. ..... ....------.. ..:--------.. ....... ..----------.. ....---.. ...---------=------::....:----:.. ..-----=+**************=-----:... .----+****************=-----... .--=****************=-----=**+:::--:.... .-#*********##**++=---====++**+-----:. ..:==********#=.+@%-==++===++===++*=----... .:=::******%%+-::=++====+==++====+**=--... .+..+*****--@===-:-%--====-=+====+*#=:.. .-.-****#=.#+===--=+===----======+*+.. .::=:+****#@#+=====-**-----*+====++**:. .. .=****=--======-..%-----%+==++***+-.. ..=++***+-.----======..*-----#+==+***#%#+.. .. .:-..---=====--------=-+==+**-@*. ............::::::.... ..=:..--===+-:-------*%+==*+#%**=. .....:-------------------------:. ..............-:..--===+-..:-------+==+==.:... ...:----------------------------::. ......:----------::=:...--==+--:.:-------+==+=:.... ....--------------------------:..... ..:::----------------=-::.:--===---:.:------=--:.... ...:---------------------------... ..:------------------=--:..:--=+=--------::.:+-..... ...------------------------------.. ...:-----------------:....----==-------:...=%*.......:::--------------------------------:.. ...--------------:+......---------------:+%%#.----------------------------:.......-----:.. .:----===-------:-.. .:--------------*%%%%=--------------========------:.......-------:... ..-:.:------------....-.=---------=#%%%%##%------------=-----------------------------------.. .:--:....:--------:##*=:.-------=#%%%%%%#%+-----------=-------------------------------------: ..:-----------------::*%%#-------=###%%%%##=--------------------------------------:............ ..:-------------------:.-#%%##+==+%%%%%%#%+------------------------...:::--------------::::::.. ...:----------------------..-#%#%%%%%%%%*+#%=-------------------------=-:....:----------------:... --------------------------:.-#%%%%%%%%+#@%%%--------------------------=++=-::---------------:... ---------------------------...+%%%%%]%%%%#---------------------------------------------:.... ............------.....----:....-#%%%%%%%*=------------------------------::+=---------....... ..:-... ....-----.. ....-=-------------------------------------.=*+=....... ..---:....:-----:.......:--------------------------:-------------==+=-...... .:-------------..........:---------------------------..:-------------==.......... ..-------------:... ...--------:...----------------....:-----------==-........... ..:----==------:..-.... ..--------:...:---------------.....:----------=+-..... ..------==+=++==-..+--:. .:--------.....-----------=*%=.. ..----------=+=...... ....------------........:... ..:--------.....-------+#%%#@%.. ...----------=+=...... ..---------===:............... .:----=+*#=. ..--+#**+%@@@#%+. ..:-:..=----=*=:..... :............:=::+=:--:=.=-.::=... ..*@*-+%@@@:.....-@@#%@*#@@@#%:.. ..-:..---:-#**+-......... .. ...::::....::-.:.::-.. ..*%:..*@@@*.. ..-%#%@@+%@@@@=.. ...::.=#=..-%*++=:... ... ...... .......... ..**...*#=*%:. .--*#@%+#%@@@@+... ..*%@%+...+%+=*+=-... ..*#:.-#+..+-. .--*#**%%*%@@%=.. ..-%@@*:..+@%=:+**+=:.. ..*@#*%%=..==. .-*%@#%@##@@@#-. ...#@@%*-=%@@#:..-=++=- .:#@*-:++..==. .=#%@%*@@*%*=+*:....:.+@*::+%@@@%=......-* .-%+...=%##%-. .+@*@%*%@##*-:*+..:-==*@=..:#@+:=*:....... .:*#:..:*@@@%:. .--%@*%@#*%*#=..-%-....:+#=-.:#%-.:+-....... .+@#=.-*@@@@*.. ....-%@@@@@@=..+%*.. .--%--=+#%-.:*-. .... .-=++++++++=... .:***+++==::::... .-%#=-*#%#**#-. '@ Write-Host $art -ForegroundColor Magenta; }; 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; } }; }; function Invoke-ParallelHashing { param([array]$Items, [string]$Algorithm); $total = $Items.Count $completed = 0 $syncHash = [System.Collections.Hashtable]::Synchronized(@{ Count = 0 }) $Items | 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 }; } 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(); }; # --- 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 = @(); $isWildcardGlobal = $false; $skipExtensions = @('.md5', '.sha1', '.sha256', '.sha384', '.sha512'); foreach ($pathEntry in ($FilePath)) { $isWildcard = ($pathEntry -match '\*'); if ($isWildcard) { $isWildcardGlobal = $true; }; $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; $results = $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 ($res in $results) { $currentIndex++; Write-Progress -Activity "Matching Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); if ($res.IsMatch) { Write-Host "$($res.TargetFile): " -NoNewline; Write-Host "OK" -ForegroundColor Green; } else { Write-Host "$($res.TargetFile): " -NoNewline; Write-Host "FAILED" -ForegroundColor Red; }; }; 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'; }; }; 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); if (Test-Path -Path ($targetFullPath) -PathType 'Leaf') { $actualHash = (Get-FileHash -Path ($targetFullPath) -Algorithm ($fileAlgo)).Hash.ToLower(); $displayPath = Resolve-Path -Path ($targetFullPath) -Relative -ErrorAction 'SilentlyContinue'; if (-not $displayPath) { $displayPath = $targetFile; }; if ($displayPath.StartsWith(".\") -or $displayPath.StartsWith("./")) { $displayPath = $displayPath.Substring(2); }; if ($actualHash -eq $expectedHash) { Write-Host "${displayPath}: " -NoNewline; Write-Host "OK" -ForegroundColor Green; } else { Write-Host "${displayPath}: " -NoNewline; Write-Host "FAILED" -ForegroundColor Red; }; } else { $displayPath = $targetFile; if ($baseDir -ne (Get-Location).Path) { $displayPath = Join-Path -Path (Resolve-Path -Path ($baseDir) -Relative -ErrorAction 'SilentlyContinue') -ChildPath ($targetFile); if ($displayPath.StartsWith(".\") -or $displayPath.StartsWith("./")) { $displayPath = $displayPath.Substring(2); }; }; Write-Host "${displayPath}: " -NoNewline; Write-Host "FAILED open or read" -ForegroundColor Red; }; }; }; }; } # --- 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 = @(); $parallelResults = Invoke-ParallelHashing -Items $filesInDir -Algorithm $Algorithm; foreach ($res in $parallelResults) { $currentIndex++; Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Directory: $(Split-Path $dirPath -Leaf) [$currentIndex/$totalFiles]" -PercentComplete (($currentIndex / $totalFiles) * 100); $linesToWrite += $res.OutputLine; Write-Output $res.OutputLine; } 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 = @(); $parallelResults = Invoke-ParallelHashing -Items $allItems -Algorithm $Algorithm; foreach ($res in $parallelResults) { $currentIndex++; Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); $linesToWrite += $res.OutputLine; Write-Output $res.OutputLine; } 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; $parallelResults = Invoke-ParallelHashing -Items $allItems -Algorithm $Algorithm; foreach ($res in $parallelResults) { $currentIndex++; Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); Write-Output $res.OutputLine; Set-Content -Path ($res.ItemOutFile) -Value $res.OutputLine -Encoding utf8; } Write-Progress -Activity "Computing Checksums ($Algorithm)" -Completed; }; }; } # Standard console output (No save flags passed) else { $totalFiles = $allItems.Count; $currentIndex = 0; $parallelResults = Invoke-ParallelHashing -Items $allItems -Algorithm $Algorithm; foreach ($res in $parallelResults) { $currentIndex++; Write-Progress -Activity "Computing Checksums ($Algorithm)" -Status "Processing file $currentIndex of $totalFiles" -PercentComplete (($currentIndex / $totalFiles) * 100); Write-Output $res.OutputLine; } Write-Progress -Activity "Computing Checksums ($Algorithm)" -Completed; }; } } end {}