# organise-isos.ps1 # Usage: .\organise-isos.ps1 [-SourceDir ] [-DestDir ] [-DryRun] param( [string]$SourceDir = ".", [string]$DestDir = ".\iso-library", [switch]$DryRun ) # ── Arch detection ───────────────────────────────────────────────────────────── $archPatterns = @( @{ Pattern = 'aarch64'; Canonical = 'aarch64' }, @{ Pattern = 'arm64'; Canonical = 'aarch64' }, @{ Pattern = 'arm(?!64)'; Canonical = 'arm' }, @{ Pattern = 'x86[_-]64'; Canonical = 'x86_64' }, @{ Pattern = 'amd64'; Canonical = 'x86_64' }, @{ Pattern = '_x64(?=[_\.\-]|$)'; Canonical = 'x86_64' }, # Win11_x64 @{ Pattern = '64bit'; Canonical = 'x86_64' }, @{ Pattern = 'x86(?![_-]64)'; Canonical = 'x86' }, @{ Pattern = 'i[3-6]86'; Canonical = 'x86' }, @{ Pattern = '32bit'; Canonical = 'x86' }, @{ Pattern = 'riscv64'; Canonical = 'riscv64' }, @{ Pattern = 'riscv32'; Canonical = 'riscv32' }, @{ Pattern = 'ppc64le'; Canonical = 'ppc64le' }, @{ Pattern = 'ppc64'; Canonical = 'ppc64' }, @{ Pattern = 'ppc'; Canonical = 'ppc' }, @{ Pattern = 's390x'; Canonical = 's390x' }, @{ Pattern = 'mips64'; Canonical = 'mips64' }, @{ Pattern = 'mips(?!64)'; Canonical = 'mips' }, @{ Pattern = 'loong64'; Canonical = 'loong64' }, @{ Pattern = 'loongarch64'; Canonical = 'loong64' }, @{ Pattern = 'ia64'; Canonical = 'ia64' }, @{ Pattern = 'itanium'; Canonical = 'ia64' }, @{ Pattern = 'sparc64'; Canonical = 'sparc64' }, @{ Pattern = 'alpha'; Canonical = 'alpha' } ) # ── Edition/noise tokens to strip ────────────────────────────────────────────── # NOTE: deliberately does NOT include 'memtest', 'rescue', 'live' as standalone # OS names — those are caught by the OS map first, stripping only runs after # the first-token OS match. $noisePattern = '(?i)\b(live|server|desktop|dvd|netinstall|netinst|installer|' + 'everything|boot|minimal|workstation|kde|gnome|xfce|lxde|mate|cinnamon|' + 'budgie|standard|full|net|bd|cd|usb|dvd1|dvd2|disc\d*|disk\d*|' + 'multiarch|hybrid|recovery|update|enterprise|community|' + 'education|educational|studio|gaming|security|nonfree|non-free|' + 'free|oss|lts|eol|stable|testing|sid|snapshot|nightly|beta|alpha|' + 'rc\d*|sp\d*|u\d+|patch|hotfix|errata|image|' + 'english|international|intl|en_us|en_gb)\b' # ── OS normalisation map ──────────────────────────────────────────────────────── $osNormMap = [ordered]@{ # ── Windows family ────────────────────────────────────────────────────── '^win(dows)?(svr|server)(2025|2022|2019|2016|2012|2008|2003)' = 'windows-server' '^win(dows)?(svr|server)' = 'windows-server' '^win(dows)?' = 'windows' '^ms.?dos|^msdos' = 'ms-dos' '^reactos' = 'reactos' # ── RHEL family ───────────────────────────────────────────────────────── '^rhel|^redhat|^red.hat' = 'rhel' '^centos.stream' = 'centos-stream' '^centos' = 'centos' '^rocky' = 'rocky' '^alma' = 'almalinux' '^oracle.linux|^oraclelinux' = 'oraclelinux' '^scientific' = 'scientific' '^eurolinux' = 'eurolinux' '^springdale' = 'springdale' '^clearos' = 'clearos' '^navylinux|^navy.linux' = 'navylinux' # ── Fedora family ─────────────────────────────────────────────────────── '^fedora' = 'fedora' '^qubes' = 'qubes' '^korora' = 'korora' # ── SUSE family ───────────────────────────────────────────────────────── '^opensuse.leap' = 'opensuse-leap' '^opensuse.tumbleweed' = 'opensuse-tumbleweed' '^opensuse.microos' = 'opensuse-microos' '^opensuse.kubic' = 'opensuse-kubic' '^opensuse' = 'opensuse' '^sles|^suse.linux.enterprise' = 'sles' '^suse' = 'opensuse' '^geckolinux' = 'geckolinux' # ── Debian family ─────────────────────────────────────────────────────── '^debian' = 'debian' '^ubuntu.mate' = 'ubuntu-mate' '^ubuntu.budgie' = 'ubuntu-budgie' '^ubuntu.kylin' = 'ubuntu-kylin' '^ubuntu.studio' = 'ubuntu-studio' '^ubuntu.unity' = 'ubuntu-unity' '^lubuntu' = 'lubuntu' '^kubuntu' = 'kubuntu' '^xubuntu' = 'xubuntu' '^ubuntu' = 'ubuntu' '^linuxmint|^linux.mint|^mint' = 'linuxmint' '^lmde' = 'lmde' '^pop.os|^popos' = 'pop_os' '^elementary' = 'elementary' '^zorin' = 'zorin' '^kali' = 'kali' '^parrot' = 'parrot' '^backbox' = 'backbox' '^tails' = 'tails' '^whonix' = 'whonix' '^raspbian|^raspberry.pi.os' = 'raspios' '^armbian' = 'armbian' '^devuan' = 'devuan' '^mx.linux|^mxlinux' = 'mx' '^antix' = 'antix' '^deepin' = 'deepin' '^pureos' = 'pureos' '^trisquel' = 'trisquel' '^siduction' = 'siduction' '^sparky' = 'sparkylinux' '^bunsenlabs|^bunsen' = 'bunsenlabs' '^crunchbang' = 'crunchbangpp' '^bodhi' = 'bodhi' '^q4os' = 'q4os' '^netrunner' = 'netrunner' '^kaisen' = 'kaisen' '^grml' = 'grml' '^libreelec' = 'libreelec' '^osmc' = 'osmc' # ── Arch family ───────────────────────────────────────────────────────── '^archlinux|^arch.linux' = 'arch' '^arch(?!ive)' = 'arch' '^manjaro' = 'manjaro' '^endeavouros|^endeavour.os' = 'endeavouros' '^garuda' = 'garuda' '^artix' = 'artix' '^parabola' = 'parabola' '^hyperbola' = 'hyperbola' '^blackarch' = 'blackarch' '^archcraft' = 'archcraft' '^arcolinux' = 'arcolinux' '^cachyos' = 'cachyos' '^crystal.linux' = 'crystallinux' '^blendos' = 'blendos' # ── Gentoo family ─────────────────────────────────────────────────────── '^gentoo' = 'gentoo' '^calculate' = 'calculate' '^sabayon' = 'sabayon' '^funtoo' = 'funtoo' '^chromeos|^chrome.os' = 'chromeos' '^chromiumos|^chromium.os' = 'chromiumos' '^fydeos' = 'fydeos' # ── Slackware family ──────────────────────────────────────────────────── '^slackware' = 'slackware' '^salix' = 'salix' '^porteus' = 'porteus' '^slax' = 'slax' '^zenwalk' = 'zenwalk' # ── Independent / other Linux ──────────────────────────────────────────── '^void' = 'void' '^nixos|^nix(?=\d)' = 'nixos' '^guix' = 'guix' '^alpine' = 'alpine' '^tinycore|^tiny.core' = 'tinycore' '^puppy' = 'puppy' '^slitaz' = 'slitaz' '^dsl.linux|^damn.small' = 'dsl' '^knoppix' = 'knoppix' '^pclinuxos' = 'pclinuxos' '^mageia' = 'mageia' '^mandriva|^mandrake' = 'mandriva' '^openmandriva' = 'openmandriva' '^solus' = 'solus' '^clear.linux' = 'clearlinux' '^photon' = 'photon' '^flatcar' = 'flatcar' '^coreos|^fedora.coreos' = 'coreos' '^bottlerocket' = 'bottlerocket' '^talos' = 'talos' '^kairos' = 'kairos' '^rancheros' = 'rancheros' '^rancher' = 'rancher' '^k3os' = 'k3os' '^openwrt' = 'openwrt' '^ddwrt|^dd.wrt' = 'ddwrt' '^pfsense' = 'pfsense' '^opnsense' = 'opnsense' '^ipfire' = 'ipfire' '^endian' = 'endian' '^untangle' = 'untangle' '^smoothwall' = 'smoothwall' '^zeroshell' = 'zeroshell' '^vyos' = 'vyos' '^astralinux|^astr.linux' = 'astralinux' '^altlinux|^alt.linux' = 'altlinux' '^redos|^red.os' = 'redos' '^elbrus' = 'elbrus' '^rosa' = 'rosa' # ── Hypervisor / virtualisation ────────────────────────────────────────── '^proxmox.ve|^pve' = 'proxmox-ve' '^proxmox.backup|^pbs' = 'proxmox-bs' '^proxmox.mail|^pmg' = 'proxmox-mg' '^proxmox' = 'proxmox-ve' '^esxi|^vmware.esxi' = 'esxi' '^vsphere' = 'vsphere' '^vmware' = 'vmware' '^xenserver|^citrix.hypervisor' = 'xenserver' '^xcp.ng' = 'xcp-ng' '^harvester' = 'harvester' '^nutanix' = 'nutanix' # ── BSD family ────────────────────────────────────────────────────────── '^freebsd' = 'freebsd' '^openbsd' = 'openbsd' '^netbsd' = 'netbsd' '^dragonfly.bsd' = 'dragonflybsd' '^truenas.scale' = 'truenas-scale' '^truenas.core|^freenas' = 'truenas-core' '^truenas' = 'truenas' '^ghostbsd' = 'ghostbsd' '^midnightbsd' = 'midnightbsd' '^hardenedbsd' = 'hardenedbsd' '^nomadbsd' = 'nomadbsd' # ── macOS / Darwin ─────────────────────────────────────────────────────── '^macos|^mac.os|^osx' = 'macos' '^darwin' = 'darwin' # ── Solaris / illumos ──────────────────────────────────────────────────── '^opensolaris' = 'opensolaris' '^openindiana' = 'openindiana' '^omnios' = 'omnios' '^tribblix' = 'tribblix' '^solaris' = 'solaris' '^smartos' = 'smartos' # ── Other Unix / exotic ────────────────────────────────────────────────── '^haiku' = 'haiku' '^freedos' = 'freedos' '^plan9' = 'plan9' '^minix' = 'minix' '^morphos' = 'morphos' '^amigaos' = 'amigaos' '^aros' = 'aros' '^serenityos' = 'serenityos' '^redox' = 'redox' '^kolibrios' = 'kolibrios' '^menuetos' = 'menuetos' '^syllable' = 'syllable' '^genode' = 'genode' '^fuchsia' = 'fuchsia' # ── Rescue / tools ─────────────────────────────────────────────────────── '^clonezilla' = 'clonezilla' '^systemrescue|^system.rescue' = 'systemrescue' '^gparted' = 'gparted' '^partedmagic' = 'partedmagic' '^hirens|^hiren' = 'hirens' '^ubcd|^ultimate.boot' = 'ubcd' '^memtest86' = 'memtest86' '^memtest' = 'memtest' '^dban' = 'dban' '^rescuezilla' = 'rescuezilla' '^winpe' = 'winpe' '^ipxe' = 'ipxe' '^kaspersky' = 'kaspersky-rescue' '^bitdefender' = 'bitdefender-rescue' '^drweb' = 'drweb-rescue' '^eset' = 'eset-rescue' } function Get-IsoMeta { param([string]$Filename) $base = [System.IO.Path]::GetFileNameWithoutExtension($Filename) # ── Arch (raw filename before any stripping) ─────────────────────────── $arch = 'noarch' foreach ($ap in $archPatterns) { if ($base -match "(?i)$($ap.Pattern)") { $arch = $ap.Canonical break } } # ── Strip arch tokens and edition noise ──────────────────────────────── $clean = $base foreach ($ap in $archPatterns) { $clean = $clean -replace "(?i)$($ap.Pattern)", '' } $clean = $clean -replace $noisePattern, '' $clean = $clean -replace '[-_\.]{2,}', '-' $clean = $clean.Trim('-_. ') # ── OS: split and take first token, cast to string explicitly ───────── $segments = @($clean -split '[-_\.]' | Where-Object { $_ -ne '' }) $firstToken = if ($segments.Count -gt 0) { [string]$segments[0] } else { 'unknown' } $firstToken = $firstToken.ToLower() $os = 'unknown' foreach ($pattern in $osNormMap.Keys) { if ($firstToken -match $pattern) { $os = $osNormMap[$pattern] break } } # Try first two tokens joined — catches "linux-mint", "pop-os" etc. if ($os -eq 'unknown' -and $segments.Count -ge 2) { $twoToken = ([string]$segments[0] + '-' + [string]$segments[1]).ToLower() foreach ($pattern in $osNormMap.Keys) { if ($twoToken -match $pattern) { $os = $osNormMap[$pattern] break } } } if ($os -eq 'unknown') { $os = $firstToken -replace '[^a-z0-9\-]', '' if (-not $os) { $os = 'unknown' } } # ── Version ─────────────────────────────────────────────────────────── # Note: version parsing uses $base (raw) for date stamps, $clean for # everything else, to avoid stripping corrupting the match. $version = 'unknown' # Windows H-series (25H2, 24H2 etc.) — run on raw base if ($base -match '(?i)\b(\d{2}H[12])\b') { $version = $matches[1].ToUpper() } # Full date stamp YYYYMMDD — extract just the year as version grouping elseif ($base -match '\b(20\d{2})(\d{2})(\d{2})\b') { $version = $matches[1] } # Semver / partial semver from cleaned string elseif ($clean -match '(? " -NoNewline -ForegroundColor DarkGray Write-Host "$($meta.OS)\$($meta.Version)\$($meta.Arch)" -ForegroundColor Cyan if (-not $DryRun) { New-Item -ItemType Directory -Path $target -Force | Out-Null Move-Item -Path $iso.FullName -Destination (Join-Path $target $iso.Name) } $results += [PSCustomObject]@{ File = $iso.Name OS = $meta.OS Version = $meta.Version Arch = $meta.Arch Target = "$($meta.OS)\$($meta.Version)\$($meta.Arch)" } } Write-Host "" $results | Format-Table -AutoSize if (-not $DryRun) { Write-Host "`nFinal structure:" Get-ChildItem -Path $DestDir -Recurse -Filter "*.iso" | ForEach-Object { $_.FullName.Replace("$((Resolve-Path $DestDir).Path)\", "") } | Sort-Object }